diff options
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/src/pages/AccountPage.svelte | 10 | ||||
| -rw-r--r-- | frontend/src/pages/AdminPage.svelte | 16 | ||||
| -rw-r--r-- | frontend/src/pages/AuthPages.svelte | 24 | ||||
| -rw-r--r-- | frontend/src/pages/ProjectBacktestTab.svelte | 56 | ||||
| -rw-r--r-- | frontend/src/pages/ProjectDataTab.svelte | 25 | ||||
| -rw-r--r-- | frontend/src/pages/ProjectResultsTab.svelte | 82 | ||||
| -rw-r--r-- | frontend/src/pages/ProjectStrategyTab.svelte | 94 | ||||
| -rw-r--r-- | frontend/src/pages/ProjectsPage.svelte | 149 | ||||
| -rw-r--r-- | frontend/src/pages/RunsPage.svelte | 26 | ||||
| -rw-r--r-- | frontend/src/pages/UsagePage.svelte | 6 |
10 files changed, 297 insertions, 191 deletions
diff --git a/frontend/src/pages/AccountPage.svelte b/frontend/src/pages/AccountPage.svelte index c85dc53..360b95c 100644 --- a/frontend/src/pages/AccountPage.svelte +++ b/frontend/src/pages/AccountPage.svelte @@ -104,10 +104,10 @@ </script> <Page title="账户与安全" subtitle="资料、密码与个人会话管理"> - <div class="flex flex-col gap-3"> + <div class="flex flex-col gap-4 md:gap-6"> <Card.Root size="sm"> <Card.Header><Card.Title>个人信息</Card.Title></Card.Header> - <form onsubmit={saveProfile} class="flex flex-wrap items-center gap-3"> + <form onsubmit={saveProfile} class="flex flex-wrap items-center gap-4"> <div class="flex-[1_1_200px]"> <Label for="acc-name">姓名</Label> <Input id="acc-name" bind:value={nameInput} autocomplete="name" /> @@ -129,8 +129,8 @@ <Card.Root size="sm"> <Card.Header><Card.Title>修改密码</Card.Title></Card.Header> - <form onsubmit={changePassword} class="flex flex-col gap-3"> - <div class="grid grid-cols-1 sm:grid-cols-3 gap-3"> + <form onsubmit={changePassword} class="flex flex-col gap-4"> + <div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div> <Label for="pw-cur">当前密码</Label> <Input id="pw-cur" type="password" bind:value={curPw} required autocomplete="current-password" /> @@ -176,7 +176,7 @@ <Table.Cell>{fmtDateTime(s.last_seen_at)}</Table.Cell> <Table.Cell class="text-sm text-muted-foreground">{s.user_agent?.slice(0, 60) ?? '—'}</Table.Cell> <Table.Cell> - <div class="flex items-center gap-2 flex-wrap"> + <div class="flex items-center gap-4 flex-wrap"> {#if s.current}<Badge variant="secondary">当前会话</Badge>{/if} <Button variant="outline" size="sm" class="text-destructive" disabled={s.current && sessions.length === 1} onclick={() => void revoke(s)}> {s.current ? '注销当前' : '撤销'} diff --git a/frontend/src/pages/AdminPage.svelte b/frontend/src/pages/AdminPage.svelte index 2a10266..8f6aded 100644 --- a/frontend/src/pages/AdminPage.svelte +++ b/frontend/src/pages/AdminPage.svelte @@ -186,11 +186,11 @@ </Tabs.Root> {/snippet} - <div class="flex flex-col gap-3"> + <div class="flex flex-col gap-4"> <Status busy={false} error={adminOpsErr} empty={null} /> {#if tab === 'users'} - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> <h2 class="text-lg font-semibold">用户</h2> <Status busy={usersBusy} error={usersErr} busyText="载入用户…" empty={null} /> {#if !usersBusy && !usersErr && users.length > 0} @@ -243,7 +243,7 @@ /> </Table.Cell> <Table.Cell> - <div class="flex items-center gap-2 flex-wrap"> + <div class="flex items-center gap-4 flex-wrap"> <Button variant={u.active ? 'destructive' : 'outline'} size="sm" onclick={() => void patchUser(u, { active: !u.active })}> {u.active ? '停用' : '启用'} </Button> @@ -258,9 +258,9 @@ <p class="text-sm text-muted-foreground">停用账户会立即吊销其会话并阻止其回测与 AI 请求。角色不能作用于他人项目,私阅策略代码不提供。</p> </Card.Root> {:else if tab === 'invites'} - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> <h2 class="text-lg font-semibold">签发邀请</h2> - <form onsubmit={makeInvite} class="flex flex-wrap items-end gap-3"> + <form onsubmit={makeInvite} class="flex flex-wrap items-end gap-4"> <div class="flex-[1_1_220px]"> <Label for="inv-em">邮箱(可选备注)</Label> <Input id="inv-em" bind:value={inviteEmail} placeholder="📬 将线下告知受邀人,用于区分" /> @@ -285,7 +285,7 @@ {/if} </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"> <h2 class="text-lg font-semibold">历史邀请</h2> <Status busy={inviteBusy} error={inviteErr} busyText="载入邀请…" empty={null} /> {#if !inviteBusy && !inviteErr && invites.length === 0} @@ -330,7 +330,7 @@ <p class="text-sm text-muted-foreground">恢复链接由“生成恢复”按钮产生,仅出现一次;不会自动发邮件。</p> </Card.Root> {:else} - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> <h2 class="text-lg font-semibold">安全审计(收尾尾部)</h2> <Status busy={auditBusy} error={auditErr} busyText="载入审计记录…" empty={null} /> {#if !auditBusy && !auditErr && audit.length === 0} @@ -367,7 +367,7 @@ {#if resetFor} <Modal title={`为 ${resetFor.email} 生成密码恢复`} onClose={() => (resetFor = null)}> - <div class="flex flex-col gap-3"> + <div class="flex flex-col gap-4"> <Alert variant="default"> <div> <div>恢复链接(管理员线下告知对方,只出现一次):</div> diff --git a/frontend/src/pages/AuthPages.svelte b/frontend/src/pages/AuthPages.svelte index da5d54c..f466cd0 100644 --- a/frontend/src/pages/AuthPages.svelte +++ b/frontend/src/pages/AuthPages.svelte @@ -95,9 +95,9 @@ <section class="min-h-screen flex items-center justify-center p-6 min-w-0"> <Card.Root class="w-[min(400px,100%)] p-8 max-[768px]:p-7" role="main"> <div class="mb-4"> - <span class="inline-block w-8 h-8 rounded-lg bg-primary mb-2" aria-hidden="true"></span> - <h1 class="text-xl font-semibold mb-1">策研 <span class="text-muted-foreground/70 font-medium text-[0.8rem]">Strategy Lab</span></h1> - <p class="text-sm text-muted-foreground max-w-[26em]">内部策略研究平台 · 仅限受邀成员</p> + <span class="inline-block w-8 h-8 bg-primary mb-2" aria-hidden="true"></span> + <h1 class="text-lg font-semibold mb-1">策研 <span class="text-xs text-muted-foreground font-medium">Strategy Lab</span></h1> + <p class="text-xs text-muted-foreground">内部策略研究平台 · 仅限受邀成员</p> </div> {#if mode === 'register'} @@ -106,7 +106,7 @@ <div> <Label for="inv">邀请令牌</Label> <Input id="inv" bind:value={inviteToken} required autocomplete="off" /> - <p class="text-sm text-muted-foreground mt-1">单次有效,来自管理员邀请</p> + <p class="text-xs text-muted-foreground mt-1">单次有效,来自管理员邀请</p> </div> <div> <Label for="nm">姓名</Label> @@ -119,11 +119,11 @@ <div> <Label for="pw">密码</Label> <Input id="pw" type="password" bind:value={password} required autocomplete="new-password" minlength={8} /> - <p class="text-sm text-muted-foreground mt-1">至少 8 位</p> + <p class="text-xs text-muted-foreground mt-1">至少 8 位</p> </div> {#if resultError}<Alert variant="destructive">{resultError}</Alert>{/if} <Button type="submit" disabled={busy}>{busy ? '创建中…' : '开通账户'}</Button> - <p class="text-sm text-muted-foreground">已有账户? <a class="hover:underline" href="#/login">登录</a></p> + <p class="text-xs text-muted-foreground">已有账户? <a class="hover:underline" href="#/login">登录</a></p> </div> </form> {:else if mode === 'reset'} @@ -136,7 +136,7 @@ <div> <Label for="rt">重置令牌</Label> <Input id="rt" bind:value={resetToken} autocomplete="off" /> - <p class="text-sm text-muted-foreground mt-1">由管理员线下发放的恢复链接自动填入;不支持邮箱自动发送</p> + <p class="text-xs text-muted-foreground mt-1">由管理员线下发放的恢复链接自动填入;不支持邮箱自动发送</p> </div> <div> <Label for="np">新密码</Label> @@ -148,7 +148,7 @@ </div> {#if resultError}<Alert variant="destructive">{resultError}</Alert>{/if} <Button type="submit" disabled={busy}>{busy ? '重置中…' : '重置密码'}</Button> - <p class="text-sm text-muted-foreground">想起密码了? <a class="hover:underline" href="#/login">直接登录</a></p> + <p class="text-xs text-muted-foreground">想起密码了? <a class="hover:underline" href="#/login">直接登录</a></p> </div> </form> {/if} @@ -165,11 +165,9 @@ </div> {#if resultError}<Alert variant="destructive">{resultError}</Alert>{/if} <Button type="submit" disabled={busy}>{busy ? '登录中…' : '登录'}</Button> - <p class="text-sm text-muted-foreground"> - 开通新账户需要管理员邀请: <a class="hover:underline" href="#/register">用邀请令牌注册</a> - {#if route.query.get('invite')}<span>(邀请令牌已自动填入)</span>{/if} - </p> - <p class="text-sm text-muted-foreground">忘记密码?请联系管理员获取 <a class="hover:underline" href="#/reset">恢复链接</a></p> + {#if route.query.get('invite')} + <p class="text-xs text-muted-foreground">邀请令牌已自动填入,可直接用邀请注册。</p> + {/if} </div> </form> {/if} diff --git a/frontend/src/pages/ProjectBacktestTab.svelte b/frontend/src/pages/ProjectBacktestTab.svelte index b640189..7d5b172 100644 --- a/frontend/src/pages/ProjectBacktestTab.svelte +++ b/frontend/src/pages/ProjectBacktestTab.svelte @@ -11,11 +11,13 @@ runStatusBadge } from '../lib/format'; import { summarizeCoverage } from '../lib/coverage'; - import type { Run, Dataset } from '../lib/types'; + import { go } from '../lib/router'; +import type { Run, Dataset } from '../lib/types'; import { Button } from '$lib/components/ui/button'; import { Alert } from '$lib/components/ui/alert'; import { Badge } from '$lib/components/ui/badge'; import * as Card from '$lib/components/ui/card'; +import * as Textarea from '$lib/components/ui/textarea'; import * as Table from '$lib/components/ui/table'; import * as Select from '$lib/components/ui/select'; import { Input } from '$lib/components/ui/input'; @@ -224,15 +226,26 @@ const canRerun = $derived(latest?.status === 'succeeded' || latest?.status === 'failed' || latest?.status === 'cancelled'); </script> -<div class="flex flex-col gap-3"> - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <h2 class="text-lg font-semibold">发起回测</h2> - <Status busy={busyDatasets} error={cfgErr} busyText="检查就绪的数据集…" empty={null} /> +<div class="flex flex-col gap-4 md:gap-6"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <div class="flex justify-between items-center gap-4"> + <h2 class="text-lg font-semibold m-0">发起回测</h2> + <Button class="w-fit" onclick={startRun} disabled={!datasetId}>开始回测</Button> + </div> + {#if busyDatasets} + <Status busy skeletonRows={3} busyText="检查就绪的数据集…" empty={null} /> + {:else if cfgErr} + <Status busy={false} error={cfgErr} empty={null}/> + {/if} {#if !busyDatasets && datasetsReady.length === 0 && !cfgErr} - <Status busy={false} empty="没有就绪的数据集" emptyHint="先在「数据」标签创建并等待数据集状态变为【就绪】,再回到这里发起回测" /> + <Status busy={false} empty="没有就绪的数据集" emptyHint="先在「数据」标签创建并等待数据集状态变为【就绪】,再回到这里发起回测"> + {#snippet emptyCta()} + <Button onclick={() => go(`/projects/${projectId}/data`)}>去接入数据</Button> + {/snippet} + </Status> {/if} {#if datasetsReady.length > 0} - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div> <Label for="bt-dataset">数据集</Label> <Select.Root @@ -261,7 +274,7 @@ <Label for="bt-capital">起始资金</Label> <Input id="bt-capital" class="tabular-nums" type="number" bind:value={capital} min="1" step="1000" /> </div> - <div class="grid grid-cols-1 sm:grid-cols-2 gap-3"> + <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div> <Label for="bt-commission">佣金率</Label> <Input id="bt-commission" class="tabular-nums" type="number" bind:value={commission} min="0" max="0.1" step="0.0001" /> @@ -277,7 +290,7 @@ </div> <div class="sm:col-span-2 lg:col-span-3"> <Label for="bt-params">策略参数(JSON,可选)</Label> - <textarea id="bt-params" class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50" rows="1" bind:value={paramsText} placeholder="输入 JSON 参数,例如 fast/slow 周期"></textarea> + <Textarea.Root id="bt-params" class="w-full" rows={1} bind:value={paramsText} placeholder="输入 JSON 参数,例如 fast/slow 周期" /> </div> </div> {#if needAck} @@ -288,28 +301,31 @@ <ul class="list-disc pl-5 mt-1"> {#each datasetWarnMsgs as m, i (i)}<li>{m}</li>{/each} </ul> - <label class="inline-flex items-center gap-2 mt-1"> + <label class="inline-flex items-center gap-4 mt-1"> <input type="checkbox" bind:checked={acknowledged} /> 我已知晓并用当前数据继续 </label> </div> </Alert> {/if} - <div class="flex items-center gap-2 flex-wrap"> - <Button onclick={startRun} disabled={!datasetId}>开始回测</Button> + <div class="flex items-center gap-4 flex-wrap"> {#if selectedDataset} - <span class="text-sm text-muted-foreground">本次回测与该数据集、当前策略快照一同记录,可按原始条件重跑</span> + <span class="text-xs text-muted-foreground">本次回测与该数据集、当前策略快照一同记录,可按原始条件重跑</span> {/if} </div> - <Status busy={false} error={cfgErr} empty={actionMsg} /> + {#if cfgErr} + <Status busy={false} error={cfgErr} empty={null} /> + {:else if actionMsg} + <Status busy={false} empty={actionMsg} /> + {/if} {/if} </Card.Root> - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <div class="flex flex-wrap justify-between items-center gap-2"> - <h2 class="text-lg font-semibold">最近一次回测</h2> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <div class="flex flex-wrap justify-between items-center gap-4"> + <h2 class="text-lg font-semibold m-0">最近一次回测</h2> {#if latest} - <span class="flex items-center gap-2"> + <span class="flex items-center gap-4"> <Badge variant="outline">Run 分支 {latest.version_id?.slice(0,8) ?? '—'}</Badge> <Badge variant={runStatusBadge[latest.status] === 'ok' ? 'secondary' : runStatusBadge[latest.status] === 'warn' ? 'outline' : 'destructive'}>{runStatusLabel[latest.status]}</Badge> </span> @@ -349,7 +365,7 @@ {#if latest.status === 'succeeded' && latest.result} <Button variant="outline" href={`#/projects/${projectId}/results?run=${latest.id}`} class="w-fit">查看完整结果与图表</Button> {/if} - <div class="flex items-center gap-2 flex-wrap"> + <div class="flex items-center gap-4 flex-wrap"> {#if canCancel} <Button variant="destructive" onclick={onCancel} disabled={cancelBusy}> {cancelBusy ? '取消中…' : '取消回测'} @@ -366,7 +382,7 @@ {/if} </Card.Root> - <div class="flex flex-col gap-3"> + <div class="flex flex-col gap-4"> <h2 class="text-lg font-semibold">回测历史</h2> <Status busy={historyBusy} error={historyErr} busyText="载入历史…" empty={null} /> {#if history && history.length > 0} diff --git a/frontend/src/pages/ProjectDataTab.svelte b/frontend/src/pages/ProjectDataTab.svelte index 7b9f8ea..b7555ad 100644 --- a/frontend/src/pages/ProjectDataTab.svelte +++ b/frontend/src/pages/ProjectDataTab.svelte @@ -59,26 +59,31 @@ } </script> -<div class="flex flex-col gap-3"> +<div class="flex flex-col gap-4 md:gap-6"> {#if wizardOpen} <DatasetWizard onCreated={() => void load()} /> - <Button variant="ghost" size="sm" onclick={() => (wizardOpen = false)}>收起新建面板</Button> + <Button variant="ghost" class="w-fit" onclick={() => (wizardOpen = false)}>收起新建面板</Button> {:else} - <Button size="sm" class="w-fit" onclick={() => (wizardOpen = true)}>新建数据集</Button> + <Button class="w-fit" onclick={() => (wizardOpen = true)}>接入数据</Button> {/if} - <h2>我的数据集</h2> - <Status busy={busy} error={error} busyText="加载数据集列表…" empty={null} /> + <h2 class="text-lg font-semibold m-0">我的数据集</h2> - {#if !busy && !error && items.length === 0} + {#if busy} + <Status busy={false} busyText="加载数据集列表…" skeletonRows={3} empty={null} /> + {:else if error} + <Status busy={false} {error} empty={null}> + {#snippet errorCta()} + <Button onclick={load}>重新加载</Button> + {/snippet} + </Status> + {:else if items.length === 0} <Status busy={false} empty="还没有数据集" - emptyHint="先创建数据集:选择标的、日期范围与复权方式,提交后异步获取并给出覆盖预览" + emptyHint="选择标的、日期范围与复权方式,提交后异步获取并给出覆盖预览" /> - {/if} - - {#if !busy && !error && items.length > 0} + {:else} {#each items as ds (ds.id)} <DatasetCard dsId={ds.id} onRecreated={() => void load()} /> {/each} 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> diff --git a/frontend/src/pages/ProjectStrategyTab.svelte b/frontend/src/pages/ProjectStrategyTab.svelte index e4269e3..53c8bea 100644 --- a/frontend/src/pages/ProjectStrategyTab.svelte +++ b/frontend/src/pages/ProjectStrategyTab.svelte @@ -9,6 +9,7 @@ import { Alert } from '$lib/components/ui/alert'; import { Badge } from '$lib/components/ui/badge'; import * as Card from '$lib/components/ui/card'; +import * as Textarea from '$lib/components/ui/textarea'; import * as Table from '$lib/components/ui/table'; import { projectsStore } from '../lib/listsStore.svelte'; import { @@ -262,28 +263,34 @@ } </script> -<div class="flex flex-col gap-3"> - <Status busy={initBusy} error={initErr} busyText="加载策略草稿…" empty={null} /> - - {#if !initBusy && !initErr} - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <div class="flex flex-wrap justify-between items-center gap-2"> - <h2 class="text-lg font-semibold">策略代码(Backtrader Python)</h2> - <span class="text-sm text-muted-foreground" role="status"> +<div class="flex flex-col gap-4 md:gap-6"> + {#if initBusy} + <Status busy skeletonRows={5} busyText="加载策略草稿…" empty={null} /> + {:else if initErr} + <Status busy={false} error={initErr} empty={null}> + {#snippet errorCta()} + <Button onclick={() => void init()}>重新加载</Button> + {/snippet} + </Status> + {:else} + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <div class="flex flex-wrap justify-between items-center gap-4"> + <h2 class="text-lg font-semibold m-0">策略代码(Backtrader Python)</h2> + <span class="text-xs text-muted-foreground" role="status" data-testid="save-status"> {saveStatus === 'saved' && `已保存${saveMsg ? ` · ${saveMsg}` : ''}`} {#if saveStatus === 'dirty'}<Badge variant="outline">有未保存修改</Badge>{/if} {#if saveStatus === 'saving'}<Badge variant="secondary">保存中…</Badge>{/if} {#if saveStatus === 'conflict'}<Badge variant="destructive">冲突</Badge>{/if} </span> </div> - <p class="text-sm text-muted-foreground"> - 编辑器修改会自动保存(约 0.8 秒停顿后提交)。草稿代 {baseGeneration} · 回测与获取 AI 建议使用当前草稿快照 + <p class="text-xs text-muted-foreground m-0"> + 编辑器修改会自动保存(约 0.8 秒停顿后提交)。草稿代 {baseGeneration} · 回测与 AI 使用当前草稿快照 </p> <CodeEditor value={code} onInput={onEditorInput} readOnly={false} label="策略源码" height={430} /> - <div class="flex flex-wrap justify-between items-center gap-2"> + <div class="flex flex-wrap justify-between items-center gap-4"> <Input bind:value={snapshotMessage} - placeholder="快照说明,例如:加入RSI过滤器(可选)" + placeholder="快照说明(可选),例如:加入RSI过滤器" class="flex-[2_1_240px] min-w-[200px]" onkeydown={(ev) => { if (ev.key === 'Enter') void snapshot(); @@ -300,7 +307,11 @@ 草稿与上一快照对比 </Button> </div> - <Status busy={false} error={snapshotErr} empty={snapshotMsg} /> + {#if snapshotErr} + <Status busy={false} error={snapshotErr} empty={null} /> + {:else if snapshotMsg} + <Status busy={false} empty={snapshotMsg} /> + {/if} {#if conflictServer} <Alert variant="default"> @@ -310,20 +321,18 @@ </span> </Alert> <DiffView before={conflictServer.code} after={code} beforeLabel="服务器" afterLabel="本机" /> - <div class="flex items-center gap-2 flex-wrap"> + <div class="flex items-center gap-4 flex-wrap"> <Button onclick={keepMine}>保留本机修改(覆盖服务器)</Button> <Button variant="outline" onclick={loadServer}>采用服务器版本</Button> </div> {/if} </Card.Root> - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <h2 class="text-lg font-semibold">AI 助手</h2> - <Label for="ai-instruction">希望 AI 帮你做什么?<span class="text-sm text-muted-foreground">例如:把均线周期改为两个可调参数并解释影响</span></Label> - <textarea id="ai-instruction" class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50" rows="3" bind:value={instruction} placeholder="描述对策略的修改意图,AI 返回完整新源码与差异预览"></textarea> - <div class="flex items-center gap-2 flex-wrap"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <div class="flex justify-between items-center gap-4"> + <h2 class="text-lg font-semibold m-0">AI 助手</h2> <Button class="w-fit" disabled={aiBusy || !instruction.trim()} onclick={askAI}> - {aiBusy ? 'AI 处理中…' : '获取建议'} + {aiBusy ? 'AI 处理中…' : 'AI 改进'} </Button> </div> {#if aiErr} @@ -333,20 +342,25 @@ </Alert> {/if} + <div> + <Label for="ai-instruction">希望 AI 帮你做什么?<span class="text-xs text-muted-foreground">例如:把均线周期改为两个可调参数并解释影响</span></Label> + <Textarea.Root id="ai-instruction" class="w-full" rows={3} bind:value={instruction} placeholder="描述对策略的修改意图,AI 返回完整新源码与差异预览" /> + </div> + {#if aiResult} - <div class="flex flex-col gap-2"> - <div class="flex flex-wrap justify-between items-center gap-2"> - <h3 class="text-base font-semibold">建议解释 <span class="text-sm text-muted-foreground">{aiResult.model}</span></h3> - <span class="text-sm text-muted-foreground"> + <div class="flex flex-col gap-4"> + <div class="flex flex-wrap justify-between items-center gap-4"> + <h3 class="text-sm font-medium m-0">建议解释 <span class="text-xs text-muted-foreground">{aiResult.model}</span></h3> + <span class="text-xs text-muted-foreground"> tokens {aiResult.usage?.input_tokens ?? '—'} 入 / {aiResult.usage?.output_tokens ?? '—'} 出 </span> </div> <p class="m-0">{aiResult.explanation}</p> - <h3 class="text-base font-semibold">完整建议源码</h3> + <h3 class="text-sm font-medium m-0">完整建议源码</h3> <CodeEditor value={aiResult.proposed_code} onInput={() => {}} readOnly={true} label="AI 返回代码(只读)" height={280} /> - <h3 class="text-base font-semibold">与当前草稿的差异</h3> + <h3 class="text-sm font-medium m-0">与当前草稿的差异</h3> <DiffView before={code} after={aiResult.proposed_code} beforeLabel="当前草稿" afterLabel="AI 建议" /> - <div class="flex items-center gap-2 flex-wrap"> + <div class="flex items-center gap-4 flex-wrap"> <Button class="w-fit" disabled={aiAcceptBusy} @@ -357,7 +371,7 @@ </Button> <Button variant="ghost" class="w-fit" disabled={aiAcceptBusy} onclick={discardAI}>放弃建议</Button> {#if aiResult.base_generation !== baseGeneration} - <span class="text-destructive">草稿代 {aiResult.base_generation} 与当前 {baseGeneration} 不一致,接受会失败;请重新发起说明</span> + <span class="text-destructive text-xs">草稿代 {aiResult.base_generation} 与当前 {baseGeneration} 不一致,接受会失败;请重新发起说明</span> {/if} {#if aiAcceptErr} <Alert variant="destructive">{aiAcceptErr}</Alert> @@ -370,14 +384,20 @@ {/if} </Card.Root> - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <h2 class="text-lg font-semibold">版本历史</h2> - <Status busy={versionsBusy} error={versionsErr} busyText="载入版本…" empty={null} /> - {#if !versionsBusy && versions.length === 0} + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <h2 class="text-lg font-semibold m-0">版本历史</h2> + {#if versionsBusy} + <Status busy skeletonRows={3} busyText="载入版本…" empty={null} /> + {:else if versionsErr} + <Status busy={false} error={versionsErr} empty={null}> + {#snippet errorCta()} + <Button onclick={() => void refreshVersions()}>重新载入</Button> + {/snippet} + </Status> + {:else if versions.length === 0} <Status busy={false} empty="还没有版本快照" emptyHint="保存第一个快照后可以在历史中查看完整代码与恢复" /> - {/if} - {#if !versionsBusy && versions.length > 0} - <div class="overflow-x-auto rounded-lg border border-border"> + {:else} + <div class="overflow-x-auto"> <Table.Root> <Table.Header> <Table.Row> @@ -394,7 +414,7 @@ <Table.Cell>{fmtDateTime(v.created_at)}</Table.Cell> <Table.Cell> {v.message?.trim() || '(无说明)'} - <span class="text-sm text-muted-foreground ml-1">版本代 {v.hash?.slice(0, 8) || '—'}</span> + <span class="text-xs text-muted-foreground ml-1">版本代 {v.hash?.slice(0, 8) || '—'}</span> </Table.Cell> <Table.Cell class="tabular-nums">{v.hash?.slice(0, 12)}</Table.Cell> <Table.Cell><Badge variant="outline">{versionSourceLabel[v.source]}</Badge></Table.Cell> @@ -413,7 +433,7 @@ </Table.Root> </div> {/if} - <p class="text-sm text-muted-foreground">版本为不可变记录,恢复均创建新草稿,从不改写历史。获取到最新草稿修改内容请刷新页面查看。</p> + <p class="text-xs text-muted-foreground m-0">版本为不可变记录,恢复均创建新草稿,从不改写历史。</p> </Card.Root> {#if comparing} diff --git a/frontend/src/pages/ProjectsPage.svelte b/frontend/src/pages/ProjectsPage.svelte index 5cbfecf..65137d2 100644 --- a/frontend/src/pages/ProjectsPage.svelte +++ b/frontend/src/pages/ProjectsPage.svelte @@ -1,20 +1,19 @@ <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, 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'; + import { fmtDateTime, fmtPct } from '../lib/format'; import { Button } from '$lib/components/ui/button'; import * as Card from '$lib/components/ui/card'; - import * as Table from '$lib/components/ui/table'; import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; import { Alert } from '$lib/components/ui/alert'; + import { Badge } from '$lib/components/ui/badge'; let busy = $state(false); let error = $state<string | null>(null); @@ -24,21 +23,20 @@ let newDesc = $state(''); let createErr = $state<string | null>(null); - // ---- round2 指标卡行输入 ---- - // projectsStore 持有项目列表;其余三项在本页拉取一次。 - // TODO: AI 用量若 /api/ai/usage 不给 totals.requests,占位 "-"(见 ProjectMetricRow)。 + // ---- 问候行 + 指标摘要(一行小字,不做 4 张大卡)---- 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 runs: { id: string; status: string; created_at?: 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 }>()); + const hours = new Date().getHours(); + const greeting = hours < 12 ? '早上好' : hours < 18 ? '下午好' : '晚上好'; + async function load() { busy = true; error = null; try { await projectsStore.load(); - // 指标卡的三项远程输入并行拉取;任何一项失败不影响项目列表本身。 - // TODO: 用量字段若有专门计数端点(总数/时长),替换 getAIUsage。 const [ds, runList, usageRes] = await Promise.allSettled([ listDatasets(), listRuns(), @@ -55,9 +53,6 @@ } } - // 159399 QA regression (2026-09-17): load() existed but was wired nowhere, - // so the projects page kept rendering the store's stale/empty state and - // users saw 还没有项目 despite existing projects on the server. $effect(() => { void load(); }); @@ -80,33 +75,61 @@ creating = false; } } + + // 项目卡一句话状态 + const runStatusOf = $derived.by(() => { + const m = new Map<string, { running: number; done: number; lastReturn?: string }>(); + for (const r of runs ?? []) { + const e = m.get(r.project_id ?? '') ?? { running: 0, done: 0 }; + if (r.status === 'running' || r.status === 'queued') e.running++; + if (r.status === 'succeeded') { + e.done++; + const tr = (r.result as { metrics?: { total_return?: number } } | undefined)?.metrics?.total_return; + if (tr !== undefined) e.lastReturn = undefined; + const tr2 = tr; + if (tr2 !== undefined) (e as { lastReturn?: string }).lastReturn = fmtPct(tr2); + } + m.set(r.project_id ?? '', e); + } + return m; + }); </script> -<Page title="我的项目" subtitle="每个项目持有独立的策略代码版本、数据集与回测记录" wide> +<Page title={`${greeting},${projectsStore.all.length} 个进行中的策略研究`} subtitle="从下面的项目继续你的研究" wide> {#snippet actions()} <Button variant={showCreate ? 'outline' : 'default'} onclick={() => (showCreate = !showCreate)} aria-expanded={showCreate}> {showCreate ? '取消新建' : '新建项目'} </Button> {/snippet} - <div class="flex flex-col gap-3"> - <ProjectMetricRow projects={projectsStore.all} {datasets} {runs} {usage} /> - <Status {busy} {error} busyText="加载项目列表…" empty={null} /> + <div class="flex flex-col gap-4 md:gap-6"> + {#if busy} + <Status {busy} busyText="加载项目列表…" skeletonRows={4} empty={null} /> + {:else if error} + <Status {error} empty={null} /> + {:else} + <p class="text-xs text-muted-foreground" data-testid="metrics-summary"> + {projectsStore.all.length} 个项目 · {(datasets ?? []).length} 个数据集 · + {[...runStatusOf.values()].reduce((a, b) => a + b.running, 0)} 个回测运行中 · + {(runs ?? []).length} 次回测 · AI 请求 {usage?.totals?.requests ?? '—'} + </p> + {/if} + {#if showCreate} <Card.Root class="p-4"> - <form class="flex flex-col gap-3" onsubmit={create}> - <h2 class="text-lg font-semibold">新建项目</h2> + <form class="flex flex-col gap-4" onsubmit={create}> + <h2 class="text-lg font-semibold m-0">新建项目</h2> <div> <Label for="pn">项目名称</Label> - <Input id="pn" bind:value={newName} required placeholder="例如:双均线ETF轮动" class="mt-1" /> + <Input id="pn" bind:value={newName} required placeholder="例如:双均线ETF轮动" /> </div> <div> <Label for="pd">简介(可选)</Label> - <Input id="pd" bind:value={newDesc} placeholder="一句话说明研究思路" class="mt-1" /> + <Input id="pd" bind:value={newDesc} placeholder="一句话说明研究思路" /> </div> - {#if createErr}<Alert variant="destructive" role="alert">{createErr}</Alert>{/if} + {#if createErr}<Alert variant="destructive">{createErr}</Alert>{/if} <div> - <Button type="submit" disabled={creating || !newName.trim()}> + <Button class="w-fit" type="submit" disabled={creating || !newName.trim()}> {creating ? '创建中…' : '创建项目'} </Button> </div> @@ -114,44 +137,58 @@ </Card.Root> {/if} + {#if !busy && error && projectsStore.all.length === 0} + <Status busy={false} {error} empty={null}> + {#snippet errorCta()} + <Button onclick={load}>重新加载</Button> + {/snippet} + </Status> + {/if} + {#if !busy && !error && projectsStore.all.length === 0} - <Status busy={false} empty="还没有项目" emptyHint="点击右上角「新建项目」开始你的第一个策略研究" /> + <Status + busy={false} + empty="还没有项目" + emptyHint="从新建项目开始你的第一个策略研究:命名 → 接入数据 → 编写策略 → 回测" + > + {#snippet emptyCta()} + <Button onclick={() => (showCreate = true)}>新建项目</Button> + {/snippet} + </Status> {/if} {#if projectsStore.all.length > 0} - <Card.Root size="sm"> - <Table.Root> - <Table.Header> - <Table.Row> - <Table.Head>名称</Table.Head> - <Table.Head>最近回测</Table.Head> - <Table.Head>简介</Table.Head> - <Table.Head>版本代</Table.Head> - <Table.Head>最近更新</Table.Head> - </Table.Row> - </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`} data-testid="project-link" class="font-semibold hover:underline">{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="text-muted-foreground text-sm">{p.description ?? '—'}</Table.Cell> - <Table.Cell class="tabular-nums">{p.draft_generation ?? '—'}</Table.Cell> - <Table.Cell>{fmtDateTime(p.updated_at)}</Table.Cell> - </Table.Row> - {/each} - </Table.Body> - </Table.Root> - </Card.Root> + <div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6" role="list" aria-label="项目列表"> + {#each projectsStore.all as p (p.id)} + {@const sp = sparks.get(p.id)} + {@const st = runStatusOf.get(p.id)} + <Card.Root size="sm" role="listitem"> + <div class="flex justify-between items-start gap-4"> + <div class="min-w-0 flex-1"> + <a href={`#/projects/${p.id}/data`} data-testid="project-link" class="text-lg font-semibold hover:underline"> + {p.name} + </a> + <p class="text-xs text-muted-foreground mt-1 truncate">{p.description ?? '—'}</p> + </div> + <Badge variant={st?.running ? 'outline' : st?.done ? 'secondary' : 'outline'}> + {st?.running ? `运行中 ${st.running}` : st?.done ? `最近 ${st.lastReturn ?? '完成'}` : '未回测'} + </Badge> + </div> + <div class="flex items-center gap-4 mt-4"> + {#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> + <span class="text-xs text-muted-foreground">最近 {runStatusOf.get(p.id)?.lastReturn ?? '完成'}</span> + {:else} + <span class="text-xs text-muted-foreground">还没有回测记录</span> + {/if} + <span class="flex-1"></span> + <span class="text-xs text-muted-foreground">更新 {fmtDateTime(p.updated_at)}</span> + </div> + </Card.Root> + {/each} + </div> {/if} </div> </Page> diff --git a/frontend/src/pages/RunsPage.svelte b/frontend/src/pages/RunsPage.svelte index 77626b5..ce66a25 100644 --- a/frontend/src/pages/RunsPage.svelte +++ b/frontend/src/pages/RunsPage.svelte @@ -103,10 +103,16 @@ </script> <Page title="实验记录" subtitle="查看与选择比较不同回测;结果仅展示真实完成任务的数据" wide> - <div class="flex flex-col gap-3"> - <Status busy={busy} error={error} busyText="正在加载实验…" empty={null} /> - - {#if !busy && !error && runs.length === 0} + <div class="flex flex-col gap-4 md:gap-6"> + {#if busy} + <Status busy skeletonRows={4} busyText="正在加载实验…" empty={null} /> + {:else if error} + <Status busy={false} {error} empty={null}> + {#snippet errorCta()} + <Button onclick={load}>重新加载</Button> + {/snippet} + </Status> + {:else if runs.length === 0} <Status busy={false} empty="还没有实验记录" emptyHint="进入项目「回测」标签发起一次回测后记录会出现在这里" /> {/if} @@ -152,12 +158,12 @@ {/each} </Table.Body> </Table.Root> - <p class="text-sm text-muted-foreground p-2">最多同时选中 4 个已完成的回测用于比较。</p> + <p class="text-xs text-muted-foreground p-4">最多同时选中 4 个已完成的回测用于比较。</p> </Card.Root> {#if selected.length > 0} - <Card.Root size="sm" class="p-4 flex flex-col gap-3 overflow-x-auto"> - <h2 class="text-lg font-semibold">条件差异</h2> + <Card.Root size="sm" class="p-4 flex flex-col gap-4 overflow-x-auto"> + <h2 class="text-lg font-semibold m-0">条件差异</h2> <Table.Root> <Table.Header> <Table.Row> @@ -179,10 +185,10 @@ </Card.Root> {#if comparable.length > 1} - <Card.Root size="sm" class="p-4 flex flex-col gap-3"> - <h2 class="text-lg font-semibold">收益曲线对齐比较(首日归一)</h2> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <h2 class="text-lg font-semibold m-0">收益曲线对齐比较(首日归一)</h2> <Chart data={compare.data} series={compare.series} ariaLabel="多回测对比曲线" height={340} /> - <p class="text-sm text-muted-foreground">起点对齐为 1,仅叠加不同起点的曲线时只在各自区间内展示。</p> + <p class="text-xs text-muted-foreground">起点对齐为 1,仅叠加不同起点的曲线时只在各自区间内展示。</p> </Card.Root> {:else} <Status busy={false} empty="请再选中至少一个已完成的回测以展示比较" /> diff --git a/frontend/src/pages/UsagePage.svelte b/frontend/src/pages/UsagePage.svelte index 018b4a0..00b5917 100644 --- a/frontend/src/pages/UsagePage.svelte +++ b/frontend/src/pages/UsagePage.svelte @@ -32,10 +32,10 @@ </script> <Page title="AI 用量" subtitle="内部 POC 阶段的用量计量与限额参考。不含报价或计费"> - <div class="flex flex-col gap-3"> + <div class="flex flex-col gap-4 md:gap-6"> <Status busy={busy} error={error} busyText="正在载入用量…" empty={null} /> {#if usage} - <Card.Root size="sm" class="p-0 [&>[data-slot=card-content]]:px-4 [&>[data-slot=card-content]]:py-3"> + <Card.Root size="sm" class="p-4"> <Card.Content> <div class="flex flex-wrap gap-x-3 gap-y-1"> <div> @@ -60,7 +60,7 @@ </Card.Root> {#if usage.items.length > 0} - <Card.Root size="sm" class="px-3 py-2"> + <Card.Root size="sm" class="p-4"> <Table.Root> <Table.Header> <Table.Row> |
