功能优化

This commit is contained in:
2025-11-14 02:43:35 +08:00
parent 6624627802
commit 7cfd502afb

View File

@@ -6,48 +6,32 @@ import { CommonService } from '@/api/common'
import { UserPromptApi } from '@/api/userPrompt'
import { useUserStore } from '@/stores/user'
import GradientButton from '@/components/GradientButton.vue'
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
// 定义组件名称
defineOptions({
name: 'ForecastView'
})
defineOptions({ name: 'ForecastView' })
// 平台列表
// const platforms = [
// { id: 'douyin', name: '抖音', color: '#FE2C55' }
// ]
// const activePlatform = ref('douyin')
// 搜索关键词
// 状态管理
const userStore = useUserStore()
const searchKeyword = ref('')
const isLoading = ref(false)
const isGenerating = ref(false)
const isAnalyzing = ref(false)
const hotTopics = ref([])
const selectedTopic = ref(null)
const currentCursor = ref(null)
const generatedContent = ref('')
// 搜索参数
const searchParams = reactive({
keyword: '',
offset: 0, // 偏移量第一次请求时为0后续从返回数据中的 cursor 获取
sort_type: 1, // 0:综合排序 1:最多点赞 2:最新发布
publish_time: 7, // 0:不限 1:最近一天 7:最近一周 180:最近半年
filter_duration: '0', // '0':不限 '0-1':1分钟以内 '1-5':1-5分钟 '5-10000':5分钟以上
content_type: 0 // 0:不限 1:视频 2:图集
offset: 0,
sort_type: 1,
publish_time: 7,
filter_duration: '0',
content_type: 0
})
// 加载状态
const isLoading = ref(false)
// 热点列表数据
const hotTopics = ref([])
// 选中的热点
const selectedTopic = ref(null)
// 当前 cursor用于翻页
const currentCursor = ref(null)
// 用户store
const userStore = useUserStore()
// 右侧详细信息
// 创作详情
const topicDetails = reactive({
title: '',
copywriting: '',
@@ -55,29 +39,18 @@ const topicDetails = reactive({
stylePromptId: null
})
// 生成的文案内容
const generatedContent = ref('')
// 生成文案的loading状态
const isGenerating = ref(false)
// 提示词相关状态
// 提示词相关
const allPrompts = ref([])
const loadingPrompts = ref(false)
const showAllPromptsModal = ref(false)
const promptSearchKeyword = ref('')
const DISPLAY_COUNT = 6 // 展示的提示词数量
const DISPLAY_COUNT = 6
// 计算属性:展示的部分提示词
const displayPrompts = computed(() => {
return allPrompts.value.slice(0, DISPLAY_COUNT)
})
// 计算属性
const displayPrompts = computed(() => allPrompts.value.slice(0, DISPLAY_COUNT))
// 计算属性:过滤后的全部提示词(用于"更多"弹窗)
const filteredPrompts = computed(() => {
if (!promptSearchKeyword.value.trim()) {
return allPrompts.value
}
if (!promptSearchKeyword.value.trim()) return allPrompts.value
const keyword = promptSearchKeyword.value.trim().toLowerCase()
return allPrompts.value.filter(p =>
p.name.toLowerCase().includes(keyword) ||
@@ -85,9 +58,33 @@ const filteredPrompts = computed(() => {
)
})
/**
* 加载用户提示词列表
*/
// 工具函数
const formatNumber = (num) => {
if (!num) return '0'
return num >= 10000 ? `${(num / 10000).toFixed(1)}w` : num.toString()
}
const truncateTitle = (title, maxLength = 30) => {
if (!title) return ''
return title.length <= maxLength ? title : `${title.substring(0, maxLength)}...`
}
const handleImageError = (event) => {
event.target.style.display = 'none'
}
const openVideo = (topic, event) => {
event.stopPropagation()
if (topic.videoUrl) window.open(topic.videoUrl, '_blank')
}
const handleSearchKeypress = (event) => {
if (event.key === 'Enter' && !isLoading.value) {
handleSearch()
}
}
// 提示词管理
async function loadUserPrompts() {
if (!userStore.userId) {
console.warn('用户未登录,无法加载提示词')
@@ -99,12 +96,12 @@ async function loadUserPrompts() {
const response = await UserPromptApi.getUserPromptPage({
pageNo: 1,
pageSize: 100,
status: 1 // 只获取启用状态的提示词
status: 1
})
if (response?.data?.list) {
allPrompts.value = response.data.list
// 如果没有选中的提示词,自动选中第一个
// 自动选中第一个提示词
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
const firstPrompt = allPrompts.value[0]
topicDetails.stylePromptId = firstPrompt.id
@@ -119,56 +116,87 @@ async function loadUserPrompts() {
}
}
/**
* 选择提示词
*/
function selectPrompt(prompt) {
if (!prompt || !prompt.content) {
if (!prompt?.content) {
message.warning('提示词内容为空')
return
}
topicDetails.stylePromptId = prompt.id
topicDetails.stylePrompt = prompt.content
showAllPromptsModal.value = false
}
// 点击创作按钮
const handleCreate = (topic) => {
// 语音分析
async function analyzeVoice(audioUrl) {
if (!audioUrl?.trim()) {
console.warn('音频链接为空,无法分析')
return
}
isAnalyzing.value = true
try {
message.info('正在分析语音链接,提取文案...')
const transcriptions = await getVoiceText([{ audio_url: audioUrl }])
const transcript = transcriptions?.[0]?.value?.trim() || ''
if (transcript) {
const hasExistingContent = topicDetails.copywriting?.trim()
topicDetails.copywriting = hasExistingContent
? `${topicDetails.copywriting}\n\n${transcript}`
: transcript
message.success(`语音分析完成,已提取 ${transcript.length} 字文案内容`)
} else {
console.warn('转写结果为空:', transcriptions)
message.warning('未从语音链接获取到可用的文案内容,请检查音频链接是否有效')
}
} catch (error) {
console.error('分析语音失败:', error)
message.error(`分析语音失败: ${error?.message || '请稍后重试'}`)
} finally {
isAnalyzing.value = false
}
}
// 创作处理
async function handleCreate(topic) {
selectedTopic.value = topic.id
topicDetails.title = topic.title
// 提取文案:使用标题作为初始文案
topicDetails.copywriting = topic.title || ''
// 保持已选择的风格提示词,如果没有则使用第一个
// 自动选中第一个提示词(如果还没有选中)
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
const firstPrompt = allPrompts.value[0]
topicDetails.stylePromptId = firstPrompt.id
topicDetails.stylePrompt = firstPrompt.content || ''
}
// 自动分析语音
if (topic.audioUrl?.trim()) {
await analyzeVoice(topic.audioUrl)
} else {
message.info('该视频暂无音频链接,无法自动提取语音文案')
}
}
// 生成文案(流式)
async function handleGenerate() {
if (!topicDetails.copywriting || !topicDetails.copywriting.trim()) {
if (!topicDetails.copywriting?.trim()) {
message.warning('请输入文案内容')
return
}
// 检查是否选择了提示词风格
if (!topicDetails.stylePrompt || !topicDetails.stylePrompt.trim()) {
if (!topicDetails.stylePrompt?.trim()) {
message.warning('请先选择提示词风格')
return
}
isGenerating.value = true
generatedContent.value = '' // 清空之前的内容
generatedContent.value = ''
try {
// 调用 callWorkflow 流式 API
const requestData = {
audio_prompt: topicDetails.stylePrompt || '', // 音频提示词
user_text: topicDetails.copywriting.trim(), // 用户输入内容
amplitude: 50 // 幅度默认50%
audio_prompt: topicDetails.stylePrompt,
user_text: topicDetails.copywriting.trim(),
amplitude: 50
}
const ctrl = new AbortController()
@@ -177,43 +205,72 @@ async function handleGenerate() {
let isResolved = false
await new Promise((resolve, reject) => {
// 设置超时
const timeout = setTimeout(() => {
let timeout = null
const cleanup = () => {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
timeout = setTimeout(() => {
if (!isResolved) {
cleanup()
ctrl.abort()
reject(new Error('请求超时,请稍后重试'))
}
}, 180000) // 3分钟超时
}, 180000)
CommonService.callWorkflowStream({
data: requestData,
ctrl,
onMessage: (event) => {
if (errorOccurred || !event?.data) return
// 处理特殊标记
const dataStr = event.data.trim()
if (dataStr === '[DONE]') {
cleanup()
if (!isResolved) {
isResolved = true
resolve()
}
return
}
if (dataStr.startsWith('[TIMEOUT]')) {
cleanup()
if (!isResolved) {
errorOccurred = true
isResolved = true
reject(new Error(dataStr.replace('[TIMEOUT]', '').trim() || '请求超时'))
}
return
}
try {
if (errorOccurred) return
const dataStr = event?.data || ''
if (!dataStr) return
try {
const obj = JSON.parse(dataStr)
// 根据实际返回格式解析
const piece = obj?.text || obj?.content || obj?.data || ''
if (piece) {
fullText += piece
generatedContent.value = fullText
}
} catch (parseErr) {
console.warn('解析流数据异常:', parseErr)
const obj = JSON.parse(event.data)
const piece = obj?.text || obj?.content || obj?.data || ''
if (piece) {
fullText += piece
generatedContent.value = fullText
}
} catch (e) {
console.warn('解析流数据异常:', e)
// 如果不是JSON可能是纯文本
if (event.data && !event.data.startsWith('[')) {
fullText += event.data
generatedContent.value = fullText
} else {
console.warn('解析流数据异常:', e)
}
}
},
onError: (err) => {
clearTimeout(timeout)
cleanup()
if (!isResolved) {
errorOccurred = true
isResolved = true
ctrl.abort()
const errorMsg = err?.message || '网络请求失败'
console.error('SSE请求错误:', err)
@@ -222,7 +279,7 @@ async function handleGenerate() {
}
},
onClose: () => {
clearTimeout(timeout)
cleanup()
if (!isResolved) {
isResolved = true
resolve()
@@ -241,123 +298,98 @@ async function handleGenerate() {
}
}
// 初始化:加载提示词
onMounted(async () => {
// 等待用户信息加载
if (userStore.userId) {
await loadUserPrompts()
} else if (userStore.isLoggedIn) {
// 如果已登录但userId未加载等待一下
setTimeout(async () => {
if (userStore.userId) {
await loadUserPrompts()
}
}, 500)
// 数据处理
function extractAudioUrl(video) {
const urlList = video?.play_addr?.url_list
if (Array.isArray(urlList) && urlList.length > 0) {
// 优先使用最后一个,如果为空则使用第一个
const lastUrl = urlList[urlList.length - 1]
const firstUrl = urlList[0]
return (lastUrl && lastUrl.trim()) || (firstUrl && firstUrl.trim()) || ''
}
})
return (video?.play_addr?.url && video.play_addr.url.trim()) || ''
}
// 切换平台
// const switchPlatform = (platformId) => {
// activePlatform.value = platformId
// selectedTopic.value = null
// topicDetails.title = ''
// topicDetails.copywriting = ''
// topicDetails.stylePrompt = ''
// }
function extractCover(video) {
return video?.origin_cover?.url_list?.[0]
|| video?.cover?.url_list?.[0]
|| video?.dynamic_cover?.url_list?.[0]
|| video?.animated_cover?.url_list?.[0]
|| ''
}
/**
* 处理搜索结果响应
*/
function processSearchResults(response, startId = 1) {
try {
// 提取 cursor用于翻页
const cursor = response?.data?.cursor || null
currentCursor.value = cursor
// 处理搜索结果
currentCursor.value = response?.data?.cursor || null
const dataList = response?.data?.data || []
const searchResults = dataList
return dataList
.map(el => el.aweme_info)
.filter(el => el)
.filter(Boolean)
.map((item, index) => ({
id: startId + index,
title: item.desc || '无标题',
videoId: item.aweme_id,
videoUrl: `https://www.douyin.com/video/${item.aweme_id}`,
// 封面图片:优先使用 origin_cover其次 cover再次 dynamic_cover最后 animated_cover
cover: item?.video?.origin_cover?.url_list?.[0]
|| item?.video?.cover?.url_list?.[0]
|| item?.video?.dynamic_cover?.url_list?.[0]
|| item?.video?.animated_cover?.url_list?.[0]
|| item?.cover?.url_list?.[0]
|| '',
// 作者信息
audioUrl: extractAudioUrl(item.video),
cover: extractCover(item.video) || item?.cover?.url_list?.[0] || '',
author: item.author?.nickname || item.author?.unique_id || '未知',
authorAvatar: item.author?.avatar_thumb?.url_list?.[0] || item.author?.avatar_larger?.url_list?.[0] || '',
// 统计数据
authorAvatar: item.author?.avatar_thumb?.url_list?.[0]
|| item.author?.avatar_larger?.url_list?.[0]
|| '',
playCount: item.statistics?.play_count || 0,
diggCount: item.statistics?.digg_count || 0,
commentCount: item.statistics?.comment_count || 0,
shareCount: item.statistics?.share_count || 0,
collectCount: item.statistics?.collect_count || 0,
}))
return searchResults
} catch (error) {
console.error('处理搜索结果失败:', error)
return []
}
}
/**
* 搜索热点
*/
const handleSearch = async () => {
if (!searchKeyword.value.trim()) {
// 搜索功能
async function handleSearch() {
const keyword = searchKeyword.value.trim()
if (!keyword) {
message.warning('请输入搜索关键词')
return
}
// 重置参数
// 重置状态
searchParams.offset = 0
searchParams.keyword = searchKeyword.value.trim()
searchParams.keyword = keyword
currentCursor.value = null
hotTopics.value = []
isLoading.value = true
try {
// 构建请求参数POST JSON 格式)
// 注意API 要求 sort_type、publish_time、content_type 为字符串类型
const urlParams = {
keyword: searchParams.keyword,
keyword: encodeURIComponent(searchParams.keyword),
offset: String(searchParams.offset),
count: '20',
sort_type: String(searchParams.sort_type),
publish_time: String(searchParams.publish_time),
filter_duration: searchParams.filter_duration, // 已经是字符串
filter_duration: searchParams.filter_duration,
content_type: String(searchParams.content_type),
}
const response = await TikhubService.postTikHup({
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH,
methodType: MethodType.POST,
urlParams: urlParams,
urlParams,
paramType: ParamType.JSON
})
// 处理搜索结果
const searchResults = processSearchResults(response)
if (searchResults.length === 0) {
message.warning('未找到相关结果')
hotTopics.value = []
return
}
// 替换列表
hotTopics.value = searchResults
message.success(`找到 ${searchResults.length} 个结果`)
} catch (error) {
console.error('搜索失败:', error)
message.error(error?.message || '搜索失败,请稍后重试')
@@ -367,71 +399,41 @@ const handleSearch = async () => {
}
}
// 回车搜索
const handleSearchKeypress = (event) => {
if (event.key === 'Enter' && !isLoading.value) {
handleSearch()
// 初始化
onMounted(async () => {
if (userStore.userId) {
await loadUserPrompts()
} else if (userStore.isLoggedIn) {
setTimeout(async () => {
if (userStore.userId) await loadUserPrompts()
}, 500)
}
}
// 格式化数字,将大数字转换为万等单位
const formatNumber = (num) => {
if (!num) return '0'
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w'
}
return num.toString()
}
// 打开视频
const openVideo = (topic, event) => {
event.stopPropagation() // 阻止事件冒泡,避免触发热点选择
if (topic.videoUrl) {
window.open(topic.videoUrl, '_blank')
}
}
// 截断标题,限制最大长度
const truncateTitle = (title, maxLength = 30) => {
if (!title) return ''
if (title.length <= maxLength) return title
return title.substring(0, maxLength) + '...'
}
// 处理图片加载错误
const handleImageError = (event) => {
event.target.style.display = 'none'
}
})
</script>
<template>
<div class="fc-page">
<div class="fc-grid">
<!-- 左侧平台栏目和热点列表 -->
<!-- 左侧热点列表 -->
<section class="fc-left">
<div class="fc-title">热点预测</div>
<!-- 平台选择 -->
<!-- <div class="platform-tabs">
<div class="flex space-x-2">
<button
v-for="platform in platforms"
:key="platform.id"
@click="switchPlatform(platform.id)"
:class="['platform-tab', activePlatform === platform.id ? 'platform-tab--active' : 'platform-tab--inactive']"
:style="activePlatform === platform.id ? { background: platform.color } : {}"
>
{{ platform.name }}
</button>
</div>
</div> -->
<!-- 搜索框 -->
<div class="search-box">
<div class="search-input-wrapper">
<input v-model="searchKeyword" type="text" placeholder="输入关键词搜索抖音热点..." class="search-input"
:disabled="isLoading" @keypress="handleSearchKeypress" />
<button @click="handleSearch" :disabled="isLoading || !searchKeyword.trim()" class="search-btn">
<input
v-model="searchKeyword"
type="text"
placeholder="输入关键词搜索抖音热点..."
class="search-input"
:disabled="isLoading"
@keypress="handleSearchKeypress"
/>
<button
@click="handleSearch"
:disabled="isLoading || !searchKeyword.trim()"
class="search-btn"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -445,9 +447,9 @@ const handleImageError = (event) => {
<div class="param-item">
<label class="param-label">排序方式</label>
<select v-model="searchParams.sort_type" class="param-select">
<option :value="0">综合排序</option>
<option :value="1">最多点赞</option>
<option :value="2">最新发布</option>
<option value="0">综合排序</option>
<option value="1">最多点赞</option>
<option value="2">最新发布</option>
</select>
</div>
@@ -499,8 +501,13 @@ const handleImageError = (event) => {
</div>
<!-- 热点列表 -->
<div v-for="topic in hotTopics" :key="topic.id" @click="handleCreate(topic)" class="topic-item"
:class="{ 'topic-item--selected': selectedTopic === topic.id }">
<div
v-for="topic in hotTopics"
:key="topic.id"
@click="handleCreate(topic)"
class="topic-item"
:class="{ 'topic-item--selected': selectedTopic === topic.id }"
>
<div class="flex flex-col flex-1 min-w-0">
<!-- 封面和标题 -->
<div class="flex items-start gap-3 mb-2">
@@ -550,7 +557,10 @@ const handleImageError = (event) => {
</div>
</div>
<!-- 统计信息 -->
<div v-if="topic.diggCount || topic.playCount || topic.commentCount || topic.collectCount || topic.shareCount" class="flex flex-wrap items-center gap-4 text-xs text-gray-500 topic-stats">
<div
v-if="topic.diggCount || topic.playCount || topic.commentCount || topic.collectCount || topic.shareCount"
class="flex flex-wrap items-center gap-4 text-xs text-gray-500 topic-stats"
>
<span v-if="topic.playCount" class="stat-item">
<svg class="inline w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" />
@@ -599,7 +609,7 @@ const handleImageError = (event) => {
</div>
</section>
<!-- 右侧详细信息 -->
<!-- 右侧创作详情 -->
<section class="fc-right">
<div class="fc-title">创作详情</div>
@@ -607,14 +617,30 @@ const handleImageError = (event) => {
<!-- 热点标题 -->
<div>
<label class="form-label">热点标题</label>
<input v-model="topicDetails.title" type="text" placeholder="选择左侧热点或手动输入标题" class="form-input" />
<input
v-model="topicDetails.title"
type="text"
placeholder="选择左侧热点或手动输入标题"
class="form-input"
/>
</div>
<!-- 文案 -->
<div>
<label class="form-label">文案</label>
<textarea v-model="topicDetails.copywriting" rows="5" placeholder="输入或AI生成文案内容"
class="form-textarea"></textarea>
<div class="form-label-wrapper">
<label class="form-label">文案</label>
<span v-if="isAnalyzing" class="analyzing-indicator">
<a-spin size="small" />
<span class="analyzing-text">正在分析语音...</span>
</span>
</div>
<textarea
v-model="topicDetails.copywriting"
rows="5"
placeholder="输入或AI生成文案内容"
class="form-textarea"
:disabled="isAnalyzing"
></textarea>
</div>
<!-- 风格提示词 -->
@@ -626,7 +652,8 @@ const handleImageError = (event) => {
size="small"
type="link"
@click="showAllPromptsModal = true"
style="padding: 0; height: auto; font-size: 12px;">
style="padding: 0; height: auto; font-size: 12px;"
>
更多 ({{ allPrompts.length }})
</a-button>
</div>
@@ -639,7 +666,8 @@ const handleImageError = (event) => {
:key="prompt.id"
class="prompt-tag"
:class="{ 'prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
@click="selectPrompt(prompt)">
@click="selectPrompt(prompt)"
>
<span class="prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
@@ -660,16 +688,25 @@ const handleImageError = (event) => {
<!-- 生成文案按钮 -->
<div class="pt-2">
<GradientButton
text="生成文案"
icon="icon-sparkle"
<button
class="cyber-button"
:class="{
'cyber-button--disabled': !topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating,
'cyber-button--loading': isGenerating
}"
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating"
:loading="isGenerating"
loading-text="生成中..."
size="middle"
:block="true"
@click="handleGenerate"
/>
>
<span v-if="isGenerating" class="cyber-button__loading">
<span class="cyber-button__spinner"></span>
<span class="cyber-button__text">生成中...</span>
</span>
<span v-else class="cyber-button__content">
<span class="cyber-button__glow"></span>
<span class="cyber-button__text">生成爆款</span>
<span class="cyber-button__arrow"></span>
</span>
</button>
</div>
<!-- 生成的文案显示区域 -->
@@ -687,14 +724,16 @@ const handleImageError = (event) => {
v-model:open="showAllPromptsModal"
title="选择提示词风格"
:width="600"
:footer="null">
:footer="null"
>
<div class="prompt-modal-content">
<!-- 搜索框 -->
<a-input
v-model:value="promptSearchKeyword"
placeholder="搜索提示词..."
style="margin-bottom: 16px;"
allow-clear>
allow-clear
>
<template #prefix>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
@@ -709,7 +748,8 @@ const handleImageError = (event) => {
:key="prompt.id"
class="all-prompt-tag"
:class="{ 'all-prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
@click="selectPrompt(prompt)">
@click="selectPrompt(prompt)"
>
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
@@ -756,39 +796,6 @@ const handleImageError = (event) => {
margin-bottom: 8px;
}
/* 平台标签区域 */
.platform-tabs {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border);
}
.platform-tab {
flex: 1;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s;
}
.platform-tab--active {
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.platform-tab--inactive {
background: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.platform-tab--inactive:hover {
background: var(--color-bg);
color: var(--color-text);
opacity: 0.8;
}
/* 搜索框 */
.search-box {
margin-bottom: 12px;
@@ -796,7 +803,6 @@ const handleImageError = (event) => {
border-bottom: 1px solid var(--color-border);
}
/* 搜索参数配置 */
.search-params {
margin-top: 12px;
padding-top: 12px;
@@ -1079,7 +1085,6 @@ const handleImageError = (event) => {
vertical-align: middle;
}
/* 详情内容 */
.detail-content {
color: var(--color-text);
@@ -1123,7 +1128,6 @@ const handleImageError = (event) => {
resize: none;
}
/* 表单标签包装器 */
.form-label-wrapper {
display: flex;
justify-content: space-between;
@@ -1261,4 +1265,161 @@ const handleImageError = (event) => {
white-space: nowrap;
user-select: none;
}
/* 赛博朋克风格按钮 - 蓝色主题 */
.cyber-button {
position: relative;
width: 100%;
padding: 12px 24px;
font-size: 14px;
font-weight: 600;
color: #1890ff;
background: transparent;
border: 1px solid #1890ff;
border-radius: 0;
cursor: pointer;
overflow: hidden;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow:
0 0 10px rgba(24, 144, 255, 0.3),
inset 0 0 10px rgba(24, 144, 255, 0.1);
}
.cyber-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(24, 144, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.cyber-button:hover::before {
left: 100%;
}
.cyber-button:hover:not(.cyber-button--disabled) {
background: rgba(24, 144, 255, 0.1);
box-shadow:
0 0 20px rgba(24, 144, 255, 0.5),
0 0 40px rgba(24, 144, 255, 0.3),
inset 0 0 20px rgba(24, 144, 255, 0.2);
transform: translateY(-1px);
}
.cyber-button:active:not(.cyber-button--disabled) {
transform: translateY(0);
box-shadow:
0 0 15px rgba(24, 144, 255, 0.4),
inset 0 0 15px rgba(24, 144, 255, 0.15);
}
.cyber-button__content {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
z-index: 1;
}
.cyber-button__glow {
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(24, 144, 255, 0.4);
transform: translate(-50%, -50%);
transition: width 0.6s ease, height 0.6s ease;
pointer-events: none;
}
.cyber-button:hover:not(.cyber-button--disabled) .cyber-button__glow {
width: 200px;
height: 200px;
}
.cyber-button__text {
position: relative;
z-index: 2;
text-shadow: 0 0 10px rgba(24, 144, 255, 0.8);
}
.cyber-button__arrow {
position: relative;
z-index: 2;
transition: transform 0.3s ease;
font-size: 16px;
}
.cyber-button:hover:not(.cyber-button--disabled) .cyber-button__arrow {
transform: translateX(4px);
}
.cyber-button__loading {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
z-index: 1;
}
.cyber-button__spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(24, 144, 255, 0.3);
border-top-color: #1890ff;
border-radius: 50%;
animation: cyber-spin 0.8s linear infinite;
}
@keyframes cyber-spin {
to {
transform: rotate(360deg);
}
}
.cyber-button--disabled,
.cyber-button:disabled {
opacity: 0.4;
cursor: not-allowed;
border-color: rgba(24, 144, 255, 0.3);
box-shadow: none;
color: rgba(24, 144, 255, 0.5);
}
.cyber-button--disabled:hover,
.cyber-button:disabled:hover {
background: transparent;
box-shadow: none;
transform: none;
}
.cyber-button--loading {
cursor: wait;
}
.cyber-button--loading:hover {
transform: none;
}
/* 语音分析loading指示器 */
.analyzing-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--color-primary);
}
.analyzing-text {
color: var(--color-primary);
}
</style>