Files
sanguo_vnpy_v2/frontend/src/views/Dashboard.vue
T
claude_dev 59da65b839 fix(backtest): 结果页指标全显"—" + 任务重启后404
两个根因:

1. relative_metrics 缺字段(routes.py): get_result 的 relative_fields 列表
   漏了 total_return/annual_return/sharpe_ratio/max_drawdown 4 个字段, 导致
   这 4 个指标永远进不了响应 → 前端 MetricCards 显"—"。补全为 11 字段。

2. 任务 task_id 不持久(runner/cta_engine): submit_cta 用 cta_{symbol}_{id(params)}
   (内存地址) 作 runner id, 而 run_cta_backtest 内部另生成 cta_{uuid} 存 DB,
   两者持久层不相交 → 重启后 pool 内存映射丢失, get_result(runner_id) 的
   load_result_by_task_id 查不到 → /result 404 → 前端指标全 0 + 图表无数据。
   改为 submit 前置生成稳定 uuid, 透传给 run_cta_backtest 复用, 使
   runner-id == DB task_id (单一 id, 重启可查)。

附: Dashboard 最近任务"策略/因子"列 min-width 150→190(长策略名不再截断)
测试: test_runner task_id 断言同步新格式
2026-07-11 22:10:48 +08:00

219 lines
6.2 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getTasks, type TaskListItem } from '@/api/backtest'
import { getStrategies } from '@/api/strategy'
const router = useRouter()
const tasks = ref<TaskListItem[]>([])
const strategyCount = ref(0)
const loading = ref(false)
onMounted(async () => {
loading.value = true
try {
const [t, s] = await Promise.all([getTasks(), getStrategies().catch(() => [])])
tasks.value = t
strategyCount.value = Array.isArray(s) ? s.length : 0
} finally {
loading.value = false
}
})
const stats = computed(() => ({
total: tasks.value.length,
done: tasks.value.filter((t) => t.status === 'done').length,
failed: tasks.value.filter((t) => t.status === 'failed').length,
strategies: strategyCount.value,
}))
const recent = computed(() => tasks.value.slice(0, 8))
const quickActions = [
{ label: '新建回测', desc: 'CTA 策略历史回测', path: '/backtest/new', mark: 'BT' },
{ label: '因子分析', desc: 'IC / 分层回测', path: '/factor/new', mark: 'FA' },
{ label: '新建模拟盘', desc: '回放 / 实盘模式', path: '/paper/new', mark: 'PA' },
{ label: '历史任务', desc: '全部回测记录', path: '/backtest/history', mark: 'HI' },
] as const
function go(path: string): void {
router.push(path)
}
function open(row: TaskListItem): void {
router.push(row.type === 'factor' ? `/factor/result/${row.task_id}` : `/backtest/result/${row.task_id}`)
}
const typeLabelMap: Record<string, string> = { cta: '回测', optimize: '优化', factor: '因子' }
function typeLabel(t: string): string {
return typeLabelMap[t] || t
}
const statusLabelMap: Record<string, string> = { done: '完成', failed: '失败', running: '运行中', pending: '排队' }
function statusLabel(s: string): string {
return statusLabelMap[s] || s
}
</script>
<template>
<div class="page dashboard" v-loading="loading">
<div class="page-head">
<div>
<h2 class="page-title">工作台</h2>
<p class="page-subtitle">投研 · 回测 · 模拟 一站式控制台</p>
</div>
</div>
<!-- 统计卡片 -->
<div class="stat-row">
<div class="stat-card">
<div class="stat-label">任务总数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
<div class="stat-card">
<div class="stat-label">完成</div>
<div class="stat-value up-color">{{ stats.done }}</div>
</div>
<div class="stat-card">
<div class="stat-label">失败</div>
<div class="stat-value down-color">{{ stats.failed }}</div>
</div>
<div class="stat-card">
<div class="stat-label">可用策略</div>
<div class="stat-value">{{ stats.strategies }}</div>
</div>
</div>
<!-- 快捷入口 -->
<div class="section-title">快捷入口</div>
<div class="action-row">
<div v-for="a in quickActions" :key="a.path" class="action-card" @click="go(a.path)">
<span class="action-mark">{{ a.mark }}</span>
<div class="action-body">
<div class="action-label">{{ a.label }}</div>
<div class="action-desc">{{ a.desc }}</div>
</div>
</div>
</div>
<!-- 最近任务 -->
<div class="section-title">
最近任务
<el-button link type="primary" @click="go('/backtest/history')">查看全部</el-button>
</div>
<el-card class="table-card" shadow="never">
<el-table :data="recent" size="small" empty-text="暂无任务">
<el-table-column label="任务 ID" min-width="180">
<template #default="{ row }"><span class="mono">{{ row.task_id }}</span></template>
</el-table-column>
<el-table-column label="类型" width="84">
<template #default="{ row }"><span class="chip" :class="`chip-${row.type}`">{{ typeLabel(row.type) }}</span></template>
</el-table-column>
<el-table-column prop="strategy" label="策略 / 因子" min-width="190" show-overflow-tooltip />
<el-table-column prop="symbol" label="标的" width="90" />
<el-table-column label="状态" width="84">
<template #default="{ row }"><span class="chip" :class="`st-${row.status}`">{{ statusLabel(row.status) }}</span></template>
</el-table-column>
<el-table-column label="操作" width="76">
<template #default="{ row }"><el-button link type="primary" @click="open(row)">查看</el-button></template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<style scoped>
.dashboard {
display: flex;
flex-direction: column;
gap: 16px;
}
.stat-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border-2);
border-radius: var(--r-md);
padding: 16px 18px;
}
.stat-label {
font-size: 12px;
color: var(--text-2);
}
.stat-value {
margin-top: 6px;
font-size: 28px;
font-weight: 700;
font-family: var(--mono);
color: var(--text);
}
.up-color {
color: var(--down);
}
.down-color {
color: var(--danger);
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: 600;
color: var(--text);
margin-top: 4px;
}
.action-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.action-card {
display: flex;
align-items: center;
gap: 12px;
background: var(--bg-card);
border: 1px solid var(--border-2);
border-radius: var(--r-md);
padding: 14px 16px;
cursor: pointer;
transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.action-card:hover {
border-color: var(--brand);
background: var(--bg-hover);
}
.action-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: var(--r-sm);
background: rgba(24, 144, 255, 0.14);
color: var(--brand);
font-weight: 700;
font-size: 13px;
font-family: var(--mono);
flex-shrink: 0;
}
.action-label {
font-size: 14px;
font-weight: 600;
color: var(--text);
}
.action-desc {
font-size: 12px;
color: var(--text-3);
margin-top: 2px;
}
.table-card {
border: 1px solid var(--border-2);
}
</style>