feat: 功能优化
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub'
|
||||
import { CommonService } from '@/api/common'
|
||||
import { UserPromptApi } from '@/api/userPrompt'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import GradientButton from '@/components/GradientButton.vue'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
@@ -18,36 +22,240 @@ defineOptions({
|
||||
// 搜索关键词
|
||||
const searchKeyword = 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:图集
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
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: '',
|
||||
stylePrompt: ''
|
||||
stylePrompt: '',
|
||||
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 displayPrompts = computed(() => {
|
||||
return allPrompts.value.slice(0, DISPLAY_COUNT)
|
||||
})
|
||||
|
||||
// 计算属性:过滤后的全部提示词(用于"更多"弹窗)
|
||||
const filteredPrompts = computed(() => {
|
||||
if (!promptSearchKeyword.value.trim()) {
|
||||
return allPrompts.value
|
||||
}
|
||||
const keyword = promptSearchKeyword.value.trim().toLowerCase()
|
||||
return allPrompts.value.filter(p =>
|
||||
p.name.toLowerCase().includes(keyword) ||
|
||||
(p.content && p.content.toLowerCase().includes(keyword))
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载用户提示词列表
|
||||
*/
|
||||
async function loadUserPrompts() {
|
||||
if (!userStore.userId) {
|
||||
console.warn('用户未登录,无法加载提示词')
|
||||
return
|
||||
}
|
||||
|
||||
loadingPrompts.value = true
|
||||
try {
|
||||
const response = await UserPromptApi.getUserPromptPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
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
|
||||
topicDetails.stylePrompt = firstPrompt.content || ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提示词失败:', error)
|
||||
message.error('加载提示词失败')
|
||||
} finally {
|
||||
loadingPrompts.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择提示词
|
||||
*/
|
||||
function selectPrompt(prompt) {
|
||||
if (!prompt || !prompt.content) {
|
||||
message.warning('提示词内容为空')
|
||||
return
|
||||
}
|
||||
|
||||
topicDetails.stylePromptId = prompt.id
|
||||
topicDetails.stylePrompt = prompt.content
|
||||
showAllPromptsModal.value = false
|
||||
}
|
||||
|
||||
// 点击创作按钮
|
||||
const handleCreate = (topic) => {
|
||||
selectedTopic.value = topic.id
|
||||
topicDetails.title = topic.title
|
||||
topicDetails.copywriting = ''
|
||||
topicDetails.stylePrompt = ''
|
||||
// 提取文案:使用标题作为初始文案
|
||||
topicDetails.copywriting = topic.title || ''
|
||||
// 保持已选择的风格提示词,如果没有则使用第一个
|
||||
if (!topicDetails.stylePromptId && allPrompts.value.length > 0) {
|
||||
const firstPrompt = allPrompts.value[0]
|
||||
topicDetails.stylePromptId = firstPrompt.id
|
||||
topicDetails.stylePrompt = firstPrompt.content || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 立即生成
|
||||
const handleGenerate = () => {
|
||||
console.log('生成内容', topicDetails)
|
||||
// TODO: 调用生成API
|
||||
// 生成文案(流式)
|
||||
async function handleGenerate() {
|
||||
if (!topicDetails.copywriting || !topicDetails.copywriting.trim()) {
|
||||
message.warning('请输入文案内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否选择了提示词风格
|
||||
if (!topicDetails.stylePrompt || !topicDetails.stylePrompt.trim()) {
|
||||
message.warning('请先选择提示词风格')
|
||||
return
|
||||
}
|
||||
|
||||
isGenerating.value = true
|
||||
generatedContent.value = '' // 清空之前的内容
|
||||
|
||||
try {
|
||||
// 调用 callWorkflow 流式 API
|
||||
const requestData = {
|
||||
audio_prompt: topicDetails.stylePrompt || '', // 音频提示词
|
||||
user_text: topicDetails.copywriting.trim(), // 用户输入内容
|
||||
amplitude: 50 // 幅度,默认50%
|
||||
}
|
||||
|
||||
const ctrl = new AbortController()
|
||||
let fullText = ''
|
||||
let errorOccurred = false
|
||||
let isResolved = false
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
// 设置超时
|
||||
const timeout = setTimeout(() => {
|
||||
if (!isResolved) {
|
||||
ctrl.abort()
|
||||
reject(new Error('请求超时,请稍后重试'))
|
||||
}
|
||||
}, 180000) // 3分钟超时
|
||||
|
||||
CommonService.callWorkflowStream({
|
||||
data: requestData,
|
||||
ctrl,
|
||||
onMessage: (event) => {
|
||||
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)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('解析流数据异常:', e)
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
clearTimeout(timeout)
|
||||
if (!isResolved) {
|
||||
errorOccurred = true
|
||||
ctrl.abort()
|
||||
const errorMsg = err?.message || '网络请求失败'
|
||||
console.error('SSE请求错误:', err)
|
||||
message.error(errorMsg)
|
||||
reject(new Error(errorMsg))
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
clearTimeout(timeout)
|
||||
if (!isResolved) {
|
||||
isResolved = true
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
generatedContent.value = fullText.trim()
|
||||
message.success('文案生成成功')
|
||||
} catch (error) {
|
||||
console.error('生成文案失败:', error)
|
||||
message.error('生成文案失败,请重试')
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化:加载提示词
|
||||
onMounted(async () => {
|
||||
// 等待用户信息加载
|
||||
if (userStore.userId) {
|
||||
await loadUserPrompts()
|
||||
} else if (userStore.isLoggedIn) {
|
||||
// 如果已登录但userId未加载,等待一下
|
||||
setTimeout(async () => {
|
||||
if (userStore.userId) {
|
||||
await loadUserPrompts()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 切换平台
|
||||
// const switchPlatform = (platformId) => {
|
||||
// activePlatform.value = platformId
|
||||
@@ -57,51 +265,103 @@ const handleGenerate = () => {
|
||||
// topicDetails.stylePrompt = ''
|
||||
// }
|
||||
|
||||
// 搜索热点
|
||||
/**
|
||||
* 处理搜索结果响应
|
||||
*/
|
||||
function processSearchResults(response, startId = 1) {
|
||||
try {
|
||||
// 提取 cursor(用于翻页)
|
||||
const cursor = response?.data?.cursor || null
|
||||
currentCursor.value = cursor
|
||||
|
||||
// 处理搜索结果
|
||||
const dataList = response?.data?.data || []
|
||||
const searchResults = dataList
|
||||
.map(el => el.aweme_info)
|
||||
.filter(el => el)
|
||||
.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]
|
||||
|| '',
|
||||
// 作者信息
|
||||
author: item.author?.nickname || item.author?.unique_id || '未知',
|
||||
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()) {
|
||||
message.warning('请输入搜索关键词')
|
||||
return
|
||||
}
|
||||
|
||||
// 重置参数
|
||||
searchParams.offset = 0
|
||||
searchParams.keyword = searchKeyword.value.trim()
|
||||
currentCursor.value = null
|
||||
hotTopics.value = []
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await TikhubService.postTikHup(
|
||||
{
|
||||
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH, // 使用网页端通用搜索结果接口
|
||||
methodType: MethodType.POST,
|
||||
urlParams: {
|
||||
keyword: searchKeyword.value.trim(),
|
||||
sort_type: '1',
|
||||
offset: 0,
|
||||
count: 20,
|
||||
publish_time: 7
|
||||
},
|
||||
paramType: ParamType.JSON
|
||||
}
|
||||
)
|
||||
// 处理搜索结果
|
||||
const searchResults = response.data.data.map(el => el.aweme_info).filter(el => el).map((item, index) => ({
|
||||
id: hotTopics.value.length + index + 1,
|
||||
title: item.desc || '无标题',
|
||||
videoId: item.aweme_id,
|
||||
videoUrl: `https://www.douyin.com/video/${item.aweme_id}`, // 视频链接
|
||||
author: item.author.nickname,
|
||||
// 统计数据
|
||||
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, // 收藏数
|
||||
}))
|
||||
// 构建请求参数(POST JSON 格式)
|
||||
// 注意:API 要求 sort_type、publish_time、content_type 为字符串类型
|
||||
const urlParams = {
|
||||
keyword: 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, // 已经是字符串
|
||||
content_type: String(searchParams.content_type),
|
||||
}
|
||||
|
||||
// 将搜索结果添加到列表顶部
|
||||
hotTopics.value = [...searchResults, ...hotTopics.value]
|
||||
const response = await TikhubService.postTikHup({
|
||||
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH,
|
||||
methodType: MethodType.POST,
|
||||
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 || '搜索失败,请稍后重试')
|
||||
hotTopics.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
@@ -137,6 +397,11 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
if (title.length <= maxLength) return title
|
||||
return title.substring(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
// 处理图片加载错误
|
||||
const handleImageError = (event) => {
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -166,20 +431,58 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
<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"
|
||||
:class="{ 'search-btn--loading': isLoading }">
|
||||
<svg v-if="!isLoading" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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" />
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索参数配置 -->
|
||||
<div class="search-params">
|
||||
<div class="param-row">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="param-item">
|
||||
<label class="param-label">发布时间</label>
|
||||
<select v-model="searchParams.publish_time" class="param-select">
|
||||
<option value="0">不限</option>
|
||||
<option value="1">最近一天</option>
|
||||
<option value="7">最近一周</option>
|
||||
<option value="180">最近半年</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="param-item">
|
||||
<label class="param-label">视频时长</label>
|
||||
<select v-model="searchParams.filter_duration" class="param-select">
|
||||
<option value="0">不限</option>
|
||||
<option value="0-1">1分钟以内</option>
|
||||
<option value="1-5">1-5分钟</option>
|
||||
<option value="5-10000">5分钟以上</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-row">
|
||||
<div class="param-item">
|
||||
<label class="param-label">内容类型</label>
|
||||
<select v-model="searchParams.content_type" class="param-select">
|
||||
<option value="0">不限</option>
|
||||
<option value="1">视频</option>
|
||||
<option value="2">图集</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热点列表 -->
|
||||
@@ -199,19 +502,52 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
<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-center mb-2">
|
||||
<span class="topic-number">{{ topic.id }}</span>
|
||||
<span
|
||||
v-if="topic.videoUrl"
|
||||
@click="openVideo(topic, $event)"
|
||||
class="flex-1 topic-title topic-title--clickable"
|
||||
:title="topic.title"
|
||||
>
|
||||
{{ truncateTitle(topic.title) }}
|
||||
</span>
|
||||
<span v-else class="flex-1 topic-title" :title="topic.title">
|
||||
{{ truncateTitle(topic.title) }}
|
||||
</span>
|
||||
<!-- 封面和标题 -->
|
||||
<div class="flex items-start gap-3 mb-2">
|
||||
<!-- 封面图片 -->
|
||||
<div class="topic-cover-wrapper">
|
||||
<img
|
||||
v-if="topic.cover"
|
||||
:src="topic.cover"
|
||||
alt="封面"
|
||||
class="topic-cover"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-else class="topic-cover-placeholder">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标题和作者 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center mb-1">
|
||||
<span class="topic-number">{{ topic.id }}</span>
|
||||
<span
|
||||
v-if="topic.videoUrl"
|
||||
@click="openVideo(topic, $event)"
|
||||
class="flex-1 topic-title topic-title--clickable"
|
||||
:title="topic.title"
|
||||
>
|
||||
{{ truncateTitle(topic.title) }}
|
||||
</span>
|
||||
<span v-else class="flex-1 topic-title" :title="topic.title">
|
||||
{{ truncateTitle(topic.title) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 作者信息 -->
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500 topic-author">
|
||||
<img
|
||||
v-if="topic.authorAvatar"
|
||||
:src="topic.authorAvatar"
|
||||
alt="作者头像"
|
||||
class="author-avatar"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<span>{{ topic.author }}</span>
|
||||
</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">
|
||||
@@ -247,11 +583,19 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click.stop="handleCreate(topic)" class="create-btn">
|
||||
创作
|
||||
</button>
|
||||
<GradientButton
|
||||
text="创作"
|
||||
size="small"
|
||||
@click.stop="handleCreate(topic)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading 指示器 -->
|
||||
<div v-if="isLoading" class="loading-indicator">
|
||||
<a-spin size="small" />
|
||||
<span class="loading-text">搜索中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -275,23 +619,107 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
|
||||
<!-- 风格提示词 -->
|
||||
<div>
|
||||
<label class="form-label">风格提示词</label>
|
||||
<input v-model="topicDetails.stylePrompt" type="text" placeholder="例如:专业、权威、温暖、幽默等" class="form-input" />
|
||||
<div class="form-label-wrapper">
|
||||
<label class="form-label">风格提示词</label>
|
||||
<a-button
|
||||
v-if="allPrompts.length > DISPLAY_COUNT"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="showAllPromptsModal = true"
|
||||
style="padding: 0; height: auto; font-size: 12px;">
|
||||
更多 ({{ allPrompts.length }})
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 提示词标签展示区域 -->
|
||||
<div v-if="displayPrompts.length > 0" class="prompt-tags-container">
|
||||
<div class="prompt-tags-grid">
|
||||
<div
|
||||
v-for="prompt in displayPrompts"
|
||||
:key="prompt.id"
|
||||
class="prompt-tag"
|
||||
:class="{ 'prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
|
||||
@click="selectPrompt(prompt)">
|
||||
<span class="prompt-tag-name">{{ prompt.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="!loadingPrompts" class="prompt-empty">
|
||||
<div style="color: var(--color-text-secondary); font-size: 12px; text-align: center; padding: 20px;">
|
||||
您可以在视频分析页面保存风格
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-else class="prompt-loading">
|
||||
<a-spin size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 立即生成按钮 -->
|
||||
<!-- 生成文案按钮 -->
|
||||
<div class="pt-2">
|
||||
<button @click="handleGenerate" class="generate-btn">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
立即生成
|
||||
</button>
|
||||
<GradientButton
|
||||
text="生成文案"
|
||||
icon="icon-sparkle"
|
||||
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.stylePromptId || isGenerating"
|
||||
:loading="isGenerating"
|
||||
loading-text="生成中..."
|
||||
size="middle"
|
||||
:block="true"
|
||||
@click="handleGenerate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 生成的文案显示区域 -->
|
||||
<div v-if="generatedContent" class="generated-content-section">
|
||||
<div class="form-label">生成结果</div>
|
||||
<div class="generated-content-wrapper">
|
||||
<div class="generated-content-text">{{ generatedContent }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 更多提示词弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showAllPromptsModal"
|
||||
title="选择提示词风格"
|
||||
:width="600"
|
||||
:footer="null">
|
||||
<div class="prompt-modal-content">
|
||||
<!-- 搜索框 -->
|
||||
<a-input
|
||||
v-model:value="promptSearchKeyword"
|
||||
placeholder="搜索提示词..."
|
||||
style="margin-bottom: 16px;"
|
||||
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>
|
||||
</svg>
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<!-- 提示词列表 -->
|
||||
<div v-if="filteredPrompts.length > 0" class="all-prompts-grid">
|
||||
<div
|
||||
v-for="prompt in filteredPrompts"
|
||||
:key="prompt.id"
|
||||
class="all-prompt-tag"
|
||||
:class="{ 'all-prompt-tag-selected': topicDetails.stylePromptId === prompt.id }"
|
||||
@click="selectPrompt(prompt)">
|
||||
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else style="text-align: center; padding: 40px; color: var(--color-text-secondary);">
|
||||
没有找到匹配的提示词
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -368,6 +796,52 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 搜索参数配置 */
|
||||
.search-params {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.param-item {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.param-select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.param-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.param-select:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -426,8 +900,18 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.search-btn--loading {
|
||||
opacity: 0.8;
|
||||
/* Loading 指示器 */
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 热点列表 */
|
||||
@@ -536,6 +1020,47 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 封面图片 */
|
||||
.topic-cover-wrapper {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
height: 45px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.topic-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.topic-cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* 作者信息 */
|
||||
.topic-author {
|
||||
padding-left: 32px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.topic-stats {
|
||||
padding-left: 32px;
|
||||
@@ -554,28 +1079,6 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 创作按钮 */
|
||||
.create-btn {
|
||||
margin-left: auto;
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
background: var(--color-primary);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 176, 48, 0.2);
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
box-shadow: 0 0 12px rgba(0, 176, 48, 0.5);
|
||||
filter: brightness(1.1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 详情内容 */
|
||||
.detail-content {
|
||||
@@ -620,23 +1123,142 @@ const truncateTitle = (title, maxLength = 30) => {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
width: 100%;
|
||||
padding: 8px 24px;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 176, 48, 0.3);
|
||||
transition: all 0.2s;
|
||||
/* 表单标签包装器 */
|
||||
.form-label-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.generate-btn:hover {
|
||||
box-shadow: 0 0 12px rgba(0, 176, 48, 0.4);
|
||||
filter: brightness(1.05);
|
||||
transform: scale(1.01);
|
||||
/* 提示词标签样式 */
|
||||
.prompt-tags-container {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.prompt-tags-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.prompt-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.prompt-tag:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.prompt-tag-selected {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.prompt-tag-selected:hover {
|
||||
background: var(--color-primary);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.prompt-tag-name {
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.prompt-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.prompt-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 生成结果区域 */
|
||||
.generated-content-section {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.generated-content-wrapper {
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.generated-content-text {
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 更多提示词弹窗样式 */
|
||||
.prompt-modal-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.all-prompts-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.all-prompt-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.all-prompt-tag:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.all-prompt-tag-selected {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.all-prompt-tag-selected:hover {
|
||||
background: var(--color-primary);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.all-prompt-tag-name {
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user