功能优化

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 { 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>