feat: 功能优化

This commit is contained in:
2025-11-14 02:15:14 +08:00
parent c652d0ddf3
commit 6624627802
10 changed files with 1224 additions and 223 deletions

View File

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