This commit is contained in:
aaron 2025-05-19 00:17:39 +08:00
parent 155a2e5cba
commit 34ff339e7f
8 changed files with 428 additions and 2940 deletions

View File

@ -5,7 +5,7 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
image: tradus-web:1.2.9 image: tradus-web:1.3.0
container_name: tradus-web container_name: tradus-web
ports: ports:
- '6000:80' - '6000:80'

View File

@ -302,19 +302,7 @@ onUnmounted(() => {
</svg> </svg>
<span class="agent-name">首页</span> <span class="agent-name">首页</span>
</RouterLink> </RouterLink>
<RouterLink to="/crypto-analysis" class="agent-item" @click="showMobileMenu = false"> <RouterLink to="/ai-agents" class="agent-item" @click="showMobileMenu = false">
<svg
class="agent-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M7 12l5-5 5 5M7 17l5-5 5 5" />
</svg>
<span class="agent-name">加密货币分析</span>
</RouterLink>
<RouterLink to="/astock-analysis" class="agent-item" @click="showMobileMenu = false">
<svg <svg
class="agent-icon" class="agent-icon"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -330,7 +318,7 @@ onUnmounted(() => {
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline> <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line> <line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg> </svg>
<span class="agent-name">A股分析</span> <span class="agent-name">AI分析智能体</span>
</RouterLink> </RouterLink>
</div> </div>

View File

@ -20,12 +20,6 @@ const router = createRouter({
title: 'AI 助理团队', title: 'AI 助理团队',
}, },
}, },
{
path: '/ai-agent',
name: 'ai-agent',
component: () => import('../views/AIAgentView.vue'),
meta: { requiresVIP: true },
},
{ {
path: '/download', path: '/download',
name: 'download', name: 'download',
@ -42,21 +36,12 @@ const router = createRouter({
}, },
}, },
{ {
path: '/crypto-analysis', path: '/analysis/:type',
name: 'crypto-analysis', name: 'universal-analysis',
component: () => import('../views/CryptoAnalysisView.vue'), component: () => import('../views/UniversalAnalysisView.vue'),
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: '加密货币分析助理', title: '智能分析助理',
},
},
{
path: '/astock-analysis',
name: 'astock-analysis',
component: () => import('../views/AStockAnalysisView.vue'),
meta: {
requiresAuth: true,
title: 'A股分析助理',
}, },
}, },
], ],

File diff suppressed because it is too large Load Diff

View File

