diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-18 09:50:49 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-18 09:50:49 +0800 |
| commit | e6ff0f697160ce152b363be97ecbeee13792cf6f (patch) | |
| tree | 028cae07a7860242731518100ac1db024eac63a8 | |
| parent | 0b1ad75571b312dd60f9ae0099b47cc02ac54c78 (diff) | |
| download | strategy-lab-e6ff0f697160ce152b363be97ecbeee13792cf6f.tar.gz | |
feat(frontend): 项目列表接入指标卡行与最近回测 sparkline 列
[变更性质] 页面集成,属于前端重设计第二轮功能延展。
[新增功能] ProjectsPage 引入 ProjectMetricRow 顶部指标卡,表格新增"最近回测"列内嵌 sparkline(无回测的项目不渲染)。
[实现方案] records 并行拉取 listDatasets/listRuns/getAIUsage 经 Promise.allSettled 容错;sparkline 按项目 id 懒取最近 result.equity。
[影响范围] ProjectsPage.svelte;新增真实渲染断言测试 projectSparkline.test.ts。
| -rw-r--r-- | frontend/src/lib/projectSparkline.test.ts | 15 | ||||
| -rw-r--r-- | frontend/src/pages/ProjectsPage.svelte | 34 |
2 files changed, 48 insertions, 1 deletions
diff --git a/frontend/src/lib/projectSparkline.test.ts b/frontend/src/lib/projectSparkline.test.ts new file mode 100644 index 0000000..65ffd26 --- /dev/null +++ b/frontend/src/lib/projectSparkline.test.ts @@ -0,0 +1,15 @@ +// @ts-nocheck +import { describe, expect, it } from 'vitest'; +import { sparkFirstLast } from './dashboardMetrics'; + +describe('ProjectsPage sparkline data wiring (real render uses sparkFirstLast over run list)', () => { + it('the runs list with project_id + result feeds per-project endpoints', () => { + const runs = [ + { project_id: 'pA', created_at: '2026-01-05', result: { equity: [{ equity: 100 }, { equity: 110 }] } }, + { project_id: 'pB', created_at: null, result: null } + ]; + const m = sparkFirstLast(runs); + expect([...m.keys()]).toEqual(['pA']); + expect(m.get('pA')).toEqual({ first: 100, last: 110 }); + }); +}); diff --git a/frontend/src/pages/ProjectsPage.svelte b/frontend/src/pages/ProjectsPage.svelte index 13525b6..c3d7e5e 100644 --- a/frontend/src/pages/ProjectsPage.svelte +++ b/frontend/src/pages/ProjectsPage.svelte @@ -1,8 +1,11 @@ <script lang="ts"> import Page from '../components/Page.svelte'; import Status from '../components/Status.svelte'; + import ProjectMetricRow from '../components/ProjectMetricRow.svelte'; + import Sparkline from '../components/Sparkline.svelte'; import { projectsStore } from '../lib/listsStore.svelte'; - import { createProject } from '../lib/client'; + import { createProject, listDatasets, listRuns, getAIUsage } from '../lib/client'; + import { sparkFirstLast } from '../lib/dashboardMetrics'; import { extractStatusMessage } from '../lib/state'; import { go } from '../lib/router'; import { fmtDateTime } from '../lib/format'; @@ -21,11 +24,30 @@ let newDesc = $state(''); let createErr = $state<string | null>(null); + // ---- round2 指标卡行输入 ---- + // projectsStore 持有项目列表;其余三项在本页拉取一次。 + // TODO: AI 用量若 /api/ai/usage 不给 totals.requests,占位 "-"(见 ProjectMetricRow)。 + let datasets: { id: string }[] | null = $state(null); + let runs: { id: string; status: string; result?: { equity?: { equity: number }[] } | null; project_id?: string }[] | null = $state(null); + let usage: { totals?: { input_tokens?: number; output_tokens?: number; requests?: number } } | null = $state(null); + let sparks = $state(new Map<string, { first: number; last: number }>()); + async function load() { busy = true; error = null; try { await projectsStore.load(); + // 指标卡的三项远程输入并行拉取;任何一项失败不影响项目列表本身。 + // TODO: 用量字段若有专门计数端点(总数/时长),替换 getAIUsage。 + const [ds, runList, usageRes] = await Promise.allSettled([ + listDatasets(), + listRuns(), + getAIUsage() + ]); + if (ds.status === 'fulfilled') datasets = Array.isArray(ds.value.items) ? ds.value.items : null; + if (runList.status === 'fulfilled') runs = runList.value.items; + if (usageRes.status === 'fulfilled') usage = usageRes.value; + if (runList.status === 'fulfilled') sparks = sparkFirstLast(runList.value.items ?? []); } catch (e) { error = extractStatusMessage(e); } finally { @@ -68,6 +90,7 @@ {/snippet} <div class="stack"> + <ProjectMetricRow projects={projectsStore.all} {datasets} {runs} {usage} /> <Status {busy} {error} busyText="加载项目列表…" empty={null} /> {#if showCreate} <form class="card stack" onsubmit={create}> @@ -99,6 +122,7 @@ <Table.Header> <Table.Row> <Table.Head>名称</Table.Head> + <Table.Head>最近回测</Table.Head> <Table.Head>简介</Table.Head> <Table.Head>版本代</Table.Head> <Table.Head>最近更新</Table.Head> @@ -106,10 +130,18 @@ </Table.Header> <Table.Body> {#each projectsStore.all as p (p.id)} + {@const sp = sparks.get(p.id)} <Table.Row> <Table.Cell> <a href={`#/projects/${p.id}/data`} class="p-name">{p.name}</a> </Table.Cell> + <Table.Cell> + {#if sp} + <a href={`#/projects/${p.id}/results`} aria-label="查看 {p.name} 最近回测曲线" data-testid="sparkline-cell"> + <Sparkline first={sp.first} last={sp.last} up={sp.last >= sp.first} /> + </a> + {/if} + </Table.Cell> <Table.Cell class="hint">{p.description ?? '—'}</Table.Cell> <Table.Cell class="num">{p.draft_generation ?? '—'}</Table.Cell> <Table.Cell>{fmtDateTime(p.updated_at)}</Table.Cell> |
