功能优化
This commit is contained in:
@@ -6,48 +6,32 @@ import { CommonService } from '@/api/common'
|
|||||||
import { UserPromptApi } from '@/api/userPrompt'
|
import { UserPromptApi } from '@/api/userPrompt'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import GradientButton from '@/components/GradientButton.vue'
|
import GradientButton from '@/components/GradientButton.vue'
|
||||||
|
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
|
||||||
|
|
||||||
// 定义组件名称
|
defineOptions({ name: 'ForecastView' })
|
||||||
defineOptions({
|
|
||||||
name: 'ForecastView'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 平台列表
|
// 状态管理
|
||||||
// const platforms = [
|
const userStore = useUserStore()
|
||||||
// { id: 'douyin', name: '抖音', color: '#FE2C55' }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
// const activePlatform = ref('douyin')
|
|
||||||
|
|
||||||
// 搜索关键词
|
|
||||||
const searchKeyword = ref('')
|
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({
|
const searchParams = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
offset: 0, // 偏移量,第一次请求时为0,后续从返回数据中的 cursor 获取
|
offset: 0,
|
||||||
sort_type: 1, // 0:综合排序 1:最多点赞 2:最新发布
|
sort_type: 1,
|
||||||
publish_time: 7, // 0:不限 1:最近一天 7:最近一周 180:最近半年
|
publish_time: 7,
|
||||||
filter_duration: '0', // '0':不限 '0-1':1分钟以内 '1-5':1-5分钟 '5-10000':5分钟以上
|
filter_duration: '0',
|
||||||
content_type: 0 // 0:不限 1:视频 2:图集
|
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({
|
const topicDetails = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
copywriting: '',
|
copywriting: '',
|
||||||
@@ -55,29 +39,18 @@ const topicDetails = reactive({
|
|||||||
stylePromptId: null
|
stylePromptId: null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 生成的文案内容
|
// 提示词相关
|
||||||
const generatedContent = ref('')
|
|
||||||
|
|
||||||
// 生成文案的loading状态
|
|
||||||
const isGenerating = ref(false)
|
|
||||||
|
|
||||||
// 提示词相关状态
|
|
||||||
const allPrompts = ref([])
|
const allPrompts = ref([])
|
||||||
const loadingPrompts = ref(false)
|
const loadingPrompts = ref(false)
|
||||||
const showAllPromptsModal = ref(false)
|
const showAllPromptsModal = ref(false)
|
||||||
const promptSearchKeyword = ref('')
|
const promptSearchKeyword = ref('')
|
||||||
const DISPLAY_COUNT = 6 // 展示的提示词数量
|
const DISPLAY_COUNT = 6
|
||||||
|
|
||||||
// 计算属性:展示的部分提示词
|
// 计算属性
|
||||||
const displayPrompts = computed(() => {
|
const displayPrompts = computed(() => allPrompts.value.slice(0, DISPLAY_COUNT))
|
||||||
return allPrompts.value.slice(0, DISPLAY_COUNT)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:过滤后的全部提示词(用于"更多"弹窗)
|
|
||||||
const filteredPrompts = computed(() => {
|
const filteredPrompts = computed(() => {
|
||||||
if (!promptSearchKeyword.value.trim()) {
|
if (!promptSearchKeyword.value.trim()) return allPrompts.value
|
||||||
return allPrompts.value
|
|
||||||
}
|
|
||||||
const keyword = promptSearchKeyword.value.trim().toLowerCase()
|
const keyword = promptSearchKeyword.value.trim().toLowerCase()
|
||||||
return allPrompts.value.filter(p =>
|
return allPrompts.value.filter(p =>
|
||||||
p.name.toLowerCase().includes(keyword) ||
|
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() {
|
async function loadUserPrompts() {
|
||||||
if (!userStore.userId) {
|
if (!userStore.userId) {
|
||||||
console.warn('用户未登录,无法加载提示词')
|
console.warn('用户未登录,无法加载提示词')
|
||||||
@@ -99,12 +96,12 @@ async function loadUserPrompts() {
|
|||||||
const response = await UserPromptApi.getUserPromptPage({
|
const response = await UserPromptApi.getUserPromptPage({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
status: 1 // 只获取启用状态的提示词
|
status: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response?.data?.list) {
|
if (response?.data?.list) {
|
||||||
allPrompts.value = response.data.list
|
allPrompts.value = response.data.list
|
||||||
// 如果没有选中的提示词,自动选中第一个
|
// 自动选中第一个提示词
|
||||||
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
|
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
|
||||||
const firstPrompt = allPrompts.value[0]
|
const firstPrompt = allPrompts.value[0]
|
||||||
topicDetails.stylePromptId = firstPrompt.id
|
topicDetails.stylePromptId = firstPrompt.id
|
||||||
@@ -119,56 +116,87 @@ async function loadUserPrompts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 选择提示词
|
|
||||||
*/
|
|
||||||
function selectPrompt(prompt) {
|
function selectPrompt(prompt) {
|
||||||
if (!prompt || !prompt.content) {
|
if (!prompt?.content) {
|
||||||
message.warning('提示词内容为空')
|
message.warning('提示词内容为空')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
topicDetails.stylePromptId = prompt.id
|
topicDetails.stylePromptId = prompt.id
|
||||||
topicDetails.stylePrompt = prompt.content
|
topicDetails.stylePrompt = prompt.content
|
||||||
showAllPromptsModal.value = false
|
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
|
selectedTopic.value = topic.id
|
||||||
topicDetails.title = topic.title
|
topicDetails.title = topic.title
|
||||||
// 提取文案:使用标题作为初始文案
|
|
||||||
topicDetails.copywriting = topic.title || ''
|
// 自动选中第一个提示词(如果还没有选中)
|
||||||
// 保持已选择的风格提示词,如果没有则使用第一个
|
|
||||||
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
|
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
|
||||||
const firstPrompt = allPrompts.value[0]
|
const firstPrompt = allPrompts.value[0]
|
||||||
topicDetails.stylePromptId = firstPrompt.id
|
topicDetails.stylePromptId = firstPrompt.id
|
||||||
topicDetails.stylePrompt = firstPrompt.content || ''
|
topicDetails.stylePrompt = firstPrompt.content || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动分析语音
|
||||||
|
if (topic.audioUrl?.trim()) {
|
||||||
|
await analyzeVoice(topic.audioUrl)
|
||||||
|
} else {
|
||||||
|
message.info('该视频暂无音频链接,无法自动提取语音文案')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成文案(流式)
|
// 生成文案(流式)
|
||||||
async function handleGenerate() {
|
async function handleGenerate() {
|
||||||
if (!topicDetails.copywriting || !topicDetails.copywriting.trim()) {
|
if (!topicDetails.copywriting?.trim()) {
|
||||||
message.warning('请输入文案内容')
|
message.warning('请输入文案内容')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否选择了提示词风格
|
if (!topicDetails.stylePrompt?.trim()) {
|
||||||
if (!topicDetails.stylePrompt || !topicDetails.stylePrompt.trim()) {
|
|
||||||
message.warning('请先选择提示词风格')
|
message.warning('请先选择提示词风格')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isGenerating.value = true
|
isGenerating.value = true
|
||||||
generatedContent.value = '' // 清空之前的内容
|
generatedContent.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用 callWorkflow 流式 API
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
audio_prompt: topicDetails.stylePrompt || '', // 音频提示词
|
audio_prompt: topicDetails.stylePrompt,
|
||||||
user_text: topicDetails.copywriting.trim(), // 用户输入内容
|
user_text: topicDetails.copywriting.trim(),
|
||||||
amplitude: 50 // 幅度,默认50%
|
amplitude: 50
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctrl = new AbortController()
|
const ctrl = new AbortController()
|
||||||
@@ -177,43 +205,72 @@ async function handleGenerate() {
|
|||||||
let isResolved = false
|
let isResolved = false
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
// 设置超时
|
let timeout = null
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
if (!isResolved) {
|
if (!isResolved) {
|
||||||
|
cleanup()
|
||||||
ctrl.abort()
|
ctrl.abort()
|
||||||
reject(new Error('请求超时,请稍后重试'))
|
reject(new Error('请求超时,请稍后重试'))
|
||||||
}
|
}
|
||||||
}, 180000) // 3分钟超时
|
}, 180000)
|
||||||
|
|
||||||
CommonService.callWorkflowStream({
|
CommonService.callWorkflowStream({
|
||||||
data: requestData,
|
data: requestData,
|
||||||
ctrl,
|
ctrl,
|
||||||
onMessage: (event) => {
|
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 {
|
try {
|
||||||
if (errorOccurred) return
|
const obj = JSON.parse(event.data)
|
||||||
|
const piece = obj?.text || obj?.content || obj?.data || ''
|
||||||
const dataStr = event?.data || ''
|
if (piece) {
|
||||||
if (!dataStr) return
|
fullText += piece
|
||||||
|
generatedContent.value = fullText
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('解析流数据异常:', e)
|
// 如果不是JSON,可能是纯文本
|
||||||
|
if (event.data && !event.data.startsWith('[')) {
|
||||||
|
fullText += event.data
|
||||||
|
generatedContent.value = fullText
|
||||||
|
} else {
|
||||||
|
console.warn('解析流数据异常:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
clearTimeout(timeout)
|
cleanup()
|
||||||
if (!isResolved) {
|
if (!isResolved) {
|
||||||
errorOccurred = true
|
errorOccurred = true
|
||||||
|
isResolved = true
|
||||||
ctrl.abort()
|
ctrl.abort()
|
||||||
const errorMsg = err?.message || '网络请求失败'
|
const errorMsg = err?.message || '网络请求失败'
|
||||||
console.error('SSE请求错误:', err)
|
console.error('SSE请求错误:', err)
|
||||||
@@ -222,7 +279,7 @@ async function handleGenerate() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
clearTimeout(timeout)
|
cleanup()
|
||||||
if (!isResolved) {
|
if (!isResolved) {
|
||||||
isResolved = true
|
isResolved = true
|
||||||
resolve()
|
resolve()
|
||||||
@@ -241,123 +298,98 @@ async function handleGenerate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化:加载提示词
|
// 数据处理
|
||||||
onMounted(async () => {
|
function extractAudioUrl(video) {
|
||||||
// 等待用户信息加载
|
const urlList = video?.play_addr?.url_list
|
||||||
if (userStore.userId) {
|
if (Array.isArray(urlList) && urlList.length > 0) {
|
||||||
await loadUserPrompts()
|
// 优先使用最后一个,如果为空则使用第一个
|
||||||
} else if (userStore.isLoggedIn) {
|
const lastUrl = urlList[urlList.length - 1]
|
||||||
// 如果已登录但userId未加载,等待一下
|
const firstUrl = urlList[0]
|
||||||
setTimeout(async () => {
|
return (lastUrl && lastUrl.trim()) || (firstUrl && firstUrl.trim()) || ''
|
||||||
if (userStore.userId) {
|
|
||||||
await loadUserPrompts()
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
})
|
return (video?.play_addr?.url && video.play_addr.url.trim()) || ''
|
||||||
|
}
|
||||||
|
|
||||||
// 切换平台
|
function extractCover(video) {
|
||||||
// const switchPlatform = (platformId) => {
|
return video?.origin_cover?.url_list?.[0]
|
||||||
// activePlatform.value = platformId
|
|| video?.cover?.url_list?.[0]
|
||||||
// selectedTopic.value = null
|
|| video?.dynamic_cover?.url_list?.[0]
|
||||||
// topicDetails.title = ''
|
|| video?.animated_cover?.url_list?.[0]
|
||||||
// topicDetails.copywriting = ''
|
|| ''
|
||||||
// topicDetails.stylePrompt = ''
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理搜索结果响应
|
|
||||||
*/
|
|
||||||
function processSearchResults(response, startId = 1) {
|
function processSearchResults(response, startId = 1) {
|
||||||
try {
|
try {
|
||||||
// 提取 cursor(用于翻页)
|
currentCursor.value = response?.data?.cursor || null
|
||||||
const cursor = response?.data?.cursor || null
|
|
||||||
currentCursor.value = cursor
|
|
||||||
|
|
||||||
// 处理搜索结果
|
|
||||||
const dataList = response?.data?.data || []
|
const dataList = response?.data?.data || []
|
||||||
const searchResults = dataList
|
|
||||||
|
return dataList
|
||||||
.map(el => el.aweme_info)
|
.map(el => el.aweme_info)
|
||||||
.filter(el => el)
|
.filter(Boolean)
|
||||||
.map((item, index) => ({
|
.map((item, index) => ({
|
||||||
id: startId + index,
|
id: startId + index,
|
||||||
title: item.desc || '无标题',
|
title: item.desc || '无标题',
|
||||||
videoId: item.aweme_id,
|
videoId: item.aweme_id,
|
||||||
videoUrl: `https://www.douyin.com/video/${item.aweme_id}`,
|
videoUrl: `https://www.douyin.com/video/${item.aweme_id}`,
|
||||||
// 封面图片:优先使用 origin_cover,其次 cover,再次 dynamic_cover,最后 animated_cover
|
audioUrl: extractAudioUrl(item.video),
|
||||||
cover: item?.video?.origin_cover?.url_list?.[0]
|
cover: extractCover(item.video) || item?.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]
|
|
||||||
|| '',
|
|
||||||
// 作者信息
|
|
||||||
author: item.author?.nickname || item.author?.unique_id || '未知',
|
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,
|
playCount: item.statistics?.play_count || 0,
|
||||||
diggCount: item.statistics?.digg_count || 0,
|
diggCount: item.statistics?.digg_count || 0,
|
||||||
commentCount: item.statistics?.comment_count || 0,
|
commentCount: item.statistics?.comment_count || 0,
|
||||||
shareCount: item.statistics?.share_count || 0,
|
shareCount: item.statistics?.share_count || 0,
|
||||||
collectCount: item.statistics?.collect_count || 0,
|
collectCount: item.statistics?.collect_count || 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return searchResults
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理搜索结果失败:', error)
|
console.error('处理搜索结果失败:', error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 搜索功能
|
||||||
* 搜索热点
|
async function handleSearch() {
|
||||||
*/
|
const keyword = searchKeyword.value.trim()
|
||||||
const handleSearch = async () => {
|
if (!keyword) {
|
||||||
if (!searchKeyword.value.trim()) {
|
|
||||||
message.warning('请输入搜索关键词')
|
message.warning('请输入搜索关键词')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置参数
|
// 重置状态
|
||||||
searchParams.offset = 0
|
searchParams.offset = 0
|
||||||
searchParams.keyword = searchKeyword.value.trim()
|
searchParams.keyword = keyword
|
||||||
currentCursor.value = null
|
currentCursor.value = null
|
||||||
hotTopics.value = []
|
hotTopics.value = []
|
||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 构建请求参数(POST JSON 格式)
|
|
||||||
// 注意:API 要求 sort_type、publish_time、content_type 为字符串类型
|
|
||||||
const urlParams = {
|
const urlParams = {
|
||||||
keyword: searchParams.keyword,
|
keyword: encodeURIComponent(searchParams.keyword),
|
||||||
offset: String(searchParams.offset),
|
offset: String(searchParams.offset),
|
||||||
count: '20',
|
|
||||||
sort_type: String(searchParams.sort_type),
|
sort_type: String(searchParams.sort_type),
|
||||||
publish_time: String(searchParams.publish_time),
|
publish_time: String(searchParams.publish_time),
|
||||||
filter_duration: searchParams.filter_duration, // 已经是字符串
|
filter_duration: searchParams.filter_duration,
|
||||||
content_type: String(searchParams.content_type),
|
content_type: String(searchParams.content_type),
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await TikhubService.postTikHup({
|
const response = await TikhubService.postTikHup({
|
||||||
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH,
|
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH,
|
||||||
methodType: MethodType.POST,
|
methodType: MethodType.POST,
|
||||||
urlParams: urlParams,
|
urlParams,
|
||||||
paramType: ParamType.JSON
|
paramType: ParamType.JSON
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理搜索结果
|
|
||||||
const searchResults = processSearchResults(response)
|
const searchResults = processSearchResults(response)
|
||||||
|
|
||||||
if (searchResults.length === 0) {
|
if (searchResults.length === 0) {
|
||||||
message.warning('未找到相关结果')
|
message.warning('未找到相关结果')
|
||||||
hotTopics.value = []
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 替换列表
|
|
||||||
hotTopics.value = searchResults
|
hotTopics.value = searchResults
|
||||||
message.success(`找到 ${searchResults.length} 个结果`)
|
message.success(`找到 ${searchResults.length} 个结果`)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索失败:', error)
|
console.error('搜索失败:', error)
|
||||||
message.error(error?.message || '搜索失败,请稍后重试')
|
message.error(error?.message || '搜索失败,请稍后重试')
|
||||||
@@ -367,71 +399,41 @@ const handleSearch = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回车搜索
|
// 初始化
|
||||||
const handleSearchKeypress = (event) => {
|
onMounted(async () => {
|
||||||
if (event.key === 'Enter' && !isLoading.value) {
|
if (userStore.userId) {
|
||||||
handleSearch()
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fc-page">
|
<div class="fc-page">
|
||||||
<div class="fc-grid">
|
<div class="fc-grid">
|
||||||
<!-- 左侧:平台栏目和热点列表 -->
|
<!-- 左侧:热点列表 -->
|
||||||
<section class="fc-left">
|
<section class="fc-left">
|
||||||
<div class="fc-title">热点预测</div>
|
<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-box">
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<input v-model="searchKeyword" type="text" placeholder="输入关键词搜索抖音热点..." class="search-input"
|
<input
|
||||||
:disabled="isLoading" @keypress="handleSearchKeypress" />
|
v-model="searchKeyword"
|
||||||
<button @click="handleSearch" :disabled="isLoading || !searchKeyword.trim()" class="search-btn">
|
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">
|
<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"
|
<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" />
|
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">
|
<div class="param-item">
|
||||||
<label class="param-label">排序方式</label>
|
<label class="param-label">排序方式</label>
|
||||||
<select v-model="searchParams.sort_type" class="param-select">
|
<select v-model="searchParams.sort_type" class="param-select">
|
||||||
<option :value="0">综合排序</option>
|
<option value="0">综合排序</option>
|
||||||
<option :value="1">最多点赞</option>
|
<option value="1">最多点赞</option>
|
||||||
<option :value="2">最新发布</option>
|
<option value="2">最新发布</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -499,8 +501,13 @@ const handleImageError = (event) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 热点列表 -->
|
<!-- 热点列表 -->
|
||||||
<div v-for="topic in hotTopics" :key="topic.id" @click="handleCreate(topic)" class="topic-item"
|
<div
|
||||||
:class="{ 'topic-item--selected': selectedTopic === topic.id }">
|
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 flex-col flex-1 min-w-0">
|
||||||
<!-- 封面和标题 -->
|
<!-- 封面和标题 -->
|
||||||
<div class="flex items-start gap-3 mb-2">
|
<div class="flex items-start gap-3 mb-2">
|
||||||
@@ -550,7 +557,10 @@ const handleImageError = (event) => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<span v-if="topic.playCount" class="stat-item">
|
||||||
<svg class="inline w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
<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" />
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 右侧:详细信息 -->
|
<!-- 右侧:创作详情 -->
|
||||||
<section class="fc-right">
|
<section class="fc-right">
|
||||||
<div class="fc-title">创作详情</div>
|
<div class="fc-title">创作详情</div>
|
||||||
|
|
||||||
@@ -607,14 +617,30 @@ const handleImageError = (event) => {
|
|||||||
<!-- 热点标题 -->
|
<!-- 热点标题 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label">热点标题</label>
|
<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>
|
||||||
|
|
||||||
<!-- 文案 -->
|
<!-- 文案 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label">文案</label>
|
<div class="form-label-wrapper">
|
||||||
<textarea v-model="topicDetails.copywriting" rows="5" placeholder="输入或AI生成文案内容"
|
<label class="form-label">文案</label>
|
||||||
class="form-textarea"></textarea>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 风格提示词 -->
|
<!-- 风格提示词 -->
|
||||||
@@ -626,7 +652,8 @@ const handleImageError = (event) => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
@click="showAllPromptsModal = true"
|
@click="showAllPromptsModal = true"
|
||||||
style="padding: 0; height: auto; font-size: 12px;">
|
style="padding: 0; height: auto; font-size: 12px;"
|
||||||
|
>
|
||||||
更多 ({{ allPrompts.length }})
|
更多 ({{ allPrompts.length }})
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -639,7 +666,8 @@ const handleImageError = (event) => {
|
|||||||
:key="prompt.id"
|
:key="prompt.id"
|
||||||
class="prompt-tag"
|
class="prompt-tag"
|
||||||
:class="{ 'prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
|
:class="{ 'prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
|
||||||
@click="selectPrompt(prompt)">
|
@click="selectPrompt(prompt)"
|
||||||
|
>
|
||||||
<span class="prompt-tag-name">{{ prompt.name }}</span>
|
<span class="prompt-tag-name">{{ prompt.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -660,16 +688,25 @@ const handleImageError = (event) => {
|
|||||||
|
|
||||||
<!-- 生成文案按钮 -->
|
<!-- 生成文案按钮 -->
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<GradientButton
|
<button
|
||||||
text="生成文案"
|
class="cyber-button"
|
||||||
icon="icon-sparkle"
|
:class="{
|
||||||
|
'cyber-button--disabled': !topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating,
|
||||||
|
'cyber-button--loading': isGenerating
|
||||||
|
}"
|
||||||
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating"
|
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating"
|
||||||
:loading="isGenerating"
|
|
||||||
loading-text="生成中..."
|
|
||||||
size="middle"
|
|
||||||
:block="true"
|
|
||||||
@click="handleGenerate"
|
@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>
|
</div>
|
||||||
|
|
||||||
<!-- 生成的文案显示区域 -->
|
<!-- 生成的文案显示区域 -->
|
||||||
@@ -687,14 +724,16 @@ const handleImageError = (event) => {
|
|||||||
v-model:open="showAllPromptsModal"
|
v-model:open="showAllPromptsModal"
|
||||||
title="选择提示词风格"
|
title="选择提示词风格"
|
||||||
:width="600"
|
:width="600"
|
||||||
:footer="null">
|
:footer="null"
|
||||||
|
>
|
||||||
<div class="prompt-modal-content">
|
<div class="prompt-modal-content">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="promptSearchKeyword"
|
v-model:value="promptSearchKeyword"
|
||||||
placeholder="搜索提示词..."
|
placeholder="搜索提示词..."
|
||||||
style="margin-bottom: 16px;"
|
style="margin-bottom: 16px;"
|
||||||
allow-clear>
|
allow-clear
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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>
|
<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"
|
:key="prompt.id"
|
||||||
class="all-prompt-tag"
|
class="all-prompt-tag"
|
||||||
:class="{ 'all-prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
|
:class="{ 'all-prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
|
||||||
@click="selectPrompt(prompt)">
|
@click="selectPrompt(prompt)"
|
||||||
|
>
|
||||||
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
|
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -756,39 +796,6 @@ const handleImageError = (event) => {
|
|||||||
margin-bottom: 8px;
|
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 {
|
.search-box {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -796,7 +803,6 @@ const handleImageError = (event) => {
|
|||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索参数配置 */
|
|
||||||
.search-params {
|
.search-params {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
@@ -1079,7 +1085,6 @@ const handleImageError = (event) => {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 详情内容 */
|
/* 详情内容 */
|
||||||
.detail-content {
|
.detail-content {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@@ -1123,7 +1128,6 @@ const handleImageError = (event) => {
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单标签包装器 */
|
|
||||||
.form-label-wrapper {
|
.form-label-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -1261,4 +1265,161 @@ const handleImageError = (event) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: none;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user