@ -6,23 +6,23 @@ const router = useRouter()
const agents = [ const agents = [
{ {
id: 'crypto-analysis', id: 'crypto-analysis',
name: '加密货币AI分析助理', name: '加密货币分析智能体',
description: '通过 AI 技术,获取加密货币的深度分析报告', description: '获取加密货币的深度AI智能分析报告',
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M7 12l5-5 5 5M7 17l5-5 5 5"/> <path d="M7 12l5-5 5 5M7 17l5-5 5 5"/>
</svg>`, </svg>`,
route: '/crypto-analysis', route: '/analysis/crypto',
}, },
{ {
id: 'astock-analysis', id: 'astock-analysis',
name: 'A股AI分析助理', name: 'A股股票分析智能体',
description: '通过 AI 技术,获取 A 股上市公司的深度分析报告', description: '获取A股上市公司的深度AI智能分析报告',
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path> <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline> <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line> <line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>`, </svg>`,
route: '/astock-analysis', route: '/analysis/stock',
}, },
] ]
@ -35,8 +35,8 @@ const navigateToAgent = (route: string) => {
<div class="ai-agents-view"> <div class="ai-agents-view">
<div class="content-container"> <div class="content-container">
<div class="header-section"> <div class="header-section">
<h1 class="title">AI 助理团队</h1> <h1 class="title">AI 分析智能体</h1>
<p class="description">选择专业的 AI 助手获取精准的分析和建议</p> <p class="description">选择专业的 AI 分析智能体获取精准的分析和建议</p>
</div> </div>
<div class="agents-grid"> <div class="agents-grid">

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ const openAuthModal = (mode: 'login' | 'register') => {
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title"> <h1 class="hero-title">
<span class="accent">tradus</span> <span class="accent">tradus</span>
<span class="free-tag">Alpha</span> <span class="free-tag">测试版</span>
</h1> </h1>
<p class="hero-subtitle">基于AI大语言模型的智能投研助理</p> <p class="hero-subtitle">基于AI大语言模型的智能投研助理</p>
<div class="hero-actions" v-if="!isAuthenticated"> <div class="hero-actions" v-if="!isAuthenticated">

View File

@ -1,8 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick } from 'vue' import { ref, computed, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router'
import { http } from '../services/api' import { http } from '../services/api'
const stockCode = ref('') //
const route = useRoute()
const analysisType = computed(() => (route.params.type as string) || 'crypto')
//
const symbolCode = ref('')
const isAnalyzing = ref(false) const isAnalyzing = ref(false)
const analysisContent = ref('') const analysisContent = ref('')
const analysisContainer = ref<HTMLElement | null>(null) const analysisContainer = ref<HTMLElement | null>(null)
@ -14,6 +20,74 @@ const copySuccess = ref(false)
const apiBaseUrl = const apiBaseUrl =
import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work' import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'
//
const pageTitle = computed(() => {
switch (analysisType.value) {
case 'stock':
return 'A股股票分析智能体'
case 'crypto':
default:
return '加密货币分析智能体'
}
})
const pageDescription = computed(() => {
switch (analysisType.value) {
case 'stock':
return '获取A股上市公司的深度AI智能分析'
case 'crypto':
default:
return '获取加密货币的深度AI智能分析'
}
})
const inputPlaceholder = computed(() => {
switch (analysisType.value) {
case 'stock':
return '请输入上市公司股票代码'
case 'crypto':
default:
return '请输入BTC、ETH 一次只能分析一个币种'
}
})
//
const isStockMode = computed(() => analysisType.value === 'stock')
//
const commonItemsList = computed(() => {
switch (analysisType.value) {
case 'stock':
return [
{ code: '000001', label: '平安银行' },
{ code: '000651', label: '格力电器' },
{ code: '601318', label: '中国平安' },
{ code: '600519', label: '贵州茅台' },
{ code: '601888', label: '中国中免' },
{ code: '000858', label: '五粮液' },
{ code: '600276', label: '恒瑞医药' },
{ code: '002594', label: '比亚迪' },
{ code: '600036', label: '招商银行' },
{ code: '603288', label: '海天味业' },
]
case 'crypto':
default:
return [
{ code: 'BTC', label: 'BTC' },
{ code: 'ETH', label: 'ETH' },
{ code: 'SOL', label: 'SOL' },
{ code: 'SUI', label: 'SUI' },
{ code: 'TRX', label: 'TRX' },
{ code: 'XRP', label: 'XRP' },
{ code: 'BNB', label: 'BNB' },
{ code: 'ADA', label: 'ADA' },
{ code: 'DOGE', label: 'DOGE' },
{ code: 'SHIB', label: 'SHIB' },
]
}
})
//
const scrollToBottom = async () => { const scrollToBottom = async () => {
await nextTick() await nextTick()
if (analysisContainer.value) { if (analysisContainer.value) {
@ -22,25 +96,26 @@ const scrollToBottom = async () => {
} }
// //
const handleKeyup = (event: KeyboardEvent) => { const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault()
handleAnalysis() handleAnalysis()
} }
} }
// //
const setErrorMessage = (message: string) => { const setErrorMessage = (message: string) => {
// 使
analysisContent.value = `错误: ${message}` analysisContent.value = `错误: ${message}`
currentThought.value = '分析失败' currentThought.value = '分析失败'
} }
//
const handleAnalysis = async () => { const handleAnalysis = async () => {
const code = stockCode.value.trim() const code = symbolCode.value.trim()
if (!code || isAnalyzing.value) return if (!code || isAnalyzing.value) return
// //
if (!/^\d{6}$/.test(code)) { if (isStockMode.value && !/^\d{6}$/.test(code)) {
setErrorMessage('请输入正确的6位股票代码') setErrorMessage('请输入正确的6位股票代码')
return return
} }
@ -51,7 +126,19 @@ const handleAnalysis = async () => {
currentThought.value = '准备开始分析...' currentThought.value = '准备开始分析...'
try { try {
const response = await http.post(`${apiBaseUrl}/adata/${code}/analysis`, {}) let response
// API
if (isStockMode.value) {
// A
response = await http.post(`${apiBaseUrl}/adata/${code}/analysis`, {})
} else {
//
const requestData = {
symbol: code.toUpperCase(),
}
response = await http.post(`${apiBaseUrl}/crypto/analysis_v2`, requestData)
}
if (!response.ok) { if (!response.ok) {
// //
@ -95,7 +182,16 @@ const handleAnalysis = async () => {
switch (data.event) { switch (data.event) {
case 'workflow_started': case 'workflow_started':
currentThought.value = '开始分析...' currentThought.value = 'Agent 正在分析...'
break
case 'node_started':
currentThought.value = `Agent 调用: ${data.data.title}`
break
case 'node_finished':
currentThought.value = `Agent 完成: ${data.data.title}`
break
case 'node_failed':
currentThought.value = `Agent 分析失败 ${data.data.title}`
break break
case 'text_chunk': case 'text_chunk':
@ -107,7 +203,6 @@ const handleAnalysis = async () => {
case 'workflow_finished': case 'workflow_finished':
if (data.data && data.data.outputs && data.data.outputs.text) { if (data.data && data.data.outputs && data.data.outputs.text) {
//
if (analysisContent.value !== data.data.outputs.text) { if (analysisContent.value !== data.data.outputs.text) {
analysisContent.value = data.data.outputs.text analysisContent.value = data.data.outputs.text
await scrollToBottom() await scrollToBottom()
@ -122,14 +217,13 @@ const handleAnalysis = async () => {
await scrollToBottom() await scrollToBottom()
break break
} }
} catch (e) { } catch (error) {
console.error('解析响应数据出错:', e) console.error('解析响应数据出错:', error)
setErrorMessage('解析响应数据时出错,请稍后重试') setErrorMessage('解析响应数据时出错,请稍后重试')
} }
} }
} }
} catch (error) { } catch {
console.error('分析请求失败:', error)
setErrorMessage('抱歉,分析请求失败,请稍后重试') setErrorMessage('抱歉,分析请求失败,请稍后重试')
} finally { } finally {
isAnalyzing.value = false isAnalyzing.value = false
@ -139,14 +233,15 @@ const handleAnalysis = async () => {
const resetView = () => { const resetView = () => {
showInitialView.value = true showInitialView.value = true
stockCode.value = '' symbolCode.value = ''
analysisContent.value = '' analysisContent.value = ''
currentThought.value = '' currentThought.value = ''
isAnalyzing.value = false isAnalyzing.value = false
} }
//
const clearInput = () => { const clearInput = () => {
stockCode.value = '' symbolCode.value = ''
analysisContent.value = '' analysisContent.value = ''
currentThought.value = '' currentThought.value = ''
} }
@ -168,37 +263,50 @@ const copyAnalysis = async () => {
console.error('复制失败') console.error('复制失败')
} }
} }
//
const handleCommonAnalysis = (item: { code: string }) => {
symbolCode.value = item.code
handleAnalysis()
}
//
watch(
() => route.params.type,
() => {
resetView()
},
)
</script> </script>
<template> <template>
<div class="stock-analysis-view"> <div class="universal-analysis-view">
<div class="content-container"> <div class="content-container">
<!-- 初始视图 --> <!-- 初始视图 -->
<div v-if="showInitialView" class="initial-content"> <div v-if="showInitialView" class="initial-content">
<div class="header-section"> <div class="header-section">
<h1 class="title">A股AI分析助理</h1> <h1 class="title">{{ pageTitle }}</h1>
<p class="description">通过 AI 技术获取 A 股上市公司的深度分析报告</p> <p class="description">{{ pageDescription }}</p>
</div> </div>
<div class="search-section"> <div class="search-section">
<div class="search-container" :class="{ 'is-analyzing': isAnalyzing }"> <div class="search-container" :class="{ 'is-analyzing': isAnalyzing }">
<div class="input-wrapper"> <div class="input-wrapper">
<div class="input-container" :class="{ 'has-input': stockCode }"> <div class="input-container">
<div class="input-area"> <div class="input-area">
<input <input
v-model="stockCode" v-model="symbolCode"
type="text" type="text"
class="search-input" class="search-input"
:class="{ 'is-selected': stockCode }" :placeholder="inputPlaceholder"
placeholder="请输入6位股票代码" @keydown="handleKeydown"
maxlength="6"
@keyup="handleKeyup"
:disabled="isAnalyzing" :disabled="isAnalyzing"
:maxlength="isStockMode ? 6 : undefined"
/> />
<button <button
v-if="stockCode" v-if="symbolCode.trim() || analysisContent"
class="clear-button" class="clear-button"
@click.stop="clearInput" @click="clearInput"
:disabled="isAnalyzing" :disabled="isAnalyzing"
> >
<svg <svg
@ -216,8 +324,8 @@ const copyAnalysis = async () => {
</div> </div>
<button <button
class="analyze-button" class="analyze-button"
@click="() => handleAnalysis()" @click="handleAnalysis"
:disabled="!stockCode || isAnalyzing" :disabled="isAnalyzing || !symbolCode.trim()"
> >
{{ isAnalyzing ? '分析中...' : '开始分析' }} {{ isAnalyzing ? '分析中...' : '开始分析' }}
</button> </button>
@ -225,14 +333,34 @@ const copyAnalysis = async () => {
</div> </div>
</div> </div>
</div> </div>
<!-- 快速分析列表 -->
<div class="common-analysis-section">
<div class="common-analysis-container">
<div class="section-header">
<span class="section-title">点击直接快速分析</span>
</div>
<div class="common-analysis-list">
<button
v-for="item in commonItemsList"
:key="item.label"
class="common-analysis-item"
@click="handleCommonAnalysis(item)"
:disabled="isAnalyzing"
>
{{ item.label }}
</button>
</div>
</div>
</div>
</div> </div>
<!-- 分析视图 --> <!-- 分析视图 -->
<div v-else class="analysis-view"> <div v-else class="analysis-view">
<div class="analysis-header"> <div class="analysis-header">
<div class="target-info"> <div class="target-info">
<span class="label">正在分析股票</span> <span class="label">正在分析</span>
<span class="value">{{ stockCode }}</span> <span class="value">{{ symbolCode.toUpperCase() }}</span>
</div> </div>
<div class="action-buttons" v-if="!isAnalyzing"> <div class="action-buttons" v-if="!isAnalyzing">
<button class="action-button" @click="resetView"> <button class="action-button" @click="resetView">
@ -271,7 +399,8 @@ const copyAnalysis = async () => {
<span></span> <span></span>
</div> </div>
<div class="status-text"> <div class="status-text">
<div class="status-label">AI 正在分析</div> <!-- <div class="status-label">Agent 正在分析</div> -->
<div class="thought-text" v-if="currentThought">{{ currentThought }}</div>
</div> </div>
</div> </div>
@ -293,8 +422,7 @@ const copyAnalysis = async () => {
</template> </template>
<style scoped> <style scoped>
/* 基础样式 */ .universal-analysis-view {
.stock-analysis-view {
min-height: 100vh; min-height: 100vh;
height: 100vh; height: 100vh;
background-color: var(--color-bg-primary); background-color: var(--color-bg-primary);
@ -311,7 +439,7 @@ const copyAnalysis = async () => {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; overflow: hidden;
justify-content: center; justify-content: center;
} }
@ -320,7 +448,7 @@ const copyAnalysis = async () => {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 15vh; margin-bottom: 15vh;
gap: 2rem; gap: 1.5rem;
transition: all 0.5s ease; transition: all 0.5s ease;
} }
@ -343,11 +471,11 @@ const copyAnalysis = async () => {
transition: all 0.5s ease; transition: all 0.5s ease;
} }
/* 搜索区域样式 */
.search-section { .search-section {
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
transition: all 0.5s ease; transition: all 0.5s ease;
padding: 0 1.5rem;
} }
.search-container { .search-container {
@ -358,6 +486,7 @@ const copyAnalysis = async () => {
.input-wrapper { .input-wrapper {
position: relative; position: relative;
margin-bottom: 0;
} }
.input-container { .input-container {
@ -365,6 +494,7 @@ const copyAnalysis = async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
min-height: 5.5rem;
border: 2px solid var(--color-accent); border: 2px solid var(--color-accent);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: var(--color-bg-primary); background-color: var(--color-bg-primary);
@ -377,14 +507,14 @@ const copyAnalysis = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
position: relative; gap: 0.5rem;
} }
.search-input { .search-input {
flex: 1; flex: 1;
border: none; border: none;
background: none; background: none;
padding: 0.5rem; padding: 0;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--color-text-primary); color: var(--color-text-primary);
outline: none; outline: none;
@ -405,10 +535,15 @@ const copyAnalysis = async () => {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.clear-button:hover { .clear-button:hover:not(:disabled) {
opacity: 1; opacity: 1;
} }
.clear-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.clear-button svg { .clear-button svg {
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -455,6 +590,100 @@ const copyAnalysis = async () => {
flex-shrink: 0; flex-shrink: 0;
} }
.analysis-status {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 1rem;
text-align: center;
gap: 1rem;
margin-top: 1rem;
}
.progress-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.progress-dots span {
width: 8px;
height: 8px;
background-color: var(--color-accent);
border-radius: 50%;
opacity: 0.3;
animation: pulse-dot 1s infinite;
}
.progress-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.progress-dots span:nth-child(3) {
animation-delay: 0.4s;
}
.status-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
.status-label {
font-weight: 500;
color: var(--color-accent);
font-size: 0.85rem;
}
.thought-text {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.analysis-container {
flex: 1;
min-height: 0;
overflow-y: auto;
border-radius: var(--border-radius);
margin-top: 1rem;
display: flex;
flex-direction: column;
}
.analysis-container.fade-in {
background-color: var(--color-bg-secondary);
}
.analysis-content {
font-size: 1rem;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: var(--color-text-primary);
padding: 1.5rem 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
}
.error-message {
color: #ff3333;
font-weight: 500;
padding: 1rem;
background-color: rgba(255, 51, 51, 0.08);
border: 1px solid rgba(255, 51, 51, 0.2);
border-radius: 0.5rem;
margin: 1rem;
text-align: center;
font-size: 1rem;
line-height: 1.5;
}
.error-message::before {
content: '⚠️ ';
}
.target-info { .target-info {
display: flex; display: flex;
align-items: center; align-items: center;
@ -517,96 +746,73 @@ const copyAnalysis = async () => {
height: 16px; height: 16px;
} }
/* 分析状态样式 */ /* 快速分析列表样式 */
.analysis-status { .common-analysis-section {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 0 1.5rem;
}
.common-analysis-container {
width: 100%;
padding: 0 1.5rem;
margin-bottom: 1.5rem;
}
.section-header {
display: flex; display: flex;
flex-direction: row; justify-content: space-between;
align-items: center; align-items: center;
justify-content: center; margin-bottom: 0.75rem;
padding: 1rem;
text-align: center;
gap: 1rem;
margin-top: 1rem;
} }
.progress-dots { .section-title {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.common-analysis-list {
display: flex; display: flex;
align-items: center; flex-wrap: wrap;
justify-content: center;
gap: 6px;
}
.progress-dots span {
width: 8px;
height: 8px;
background-color: var(--color-accent);
border-radius: 50%;
opacity: 0.3;
animation: pulse-dot 1s infinite;
}
.progress-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.progress-dots span:nth-child(3) {
animation-delay: 0.4s;
}
.status-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
.status-label { .common-analysis-item {
font-weight: 500; padding: 0.35rem 0.75rem;
font-size: 0.9rem;
color: var(--color-accent); color: var(--color-accent);
font-size: 0.85rem;
}
/* 分析内容样式 */
.analysis-container {
flex: 1;
min-height: 0;
overflow-y: auto;
border-radius: var(--border-radius);
margin-top: 1rem;
display: flex;
flex-direction: column;
}
.analysis-container.fade-in {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
border: 1px solid var(--color-accent);
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s ease;
} }
.analysis-content { .common-analysis-item:hover:not(:disabled) {
padding: 1.5rem; background-color: var(--color-accent);
line-height: 2; color: white;
white-space: pre-wrap;
word-break: break-word;
color: var(--color-text-primary);
} }
.error-message { .common-analysis-item:disabled {
color: #ff3333; opacity: 0.6;
font-weight: 500; cursor: not-allowed;
padding: 1rem; border-color: var(--color-border);
background-color: rgba(255, 51, 51, 0.08); color: var(--color-text-secondary);
border: 1px solid rgba(255, 51, 51, 0.2);
border-radius: 0.5rem;
margin: 1rem;
text-align: center;
font-size: 1rem;
line-height: 1.5;
}
.error-message::before {
content: '⚠️ ';
} }
/* 动画 */ /* 动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-dot { @keyframes pulse-dot {
0%, 0%,
100% { 100% {
@ -619,43 +825,10 @@ const copyAnalysis = async () => {
} }
} }
.fade-in { /* 响应式设计 */
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式样式 */
@media (max-width: 768px) { @media (max-width: 768px) {
.initial-content { .universal-analysis-view {
margin-bottom: 12vh; padding: 0.75rem;
gap: 1.5rem;
}
.title {
font-size: 1.75rem;
}
.search-container {
padding: 1rem;
}
.search-input {
font-size: 0.9rem;
}
.analysis-content {
padding: 1.25rem 1.5rem;
font-size: 0.95rem;
} }
.analysis-view { .analysis-view {
@ -664,7 +837,7 @@ const copyAnalysis = async () => {
} }
.analysis-header { .analysis-header {
padding: 0.75rem; padding: 1rem;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
align-items: flex-start; align-items: flex-start;
@ -682,9 +855,81 @@ const copyAnalysis = async () => {
padding: 0.6rem; padding: 0.6rem;
font-size: 0.85rem; font-size: 0.85rem;
} }
.analysis-content {
padding: 1.25rem 1.5rem;
font-size: 0.95rem;
}
.initial-content {
margin-bottom: 12vh;
gap: 1.5rem;
}
.title {
font-size: 1.75rem;
}
.description {
font-size: 0.9rem;
}
.search-input {
font-size: 0.9rem;
}
.analyze-button {
font-size: 0.85rem;
}
.search-container {
padding: 1rem;
}
.search-section {
padding: 0 1rem;
}
.common-analysis-section {
padding: 0 1rem;
}
.common-analysis-container {
padding: 0 1rem;
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.universal-analysis-view {
padding: 0.5rem;
}
.analysis-view {
padding: 0.5rem;
}
.analysis-header {
padding: 1rem;
}
.target-info {
flex-wrap: wrap;
}
.target-info .label {
font-size: 0.8rem;
}
.target-info .value {
font-size: 0.85rem;
}
.analysis-content {
padding: 1rem;
font-size: 0.9rem;
line-height: 2;
}
.initial-content { .initial-content {
margin-bottom: 10vh; margin-bottom: 10vh;
gap: 1rem; gap: 1rem;
@ -707,20 +952,6 @@ const copyAnalysis = async () => {
padding: 0.6rem; padding: 0.6rem;
} }
.analysis-content {
padding: 1rem;
font-size: 0.9rem;
line-height: 2;
}
.target-info .label {
font-size: 0.8rem;
}
.target-info .value {
font-size: 0.85rem;
}
.action-buttons { .action-buttons {
gap: 0.5rem; gap: 0.5rem;
} }
@ -729,24 +960,26 @@ const copyAnalysis = async () => {
padding: 0.5rem; padding: 0.5rem;
font-size: 0.8rem; font-size: 0.8rem;
} }
}
/* 滚动条样式 */ .search-container {
:deep(*::-webkit-scrollbar) { padding: 0.75rem;
width: 6px; }
height: 6px;
}
:deep(*::-webkit-scrollbar-track) { .search-section {
background: transparent; padding: 0 0.75rem;
} }
:deep(*::-webkit-scrollbar-thumb) { .common-analysis-section {
background-color: rgba(125, 125, 125, 0.2); padding: 0 0.75rem;
border-radius: 3px; }
}
:deep(*::-webkit-scrollbar-thumb:hover) { .common-analysis-container {
background-color: rgba(125, 125, 125, 0.3); padding: 0 0.75rem;
}
.common-analysis-item {
font-size: 0.85rem;
padding: 0.3rem 0.6rem;
}
} }
</style> </style>