功能优化
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user