1431 lines
31 KiB
Vue
1431 lines
31 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, nextTick, watch, onMounted } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { http } from '../services/api'
|
||
import { marked } from 'marked'
|
||
|
||
// 配置marked选项
|
||
onMounted(() => {
|
||
marked.setOptions({
|
||
breaks: true,
|
||
gfm: true, // GitHub风格Markdown,支持表格等扩展语法
|
||
})
|
||
})
|
||
|
||
// 获取路由参数,确定分析类型
|
||
const route = useRoute()
|
||
const analysisType = computed(() => (route.params.type as string) || 'crypto')
|
||
|
||
// 分析标的代码
|
||
const symbolCode = ref('')
|
||
const isAnalyzing = ref(false)
|
||
const analysisContent = ref('')
|
||
const analysisContainer = ref<HTMLElement | null>(null)
|
||
const currentThought = ref('')
|
||
const showInitialView = ref(true)
|
||
const copySuccess = ref(false)
|
||
|
||
// 时间周期选项和选择值(加密货币模式下使用)
|
||
const timeframeOptions = [
|
||
{ value: '15m', label: '15分钟' },
|
||
{ value: '1h', label: '1小时' },
|
||
{ value: '4h', label: '4小时' },
|
||
{ value: '1d', label: '1天' },
|
||
{ value: '1w', label: '1周' },
|
||
]
|
||
const selectedTimeframe = ref('1h') // 默认改为1小时
|
||
|
||
// 获取时间周期的友好显示文本
|
||
const timeframeLabel = computed(() => {
|
||
const option = timeframeOptions.find((opt) => opt.value === selectedTimeframe.value)
|
||
return option ? option.label : selectedTimeframe.value
|
||
})
|
||
|
||
// 根据环境选择API基础URL
|
||
const apiBaseUrl =
|
||
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 () => {
|
||
await nextTick()
|
||
if (analysisContainer.value) {
|
||
analysisContainer.value.scrollTop = analysisContainer.value.scrollHeight
|
||
}
|
||
}
|
||
|
||
// 处理回车键
|
||
const handleKeydown = (event: KeyboardEvent) => {
|
||
if (event.key === 'Enter') {
|
||
event.preventDefault()
|
||
handleAnalysis()
|
||
}
|
||
}
|
||
|
||
// 处理错误消息的显示
|
||
const setErrorMessage = (message: string) => {
|
||
analysisContent.value = `错误: ${message}`
|
||
currentThought.value = '分析失败'
|
||
}
|
||
|
||
// 保存分析结果到服务器
|
||
const saveAnalysisHistory = async () => {
|
||
try {
|
||
// 准备提交参数
|
||
const payload: Record<string, string> = {
|
||
content: analysisContent.value,
|
||
}
|
||
|
||
// 根据不同模式设置不同参数
|
||
if (isStockMode.value) {
|
||
payload.stock_code = symbolCode.value.trim()
|
||
payload.type = 'astock'
|
||
} else {
|
||
payload.symbol = symbolCode.value.toUpperCase().trim()
|
||
payload.timeframe = selectedTimeframe.value
|
||
payload.type = 'crypto'
|
||
}
|
||
|
||
// 使用http.post发送请求保存分析历史
|
||
const response = await http.post(`${apiBaseUrl}/analysis/analysis_history`, payload)
|
||
|
||
if (!response.ok) {
|
||
console.error('保存分析历史失败:', response.status)
|
||
}
|
||
} catch (error) {
|
||
console.error('保存分析历史异常:', error)
|
||
}
|
||
}
|
||
|
||
// 分析逻辑
|
||
const handleAnalysis = async () => {
|
||
const code = symbolCode.value.trim()
|
||
if (!code || isAnalyzing.value) return
|
||
|
||
// 股票模式下验证股票代码格式
|
||
if (isStockMode.value && !/^\d{6}$/.test(code)) {
|
||
setErrorMessage('请输入正确的6位股票代码')
|
||
return
|
||
}
|
||
|
||
// 先切换到分析视图,再设置分析状态
|
||
showInitialView.value = false
|
||
isAnalyzing.value = true
|
||
analysisContent.value = ''
|
||
currentThought.value = '准备开始分析...'
|
||
|
||
try {
|
||
let requestData
|
||
|
||
// 根据分析类型构造不同的请求参数
|
||
if (isStockMode.value) {
|
||
// A股分析参数
|
||
requestData = {
|
||
stock_code: code,
|
||
type: 'astock',
|
||
}
|
||
} else {
|
||
// 加密货币分析参数
|
||
requestData = {
|
||
symbol: code.toUpperCase(),
|
||
timeframe: selectedTimeframe.value,
|
||
type: 'crypto',
|
||
}
|
||
}
|
||
|
||
// 统一调用 /analysis/analysis 接口
|
||
const response = await http.post(`${apiBaseUrl}/analysis/analysis`, requestData)
|
||
|
||
if (!response.ok) {
|
||
// 解析错误响应
|
||
try {
|
||
const errorData = await response.json()
|
||
if (errorData && errorData.detail) {
|
||
setErrorMessage(errorData.detail)
|
||
} else {
|
||
setErrorMessage(`请求失败,状态码: ${response.status}`)
|
||
}
|
||
} catch {
|
||
setErrorMessage(`请求失败,状态码: ${response.status}`)
|
||
}
|
||
return
|
||
}
|
||
|
||
const reader = response.body?.getReader()
|
||
if (!reader) {
|
||
throw new Error('无法获取响应流')
|
||
}
|
||
|
||
const decoder = new TextDecoder()
|
||
let buffer = ''
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
|
||
const chunk = decoder.decode(value, { stream: true })
|
||
buffer += chunk
|
||
|
||
const lines = buffer.split('\n')
|
||
buffer = lines.pop() || ''
|
||
|
||
for (const line of lines) {
|
||
if (!line.trim() || !line.startsWith('data: ')) continue
|
||
|
||
try {
|
||
const jsonStr = line.slice(6)
|
||
const data = JSON.parse(jsonStr)
|
||
|
||
switch (data.event) {
|
||
case 'workflow_started':
|
||
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
|
||
|
||
case 'text_chunk':
|
||
if (data.data && data.data.text) {
|
||
analysisContent.value += data.data.text
|
||
await scrollToBottom()
|
||
}
|
||
break
|
||
|
||
case 'workflow_finished':
|
||
if (data.data && data.data.outputs && data.data.outputs.text) {
|
||
if (analysisContent.value !== data.data.outputs.text) {
|
||
analysisContent.value = data.data.outputs.text
|
||
await scrollToBottom()
|
||
}
|
||
currentThought.value = `分析完成 (用时 ${Math.round(data.data.elapsed_time)}秒)`
|
||
|
||
// 分析完成后保存结果
|
||
await saveAnalysisHistory()
|
||
}
|
||
break
|
||
|
||
case 'error':
|
||
const errorMessage = data.error || '未知错误'
|
||
setErrorMessage(errorMessage)
|
||
await scrollToBottom()
|
||
break
|
||
}
|
||
} catch (error) {
|
||
console.error('解析响应数据出错:', error)
|
||
setErrorMessage('解析响应数据时出错,请稍后重试')
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
setErrorMessage('抱歉,分析请求失败,请稍后重试')
|
||
} finally {
|
||
isAnalyzing.value = false
|
||
await scrollToBottom()
|
||
}
|
||
}
|
||
|
||
const resetView = () => {
|
||
showInitialView.value = true
|
||
symbolCode.value = ''
|
||
analysisContent.value = ''
|
||
currentThought.value = ''
|
||
isAnalyzing.value = false
|
||
// 重置为默认时间周期
|
||
selectedTimeframe.value = '1h'
|
||
}
|
||
|
||
// 清除输入
|
||
const clearInput = () => {
|
||
symbolCode.value = ''
|
||
analysisContent.value = ''
|
||
currentThought.value = ''
|
||
}
|
||
|
||
const copyAnalysis = async () => {
|
||
if (!analysisContent.value) return
|
||
|
||
try {
|
||
// 复制原始文本内容,而不是渲染后的HTML
|
||
await navigator.clipboard.writeText(analysisContent.value)
|
||
copySuccess.value = true
|
||
setTimeout(() => {
|
||
copySuccess.value = false
|
||
}, 2000)
|
||
} catch {
|
||
console.error('复制失败')
|
||
}
|
||
}
|
||
|
||
// 处理快速分析项的点击
|
||
const handleCommonAnalysis = (item: { code: string }) => {
|
||
symbolCode.value = item.code
|
||
handleAnalysis()
|
||
}
|
||
|
||
// 监听路由变化,重置视图
|
||
watch(
|
||
() => route.params.type,
|
||
() => {
|
||
resetView()
|
||
},
|
||
)
|
||
|
||
// 解析markdown内容
|
||
const parsedContent = computed(() => {
|
||
if (!analysisContent.value || analysisContent.value.startsWith('错误:')) {
|
||
return ''
|
||
}
|
||
|
||
// 处理markdown内容
|
||
let html = marked(analysisContent.value) as string
|
||
|
||
// 将表格包装在div中以提供更好的滚动支持
|
||
html = html.replace(/<table>/g, '<div class="table-container"><table>')
|
||
html = html.replace(/<\/table>/g, '</table></div>')
|
||
|
||
return html
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="universal-analysis-view">
|
||
<div class="content-container">
|
||
<!-- 初始视图 -->
|
||
<div v-if="showInitialView" class="initial-content">
|
||
<div class="header-section">
|
||
<h1 class="title">{{ pageTitle }}</h1>
|
||
<p class="description">{{ pageDescription }}</p>
|
||
</div>
|
||
|
||
<div class="search-section">
|
||
<div class="search-container">
|
||
<div class="input-wrapper">
|
||
<div class="input-container">
|
||
<div class="input-area">
|
||
<input
|
||
v-model="symbolCode"
|
||
type="text"
|
||
class="search-input"
|
||
:placeholder="inputPlaceholder"
|
||
@keydown="handleKeydown"
|
||
:disabled="isAnalyzing"
|
||
:maxlength="isStockMode ? 6 : undefined"
|
||
/>
|
||
<button
|
||
v-if="symbolCode.trim() || analysisContent"
|
||
class="clear-button"
|
||
@click="clearInput"
|
||
:disabled="isAnalyzing"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 加密货币模式下的时间周期选择 -->
|
||
<div v-if="!isStockMode" class="timeframe-selector">
|
||
<label class="timeframe-label">选择时间周期:</label>
|
||
<div class="timeframe-options">
|
||
<button
|
||
v-for="option in timeframeOptions"
|
||
:key="option.value"
|
||
class="timeframe-option"
|
||
:class="{ active: selectedTimeframe === option.value }"
|
||
@click="selectedTimeframe = option.value"
|
||
:disabled="isAnalyzing"
|
||
>
|
||
{{ option.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
class="analyze-button"
|
||
@click="handleAnalysis"
|
||
:disabled="isAnalyzing || !symbolCode.trim()"
|
||
>
|
||
{{ isAnalyzing ? '正在跳转...' : '开始分析' }}
|
||
</button>
|
||
</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 v-else class="analysis-view">
|
||
<div class="analysis-header">
|
||
<div class="target-info">
|
||
<span class="label">分析目标:</span>
|
||
<span class="value">{{ symbolCode.toUpperCase() }}</span>
|
||
<span v-if="!isStockMode" class="label timeframe-info">时间周期:</span>
|
||
<span v-if="!isStockMode" class="value">{{ timeframeLabel }}</span>
|
||
</div>
|
||
<div class="action-buttons" v-if="!isAnalyzing">
|
||
<button class="action-button" @click="resetView">
|
||
<svg
|
||
class="icon"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<polyline points="22 12 18 12 15 12"></polyline>
|
||
<path d="M11.5 3a17 17 0 0 0-6.5 2.5L2 8"></path>
|
||
<path d="M2 3v5h5"></path>
|
||
<path d="M13.5 21a17 17 0 0 0 6.5-2.5L22 16"></path>
|
||
<path d="M22 21v-5h-5"></path>
|
||
</svg>
|
||
<span>重新分析</span>
|
||
</button>
|
||
<button
|
||
class="action-button copy"
|
||
@click="copyAnalysis"
|
||
:class="{ success: copySuccess }"
|
||
>
|
||
<span v-if="!copySuccess">复制结果</span>
|
||
<span v-else>复制成功</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isAnalyzing" class="analysis-status">
|
||
<div class="simple-loader"></div>
|
||
<div class="status-text">
|
||
<div class="thought-text" v-if="currentThought">{{ currentThought }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="analysis-container"
|
||
ref="analysisContainer"
|
||
:class="{ 'fade-in': analysisContent }"
|
||
>
|
||
<div v-if="analysisContent.startsWith('错误:')" class="error-message">
|
||
{{ analysisContent.substring(4) }}
|
||
</div>
|
||
<div v-else class="analysis-content markdown-content" v-html="parsedContent"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:root {
|
||
--color-accent-rgb: 51, 85, 255; /* 假设accent颜色为#3355ff */
|
||
}
|
||
|
||
.universal-analysis-view {
|
||
min-height: 100vh;
|
||
height: 100vh;
|
||
background-color: var(--color-bg-primary);
|
||
padding: 1rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.content-container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
justify-content: center;
|
||
}
|
||
|
||
.initial-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 15vh;
|
||
gap: 1.5rem;
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.header-section {
|
||
text-align: center;
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.title {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
color: var(--color-text-primary);
|
||
margin-bottom: 0.5rem;
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.description {
|
||
font-size: 0.95rem;
|
||
color: var(--color-text-secondary);
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.search-section {
|
||
width: 100%;
|
||
max-width: 800px;
|
||
transition: all 0.5s ease;
|
||
padding: 0 1.5rem;
|
||
}
|
||
|
||
.search-container {
|
||
position: relative;
|
||
padding: 1.5rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.input-wrapper {
|
||
position: relative;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.input-container {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
min-height: 5.5rem;
|
||
border: 2px solid var(--color-accent);
|
||
border-radius: var(--border-radius);
|
||
background-color: var(--color-bg-primary);
|
||
padding: 0.75rem 1rem;
|
||
transition: all 0.3s ease;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.input-area {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
border: none;
|
||
background: none;
|
||
padding: 0;
|
||
font-size: 0.95rem;
|
||
color: var(--color-text-primary);
|
||
outline: none;
|
||
height: 2.5rem;
|
||
}
|
||
|
||
.clear-button {
|
||
position: absolute;
|
||
right: 0.5rem;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
background: none;
|
||
border: none;
|
||
padding: 0.25rem;
|
||
cursor: pointer;
|
||
color: var(--color-text-secondary);
|
||
opacity: 0.6;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.clear-button:hover:not(:disabled) {
|
||
opacity: 1;
|
||
}
|
||
|
||
.clear-button:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.clear-button svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.analyze-button {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: white;
|
||
background-color: var(--color-accent);
|
||
border: none;
|
||
border-radius: var(--border-radius);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.analyze-button:hover:not(:disabled) {
|
||
background-color: var(--color-accent-hover);
|
||
}
|
||
|
||
.analyze-button:disabled {
|
||
opacity: 0.7;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 分析视图样式 */
|
||
.analysis-view {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 4.5rem);
|
||
padding: 0.5rem;
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
|
||
.analysis-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1rem;
|
||
background-color: var(--color-bg-secondary);
|
||
border-radius: var(--border-radius);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.analysis-status {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding: 1rem;
|
||
text-align: left;
|
||
gap: 1rem;
|
||
margin-top: 1rem;
|
||
background: transparent;
|
||
border-radius: var(--border-radius);
|
||
}
|
||
|
||
.simple-loader {
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid var(--color-border);
|
||
border-top-color: var(--color-text-secondary);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.status-text {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.thought-text {
|
||
font-size: 0.95rem;
|
||
font-weight: 400;
|
||
color: var(--color-text-secondary);
|
||
position: relative;
|
||
padding-right: 0.5rem;
|
||
animation: none;
|
||
}
|
||
|
||
.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 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.target-info .label {
|
||
color: var(--color-text-secondary);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.target-info .value {
|
||
color: var(--color-accent);
|
||
font-weight: 500;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.action-button {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 1rem;
|
||
border: 1px solid var(--color-accent);
|
||
border-radius: var(--border-radius);
|
||
background: transparent;
|
||
color: var(--color-accent);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.action-button:hover {
|
||
background-color: var(--color-accent);
|
||
color: white;
|
||
}
|
||
|
||
.action-button.copy {
|
||
background-color: var(--color-accent);
|
||
color: white;
|
||
}
|
||
|
||
.action-button.copy:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.action-button.copy.success {
|
||
background-color: #4caf50;
|
||
border-color: #4caf50;
|
||
}
|
||
|
||
.action-button .icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
/* 快速分析列表样式 */
|
||
.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;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 0.9rem;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.common-analysis-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.common-analysis-item {
|
||
padding: 0.35rem 0.75rem;
|
||
font-size: 0.9rem;
|
||
color: var(--color-accent);
|
||
background-color: var(--color-bg-secondary);
|
||
border: 1px solid var(--color-accent);
|
||
border-radius: var(--border-radius);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.common-analysis-item:hover:not(:disabled) {
|
||
background-color: var(--color-accent);
|
||
color: white;
|
||
}
|
||
|
||
.common-analysis-item:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
border-color: var(--color-border);
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
/* 动画 */
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.universal-analysis-view {
|
||
padding: 0.75rem;
|
||
}
|
||
|
||
.analysis-view {
|
||
padding: 0.5rem;
|
||
margin-top: 4.5rem;
|
||
}
|
||
|
||
.analysis-header {
|
||
padding: 1rem;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.action-buttons {
|
||
width: 100%;
|
||
flex-direction: row;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.action-button {
|
||
flex: 1;
|
||
justify-content: center;
|
||
padding: 0.6rem;
|
||
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;
|
||
}
|
||
|
||
.analysis-status {
|
||
padding: 1rem;
|
||
margin-top: 0.75rem;
|
||
}
|
||
|
||
.simple-loader {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-width: 2px;
|
||
}
|
||
|
||
.thought-text {
|
||
font-size: 0.85rem;
|
||
}
|
||
}
|
||
|
||
@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 {
|
||
margin-bottom: 10vh;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.title {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.description {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.search-input {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.analyze-button {
|
||
font-size: 0.85rem;
|
||
padding: 0.6rem;
|
||
}
|
||
|
||
.action-buttons {
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.action-button {
|
||
padding: 0.5rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.search-container {
|
||
padding: 0.75rem;
|
||
}
|
||
|
||
.search-section {
|
||
padding: 0 0.75rem;
|
||
}
|
||
|
||
.common-analysis-section {
|
||
padding: 0 0.75rem;
|
||
}
|
||
|
||
.common-analysis-container {
|
||
padding: 0 0.75rem;
|
||
}
|
||
|
||
.common-analysis-item {
|
||
font-size: 0.85rem;
|
||
padding: 0.3rem 0.6rem;
|
||
}
|
||
|
||
.analysis-status {
|
||
padding: 0.75rem;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.simple-loader {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-width: 1.5px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.thought-text {
|
||
font-size: 0.8rem;
|
||
text-align: center;
|
||
}
|
||
}
|
||
|
||
.timeframe-selector {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: 0.75rem;
|
||
margin-top: 0.5rem;
|
||
align-items: flex-start;
|
||
width: 100%;
|
||
}
|
||
|
||
.timeframe-label {
|
||
font-size: 0.85rem;
|
||
color: var(--color-text-secondary);
|
||
margin-bottom: 0.5rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.timeframe-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.timeframe-option {
|
||
padding: 0.4rem 0.75rem;
|
||
font-size: 0.85rem;
|
||
background-color: var(--color-bg-secondary);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--border-radius);
|
||
color: var(--color-text-secondary);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.timeframe-option:hover:not(:disabled) {
|
||
border-color: var(--color-accent);
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
.timeframe-option.active {
|
||
background-color: var(--color-accent);
|
||
color: white !important;
|
||
border-color: var(--color-accent);
|
||
}
|
||
|
||
.timeframe-option:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.timeframe-info {
|
||
margin-left: 1rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.timeframe-options {
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.timeframe-option {
|
||
padding: 0.35rem 0.6rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.timeframe-label {
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.timeframe-options {
|
||
gap: 0.3rem;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.timeframe-option {
|
||
padding: 0.3rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
flex: none;
|
||
text-align: center;
|
||
}
|
||
|
||
.target-info {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
overflow-x: auto;
|
||
padding-bottom: 0.5rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.target-info .label,
|
||
.target-info .value {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.timeframe-option.active {
|
||
background-color: var(--color-accent);
|
||
color: white !important;
|
||
border-color: var(--color-accent);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* Markdown 样式 */
|
||
.markdown-content {
|
||
white-space: normal !important;
|
||
}
|
||
|
||
.markdown-content h1,
|
||
.markdown-content h2,
|
||
.markdown-content h3,
|
||
.markdown-content h4,
|
||
.markdown-content h5,
|
||
.markdown-content h6 {
|
||
margin-top: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
color: var(--color-text-primary);
|
||
font-size: 1.2rem; /* 统一所有标题的字体大小 */
|
||
}
|
||
|
||
.markdown-content h1 {
|
||
border-bottom: 1px solid var(--color-border);
|
||
padding-bottom: 0.3em;
|
||
}
|
||
|
||
.markdown-content h2 {
|
||
border-bottom: 1px solid var(--color-border);
|
||
padding-bottom: 0.3em;
|
||
}
|
||
|
||
.markdown-content h3 {
|
||
}
|
||
|
||
.markdown-content h4 {
|
||
}
|
||
|
||
.markdown-content h5 {
|
||
}
|
||
|
||
.markdown-content h6 {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.markdown-content p {
|
||
margin-top: 0;
|
||
margin-bottom: 1.5rem;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.markdown-content ul,
|
||
.markdown-content ol {
|
||
margin-top: 0;
|
||
margin-bottom: 1.5rem;
|
||
padding-left: 2rem;
|
||
}
|
||
|
||
.markdown-content li {
|
||
margin-bottom: 0.5rem;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.markdown-content li p {
|
||
margin-bottom: 0.7rem;
|
||
}
|
||
|
||
.markdown-content li:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.markdown-content blockquote {
|
||
padding: 0.5rem 1.5rem;
|
||
color: var(--color-text-secondary);
|
||
border-left: 0.3rem solid var(--color-border);
|
||
margin: 0 0 1.5rem;
|
||
}
|
||
|
||
.markdown-content blockquote p:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.markdown-content pre {
|
||
margin-top: 0;
|
||
margin-bottom: 1rem;
|
||
padding: 1rem;
|
||
overflow: auto;
|
||
font-size: 85%;
|
||
line-height: 1.45;
|
||
background-color: var(--color-bg-secondary);
|
||
border-radius: var(--border-radius);
|
||
}
|
||
|
||
.markdown-content code {
|
||
font-family:
|
||
SFMono-Regular,
|
||
Consolas,
|
||
Liberation Mono,
|
||
Menlo,
|
||
monospace;
|
||
font-size: 85%;
|
||
padding: 0.2em 0.4em;
|
||
margin: 0;
|
||
background-color: var(--color-bg-secondary);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.markdown-content pre code {
|
||
padding: 0;
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* 表格容器样式 */
|
||
.markdown-content .table-container {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.markdown-content table {
|
||
width: 100%;
|
||
margin-top: 0;
|
||
margin-bottom: 0;
|
||
border-spacing: 0;
|
||
border-collapse: collapse;
|
||
max-width: 100%;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
border: 1px solid var(--color-border);
|
||
overflow-x: auto;
|
||
display: block;
|
||
}
|
||
|
||
.markdown-content table th,
|
||
.markdown-content table td {
|
||
padding: 0.75rem 1rem;
|
||
border: 1px solid var(--color-border);
|
||
text-align: left;
|
||
min-width: 100px; /* 防止列过窄 */
|
||
vertical-align: top;
|
||
white-space: normal; /* 单元格内容可以换行 */
|
||
word-break: break-word;
|
||
}
|
||
|
||
.markdown-content table th {
|
||
font-weight: 600;
|
||
background-color: var(--color-bg-secondary);
|
||
white-space: nowrap; /* 表头不换行 */
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
.markdown-content table tr {
|
||
background-color: var(--color-bg-primary);
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
|
||
.markdown-content table tr:nth-child(2n) {
|
||
background-color: var(--color-bg-secondary);
|
||
}
|
||
|
||
.markdown-content a {
|
||
color: var(--color-accent);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.markdown-content a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.markdown-content img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
display: block;
|
||
margin: 1rem auto;
|
||
}
|
||
|
||
.markdown-content hr {
|
||
height: 0.25em;
|
||
padding: 0;
|
||
margin: 1.5rem 0;
|
||
background-color: var(--color-border);
|
||
border: 0;
|
||
}
|
||
|
||
/* 响应式样式 */
|
||
@media (max-width: 768px) {
|
||
.markdown-content h1,
|
||
.markdown-content h2,
|
||
.markdown-content h3,
|
||
.markdown-content h4,
|
||
.markdown-content h5,
|
||
.markdown-content h6 {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.markdown-content table {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.markdown-content table th,
|
||
.markdown-content table td {
|
||
padding: 0.6rem 0.75rem;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.markdown-content h1,
|
||
.markdown-content h2,
|
||
.markdown-content h3,
|
||
.markdown-content h4,
|
||
.markdown-content h5,
|
||
.markdown-content h6 {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.markdown-content h1,
|
||
.markdown-content h2 {
|
||
padding-bottom: 0.2em;
|
||
}
|
||
|
||
.markdown-content table {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.markdown-content table th,
|
||
.markdown-content table td {
|
||
padding: 0.5rem 0.6rem;
|
||
min-width: 80px;
|
||
}
|
||
}
|
||
</style>
|