feat(frontend): P1仪表盘首页(统计卡+快捷入口+最近任务)+全局chip样式+侧栏工作台入口
This commit is contained in:
@@ -7,7 +7,8 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
component: () => import('@/views/Layout.vue'),
|
||||
children: [
|
||||
{ path: '', redirect: '/backtest/new' },
|
||||
{ path: '', redirect: '/dashboard' },
|
||||
{ path: 'dashboard', name: 'dashboard', component: () => import('@/views/Dashboard.vue') },
|
||||
{ path: 'backtest/new', name: 'bt-new', component: () => import('@/views/backtest/New.vue') },
|
||||
{ path: 'backtest/progress/:id', name: 'bt-progress', component: () => import('@/views/backtest/Progress.vue') },
|
||||
{ path: 'backtest/result/:id', name: 'bt-result', component: () => import('@/views/backtest/Result.vue') },
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
@import './styles/tokens.css';
|
||||
@import './styles/element-dark.css';
|
||||
@import './styles/reset.css';
|
||||
@import './styles/chips.css';
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/* 全局 chip / 状态色 —— 所有页面复用(红涨绿跌 A 股配色,深色主题) */
|
||||
.chip {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 任务类型 */
|
||||
.chip-cta {
|
||||
color: var(--brand);
|
||||
background: rgba(24, 144, 255, 0.14);
|
||||
}
|
||||
.chip-optimize {
|
||||
color: #13c2c2;
|
||||
background: rgba(19, 194, 194, 0.14);
|
||||
}
|
||||
.chip-factor {
|
||||
color: #b37feb;
|
||||
background: rgba(179, 127, 235, 0.14);
|
||||
}
|
||||
.chip-paper {
|
||||
color: #d29922;
|
||||
background: rgba(210, 153, 34, 0.14);
|
||||
}
|
||||
|
||||
/* 状态 */
|
||||
.st-done {
|
||||
color: var(--down);
|
||||
background: rgba(63, 185, 80, 0.14);
|
||||
}
|
||||
.st-failed {
|
||||
color: var(--danger);
|
||||
background: rgba(248, 81, 73, 0.14);
|
||||
}
|
||||
.st-running {
|
||||
color: var(--brand);
|
||||
background: rgba(24, 144, 255, 0.14);
|
||||
}
|
||||
.st-pending {
|
||||
color: var(--warn);
|
||||
background: rgba(210, 153, 34, 0.14);
|
||||
}
|
||||
|
||||
/* 涨跌(红涨绿跌) */
|
||||
.up {
|
||||
color: var(--up);
|
||||
}
|
||||
.down {
|
||||
color: var(--down);
|
||||
}
|
||||
|
||||
/* 通用文字层级 */
|
||||
.mono {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
.muted {
|
||||
color: var(--text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 计数徽标 */
|
||||
.count-badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 0 8px;
|
||||
min-width: 24px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
background: var(--bg-hover);
|
||||
border-radius: 11px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<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="150" 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>
|
||||
@@ -32,6 +32,7 @@ function onLogout(): void {
|
||||
<span class="logo-text">三国量化</span>
|
||||
</div>
|
||||
<el-menu :default-active="activeMenu" class="nav" @select="navigate">
|
||||
<el-menu-item index="/dashboard"><span class="nav-label">工作台</span></el-menu-item>
|
||||
<el-sub-menu index="backtest">
|
||||
<template #title>
|
||||
<span class="nav-label">回测</span>
|
||||
|
||||
Reference in New Issue
Block a user