diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-18 18:11:53 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-18 18:11:53 +0800 |
| commit | c52a727d11b87d70ec00a7e2569841e6e6abcd9c (patch) | |
| tree | c7bc7217cb0ef02613b34e5a1f228ac3a68431a9 /frontend/src/pages/ProjectResultsTab.svelte | |
| parent | 63098a260dadf9e5d01650afb02570343abe20d9 (diff) | |
| download | strategy-lab-c52a727d11b87d70ec00a7e2569841e6e6abcd9c.tar.gz | |
[变更性质] 用户旅程重排:项目首页指标摘要+项目卡、结果页首屏 capital 三卡+交易分页、数据/策略/回测 CTA 与三态统一,token 白名单清扫(docs/ui-user-journey-round6.md §1/§2)
Diffstat (limited to 'frontend/src/pages/ProjectResultsTab.svelte')
| -rw-r--r-- | frontend/src/pages/ProjectResultsTab.svelte | 82 |
1 files changed, 53 insertions, 29 deletions
diff --git a/frontend/src/pages/ProjectResultsTab.svelte b/frontend/src/pages/ProjectResultsTab.svelte index 181d6a4..860fab2 100644 --- a/frontend/src/pages/ProjectResultsTab.svelte +++ b/frontend/src/pages/ProjectResultsTab.svelte @@ -21,7 +21,8 @@ import * as Table from '$lib/components/ui/table'; import * as Select from '$lib/components/ui/select'; import * as Tabs from '$lib/components/ui/tabs'; - import type { Run, BacktestResult } from '../lib/types'; + import { go } from '../lib/router'; +import type { Run, BacktestResult } from '../lib/types'; interface Props { projectId: string; @@ -116,33 +117,49 @@ // 投入口径 reporting(任务 A):投入/持仓估值/回报,口径注明不含闲置现金 const cap = $derived(result ? capitalReporting(result) : null); - const tile = 'gap-1 min-w-0'; + const tile = 'p-4 flex flex-col gap-1 min-w-0'; const tileLabel = 'text-xs text-muted-foreground'; const tileValue = 'text-2xl font-bold tabular-nums text-foreground leading-tight break-all'; - const metricValueProps = 'data-testid="metric-value"'; + + let tradePage = $state(0); + const TRADE_PER_PAGE = 20; + const tradePageCount = $derived(result ? Math.max(1, Math.ceil(result.trades.length / TRADE_PER_PAGE)) : 1); + const tradePageRows = $derived(result ? result.trades.slice(tradePage * TRADE_PER_PAGE, (tradePage + 1) * TRADE_PER_PAGE) : []); </script> -<div class="flex flex-col gap-3"> - <div class="flex flex-wrap justify-between items-center gap-2"> +<div class="flex flex-col gap-4 md:gap-6"> + <div class="flex flex-wrap justify-between items-center gap-4"> <h2 class="text-lg font-semibold">回测结果</h2> {#if run} - <span class="flex items-center gap-2"> - <span class="text-sm text-muted-foreground">Run {run.id.slice(0, 8)}…</span> + <span class="flex items-center gap-4"> + <span class="text-xs text-muted-foreground">Run {run.id.slice(0, 8)}…</span> <Badge variant={runStatusBadge[run.status] === 'ok' ? 'secondary' : runStatusBadge[run.status] === 'warn' ? 'outline' : 'destructive'}>{runStatusLabel[run.status]}</Badge> </span> {/if} </div> - <Status busy={busy} error={error} busyText="加载结果…" empty={null} /> + {#if busy} + <Status busy skeletonRows={4} busyText="加载结果…" empty={null} /> + {:else if error} + <Status busy={false} {error} empty={null}> + {#snippet errorCta()} + <Button onclick={() => void load()}>重新加载</Button> + {/snippet} + </Status> + {/if} {#if !busy && !error && !run} - <Status busy={false} empty="此项目还没有回测结果" emptyHint="先在「回测」标签发起一次回测" /> + <Status busy={false} empty="此项目还没有回测结果" emptyHint="先在「回测」标签发起一次回测"> + {#snippet emptyCta()} + <Button onclick={() => go(`/projects/${projectId}/backtest`)}>去发起回测</Button> + {/snippet} + </Status> {/if} {#if run && !busy && !error} {#if history.length > 1} - <div class="flex items-center gap-2"> - <span class="text-sm text-muted-foreground">查看其他回测:</span> + <div class="flex items-center gap-4"> + <span class="text-xs text-muted-foreground">查看其他回测:</span> <Select.Root type="single" value={run?.id ?? ''} @@ -180,10 +197,10 @@ </Alert> {/if} - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> {#if result.metrics} {@const mrow = result.metrics} - <div class="grid grid-cols-2 md:grid-cols-3 gap-2.5"> + <div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> <Card.Root size="sm" class={tile}><div class={tileLabel}>总收益率</div><div class={tileValue} data-testid="metric-value">{fmtPct(mrow.total_return)}</div></Card.Root> <Card.Root size="sm" class={tile}><div class={tileLabel}>年化收益率</div><div class={tileValue} data-testid="metric-value">{fmtPct(mrow.annual_return)}</div></Card.Root> <Card.Root size="sm" class={tile}><div class={tileLabel}>最大回撤</div><div class={tileValue} data-testid="metric-value">{fmtPct(mrow.max_drawdown)}</div></Card.Root> @@ -193,14 +210,14 @@ </div> {/if} {#if cap} - <div class="grid grid-cols-2 md:grid-cols-3 gap-2.5" data-testid="capital-reporting"> + <div class="grid grid-cols-1 sm:grid-cols-3 gap-4" data-testid="capital-reporting"> <Card.Root size="sm" class={tile}><div class={tileLabel}>累计投入</div><div class={tileValue} data-testid="metric-value">{formatMoney(cap.invested)}</div></Card.Root> <Card.Root size="sm" class={tile}><div class={tileLabel}>最终持仓估值</div><div class={tileValue} data-testid="metric-value">{formatMoney(cap.holdingValue)}</div></Card.Root> <Card.Root size="sm" class={tile}><div class={tileLabel}>投入回报率</div><div class={tileValue} data-testid="metric-value">{cap.returnPct ?? '—'}</div></Card.Root> </div> - <p class="text-sm text-muted-foreground">投入口径不含初始账户闲置现金:累计投入 = 首日现金 − 末日现金;持仓估值 = 末日持仓 × 最新收盘价。</p> + <p class="text-xs text-muted-foreground">投入口径不含初始账户闲置现金:累计投入 = 首日现金 − 末日现金;持仓估值 = 末日持仓 × 最新收盘价。</p> {/if} - <p class="text-sm text-muted-foreground"> + <p class="text-xs text-muted-foreground"> 平仓回合数为已完成的开仓并全部平仓的完整回合(1 个回合 = 1 次平仓),不含撤单与未平仓的单笔成交;成交记录中的“金额”为真实成交额(数量 × 成交价),非持仓成本。结果非投资建议;未完全模拟流动性/涨跌停等限制。 引擎 {result.engine?.name} {result.engine?.version} {#if result.elapsed_ms}<span> · 耗时 {(result.elapsed_ms / 1000).toFixed(1)}s</span>{/if} @@ -208,7 +225,7 @@ </p> </Card.Root> - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> <Tabs.Root value={chartMode} onValueChange={(v) => (chartMode = v as 'equity' | 'drawdown')} class="w-fit"> <Tabs.List variant="line"> <Tabs.Trigger value="equity">权益曲线({result.equity.length} 点)</Tabs.Trigger> @@ -226,7 +243,7 @@ <h2 class="text-lg font-semibold m-0">订单与持仓记录</h2> <div class="grid md:grid-cols-[1.4fr_1fr] gap-4 items-start"> <div> - <h3 class="text-base font-semibold">成交记录 ({result.trades.length})</h3> + <h3 class="text-sm font-medium">成交记录 ({result.trades.length})</h3> {#if result.trades.length > 0} <div class="overflow-x-auto"> <Table.Root> @@ -242,8 +259,8 @@ </Table.Row> </Table.Header> <Table.Body> - {#each result.trades.slice(0, 80) as t, i (i)} - <Table.Row> + {#each tradePageRows as t, i (tradePage * TRADE_PER_PAGE + i)} + <Table.Row data-testid="trade-row"> <Table.Cell>{t.date}</Table.Cell> <Table.Cell class="tabular-nums">{t.symbol}</Table.Cell> <Table.Cell>{t.side === "buy" ? "买入" : "卖出"}</Table.Cell> @@ -256,28 +273,35 @@ </Table.Body> </Table.Root> </div> - {#if result.trades.length > 80} - <p class="text-sm text-muted-foreground">仅显示前 80 条,共 {result.trades.length} 条</p> + {#if result.trades.length > TRADE_PER_PAGE} + <div class="flex items-center gap-4 mt-2"> + <span class="text-xs text-muted-foreground"> + 第 {tradePage + 1} / {tradePageCount} 页 · 共 {result.trades.length} 条 + </span> + <span class="flex-1"></span> + <Button variant="outline" size="sm" disabled={tradePage === 0} onclick={() => tradePage--}>上一页</Button> + <Button variant="outline" size="sm" disabled={tradePage >= tradePageCount - 1} onclick={() => tradePage++}>下一页</Button> + </div> {/if} {:else} - <p class="text-sm text-muted-foreground">本区间未发生成交</p> + <p class="text-xs text-muted-foreground">本区间未发生成交</p> {/if} </div> <div> - <h3 class="text-base font-semibold">策略日志</h3> - <Button variant="outline" size="sm" onclick={() => (logsOpen = !logsOpen)} aria-expanded={logsOpen}> + <h3 class="text-sm font-medium">策略日志</h3> + <Button variant="outline" size="sm" class="w-fit" onclick={() => (logsOpen = !logsOpen)} aria-expanded={logsOpen}> {logsOpen ? "收起日志" : `展开日志(${result.logs.length} 行)`} </Button> {#if logsOpen} - <pre class="font-mono text-sm p-2 bg-muted rounded-md overflow-x-auto whitespace-pre-wrap break-all mt-2">{result.logs.join("\n")}</pre> + <pre class="font-mono text-sm p-4 bg-card overflow-x-auto whitespace-pre-wrap break-all mt-2">{result.logs.join("\n")}</pre> {/if} </div> </div> <div> - <h3 class="text-base font-semibold"> + <h3 class="text-sm font-medium"> 数据与引擎 - <span class="text-sm text-muted-foreground font-normal">本轮使用的引擎版本、耗时与消耗内存均在上方备注。</span> + <span class="text-xs text-muted-foreground font-normal">本轮使用的引擎版本、耗时与消耗内存均在上方备注。</span> </h3> - <p class="text-sm text-muted-foreground"> + <p class="text-xs text-muted-foreground"> 结果由对应策略代码、配置、数据清单(数据哈希 {result.data_manifest_hash?.slice(0, 12)})以及当时引擎共同决定。 </p> </div> |
