Files
sionrui/frontend/app/web-gold/src/views/trends/Forecast.vue
2026-02-25 16:28:31 +08:00

1288 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { onMounted, reactive, ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import TikhubService, { InterfaceType, MethodType, ParamType } from '@/api/tikhub'
import { rewriteStream } from '@/api/forecast'
import { getAgentList } from '@/api/agent'
import { useUserStore } from '@/stores/user'
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
import { copyToClipboard } from '@/utils/clipboard'
defineOptions({ name: 'ForecastView' })
// 状态管理
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: '',
cursor: 0,
sort_type: '0',
publish_time: '0',
filter_duration: '0',
content_type: '0',
search_id: '',
backtrace: ''
})
// 创作详情
const topicDetails = reactive({
copywriting: '',
selectedAgentId: null,
modelType: 'forecast_standard',
level: 50
})
// 智能体列表
const agentList = ref([])
const loadingAgents = ref(false)
const agentSearchKeyword = ref('')
// 当前选中的智能体
const selectedAgent = ref(null)
// 过滤后的智能体列表
const filteredAgentList = computed(() => {
if (!agentSearchKeyword.value.trim()) {
return agentList.value
}
const keyword = agentSearchKeyword.value.trim().toLowerCase()
return agentList.value.filter(agent =>
agent.name.toLowerCase().includes(keyword) ||
(agent.description && agent.description.toLowerCase().includes(keyword))
)
})
// 工具函数
function formatNumber(num) {
if (!num) return '0'
return num >= 10000 ? `${(num / 10000).toFixed(1)}w` : String(num)
}
function truncateTitle(title, maxLength = 28) {
if (!title) return ''
return title.length <= maxLength ? title : `${title.substring(0, maxLength)}...`
}
function handleImageError(event) {
event.target.style.display = 'none'
}
function openVideo(topic, event) {
event.stopPropagation()
if (topic.videoUrl) window.open(topic.videoUrl, '_blank')
}
function handleSearchKeypress(event) {
if (event.key === 'Enter' && !isLoading.value) {
handleSearch()
}
}
async function copyContent() {
const success = await copyToClipboard(generatedContent.value)
if (success) {
message.success('已复制')
} else {
message.error('复制失败')
}
}
// 加载智能体列表
async function loadAgentList() {
loadingAgents.value = true
try {
const res = await getAgentList()
if (res.code === 0 && res.data) {
agentList.value = res.data.map(item => ({
id: item.id,
agentId: item.agentId,
name: item.agentName,
description: item.description,
systemPrompt: item.systemPrompt,
avatar: item.icon,
categoryName: item.categoryName || '其他'
}))
// 默认选中第一个
if (agentList.value.length > 0 && !topicDetails.selectedAgentId) {
topicDetails.selectedAgentId = agentList.value[0].id
selectedAgent.value = agentList.value[0]
}
}
} catch (error) {
console.error('加载智能体列表失败:', error)
} finally {
loadingAgents.value = false
}
}
// 处理智能体选择
function handleAgentChange(agentId) {
const agent = agentList.value.find(a => a.id === agentId)
if (agent) {
selectedAgent.value = agent
topicDetails.selectedAgentId = agentId
}
}
// 语音分析
async function analyzeVoice(audioUrl) {
if (!audioUrl?.trim()) {
console.warn('音频链接为空,无法分析')
return
}
isAnalyzing.value = true
try {
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
} else {
message.warning('未获取到可用文案')
}
} catch (error) {
console.error('分析语音失败:', error)
} finally {
isAnalyzing.value = false
}
}
// 选择话题
function handleSelectTopic(topic) {
selectedTopic.value = topic.id
}
// 创作处理
async function handleCreate(topic) {
selectedTopic.value = topic.id
if (topic.audioUrl?.trim()) {
await analyzeVoice(topic.audioUrl)
}
}
// 生成文案(流式)
async function handleGenerate() {
if (!topicDetails.copywriting?.trim()) {
message.warning('请输入文案内容')
return
}
if (!selectedAgent.value?.id) {
message.warning('请先选择文案风格')
return
}
isGenerating.value = true
generatedContent.value = ''
try {
const ctrl = new AbortController()
let fullText = ''
let completed = false
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (!completed) {
completed = true
ctrl.abort()
reject(new Error('请求超时'))
}
}, 180000)
rewriteStream({
agentId: selectedAgent.value.id,
userText: topicDetails.copywriting.trim(),
level: topicDetails.level,
modelType: topicDetails.modelType,
ctrl,
onMessage: (data) => {
if (completed) return
// 处理 Dify 响应格式
if (data.event === 'message' && data.content) {
fullText += data.content
generatedContent.value = fullText
} else if (data.event === 'done') {
completed = true
clearTimeout(timeout)
resolve()
} else if (data.event === 'error') {
completed = true
clearTimeout(timeout)
reject(new Error(data.errorMessage || '生成失败'))
}
},
onError: (err) => {
if (!completed) {
completed = true
clearTimeout(timeout)
ctrl.abort()
message.error(err?.message || '网络请求失败')
reject(new Error(err?.message || '网络请求失败'))
}
},
onClose: () => {
if (!completed) {
completed = true
clearTimeout(timeout)
resolve()
}
}
})
})
generatedContent.value = fullText.trim()
message.success('文案生成成功')
} catch (error) {
console.error('生成文案失败:', error)
message.error('生成文案失败')
} finally {
isGenerating.value = false
}
}
// 数据处理
function extractAudioUrl(video) {
const urlList = video?.play_addr?.url_list
if (Array.isArray(urlList) && urlList.length > 0) {
const lastUrl = urlList[urlList.length - 1]?.trim()
const firstUrl = urlList[0]?.trim()
return lastUrl || firstUrl || ''
}
return video?.play_addr?.url?.trim() || ''
}
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 {
// 保存分页信息用于下次请求
const responseData = response?.data || {}
currentCursor.value = responseData.cursor || null
searchParams.search_id = responseData.search_id || ''
searchParams.backtrace = responseData.backtrace || ''
const dataList = responseData.data || []
return dataList
.map(el => el.aweme_info)
.filter(Boolean)
.map((item, index) => ({
id: startId + index,
title: item.desc || '无标题',
videoId: item.aweme_id,
videoUrl: `https://www.douyin.com/video/${item.aweme_id}`,
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]
|| '',
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,
}))
} catch (error) {
console.error('处理搜索结果失败:', error)
return []
}
}
// 搜索功能
async function handleSearch() {
const keyword = searchKeyword.value.trim()
if (!keyword) {
message.warning('请输入搜索关键词')
return
}
// 重置分页参数
searchParams.keyword = keyword
searchParams.cursor = 0
searchParams.search_id = ''
searchParams.backtrace = ''
currentCursor.value = null
hotTopics.value = []
isLoading.value = true
try {
const urlParams = {
keyword: searchParams.keyword,
cursor: searchParams.cursor,
sort_type: searchParams.sort_type,
publish_time: searchParams.publish_time,
filter_duration: searchParams.filter_duration,
content_type: searchParams.content_type,
search_id: searchParams.search_id,
backtrace: searchParams.backtrace
}
const response = await TikhubService.postTikHup({
type: InterfaceType.DOUYIN_SEARCH_GENERAL_SEARCH,
methodType: MethodType.POST,
urlParams,
paramType: ParamType.JSON
})
const searchResults = processSearchResults(response)
if (searchResults.length === 0) {
message.warning('未找到相关结果')
return
}
hotTopics.value = searchResults
message.success(`找到 ${searchResults.length} 个结果`)
} catch (error) {
console.error('搜索失败:', error)
message.error(error?.message || '搜索失败')
hotTopics.value = []
} finally {
isLoading.value = false
}
}
// 初始化
onMounted(() => {
loadAgentList()
})
</script>
<template>
<div class="forecast-page">
<!-- 左侧热点搜索 -->
<section class="search-panel">
<div class="panel-header">
<h2 class="panel-title">热点预测</h2>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<div class="search-input-wrap">
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
<input
v-model="searchKeyword"
type="text"
placeholder="搜索热门内容..."
class="search-input"
:disabled="isLoading"
@keypress="handleSearchKeypress"
/>
</div>
<button
@click="handleSearch"
:disabled="isLoading || !searchKeyword.trim()"
class="search-btn"
>
<span v-if="isLoading" class="btn-spinner"></span>
<span v-else>搜索</span>
</button>
</div>
<!-- 筛选器 -->
<div class="filters-row">
<div class="filter-item">
<span class="filter-label">排序</span>
<a-select v-model:value="searchParams.sort_type" size="small">
<a-select-option value="0">综合</a-select-option>
<a-select-option value="1">最多点赞</a-select-option>
<a-select-option value="2">最新发布</a-select-option>
</a-select>
</div>
<div class="filter-item">
<span class="filter-label">时间</span>
<a-select v-model:value="searchParams.publish_time" size="small">
<a-select-option value="0">不限</a-select-option>
<a-select-option value="1">一天</a-select-option>
<a-select-option value="7">一周</a-select-option>
<a-select-option value="180">半年</a-select-option>
</a-select>
</div>
<div class="filter-item">
<span class="filter-label">时长</span>
<a-select v-model:value="searchParams.filter_duration" size="small">
<a-select-option value="0">不限</a-select-option>
<a-select-option value="0-1">1分钟内</a-select-option>
<a-select-option value="1-5">1-5分钟</a-select-option>
<a-select-option value="5-10000">5分钟+</a-select-option>
</a-select>
</div>
</div>
<!-- 结果列表 -->
<div class="results-area">
<!-- 空状态 -->
<div v-if="hotTopics.length === 0 && !isLoading" class="empty-state">
<svg class="empty-icon" viewBox="0 0 48 48" fill="none">
<circle cx="20" cy="20" r="14" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<path d="M32 32L42 42" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
</svg>
<p class="empty-text">搜索热门内容获取灵感</p>
</div>
<!-- 加载状态 -->
<div v-else-if="isLoading" class="loading-state">
<div class="loading-ring"></div>
<span>搜索中</span>
</div>
<!-- 结果卡片 -->
<div v-else class="topic-list">
<div
v-for="topic in hotTopics"
:key="topic.id"
@click="handleSelectTopic(topic)"
class="topic-card"
:class="{ selected: selectedTopic === topic.id }"
>
<div class="topic-cover">
<img
v-if="topic.cover"
:src="topic.cover"
alt=""
@error="handleImageError"
/>
<div v-else class="cover-fallback">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm0 2v12h16V6H4zm6 2l5 4-5 4V8z"/>
</svg>
</div>
<span class="topic-num">{{ topic.id }}</span>
</div>
<div class="topic-content">
<h3 class="topic-title" @click.stop="openVideo(topic, $event)" :title="topic.title">
{{ truncateTitle(topic.title, 32) }}
</h3>
<div class="topic-meta">
<img v-if="topic.authorAvatar" :src="topic.authorAvatar" alt="" class="author-avatar" @error="handleImageError"/>
<span class="author-name">{{ topic.author }}</span>
</div>
<div class="topic-stats">
<span class="stat-item">
<svg viewBox="0 0 20 20" fill="currentColor"><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"/></svg>
{{ formatNumber(topic.playCount) }}
</span>
<span class="stat-item">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.834a1 1 0 001.555.832L12 13.202a5 5 0 001.196-.599l.707-.707a1 1 0 00.293-.707V8.465a1 1 0 00-1.707-.707L12 8.465V5a2 2 0 00-2-2H7a1 1 0 000 2h3v3z"/></svg>
{{ formatNumber(topic.diggCount) }}
</span>
</div>
</div>
<button class="action-btn" @click.stop="handleCreate(topic)" title="提取文案">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
</button>
</div>
</div>
</div>
</section>
<!-- 右侧创作工具 -->
<section class="create-panel">
<div class="panel-header">
<h2 class="panel-title">创作工具</h2>
</div>
<div class="create-form">
<!-- 文案输入 -->
<div class="form-block">
<div class="block-header">
<label>文案内容</label>
<span v-if="isAnalyzing" class="status-tag analyzing">
<span class="tag-dot"></span>
分析中
</span>
</div>
<a-textarea
v-model:value="topicDetails.copywriting"
:rows="5"
placeholder="输入文案或点击热点卡片自动提取..."
:disabled="isAnalyzing"
class="content-input"
/>
</div>
<!-- 风格选择 -->
<div class="form-block">
<label>文案风格</label>
<a-select
v-model:value="topicDetails.selectedAgentId"
placeholder="选择风格"
style="width: 100%"
:loading="loadingAgents"
show-search
:filter-option="false"
@change="handleAgentChange"
@search="agentSearchKeyword = $event"
>
<a-select-option
v-for="agent in filteredAgentList"
:key="agent.id"
:value="agent.id"
>
<span>{{ agent.name }}</span>
<span class="agent-category">{{ agent.categoryName }}</span>
</a-select-option>
</a-select>
</div>
<!-- 模式 & 幅度 -->
<div class="form-row">
<div class="form-block half">
<label>生成模式</label>
<div class="mode-switch">
<button
:class="['mode-btn', { active: topicDetails.modelType === 'forecast_standard' }]"
@click="topicDetails.modelType = 'forecast_standard'"
>标准</button>
<button
:class="['mode-btn', { active: topicDetails.modelType === 'forecast_pro' }]"
@click="topicDetails.modelType = 'forecast_pro'"
>PRO</button>
</div>
</div>
<div class="form-block half">
<div class="block-header">
<label>改写幅度</label>
<span class="level-num">{{ topicDetails.level }}%</span>
</div>
<input
type="range"
v-model.number="topicDetails.level"
min="10"
max="100"
step="10"
class="level-slider"
/>
</div>
</div>
<!-- 生成按钮 -->
<button
class="generate-btn"
:class="{ loading: isGenerating }"
:disabled="!topicDetails.copywriting?.trim() || !topicDetails.selectedAgentId || isGenerating"
@click="handleGenerate"
>
<template v-if="isGenerating">
<span class="btn-spinner light"></span>
<span>生成中</span>
</template>
<template v-else>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
<span>生成爆款</span>
</template>
</button>
<!-- 生成结果 -->
<Transition name="slide-up">
<div v-if="generatedContent" class="result-block">
<div class="result-header">
<span class="result-title">生成结果</span>
<button class="copy-btn" @click="copyContent">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
复制
</button>
</div>
<div class="result-content">{{ generatedContent }}</div>
</div>
</Transition>
</div>
</section>
</div>
</template>
<style scoped lang="less">
// 设计系统变量
@primary: #00b030;
@primary-light: #00c838;
@primary-glow: rgba(0, 176, 48, 0.12);
@bg-base: #f5f6f8;
@bg-card: #ffffff;
@bg-elevated: #fafbfc;
@border-subtle: rgba(0, 0, 0, 0.04);
@border-default: rgba(0, 0, 0, 0.08);
@text-primary: rgba(0, 0, 0, 0.88);
@text-secondary: rgba(0, 0, 0, 0.55);
@text-tertiary: rgba(0, 0, 0, 0.35);
@radius-sm: 6px;
@radius-md: 10px;
@radius-lg: 14px;
// 页面布局
.forecast-page {
display: grid;
grid-template-columns: 1fr 400px;
gap: 16px;
height: 100%;
background: @bg-base;
}
// 面板基础
.search-panel,
.create-panel {
background: @bg-card;
border: 1px solid @border-subtle;
border-radius: @radius-lg;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid @border-subtle;
}
.panel-title {
font-size: 15px;
font-weight: 600;
color: @text-primary;
margin: 0;
letter-spacing: -0.01em;
}
// ========== 搜索面板 ==========
.search-bar {
display: flex;
gap: 8px;
padding: 12px 16px;
}
.search-input-wrap {
flex: 1;
position: relative;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: @text-tertiary;
pointer-events: none;
}
.search-input {
width: 100%;
height: 38px;
padding: 0 12px 0 38px;
font-size: 14px;
color: @text-primary;
background: @bg-base;
border: 1px solid @border-default;
border-radius: @radius-sm;
transition: all 0.2s;
&::placeholder { color: @text-tertiary; }
&:focus {
outline: none;
border-color: @primary;
box-shadow: 0 0 0 3px @primary-glow;
}
&:disabled { opacity: 0.5; }
}
.search-btn {
padding: 0 18px;
height: 38px;
font-size: 14px;
font-weight: 500;
color: #fff;
background: @primary;
border: none;
border-radius: @radius-sm;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
&:hover:not(:disabled) { background: @primary-light; }
&:disabled { opacity: 0.5; cursor: not-allowed; }
}
.btn-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
&.light {
border-color: rgba(255,255,255,0.2);
border-top-color: #fff;
}
}
// 筛选器
.filters-row {
display: flex;
gap: 12px;
padding: 0 16px 12px;
border-bottom: 1px solid @border-subtle;
}
.filter-item {
display: flex;
align-items: center;
gap: 6px;
}
.filter-label {
font-size: 12px;
color: @text-tertiary;
}
:deep(.ant-select) {
width: 88px;
.ant-select-selector {
background: @bg-base !important;
border-color: @border-default !important;
border-radius: @radius-sm !important;
height: 26px !important;
.ant-select-selection-item {
color: @text-primary !important;
font-size: 12px !important;
line-height: 24px !important;
}
}
}
// 结果区域
.results-area {
flex: 1;
overflow-y: auto;
padding: 12px;
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-track { background: transparent; }
&::-webkit-scrollbar-thumb { background: @border-default; border-radius: 2px; }
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 20px;
}
.empty-icon {
width: 48px;
height: 48px;
color: @text-tertiary;
margin-bottom: 12px;
opacity: 0.6;
}
.empty-text {
font-size: 14px;
color: @text-secondary;
margin: 0;
}
// 加载状态
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 20px;
gap: 12px;
color: @text-secondary;
font-size: 13px;
}
.loading-ring {
width: 28px;
height: 28px;
border: 2px solid @border-default;
border-top-color: @primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
// 热点卡片
.topic-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.topic-card {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: @bg-card;
border: 1px solid @border-subtle;
border-radius: @radius-md;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: @border-default;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
&.selected {
border-color: @primary;
box-shadow: 0 0 0 3px @primary-glow;
}
}
.topic-cover {
position: relative;
flex-shrink: 0;
width: 88px;
height: 50px;
background: @bg-base;
border-radius: @radius-sm;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: @text-tertiary;
svg { width: 20px; height: 20px; opacity: 0.4; }
}
}
.topic-num {
position: absolute;
top: 4px;
left: 4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
font-size: 10px;
font-weight: 600;
color: #fff;
background: rgba(0, 0, 0, 0.6);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
}
.topic-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.topic-title {
font-size: 13px;
font-weight: 500;
color: @text-primary;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
cursor: pointer;
transition: color 0.15s;
&:hover { color: @primary; }
}
.topic-meta {
display: flex;
align-items: center;
gap: 6px;
}
.author-avatar {
width: 14px;
height: 14px;
border-radius: 50%;
object-fit: cover;
}
.author-name {
font-size: 11px;
color: @text-secondary;
}
.topic-stats {
display: flex;
gap: 10px;
}
.stat-item {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
color: @text-tertiary;
svg { width: 11px; height: 11px; }
}
.action-btn {
flex-shrink: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
color: @primary;
background: @primary-glow;
border: none;
border-radius: @radius-sm;
cursor: pointer;
transition: all 0.15s;
svg { width: 16px; height: 16px; }
&:hover {
background: @primary;
color: #fff;
}
}
// ========== 创作面板 ==========
.create-form {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-track { background: transparent; }
&::-webkit-scrollbar-thumb { background: @border-default; border-radius: 2px; }
}
.form-block {
display: flex;
flex-direction: column;
gap: 8px;
&.half { flex: 1; }
label {
font-size: 12px;
font-weight: 500;
color: @text-secondary;
}
}
.block-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.status-tag {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: @primary;
&.analyzing .tag-dot {
width: 5px;
height: 5px;
background: @primary;
border-radius: 50%;
animation: pulse 1s ease-in-out infinite;
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.content-input {
:deep(.ant-input) {
background: @bg-base !important;
border-color: @border-default !important;
color: @text-primary !important;
border-radius: @radius-sm !important;
font-size: 13px !important;
line-height: 1.6 !important;
&::placeholder { color: @text-tertiary !important; }
&:focus {
border-color: @primary !important;
box-shadow: 0 0 0 2px @primary-glow !important;
}
}
}
// 下拉选择器
:deep(.ant-select) {
.ant-select-selector {
background: @bg-base !important;
border-color: @border-default !important;
border-radius: @radius-sm !important;
height: 36px !important;
.ant-select-selection-item,
.ant-select-selection-placeholder {
color: @text-primary !important;
font-size: 13px !important;
line-height: 34px !important;
}
}
&.ant-select-focused .ant-select-selector {
border-color: @primary !important;
box-shadow: 0 0 0 2px @primary-glow !important;
}
}
.agent-category {
margin-left: 8px;
font-size: 11px;
color: @text-tertiary;
}
// 表单行
.form-row {
display: flex;
gap: 12px;
}
// 模式切换
.mode-switch {
display: flex;
gap: 6px;
}
.mode-btn {
flex: 1;
height: 36px;
font-size: 13px;
font-weight: 500;
color: @text-secondary;
background: @bg-base;
border: 1px solid @border-default;
border-radius: @radius-sm;
cursor: pointer;
transition: all 0.15s;
&:hover { border-color: @border-subtle; color: @text-primary; }
&.active {
color: #fff;
background: @primary;
border-color: @primary;
}
}
// 幅度滑块
.level-num {
font-size: 12px;
font-weight: 600;
color: @primary;
}
.level-slider {
width: 100%;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: @bg-base;
border-radius: 2px;
outline: none;
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: @primary;
border-radius: 50%;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
&:hover {
transform: scale(1.15);
box-shadow: 0 0 0 4px @primary-glow;
}
}
}
// 生成按钮
.generate-btn {
width: 100%;
height: 44px;
font-size: 14px;
font-weight: 600;
color: #fff;
background: @primary;
border: none;
border-radius: @radius-md;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
margin-top: auto;
svg { width: 16px; height: 16px; }
&:hover:not(:disabled) {
background: @primary-light;
transform: translateY(-1px);
box-shadow: 0 6px 20px @primary-glow;
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&.loading {
background: @primary;
}
}
// 结果区域
.result-block {
padding-top: 16px;
border-top: 1px solid @border-subtle;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.result-title {
font-size: 12px;
font-weight: 500;
color: @text-secondary;
}
.copy-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
font-size: 12px;
color: @text-secondary;
background: @bg-base;
border: 1px solid @border-default;
border-radius: @radius-sm;
cursor: pointer;
transition: all 0.15s;
svg { width: 12px; height: 12px; }
&:hover {
color: @text-primary;
border-color: @text-tertiary;
}
}
.result-content {
padding: 14px;
font-size: 13px;
line-height: 1.7;
color: @text-primary;
background: @bg-base;
border-radius: @radius-sm;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 240px;
overflow-y: auto;
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-track { background: transparent; }
&::-webkit-scrollbar-thumb { background: @border-default; border-radius: 2px; }
}
// 过渡动画
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.25s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(-8px);
}
// 响应式
@media (max-width: 900px) {
.forecast-page {
grid-template-columns: 1fr;
}
.create-panel {
min-height: 400px;
}
}
</style>