Files
DinQuant/quantdinger_vue/src/views/ai-analysis/index.vue
T
TIANHE d8e093bd60 new
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
2025-12-30 18:47:42 +08:00

2463 lines
66 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.
<template>
<div class="ai-analysis-container" :class="{ 'theme-dark': isDarkTheme }" :style="{ '--primary-color': primaryColor }">
<!-- 主内容区域 -->
<div class="main-content">
<!-- 分析区域 -->
<div class="analysis-section">
<!-- 顶部标题和标的选择 -->
<div class="analysis-header">
<div class="header-content">
<div class="center-header-info">
<div class="logo-section">
<a-icon type="thunderbolt" theme="twoTone" :twoToneColor="primaryColor" class="logo-icon" />
<span class="logo-text">QUANT<span class="ant-btn-link">DINGER</span> AI LAB</span>
</div>
<div class="status-bar">
<div class="status-item">
<span class="label">SYSTEM:</span>
<span class="value online">{{ $t('ai-analysis.system.online') || 'ONLINE' }}</span>
</div>
<div class="status-item">
<span class="label">{{ $t('ai-analysis.system.agents') || 'AGENTS' }}:</span>
<span class="ant-btn-link">13 {{ $t('ai-analysis.system.active') || 'ACTIVE' }}</span>
</div>
<div class="status-item">
<span class="label">{{ $t('ai-analysis.system.stage') || 'STAGE' }}:</span>
<span class="ant-btn-link">{{ analyzing ? 'RUNNING' : 'IDLE' }}</span>
</div>
</div>
</div>
<div class="symbol-selector-wrapper">
<a-select
v-model="selectedSymbol"
:placeholder="$t('dashboard.analysis.empty.selectSymbol')"
size="large"
show-search
allow-clear
:filter-option="filterSymbolOption"
@change="handleSymbolChange"
@search="handleSymbolSearch"
:open="symbolSearchOpen"
@dropdownVisibleChange="handleDropdownVisibleChange"
class="symbol-selector"
>
<!-- 当没有自选股且用户输入时显示提示选项 -->
<a-select-option
v-for="stock in (watchlist || [])"
:key="`${stock.market}-${stock.symbol}`"
:value="`${stock.market}:${stock.symbol}`"
>
<span class="symbol-option">
<a-tag :color="getMarketColor(stock.market)" style="margin-right: 8px;">
{{ getMarketName(stock.market) }}
</a-tag>
<strong>{{ stock.symbol }}</strong>
<span v-if="stock.name" style="color: #999; margin-left: 8px;">{{ stock.name }}</span>
</span>
</a-select-option>
<a-select-option
key="add-stock-option"
value="__add_stock_option__"
class="add-stock-option"
>
<div style="width: 100%; text-align: center; padding: 4px 0; color: #1890ff; cursor: pointer;">
<a-icon type="plus" style="margin-right: 4px;" />
<span>{{ $t('dashboard.analysis.watchlist.add') }}</span>
</div>
</a-select-option>
</a-select>
<!-- 模型选择器 -->
<a-select
v-model="selectedModel"
:placeholder="$t('dashboard.analysis.selectModel')"
size="large"
style="width: 200px;"
class="model-selector"
>
<a-select-option v-for="model in modelOptions" :key="model.value" :value="model.value">
{{ model.label }}
</a-select-option>
</a-select>
<a-button
type="primary"
size="large"
icon="thunderbolt"
@click="startMultiAnalysis"
:loading="analyzing"
:disabled="!selectedSymbol"
class="analyze-button"
>
<span>{{ $t('dashboard.analysis.startAnalysis') }}</span>
</a-button>
<a-button
type="default"
size="large"
icon="history"
@click="showHistoryModal = true; loadHistoryList()"
class="history-button"
style="margin-left: 12px;"
>
<span>{{ $t('dashboard.analysis.history') }}</span>
</a-button>
</div>
</div>
</div>
<!-- 空状态和加载展示 (Metaverse Component) -->
<metaverse-analysis
:symbol="selectedSymbol"
:analyzing="analyzing"
:taskId="currentTaskId"
:analysisResults="analysisResults"
/>
</div>
<!-- 右侧自选股区域 -->
<div class="watchlist-section">
<div class="section-header">
<h3>
<a-icon type="star" />
<span>{{ $t('dashboard.analysis.watchlist.title') }}</span>
</h3>
<a-button type="link" size="small" @click="showAddStockModal = true">
<a-icon type="plus" />
<span>{{ $t('dashboard.analysis.watchlist.add') }}</span>
</a-button>
</div>
<div class="watchlist-container">
<div
v-for="stock in (watchlist || [])"
:key="`${stock.market}-${stock.symbol}`"
class="watchlist-item"
>
<div class="stock-info">
<div class="stock-symbol">{{ stock.symbol }}</div>
<div class="stock-name">{{ stock.name || stock.symbol }}</div>
</div>
<div class="stock-price">
<div class="price-value" v-if="stock.price > 0">
{{ getCurrencySymbol(stock.market) }}{{ formatNumber(stock.price) }}
</div>
<div class="price-value" v-else>
<a-spin size="small" />
</div>
<div class="price-change" :class="stock.change >= 0 ? 'positive' : 'negative'" v-if="stock.price > 0">
<span :class="stock.change >= 0 ? 'up' : 'down'">
{{ stock.change >= 0 ? '▲' : '▼' }}{{ Math.abs(stock.changePercent).toFixed(2) }}%
</span>
</div>
</div>
<a-icon type="close" class="remove-icon" @click.stop="removeFromWatchlist(stock.symbol, stock.market)" />
</div>
<div v-if="!watchlist || watchlist.length === 0" class="empty-watchlist">
<a-empty :description="$t('dashboard.analysis.empty.noWatchlist')" :image="false">
<a-button type="primary" @click="showAddStockModal = true">
<a-icon type="plus" />
{{ $t('dashboard.analysis.watchlist.addStock') }}
</a-button>
</a-empty>
</div>
</div>
</div>
</div>
<!-- 添加股票弹窗 -->
<a-modal
:title="$t('dashboard.analysis.modal.addStock.title')"
:visible="showAddStockModal"
@ok="handleAddStock"
@cancel="handleCloseAddStockModal"
:confirmLoading="addingStock"
width="600px"
:okText="$t('dashboard.analysis.modal.addStock.confirm')"
:cancelText="$t('dashboard.analysis.modal.addStock.cancel')"
>
<div class="add-stock-modal-content">
<!-- Tab标签 -->
<a-tabs v-model="selectedMarketTab" @change="handleMarketTabChange" class="market-tabs">
<a-tab-pane
v-for="marketType in marketTypes"
:key="marketType.value"
:tab="$t(marketType.i18nKey || `dashboard.analysis.market.${marketType.value}`)"
>
</a-tab-pane>
</a-tabs>
<!-- 搜索/输入框整合搜索和手动输入 -->
<div class="symbol-search-section">
<a-input-search
v-model="symbolSearchKeyword"
:placeholder="$t('dashboard.analysis.modal.addStock.searchOrInputPlaceholder')"
@search="handleSearchOrInput"
@change="handleSymbolSearchInput"
:loading="searchingSymbols"
size="large"
allow-clear
>
<a-button slot="enterButton" type="primary" icon="search">
{{ $t('dashboard.analysis.modal.addStock.search') }}
</a-button>
</a-input-search>
</div>
<!-- 搜索结果 -->
<div v-if="symbolSearchResults.length > 0" class="search-results-section">
<div class="section-title">
<a-icon type="search" style="margin-right: 4px;" />
{{ $t('dashboard.analysis.modal.addStock.searchResults') }}
</div>
<a-list
:data-source="symbolSearchResults"
:loading="searchingSymbols"
size="small"
class="symbol-list"
>
<a-list-item slot="renderItem" slot-scope="item" class="symbol-list-item" @click="selectSymbol(item)">
<a-list-item-meta>
<template slot="title">
<div class="symbol-item-content">
<span class="symbol-code">{{ item.symbol }}</span>
<span class="symbol-name">{{ item.name }}</span>
<a-tag v-if="item.exchange" size="small" color="blue" style="margin-left: 8px;">
{{ item.exchange }}
</a-tag>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</div>
<!-- 热门标的 -->
<div class="hot-symbols-section">
<div class="section-title">
<a-icon type="fire" style="color: #ff4d4f; margin-right: 4px;" />
{{ $t('dashboard.analysis.modal.addStock.hotSymbols') }}
</div>
<a-spin :spinning="loadingHotSymbols">
<a-list
v-if="hotSymbols.length > 0"
:data-source="hotSymbols"
size="small"
class="symbol-list"
>
<a-list-item slot="renderItem" slot-scope="item" class="symbol-list-item" @click="selectSymbol(item)">
<a-list-item-meta>
<template slot="title">
<div class="symbol-item-content">
<span class="symbol-code">{{ item.symbol }}</span>
<span class="symbol-name">{{ item.name }}</span>
<a-tag v-if="item.exchange" size="small" color="orange" style="margin-left: 8px;">
{{ item.exchange }}
</a-tag>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
<a-empty v-else :description="$t('dashboard.analysis.modal.addStock.noHotSymbols')" :image="false" />
</a-spin>
</div>
<!-- 选中的标的显示 -->
<div v-if="selectedSymbolForAdd" class="selected-symbol-section">
<a-alert
:message="$t('dashboard.analysis.modal.addStock.selectedSymbol')"
type="info"
show-icon
closable
@close="selectedSymbolForAdd = null"
>
<template slot="description">
<div class="selected-symbol-info">
<a-tag :color="getMarketColor(selectedSymbolForAdd.market)" style="margin-right: 8px;">
{{ $t(`dashboard.analysis.market.${selectedSymbolForAdd.market}`) }}
</a-tag>
<strong>{{ selectedSymbolForAdd.symbol }}</strong>
<span v-if="selectedSymbolForAdd.name" style="color: #999; margin-left: 8px;">{{ selectedSymbolForAdd.name }}</span>
<span v-else style="color: #999; margin-left: 8px; font-style: italic;">{{ $t('dashboard.analysis.modal.addStock.nameWillBeFetched') }}</span>
</div>
</template>
</a-alert>
</div>
</div>
</a-modal>
<!-- 历史分析列表弹窗 -->
<a-modal
:title="$t('dashboard.analysis.modal.history.title')"
:visible="showHistoryModal"
@cancel="showHistoryModal = false"
:footer="null"
width="800px"
:bodyStyle="{ maxHeight: '60vh', overflowY: 'auto' }"
>
<a-spin :spinning="historyLoading">
<a-list
:data-source="historyList"
:pagination="{
current: historyPage,
pageSize: historyPageSize,
total: historyTotal,
onChange: (page) => { historyPage = page; loadHistoryList() },
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
onShowSizeChange: (current, size) => { historyPageSize = size; historyPage = 1; loadHistoryList() }
}"
>
<a-list-item slot="renderItem" slot-scope="item">
<a-list-item-meta>
<template slot="title">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<a-tag :color="getMarketColor(item.market)" style="margin-right: 8px;">
{{ getMarketName(item.market) }}
</a-tag>
<strong>{{ item.symbol }}</strong>
<a-tag :color="getStatusColor(item.status)" style="margin-left: 12px;">
{{ getStatusText(item.status) }}
</a-tag>
</div>
<div>
<a-button
v-if="item.has_result"
type="link"
size="small"
icon="eye"
@click="viewHistoryResult(item)"
>
{{ $t('dashboard.analysis.modal.history.viewResult') }}
</a-button>
<!-- <span style="color: #999; font-size: 12px; margin-left: 12px;">
{{ formatTime(item.createtime) }}
</span> -->
</div>
</div>
</template>
<template slot="description">
<div v-if="item.error_message" style="color: #ff4d4f; font-size: 12px;">
{{ $t('dashboard.analysis.modal.history.error') }}: {{ item.error_message }}
</div>
<div v-else-if="item.completetime" style="color: #999; font-size: 12px;">
{{ $t('dashboard.analysis.modal.history.completeTime') }}: {{ formatTime(item.completetime) }}
</div>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
<a-empty v-if="!historyLoading && historyList.length === 0" :description="$t('dashboard.analysis.empty.noHistory')" />
</a-spin>
</a-modal>
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
import { getUserInfo } from '@/api/login'
import { getWatchlist, addWatchlist, removeWatchlist, getWatchlistPrices, multiAnalysis, getAnalysisTaskStatus, getAnalysisHistoryList, getConfig, getMarketTypes, searchSymbols, getHotSymbols } from '@/api/market'
import MetaverseAnalysis from './components/index'
import { DEFAULT_AI_MODEL_MAP, mergeModelMaps, modelMapToOptions } from '@/config/aiModels'
export default {
name: 'Analysis',
components: {
MetaverseAnalysis
},
data () {
return {
watchlistPriceTimer: null, // 自选股价格刷新定时器
localUserInfo: {}, // 本地用户信息(从 API 获取)
loadingUserInfo: false,
// Local-only mode: single user (id=1). Keep a default so watchlist works even
// when user info is loaded asynchronously.
userId: 1,
// 自选股数据
watchlist: [],
loadingWatchlist: false,
showAddStockModal: false,
addingStock: false,
stockForm: this.$form.createForm(this),
// 多维度分析相关
selectedSymbol: undefined, // 当前选中的标的 (格式: "market:symbol")undefined 时会显示 placeholder
analyzing: false, // 是否正在分析
currentTaskId: null, // 当前任务ID
taskStatusTimer: null, // 任务状态轮询定时器
symbolSearchValue: '', // 标的选择器搜索输入值
symbolSearchOpen: false, // 标的选择器下拉框是否打开
// 模型选择
selectedModel: 'openai/gpt-4o-mini',
modelOptions: modelMapToOptions(DEFAULT_AI_MODEL_MAP),
analysisResults: {
overview: null,
fundamental: null,
technical: null,
news: null,
sentiment: null,
risk: null,
debate: null,
trader_decision: null,
risk_debate: null,
final_decision: null
},
// 历史分析列表
showHistoryModal: false,
historyList: [],
historyLoading: false,
historyPage: 1,
historyPageSize: 20,
historyTotal: 0,
// 股票类型列表
marketTypes: [],
// 添加股票弹窗相关
selectedMarketTab: '', // 当前选中的市场类型tab
symbolSearchKeyword: '', // 搜索关键词
symbolSearchResults: [], // 搜索结果
searchingSymbols: false, // 是否正在搜索
hotSymbols: [], // 热门标的列表
loadingHotSymbols: false, // 是否正在加载热门标的
selectedSymbolForAdd: null, // 选中的标的(用于添加)
searchTimer: null, // 搜索防抖定时器
hasSearched: false // 是否已经搜索过(用于显示无结果提示)
}
},
computed: {
...mapGetters(['userInfo']),
...mapState({
navTheme: state => state.app.theme,
primaryColor: state => state.app.color || '#1890ff'
}),
// 判断是否为暗黑主题
isDarkTheme () {
return this.navTheme === 'dark' || this.navTheme === 'realdark'
},
storeUserInfo () {
return this.userInfo || {}
},
// 合并本地和 store 中的用户信息
mergedUserInfo () {
return this.localUserInfo && this.localUserInfo.email ? this.localUserInfo : this.storeUserInfo
},
userAvatar () {
return this.mergedUserInfo?.avatar || ''
},
displayUserInfo () {
return this.mergedUserInfo
}
},
created () {
this.loadUserInfo()
this.loadConfig()
// Local-only mode: load watchlist immediately (userId defaults to 1).
// This also prevents a blank watchlist if user info fetch is slow/fails.
this.loadWatchlist()
},
mounted () {
// 启动自选股价格定时刷新
this.startWatchlistPriceRefresh()
},
beforeDestroy () {
if (this.watchlistPriceTimer) {
clearInterval(this.watchlistPriceTimer)
}
if (this.taskStatusTimer) {
clearInterval(this.taskStatusTimer)
}
},
methods: {
toggleSidebar () {
// this.sidebarCollapsed = !this.sidebarCollapsed
},
// 多维度分析相关方法
filterSymbolOption (input, option) {
const value = option.componentOptions?.propsData?.value || ''
// 如果是提示选项或添加按钮,始终显示
if (value === '__empty_watchlist_hint__' || value === '__add_stock_option__') {
return true
}
return value.toLowerCase().includes(input.toLowerCase())
},
handleSymbolSearch (value) {
this.symbolSearchValue = value
// 如果没有自选股且用户输入了内容,打开下拉框
if (this.watchlist.length === 0 && value) {
this.symbolSearchOpen = true
}
},
handleDropdownVisibleChange (open) {
this.symbolSearchOpen = open
// 如果关闭下拉框,清空搜索值
if (!open) {
this.symbolSearchValue = ''
}
},
handleSymbolChange (value) {
// 如果是提示选项,不处理
if (value === '__empty_watchlist_hint__') {
return
}
// 如果是添加股票选项
if (value === '__add_stock_option__') {
this.showAddStockModal = true
// 重置选中项,避免显示内部值
this.$nextTick(() => {
this.selectedSymbol = undefined
})
return
}
this.selectedSymbol = value
// 切换标的时清空分析结果
this.analysisResults = {
overview: null,
fundamental: null,
technical: null,
news: null,
sentiment: null,
risk: null,
debate: null,
trader_decision: null,
risk_debate: null,
final_decision: null
}
},
getMarketColor (market) {
const colors = {
'USStock': 'green',
'Crypto': 'purple',
'Forex': 'gold',
'Futures': 'cyan',
'AShare': 'blue',
'HShare': 'orange'
}
return colors[market] || 'default'
},
// 获取货币符号
getCurrencySymbol (market) {
// 美股、加密货币、外汇、期货:美元
// A股、港股:人民币
const dollarMarkets = ['USStock', 'Crypto', 'Forex', 'Futures']
return dollarMarkets.includes(market) ? '$' : '¥'
},
async startMultiAnalysis () {
if (!this.selectedSymbol) {
this.$message.warning(this.$t('dashboard.analysis.message.selectSymbol'))
return
}
this.analyzing = true
const [market, symbol] = this.selectedSymbol.split(':')
// 获取当前语言设置
const language = this.$store.getters.lang || 'zh-CN'
try {
// 调用后端API创建分析任务(异步)
// 可以从配置或用户设置中获取 use_multi_agent,默认使用多智能体模式
const useMultiAgent = this.$store.getters.useMultiAgent !== false // 默认启用多智能体模式
const res = await multiAnalysis({
userid: this.userId,
market: market,
symbol: symbol,
language: language,
use_multi_agent: useMultiAgent,
model: this.selectedModel // 传递选中的模型
})
if (res && res.code === 1 && res.data) {
// 检查是否是缓存数据(直接返回分析结果)
if (res.data.overview || res.data.fundamental || res.data.technical) {
// 这是缓存数据,直接显示结果
this.analysisResults = {
overview: res.data.overview || null,
fundamental: res.data.fundamental || null,
technical: res.data.technical || null,
news: res.data.news || null,
sentiment: res.data.sentiment || null,
risk: res.data.risk || null,
debate: res.data.debate || null,
trader_decision: res.data.trader_decision || null,
risk_debate: res.data.risk_debate || null,
final_decision: res.data.final_decision || null
}
this.$message.success(this.$t('dashboard.analysis.message.analysisCompleteCache'))
this.analyzing = false
} else if (res.data.task_id) {
// 这是新任务,需要轮询
this.currentTaskId = Number(res.data.task_id) || null
this.$message.info(this.$t('dashboard.analysis.message.taskCreated'))
// 开始轮询任务状态
this.startTaskStatusPolling()
} else {
throw new Error('返回数据格式错误')
}
} else {
throw new Error(res?.msg || '创建分析任务失败')
}
} catch (error) {
// 如果错误信息包含QDT相关提示,直接显示;否则显示默认错误
const errorMsg = error?.response?.data?.msg || error?.message || this.$t('dashboard.analysis.message.analysisFailed')
this.$message.error(errorMsg)
this.analyzing = false
}
},
// 开始轮询任务状态
startTaskStatusPolling () {
if (this.taskStatusTimer) {
clearInterval(this.taskStatusTimer)
}
this.taskStatusTimer = setInterval(async () => {
if (!this.currentTaskId) {
clearInterval(this.taskStatusTimer)
return
}
try {
const res = await getAnalysisTaskStatus({
task_id: this.currentTaskId
})
if (res && res.code === 1 && res.data) {
const task = res.data
if (task.status === 'completed') {
// 任务完成,更新分析结果
this.analysisResults = {
overview: task.result.overview || null,
fundamental: task.result.fundamental || null,
technical: task.result.technical || null,
news: task.result.news || null,
sentiment: task.result.sentiment || null,
risk: task.result.risk || null,
debate: task.result.debate || null,
trader_decision: task.result.trader_decision || null,
risk_debate: task.result.risk_debate || null,
final_decision: task.result.final_decision || null
}
this.$message.success(this.$t('dashboard.analysis.message.analysisComplete'))
this.analyzing = false
clearInterval(this.taskStatusTimer)
this.currentTaskId = null
} else if (task.status === 'failed') {
// 任务失败
this.$message.error(task.error_message || '分析失败')
this.analyzing = false
clearInterval(this.taskStatusTimer)
this.currentTaskId = null
}
// processing 状态继续轮询
}
} catch (error) {
// 继续轮询,不中断
}
}, 2000) // 每2秒轮询一次
},
// 加载历史分析列表
async loadHistoryList () {
if (!this.userId) return
this.historyLoading = true
try {
const res = await getAnalysisHistoryList({
userid: this.userId,
page: this.historyPage,
pagesize: this.historyPageSize
})
if (res && res.code === 1 && res.data) {
this.historyList = res.data.list || []
this.historyTotal = res.data.total || 0
}
} catch (error) {
this.$message.error('加载历史记录失败')
} finally {
this.historyLoading = false
}
},
// 查看历史分析结果
async viewHistoryResult (task) {
if (task.status !== 'completed') {
this.$message.warning(this.$t('dashboard.analysis.status.processing'))
return
}
try {
const res = await getAnalysisTaskStatus({
task_id: task.id
})
if (res && res.code === 1 && res.data && res.data.result) {
// 设置选中的标的
this.selectedSymbol = `${res.data.market}:${res.data.symbol}`
// 更新分析结果
this.analysisResults = {
overview: res.data.result.overview || null,
fundamental: res.data.result.fundamental || null,
technical: res.data.result.technical || null,
news: res.data.result.news || null,
sentiment: res.data.result.sentiment || null,
risk: res.data.result.risk || null,
debate: res.data.result.debate || null,
trader_decision: res.data.result.trader_decision || null,
risk_debate: res.data.result.risk_debate || null,
final_decision: res.data.result.final_decision || null
}
// 关闭历史列表弹窗
this.showHistoryModal = false
this.$message.success(this.$t('dashboard.analysis.message.analysisComplete'))
}
} catch (error) {
this.$message.error(this.$t('dashboard.analysis.message.analysisFailed'))
}
},
// 格式化时间
formatTime (timestamp) {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
return date.toLocaleString('zh-CN')
},
// 获取状态标签颜色
getStatusColor (status) {
const colors = {
'pending': 'orange',
'processing': 'blue',
'completed': 'green',
'failed': 'red'
}
return colors[status] || 'default'
},
// 获取状态文本
getStatusText (status) {
const statusMap = {
'pending': 'dashboard.analysis.status.pending',
'processing': 'dashboard.analysis.status.processing',
'completed': 'dashboard.analysis.status.completed',
'failed': 'dashboard.analysis.status.failed'
}
const key = statusMap[status]
return key ? this.$t(key) : status
},
async loadUserInfo () {
this.loadingUserInfo = true
try {
// 先从 store 获取
if (this.storeUserInfo && this.storeUserInfo.email) {
this.localUserInfo = this.storeUserInfo
this.userId = this.storeUserInfo.id
this.loadingUserInfo = false
// 加载数据
this.loadWatchlist()
return
}
// 如果 store 中没有,从 API 获取
const res = await getUserInfo()
if (res && res.code === 1 && res.data) {
this.localUserInfo = res.data
this.userId = res.data.id
// 更新 store
this.$store.commit('SET_INFO', res.data)
// 加载数据
this.loadWatchlist()
}
} catch (error) {
} finally {
this.loadingUserInfo = false
}
},
// 加载自选股
async loadWatchlist () {
if (!this.userId) return
this.loadingWatchlist = true
try {
const res = await getWatchlist({ userid: this.userId })
if (res && res.code === 1 && res.data) {
this.watchlist = res.data.map(item => ({
...item,
price: 0,
change: 0,
changePercent: 0
}))
// 加载价格数据
await this.loadWatchlistPrices()
}
} catch (error) {
this.$message.error(this.$t('dashboard.analysis.message.addStockFailed'))
} finally {
this.loadingWatchlist = false
}
},
// 加载自选股价格
async loadWatchlistPrices () {
if (!this.watchlist || this.watchlist.length === 0) return
try {
// 构建请求数据
const watchlistData = this.watchlist.map(item => ({
market: item.market,
symbol: item.symbol
}))
// 通过 PHP 接口调用 Python API 获取价格
const res = await getWatchlistPrices({
watchlist: watchlistData
})
if (res && res.code === 1 && res.data) {
const priceMap = {}
res.data.forEach(item => {
priceMap[`${item.market}-${item.symbol}`] = item
})
// 更新价格数据
this.watchlist = this.watchlist.map(item => {
const key = `${item.market}-${item.symbol}`
const priceData = priceMap[key]
if (priceData) {
return {
...item,
price: priceData.price || 0,
change: priceData.change || 0,
changePercent: priceData.changePercent || 0
}
}
return item
})
}
} catch (error) {
// 不显示错误提示,避免干扰用户体验
}
},
// 启动自选股价格定时刷新
startWatchlistPriceRefresh () {
// 每30秒刷新一次价格
this.watchlistPriceTimer = setInterval(() => {
if (this.watchlist && this.watchlist.length > 0) {
this.loadWatchlistPrices()
}
}, 30000)
// 立即加载一次
if (this.watchlist && this.watchlist.length > 0) {
this.loadWatchlistPrices()
}
},
// 添加自选股
async handleAddStock () {
let market = ''
let symbol = ''
let name = ''
// 检查是否选中了标的(从数据库选择或手动输入)
if (this.selectedSymbolForAdd) {
market = this.selectedSymbolForAdd.market
symbol = this.selectedSymbolForAdd.symbol.toUpperCase()
name = this.selectedSymbolForAdd.name || ''
} else if (this.symbolSearchKeyword && this.symbolSearchKeyword.trim()) {
// 如果没有选中,但搜索框有输入,使用搜索框的值
if (!this.selectedMarketTab) {
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
return
}
market = this.selectedMarketTab
symbol = this.symbolSearchKeyword.trim().toUpperCase()
name = ''
} else {
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectOrEnterSymbol'))
return
}
this.addingStock = true
try {
const res = await addWatchlist({
userid: this.userId,
market: market,
symbol: symbol,
name: name
})
if (res && res.code === 1) {
this.$message.success(this.$t('dashboard.analysis.message.addStockSuccess'))
this.handleCloseAddStockModal()
// 重新加载自选股
await this.loadWatchlist()
} else {
this.$message.error(res?.msg || this.$t('dashboard.analysis.message.addStockFailed'))
}
} catch (error) {
// 如果错误信息包含QDT相关提示,直接显示;否则显示默认错误
const errorMsg = error?.response?.data?.msg || error?.message || this.$t('dashboard.analysis.message.addStockFailed')
this.$message.error(errorMsg)
} finally {
this.addingStock = false
}
},
// 关闭添加股票弹窗
handleCloseAddStockModal () {
this.showAddStockModal = false
this.selectedSymbolForAdd = null
this.symbolSearchKeyword = ''
this.symbolSearchResults = []
this.hasSearched = false
this.selectedMarketTab = this.marketTypes.length > 0 ? this.marketTypes[0].value : ''
},
// 市场类型Tab切换
handleMarketTabChange (activeKey) {
this.selectedMarketTab = activeKey
this.symbolSearchKeyword = ''
this.symbolSearchResults = []
this.selectedSymbolForAdd = null
this.hasSearched = false
// 加载该市场类型的热门标的
this.loadHotSymbols(activeKey)
},
// 搜索标的输入变化(防抖)
handleSymbolSearchInput (e) {
const keyword = e.target.value
this.symbolSearchKeyword = keyword
// 清除之前的定时器
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
// 如果关键词为空,清空搜索结果和状态
if (!keyword || keyword.trim() === '') {
this.symbolSearchResults = []
this.hasSearched = false
this.selectedSymbolForAdd = null
return
}
// 防抖:500ms后执行搜索
this.searchTimer = setTimeout(() => {
this.searchSymbolsInModal(keyword)
}, 500)
},
// 搜索或直接添加(整合逻辑)
handleSearchOrInput (keyword) {
if (!keyword || !keyword.trim()) {
return
}
if (!this.selectedMarketTab) {
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
return
}
// 如果有搜索结果,不处理(让用户选择)
if (this.symbolSearchResults.length > 0) {
return
}
// 如果没有搜索结果,直接添加
if (this.hasSearched && this.symbolSearchResults.length === 0) {
this.handleDirectAdd()
} else {
// 执行搜索
this.searchSymbolsInModal(keyword)
}
},
// 搜索标的(在添加股票弹窗中)
async searchSymbolsInModal (keyword) {
if (!keyword || keyword.trim() === '') {
this.symbolSearchResults = []
this.hasSearched = false
return
}
if (!this.selectedMarketTab) {
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
return
}
this.searchingSymbols = true
this.hasSearched = true
try {
const res = await searchSymbols({
market: this.selectedMarketTab,
keyword: keyword.trim(),
limit: 20
})
if (res && res.code === 1 && res.data && res.data.length > 0) {
this.symbolSearchResults = res.data
} else {
// 搜索无结果,不报错,允许直接添加
this.symbolSearchResults = []
// 自动设置为手动输入模式
this.selectedSymbolForAdd = {
market: this.selectedMarketTab,
symbol: keyword.trim().toUpperCase(),
name: '' // 名称由后端通过API获取
}
}
} catch (error) {
// 搜索失败也不报错,允许直接添加
this.symbolSearchResults = []
this.selectedSymbolForAdd = {
market: this.selectedMarketTab,
symbol: keyword.trim().toUpperCase(),
name: '' // 名称由后端通过API获取
}
} finally {
this.searchingSymbols = false
}
},
// 直接添加(搜索无结果时)
handleDirectAdd () {
if (!this.symbolSearchKeyword || !this.symbolSearchKeyword.trim()) {
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseEnterSymbol'))
return
}
if (!this.selectedMarketTab) {
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
return
}
// 设置选中的标的(手动输入,名称会在后端获取)
this.selectedSymbolForAdd = {
market: this.selectedMarketTab,
symbol: this.symbolSearchKeyword.trim().toUpperCase(),
name: '' // 名称由后端通过API获取
}
},
// 选择标的
selectSymbol (symbol) {
this.selectedSymbolForAdd = {
market: symbol.market,
symbol: symbol.symbol,
name: symbol.name || symbol.symbol
}
},
// 加载热门标的
async loadHotSymbols (market) {
if (!market) {
market = this.selectedMarketTab || (this.marketTypes.length > 0 ? this.marketTypes[0].value : '')
}
if (!market) {
return
}
this.loadingHotSymbols = true
try {
const res = await getHotSymbols({
market: market,
limit: 10
})
if (res && res.code === 1 && res.data) {
this.hotSymbols = res.data
} else {
this.hotSymbols = []
}
} catch (error) {
this.hotSymbols = []
} finally {
this.loadingHotSymbols = false
}
},
// 删除自选股
async removeFromWatchlist (symbol, market) {
if (!this.userId) return
try {
const res = await removeWatchlist({
userid: this.userId,
symbol: symbol
})
if (res && res.code === 1) {
this.$message.success(this.$t('dashboard.analysis.message.removeStockSuccess'))
// 重新加载自选股
await this.loadWatchlist()
} else {
this.$message.error(res?.msg || this.$t('dashboard.analysis.message.removeStockFailed'))
}
} catch (error) {
this.$message.error(this.$t('dashboard.analysis.message.removeStockFailed'))
}
},
// 获取市场名称
getMarketName (market) {
return this.$t(`dashboard.analysis.market.${market}`) || market
},
// 格式化数字
formatNumber (num) {
if (typeof num === 'string') {
return num
}
return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
},
// 加载配置
async loadConfig () {
try {
const res = await getConfig()
if (res && res.code === 1 && res.data && res.data.models) {
const mergedModels = mergeModelMaps(DEFAULT_AI_MODEL_MAP, res.data.models)
this.modelOptions = modelMapToOptions(mergedModels)
// 如果当前选中的模型不在列表中,且列表不为空,则默认选中第一个
if (this.modelOptions.length > 0 && !this.modelOptions.find(m => m.value === this.selectedModel)) {
this.selectedModel = this.modelOptions[0].value
}
}
} catch (error) {
}
// 加载股票类型列表
await this.loadMarketTypes()
},
// 加载股票类型列表
async loadMarketTypes () {
try {
const res = await getMarketTypes()
if (res && res.code === 1 && res.data && Array.isArray(res.data)) {
// 如果返回的是数组格式 [{value: 'AShare', i18nKey: 'dashboard.analysis.market.AShare'}, ...]
this.marketTypes = res.data.map(item => ({
value: item.value,
i18nKey: item.i18nKey || `dashboard.analysis.market.${item.value}`
}))
} else if (res && res.code === 1 && res.data && typeof res.data === 'object') {
// 如果返回的是对象格式 {AShare: 'A股', USStock: '美股', ...}(兼容旧格式)
this.marketTypes = Object.keys(res.data).map(key => ({
value: key,
i18nKey: `dashboard.analysis.market.${key}`
}))
} else {
// 如果获取失败,使用默认值
this.marketTypes = [
{ value: 'USStock', i18nKey: 'dashboard.analysis.market.USStock' },
{ value: 'Crypto', i18nKey: 'dashboard.analysis.market.Crypto' },
{ value: 'Forex', i18nKey: 'dashboard.analysis.market.Forex' },
{ value: 'Futures', i18nKey: 'dashboard.analysis.market.Futures' },
{ value: 'AShare', i18nKey: 'dashboard.analysis.market.AShare' },
{ value: 'HShare', i18nKey: 'dashboard.analysis.market.HShare' }
]
}
} catch (error) {
// 如果获取失败,使用默认值
this.marketTypes = [
{ value: 'USStock', i18nKey: 'dashboard.analysis.market.USStock' },
{ value: 'Crypto', i18nKey: 'dashboard.analysis.market.Crypto' },
{ value: 'Forex', i18nKey: 'dashboard.analysis.market.Forex' },
{ value: 'Futures', i18nKey: 'dashboard.analysis.market.Futures' },
{ value: 'AShare', i18nKey: 'dashboard.analysis.market.AShare' },
{ value: 'HShare', i18nKey: 'dashboard.analysis.market.HShare' }
]
}
// 初始化选中的市场类型tab
if (this.marketTypes.length > 0 && !this.selectedMarketTab) {
this.selectedMarketTab = this.marketTypes[0].value
}
}
},
watch: {
// 监听弹窗打开,初始化数据
showAddStockModal (newVal) {
if (newVal) {
// 初始化选中的市场类型
if (this.marketTypes.length > 0 && !this.selectedMarketTab) {
this.selectedMarketTab = this.marketTypes[0].value
}
// 加载热门标的
if (this.selectedMarketTab) {
this.loadHotSymbols(this.selectedMarketTab)
}
} else {
// 关闭时清理数据
this.selectedSymbolForAdd = null
this.symbolSearchKeyword = ''
this.symbolSearchResults = []
this.hasSearched = false
if (this.searchTimer) {
clearTimeout(this.searchTimer)
this.searchTimer = null
}
}
}
}
}
</script>
<style lang="less" scoped>
.ai-analysis-container {
display: flex;
height: calc(100vh - 120px); // 减去顶部导航栏高度
background: #f0f2f5;
overflow: hidden; // 防止容器本身滚动
width: 100%;
// max-width: 100%;
box-sizing: border-box;
}
@media (max-width: 768px) {
.ai-analysis-container {
display: flex;
height: calc(100vh - 120px); // 减去顶部导航栏高度
background: #f0f2f5;
overflow: hidden; // 防止容器本身滚动
width: calc(100% + 44px)!important;
// max-width: 100%;
margin: -22px;
box-sizing: border-box;
}
}
// Removed .sidebar related styles
.main-content {
flex: 1;
display: flex;
gap: 20px;
overflow: hidden;
background: #f5f5f5;
padding: 0px;
height: 100%;
min-width: 0; // 允许 flex 子元素收缩
width: 0; // 配合 flex: 1 使用,确保正确收缩
box-sizing: border-box;
}
// 分析区域
.analysis-section {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
border-radius: 12px;
padding: 24px;
gap: 24px;
height: 100%;
min-height: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
box-sizing: border-box;
width: 100%;
max-width: 100%;
// 科技感动态背景层
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(135deg, rgba(24, 144, 255, 0.03) 0%, rgba(102, 126, 234, 0.03) 50%, rgba(135, 206, 250, 0.02) 100%),
radial-gradient(circle at 20% 30%, rgba(24, 144, 255, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(102, 126, 234, 0.05) 0%, transparent 50%);
background-size: 100% 100%, 800px 800px, 600px 600px;
background-position: 0 0, 0 0, 100% 100%;
animation: backgroundShift 20s ease-in-out infinite;
z-index: 0;
pointer-events: none;
}
// 动态网格背景
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(rgba(24, 144, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(24, 144, 255, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
background-position: 0 0, 0 0;
animation: gridMove 30s linear infinite;
z-index: 0;
pointer-events: none;
opacity: 0.6;
}
// 确保内容在背景之上
> * {
position: relative;
z-index: 1;
}
.center-header-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
.logo-section {
font-size: 24px;
font-weight: 700;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 10px;
.logo-icon { font-size: 32px; }
.logo-text { color: #262626; }
.highlight { color: var(--primary-color); }
}
.status-bar {
display: flex;
gap: 24px;
font-size: 13px;
.status-item {
display: flex;
gap: 8px;
align-items: center;
.label { color: #8c8c8c; font-weight: 600; }
.value { color: var(--primary-color); font-weight: bold; font-family: 'Orbitron', sans-serif; }
.value.online { color: #52c41a; }
}
}
}
.analysis-header {
position: relative;
z-index: 1;
.header-content {
.title-section {
margin-bottom: 24px;
.main-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
color: #262626;
line-height: 1.2;
display: flex;
align-items: center;
gap: 12px;
.title-icon {
font-size: 32px;
animation: pulse 2s ease-in-out infinite;
}
}
.title-subtitle {
font-size: 14px;
color: #8c8c8c;
margin: 0;
font-weight: 400;
}
}
.symbol-selector-wrapper {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
.symbol-selector {
flex: 1;
min-width: 200px;
}
.analyze-button {
height: 40px;
padding: 0 24px;
font-weight: 600;
flex-shrink: 0;
}
.history-button {
flex-shrink: 0;
}
.symbol-option {
display: flex;
align-items: center;
}
}
// 空自选股提示选项样式
::v-deep .empty-watchlist-hint-option {
.ant-select-item-option-content {
padding: 0;
}
.empty-watchlist-hint {
display: flex;
align-items: center;
padding: 8px 12px;
color: #595959;
font-size: 14px;
.anticon {
color: var(--primary-color);
margin-right: 8px;
}
.ant-btn-link {
padding: 0;
height: auto;
line-height: 1.5;
font-size: 14px;
color: var(--primary-color) !important;
transition: color 0.3s;
&:hover {
color: var(--primary-color) !important;
opacity: 0.8;
}
}
}
}
}
}
}
// 右侧自选股区域
.watchlist-section {
width: 320px;
min-width: 280px;
background: #fff;
border-radius: 12px;
padding: 24px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
min-height: 0;
position: relative;
flex-shrink: 0;
box-sizing: border-box;
max-width: 100%;
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
h3 {
font-size: 18px;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
color: #262626;
font-weight: 700;
.anticon {
color: #1890ff;
font-size: 20px;
}
}
}
.watchlist-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; // 允许 flex 子元素收缩
// 隐藏滚动条但保持滚动功能
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE 和 Edge
&::-webkit-scrollbar {
display: none; // Chrome, Safari, Opera
}
.watchlist-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
margin-bottom: 8px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #e8e8e8;
transition: all 0.3s;
&:hover {
background: #f0f0f0;
border-color: #1890ff;
.remove-icon {
opacity: 1;
}
}
.stock-info {
flex: 1;
.stock-symbol {
font-size: 16px;
font-weight: 600;
color: #262626;
margin-bottom: 4px;
}
.stock-name {
font-size: 12px;
color: #999;
}
.stock-market {
font-size: 10px;
color: #1890ff;
background: #e6f7ff;
padding: 2px 6px;
border-radius: 2px;
display: inline-block;
margin-top: 4px;
}
}
.stock-price {
text-align: right;
margin-right: 12px;
.price-value {
font-size: 16px;
font-weight: 600;
color: #262626;
margin-bottom: 4px;
}
.price-change {
font-size: 12px;
font-weight: 600;
.up {
color: #52c41a;
}
.down {
color: #ff4d4f;
}
}
}
.remove-icon {
cursor: pointer;
color: #ff4d4f;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s;
&:hover {
color: #cf1322;
}
}
}
.empty-watchlist {
padding: 40px 0;
text-align: center;
}
}
}
// 消息样式(在问答区域中使用)
.messages-list {
.message-item {
display: flex;
margin-bottom: 24px;
animation: fadeIn 0.3s;
&.user-message {
flex-direction: row-reverse;
.message-content {
background: #1890ff;
color: #fff;
margin-right: 12px;
margin-left: 0;
}
}
&.ai-message {
.message-content {
background: #fff;
color: #262626;
margin-left: 12px;
margin-right: 0;
border: 1px solid #e8e8e8;
}
}
&.loading {
.message-content {
display: flex;
align-items: center;
gap: 8px;
}
}
.message-avatar {
flex-shrink: 0;
}
.message-content {
max-width: 75%;
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.message-text {
line-height: 1.6;
word-wrap: break-word;
}
.message-time {
font-size: 12px;
color: #8c8c8c;
margin-top: 8px;
}
.ai-report {
margin-top: 16px;
.report-card {
background: #f5f5f5;
}
.report-content {
.report-section {
margin-bottom: 16px;
h4 {
font-size: 14px;
font-weight: bold;
margin-bottom: 8px;
}
p {
font-size: 13px;
line-height: 1.6;
margin-bottom: 8px;
}
.recommendation {
margin-top: 8px;
}
}
}
}
}
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.1);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// 科技感背景动画
@keyframes backgroundShift {
0%, 100% {
background-position: 0 0, 0 0, 100% 100%;
opacity: 1;
}
50% {
background-position: 0 0, 20% 20%, 80% 80%;
opacity: 0.8;
}
}
@keyframes gridMove {
0% {
background-position: 0 0, 0 0;
}
100% {
background-position: 40px 40px, 40px 40px;
}
}
// 响应式设计
@media (max-width: 1400px) {
.main-content {
padding: 16px;
gap: 16px;
}
.analysis-section {
padding: 20px;
gap: 20px;
}
.watchlist-section {
width: 280px;
min-width: 260px;
padding: 20px;
}
}
@media (max-width: 1200px) {
// Removed .sidebar styles
.main-content {
padding: 12px;
gap: 12px;
}
.analysis-section {
padding: 16px;
gap: 16px;
.analysis-header {
.header-content {
.title-section {
.main-title {
font-size: 24px;
}
}
}
}
}
.watchlist-section {
width: 260px;
min-width: 240px;
padding: 16px;
}
}
@media (max-width: 992px) {
.ai-analysis-container {
flex-direction: column;
height: auto;
min-height: calc(100vh - 64px);
overflow-y: auto;
overflow-x: hidden;
}
// Removed .sidebar styles
.main-content {
flex-direction: column;
height: auto;
padding: 0px;
gap: 12px;
width: 100% !important;
min-width: 100% !important;
overflow-x: hidden;
}
.analysis-section {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
height: auto;
min-height: 500px;
flex-shrink: 0;
.center-header-info {
flex-direction: column;
align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 16px;
.logo-section {
font-size: 20px;
width: 100%;
.logo-icon {
font-size: 28px;
}
}
.status-bar {
flex-wrap: nowrap;
gap: 8px;
font-size: 12px;
width: 100%;
overflow-x: auto;
.status-item {
gap: 4px;
flex-shrink: 0;
white-space: nowrap;
}
}
}
}
.watchlist-section {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
height: auto;
max-height: 400px;
order: -1;
flex-shrink: 0;
}
.symbol-selector-wrapper {
flex-direction: column;
width: 100%;
gap: 8px;
.symbol-selector {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
margin-bottom: 0;
}
.model-selector {
width: 100% !important;
}
.analyze-button,
.history-button {
width: 100%;
height: 44px;
padding: 0 16px;
font-size: 14px;
flex: none;
min-width: 0;
margin-left: 0 !important;
}
}
}
@media (max-width: 768px) {
.ai-analysis-container {
height: auto;
min-height: calc(100vh - 64px);
overflow-y: auto;
overflow-x: hidden;
}
// Removed .sidebar styles
.main-content {
padding: 8px;
gap: 8px;
width: 100% !important;
min-width: 100% !important;
overflow-x: hidden;
}
.analysis-section {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 12px;
gap: 12px;
border-radius: 8px;
.center-header-info {
flex-direction: column;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 12px;
.logo-section {
font-size: 18px;
width: 100%;
.logo-icon {
font-size: 24px;
}
.logo-text {
font-size: 16px;
line-height: 1.4;
}
}
.status-bar {
flex-wrap: nowrap;
gap: 6px;
font-size: 12px;
width: 100%;
overflow-x: auto;
.status-item {
gap: 3px;
flex-shrink: 0;
white-space: nowrap;
.label {
font-size: 10px;
}
.value {
font-size: 12px;
}
}
}
}
.analysis-header {
.header-content {
.title-section {
margin-bottom: 16px;
.main-title {
font-size: 20px;
flex-wrap: wrap;
.title-icon {
font-size: 24px;
}
}
.title-subtitle {
font-size: 12px;
}
}
.symbol-selector-wrapper {
gap: 8px;
.symbol-selector {
width: 100% !important;
}
.model-selector {
width: 100% !important;
}
.analyze-button,
.history-button {
width: 100%;
height: 40px;
font-size: 13px;
padding: 0 12px;
}
}
}
}
}
.watchlist-section {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 12px;
border-radius: 8px;
max-height: 350px;
.section-header {
h3 {
font-size: 16px;
}
}
.watchlist-item {
padding: 10px;
.stock-info {
.stock-symbol {
font-size: 14px;
}
.stock-name {
font-size: 11px;
}
}
.stock-price {
.price-value {
font-size: 14px;
}
}
}
}
}
@media (max-width: 576px) {
.ai-analysis-container {
padding: 0;
}
// Removed .sidebar styles
.main-content {
padding: 4px;
gap: 4px;
width: 100% !important;
min-width: 100% !important;
}
.analysis-section {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 8px;
gap: 8px;
.center-header-info {
flex-direction: column;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
padding-bottom: 10px;
.logo-section {
font-size: 16px;
width: 100%;
.logo-icon {
font-size: 20px;
}
.logo-text {
font-size: 14px;
line-height: 1.3;
word-break: break-word;
}
}
.status-bar {
flex-wrap: nowrap;
gap: 4px;
font-size: 12px;
width: 100%;
overflow-x: auto;
.status-item {
gap: 3px;
flex-shrink: 0;
white-space: nowrap;
.label {
font-size: 10px;
}
.value {
font-size: 12px;
}
}
}
}
.analysis-header {
.header-content {
.title-section {
.main-title {
font-size: 18px;
}
}
.symbol-selector-wrapper {
flex-direction: column;
width: 100%;
gap: 6px;
.symbol-selector {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
}
.model-selector {
width: 100% !important;
}
.analyze-button,
.history-button {
width: 100%;
height: 40px;
font-size: 13px;
padding: 0 12px;
margin-left: 0 !important;
margin-top: 0;
span {
display: inline;
}
}
}
}
}
}
.watchlist-section {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 8px;
.section-header {
h3 {
font-size: 14px;
.anticon {
font-size: 16px;
}
}
}
}
}
@media (max-width: 480px) {
.analysis-section {
padding: 6px;
gap: 6px;
.center-header-info {
gap: 8px;
margin-bottom: 8px;
padding-bottom: 8px;
.logo-section {
font-size: 14px;
.logo-icon {
font-size: 18px;
}
.logo-text {
font-size: 12px;
}
}
.status-bar {
flex-wrap: nowrap;
gap: 3px;
font-size: 12px;
width: 100%;
overflow-x: auto;
.status-item {
flex-shrink: 0;
white-space: nowrap;
gap: 2px;
.label {
font-size: 10px;
}
.value {
font-size: 12px;
}
}
}
}
.analysis-header {
.header-content {
.title-section {
.main-title {
font-size: 16px;
.title-icon {
font-size: 20px;
}
}
.title-subtitle {
font-size: 11px;
}
}
.symbol-selector-wrapper {
gap: 6px;
.symbol-selector {
::v-deep .ant-select-selection {
font-size: 13px;
}
}
.model-selector {
::v-deep .ant-select-selection {
font-size: 13px;
}
}
.analyze-button,
.history-button {
width: 100%;
height: 38px;
font-size: 12px;
padding: 0 10px;
margin-top: 0;
}
}
}
}
}
}
/* ========== 暗黑主题样式 ========== */
.ai-analysis-container.theme-dark,
:global(body.dark) .ai-analysis-container,
:global(body.realdark) .ai-analysis-container {
background: #131722;
color: #d1d4dc;
.main-content {
background: #131722;
}
.analysis-section {
background: #1e222d;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.center-header-info {
border-bottom-color: rgba(255, 255, 255, 0.08);
.logo-section {
.logo-text { color: #d1d4dc; }
.highlight { color: #00e5ff; }
}
.status-bar {
.status-item {
.label { color: #868993; }
.value { color: #00e5ff; }
.value.online { color: #52c41a; }
}
}
}
&::before {
background:
linear-gradient(135deg, rgba(24, 144, 255, 0.05) 0%, rgba(102, 126, 234, 0.05) 50%, rgba(135, 206, 250, 0.03) 100%),
radial-gradient(circle at 20% 30%, rgba(24, 144, 255, 0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(102, 126, 234, 0.08) 0%, transparent 50%);
}
&::after {
background-image:
linear-gradient(rgba(24, 144, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(24, 144, 255, 0.05) 1px, transparent 1px);
}
.analysis-header {
.header-content {
.title-section {
.main-title {
color: #d1d4dc;
}
.title-subtitle {
color: #868993;
}
}
}
}
::v-deep .symbol-selector {
.ant-select-selection {
background-color: #2a2e39;
border-color: #363c4e;
color: #d1d4dc;
&:hover {
border-color: #1890ff;
}
}
.ant-select-focused .ant-select-selection {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3);
}
.ant-select-arrow {
color: #868993;
}
.ant-select-selection__placeholder {
color: #868993;
}
}
}
.watchlist-section {
background: #1e222d;
border-color: #363c4e;
.section-header {
h3 {
color: #d1d4dc;
}
}
.watchlist-container {
.watchlist-item {
background: #2a2e39;
border-color: #363c4e;
&:hover {
background: #363c4e;
border-color: #1890ff;
}
.stock-info {
.stock-symbol {
color: #d1d4dc;
}
.stock-name {
color: #868993;
}
}
.stock-price {
.price-value {
color: #d1d4dc;
}
}
}
.empty-watchlist {
::v-deep .ant-empty-description {
color: #868993;
}
}
}
}
}
/* 添加股票弹窗样式 */
.add-stock-modal-content {
.market-tabs {
margin-bottom: 16px;
}
.symbol-search-section {
margin-bottom: 24px;
}
.search-results-section,
.hot-symbols-section {
margin-bottom: 24px;
.section-title {
font-size: 14px;
font-weight: 600;
color: #262626;
margin-bottom: 12px;
display: flex;
align-items: center;
}
}
.symbol-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e8e8e8;
border-radius: 4px;
.symbol-list-item {
cursor: pointer;
padding: 8px 12px;
transition: background-color 0.3s;
&:hover {
background-color: #f5f5f5;
}
.symbol-item-content {
display: flex;
align-items: center;
gap: 8px;
.symbol-code {
font-weight: 600;
color: #262626;
min-width: 80px;
}
.symbol-name {
color: #595959;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.selected-symbol-section {
margin-top: 16px;
.selected-symbol-info {
display: flex;
align-items: center;
}
}
}
/* 暗黑主题下的弹窗样式 */
.ai-analysis-container.theme-dark,
:global(body.dark) .ai-analysis-container,
:global(body.realdark) .ai-analysis-container {
.add-stock-modal-content {
.search-results-section,
.hot-symbols-section {
.section-title {
color: #d1d4dc;
}
}
.symbol-list {
border-color: #363c4e;
background-color: #2a2e39;
.symbol-list-item {
&:hover {
background-color: #363c4e;
}
.symbol-item-content {
.symbol-code {
color: #d1d4dc;
}
.symbol-name {
color: #868993;
}
}
}
}
}
}
</style>