1288 lines
31 KiB
Vue
1288 lines
31 KiB
Vue
<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>
|