summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectResultsTab.svelte
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 18:11:53 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 18:11:53 +0800
commitc52a727d11b87d70ec00a7e2569841e6e6abcd9c (patch)
treec7bc7217cb0ef02613b34e5a1f228ac3a68431a9 /frontend/src/pages/ProjectResultsTab.svelte
parent63098a260dadf9e5d01650afb02570343abe20d9 (diff)
downloadstrategy-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.svelte82
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>