TradingAgents-CN 仪表板数据修复实战:从硬编码假数据到真实 API 数据接入
【免费下载链接】TradingAgents-CN基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN
仪表板(Dashboard)是 TradingAgents-CN 前端用户登录后最先看到的核心页面,它汇总了自选股、最近分析、市场快讯、模拟交易账户等多维信息。本文以仓库内 DASHBOARD_DATA_FIX.md 修复记录为主线,完整还原"仪表板显示硬编码假数据"这一典型问题的定位过程、修复方案与最终效果,并结合当前仓库的前后端源码逐层剖析真实数据链路的实现细节。读完本文,你将掌握如何在该项目中定位前端占位数据、如何通过favoritesApi与analysisApi接入真实接口、以及如何设计带空数据兜底的响应式数据加载逻辑。
问题描述:仪表板展示的是"看起来正确"的假数据
用户反馈仪表板页面显示的数据不是真实数据,具体表现为三个区块全部为硬编码内容:
- 自选股:显示的是硬编码的假数据(
000001、000002、600036、600519等固定代码),无论用户实际添加了哪些自选股,页面展示始终不变; - 最近分析:显示的是硬编码的假数据(
task_001之类的虚构任务 ID),与用户真实执行过的分析任务毫无关联; - 市场快讯:显示的是硬编码的假数据,未对接任何新闻数据源。
这类问题在前后端分离的项目中极具代表性:页面结构与 UI 已经完整,但数据层被"模拟数据"占位,导致功能看起来可用、实际却是静态的。它比"页面报错"更隐蔽,因为用户看到的是正常渲染的界面,只有与真实操作对比后才能发现数据是假的。
根本原因:数据加载函数只打印日志、未调用 API
根据修复记录,问题根源集中在 frontend/src/views/Dashboard/index.vue 这一仪表板页面组件中:
- 自选股数据使用硬编码数组(原文档定位约第 274-299 行);
- 最近分析数据使用硬编码数组(约第 256-271 行);
- 市场快讯数据使用硬编码数组(约第 301-317 行);
loadFavoriteStocks()函数只是打印日志(约第 404-415 行),没有真正发起任何网络请求。
从当前仓库的最终实现可以验证这一修复路径。在 frontend/src/views/Dashboard/index.vue 中,响应式数据已统一初始化为空数组:
const recentAnalyses = ref<AnalysisTask[]>([]) // 自选股数据 const favoriteStocks = ref<any[]>([]) // 市场快讯数据 const marketNews = ref<any[]>([])数据初始化从"写死的对象字面量"改为"空引用",再通过异步加载函数在挂载后填充,这正是本次修复的核心思路:组件只负责渲染,数据全部来自后端接口。
修复方案一:自选股接入/api/favorites/真实数据
修改前的问题代码
原文档中给出了修复前的典型实现——自选股直接声明为常量数组,加载函数只有console.log:
const favoriteStocks = ref([ { stock_code: '000001', stock_name: '平安银行', current_price: 12.50, change_percent: 2.1 }, // ... 更多硬编码数据 ]) const loadFavoriteStocks = async () => { try { // 目前使用模拟数据 console.log('加载自选股数据') } catch (error) { console.error('加载自选股失败:', error) } }修改后的实现
修复后通过favoritesApi.list()拉取真实数据,并对缺失的实时行情字段做兜底(|| 0):
import { favoritesApi } from '@/api/favorites' const favoriteStocks = ref<any[]>([]) const loadFavoriteStocks = async () => { try { const response = await favoritesApi.list() if (response.success && response.data) { favoriteStocks.value = response.data.map((item: any) => ({ stock_code: item.stock_code, stock_name: item.stock_name, current_price: item.current_price || 0, change_percent: item.change_percent || 0 })) } } catch (error) { console.error('加载自选股失败:', error) } }当前仓库 frontend/src/api/favorites.ts 中favoritesApi的封装与修复后的调用完全对应:
export const favoritesApi = { /** 获取收藏列表 */ list: () => ApiClient.get<FavoriteItem[]>('/api/favorites/'), /** 添加收藏 */ add: (payload: AddFavoriteReq) => ApiClient.post<{ message: string; symbol?: string; stock_code?: string }>('/api/favorites/', payload), /** 更新收藏(标签、备注、价格提醒) */ update: (symbol: string, payload) => ApiClient.put<{ message: string; symbol?: string; stock_code?: string }>(`/api/favorites/${symbol}`, payload), /** 删除收藏 */ remove: (symbol: string) => ApiClient.delete<{ message: string; symbol?: string; stock_code?: string }>(`/api/favorites/${symbol}`), /** 检查是否已收藏 */ check: (symbol: string) => ApiClient.get<{ symbol?: string; stock_code?: string; is_favorite: boolean }>(`/api/favorites/check/${symbol}`), /** 同步自选股实时行情 */ syncRealtime: (data_source: string = 'tushare') => ApiClient.post<{ total: number; success_count: number; failed_count: number; symbols: string[]; data_source: string; message: string }>( '/api/favorites/sync-realtime', { data_source } ) }值得说明的是:favorites.ts中的字段同时兼容了symbol(主字段)与stock_code(已废弃兼容字段)两种命名,这与后端演进保持一致,前端接入时需注意新旧字段的映射。
后端自选股接口
后端路由位于 app/routers/favorites.py,GET /api/favorites/通过favorites_service.get_user_favorites(current_user["id"])按当前登录用户返回自选股列表:
@router.get("/", response_model=dict) async def get_favorites( current_user: dict = Depends(get_current_user) ): """获取用户自选股列表""" try: favorites = await favorites_service.get_user_favorites(current_user["id"]) return ok(favorites) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"获取自选股失败: {str(e)}" )同文件中定义的自选股响应模型(约第 40-52 行)明确携带了实时行情字段,说明后端在设计上就预留了current_price、change_percent、volume三个可选字段,由实时行情同步逻辑填充:
"""自选股响应""" stock_code: str stock_name: str market: str added_at: str tags: List[str] notes: str alert_price_high: Optional[float] alert_price_low: Optional[float] # 实时数据 current_price: Optional[float] = None change_percent: Optional[float] = None volume: Optional[int] = None响应示例(对应原文档的接口说明):
{ "success": true, "data": [ { "stock_code": "601398", "stock_name": "工商银行", "market": "A股", "current_price": 7.30, "change_percent": -0.41, "added_at": "2025-01-01T00:00:00Z" } ] }修复方案二:最近分析接入/api/analysis/user/history
修改前的问题代码
最近分析区块原先同样是硬编码的任务数组:
const recentAnalyses = ref<AnalysisTask[]>([ { id: '1', task_id: 'task_001', user_id: 'user_1', stock_code: '000001', stock_name: '平安银行', status: 'completed', // ... 更多硬编码数据 } ])修改后的实现
修复后改为调用分析历史接口,并在拿到任务列表后同步更新页面上的用户统计(总分析数、成功分析数):
const recentAnalyses = ref<AnalysisTask[]>([]) const loadRecentAnalyses = async () => { try { const response = await getAnalysisHistory({ page: 1, page_size: 5, status: undefined }) if (response.success && response.data) { recentAnalyses.value = response.data.tasks || [] // 更新统计数据 userStats.value.totalAnalyses = response.data.total || 0 userStats.value.successfulAnalyses = response.data.tasks?.filter( (item: any) => item.status === 'completed' ).length || 0 } } catch (error) { console.error('加载最近分析失败:', error) } }API 封装:getAnalysisHistory与analysisApi
原文档要求在 frontend/src/api/analysis.ts 中新增getAnalysisHistory函数。当前仓库中该函数已经落地,同时保留了更完整的analysisApi.getHistory对象方法:
/** * 获取分析历史记录(当前用户) */ export const getAnalysisHistory = async (params: { page?: number page_size?: number status?: string }) => { const response = await request.get('/api/analysis/user/history', { params }) return response.data }在 frontend/src/api/analysis.ts 的analysisApi中,历史查询接口封装为:
// 获取分析历史(用户维度) getHistory(params?: { page?: number page_size?: number market_type?: string symbol?: string // 主字段:股票代码 stock_code?: string // 兼容字段(已废弃) start_date?: string end_date?: string status?: string }): Promise<any> { return request.get('/api/analysis/user/history', { params }) },此外,当前仪表板实际调用的是任务列表接口analysisApi.getTaskList(GET /api/analysis/tasks,支持status/limit/offset参数),并对不同返回结构做了兼容处理:
const loadRecentAnalyses = async () => { try { // 使用任务中心的用户任务接口,获取最近10条 const res = await analysisApi.getTaskList({ limit: 10, offset: 0, // 不限定状态,展示最近任务;如需仅展示已完成可设为 'completed' status: undefined }) // 兼容不同返回结构(ApiResponse 或直接 data) const body: any = (res as any)?.data?.data || (res as any)?.data || res || {} const tasks = body.tasks || [] recentAnalyses.value = tasks userStats.value.totalAnalyses = body.total ?? tasks.length userStats.value.successfulAnalyses = tasks.filter((item: any) => item.status === 'completed').length } catch (error) { console.error('加载最近分析失败:', error) recentAnalyses.value = [] } }这段实现体现了两个工程要点:一是请求参数使用limit/offset风格(新版 simple service 的任务接口),二是通过多重兜底(res)?.data?.data || (res)?.data || res || {}兼容新旧后端返回结构,避免因响应体嵌套层次变化导致页面崩溃。
后端分析历史接口
后端实现在 app/routers/analysis.py,GET /api/analysis/user/history的完整查询参数如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
status | string | 无 | 任务状态过滤 |
start_date | string | 无 | 开始日期,格式YYYY-MM-DD |
end_date | string | 无 | 结束日期,格式YYYY-MM-DD |
symbol | string | 无 | 股票代码(主字段) |
stock_code | string | 无 | 股票代码(已废弃,兼容用) |
market_type | string | 无 | 市场类型 |
page | int | 1 | 页码,ge=1 |
page_size | int | 20 | 每页大小,ge=1, le=100 |
其核心处理逻辑为:先通过get_simple_analysis_service().list_user_tasks()获取当前用户的任务列表(内存优先、MongoDB 兜底),再在 Python 层完成时间范围、股票代码(symbol or stock_code兼容取值)、市场类型等基础筛选,最后返回分页结构:
return { "success": True, "data": { "tasks": filtered, "total": len(filtered), "page": page, "page_size": page_size }, "message": "历史查询成功" }响应示例:
{ "success": true, "data": { "tasks": [ { "task_id": "abc-123", "stock_code": "601398", "stock_name": "工商银行", "status": "completed", "progress": 100, "created_at": "2025-01-01T00:00:00Z" } ], "total": 10, "page": 1, "page_size": 5 } }修复方案三:生命周期钩子串行加载真实数据
修改前
onMounted中虽然写了加载自选股的调用,但由于函数体内没有真正的请求,实际效果等于什么都没做:
onMounted(async () => { // 加载用户统计数据 // 加载系统状态 // 加载最近分析 // 加载市场快讯 // 加载自选股数据 await loadFavoriteStocks() })修改后
修复后onMounted明确串行加载自选股与最近分析数据:
onMounted(async () => { // 加载自选股数据 await loadFavoriteStocks() // 加载最近分析 await loadRecentAnalyses() })当前仓库中的onMounted在本次修复的基础上进一步扩展为四个数据源串行加载(frontend/src/views/Dashboard/index.vue):
onMounted(async () => { // 加载自选股数据 await loadFavoriteStocks() // 加载最近分析 await loadRecentAnalyses() // 加载市场快讯 await loadMarketNews() // 加载模拟交易账户 await loadPaperAccount() })市场快讯:从"暂留硬编码"到真实新闻 API 的演进
原文档在"修复效果"一节中明确标注:市场快讯暂时保留硬编码数据,后续可接入真实新闻 API。而当前仓库已经完成了这一步,loadMarketNews通过 frontend/src/api/news.ts 中封装的newsApi.getLatestNews拉取真实新闻,并做了两级兜底策略:
const loadMarketNews = async () => { try { // 先尝试获取最近 24 小时的新闻 let response = await newsApi.getLatestNews(undefined, 10, 24) // 如果最近 24 小时没有新闻,则获取最新的 10 条(不限时间) if (response.success && response.data && response.data.news.length === 0) { console.log('最近 24 小时没有新闻,获取最新的 10 条新闻(不限时间)') response = await newsApi.getLatestNews(undefined, 10, 24 * 365) // 回溯 1 年 } if (response.success && response.data) { marketNews.value = response.data.news.map((item: any) => ({ id: item.id || item.title, title: item.title, time: item.publish_time, url: item.url, source: item.source })) } } catch (error) { console.error('加载市场快讯失败:', error) // 如果加载失败,显示提示信息 marketNews.value = [] } }newsApi.getLatestNews的签名在 frontend/src/api/news.ts 中定义为getLatestNews(symbol?: string, limit: number = 10, hours_back: number = 24),其中limit控制返回条数、hours_back控制回溯小时数。模板侧则通过v-if="marketNews.length > 0"与v-else展示"暂无市场快讯"空状态,与自选股的空状态处理保持一致。该后续修复在 DASHBOARD_MARKET_NEWS_EMPTY_FIX.md 中有单独记录,可作延伸阅读。
请求封装与错误处理约定
仪表板所有数据加载统一经由 frontend/src/api/request.ts 的ApiClient/request封装发出。其响应拦截器会做统一处理:
- 将 HTTP 响应解包为
ApiResponse(包含success: boolean与data字段),调用方直接访问response.success与response.data; success === false时自动触发业务错误提示与 401 登录态处理;- 网络错误、请求参数错误等场景统一拦截并提示。
因此前端加载函数中的if (response.success && response.data)判断与catch分支中的console.error是双保险:接口层失败由拦截器提示用户,业务层失败由组件兜底并保证页面不白屏。
修复效果与空数据兜底
修复完成后,仪表板三个核心区块的行为如下:
自选股
- ✅ 从
GET /api/favorites/端点获取真实的自选股数据; - ✅ 显示用户实际添加的自选股;
- ✅ 显示实时价格和涨跌幅(如果后端已同步实时行情);
- ✅ 如果没有自选股,显示"暂无自选股"提示(模板通过
v-if="favoriteStocks.length === 0"渲染el-empty,并提供"添加自选股"按钮引导)。
最近分析
- ✅ 从
GET /api/analysis/user/history端点获取真实的分析历史; - ✅ 显示最近的分析记录(当前实现取最近 10 条);
- ✅ 显示真实的股票代码、名称、状态、创建时间;
- ✅ 同步更新用户统计数据(总分析数、成功分析数)。
市场快讯
- ✅ 已从硬编码数据演进为真实新闻接口(
newsApi.getLatestNews,先取 24 小时内、为空则回溯 1 年),空数据时展示"暂无市场快讯"。
测试建议
修复记录给出了清晰的三步验证清单,可直接用于回归测试:
- 测试自选股显示:添加几只自选股 → 刷新仪表板页面 → 验证显示的是真实的自选股数据(而非固定代码);
- 测试最近分析显示:执行几次股票分析 → 刷新仪表板页面 → 验证显示的是真实的分析历史(任务 ID、状态、时间与任务中心一致);
- 测试空数据情况:清空所有自选股 → 刷新仪表板页面 → 验证显示"暂无自选股"提示而非报错或残留旧数据。
相关文件索引
| 角色 | 文件 |
|---|---|
| 仪表板页面组件 | frontend/src/views/Dashboard/index.vue |
| 分析 API 封装 | frontend/src/api/analysis.ts |
| 自选股 API 封装 | frontend/src/api/favorites.ts |
| 新闻 API 封装 | frontend/src/api/news.ts |
| HTTP 请求封装 | frontend/src/api/request.ts |
| 后端分析历史路由 | app/routers/analysis.py |
| 后端自选股路由 | app/routers/favorites.py |
注意事项与经验总结
- 市场快讯的演进路径:原修复记录中"暂时保留硬编码"是合理的阶段性取舍,当前仓库已通过
newsApi完成真实接入,并采用"24 小时优先、一年兜底"的降级策略,避免周末或非交易时段出现空页面; - 用户统计的默认值:部分统计数据(如每日配额、并发限制)仍使用默认值(当前代码中
dailyQuota: 1000、concurrentLimit: 3),如需展示真实配额需后续对接用户/系统配置接口; - 错误处理约定:API 调用失败时在控制台打印错误,同时将列表置空以保证页面正常渲染,不会因单个区块失败导致整个仪表板不可用;
- 字段兼容策略:前后端字段命名经历了
stock_code→symbol的演进,前端多处采用a || b的兼容取值方式,在做数据映射时务必沿用这一约定,避免遗漏旧字段导致数据丢失; - 空状态是功能的一部分:本次修复中"暂无自选股""暂无市场快讯"等空状态与真实数据加载同等重要,它是区分"加载完成但无数据"与"加载失败"的关键用户体验。
本次修复本质上是一次"数据层去伪存真"的工程实践:前端组件职责回归渲染,数据完全交由后端接口驱动,并通过空状态、错误兜底、字段兼容保证了页面在任意数据条件下都能稳定呈现。对于任何存在"看起来正常但数据是假的"的前后端分离项目,这套定位与修复方法论都同样适用。
【免费下载链接】TradingAgents-CN基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考