summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectStrategyTab.svelte
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 15:49:44 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 15:49:44 +0800
commita7aab01f051487327e5ce52b6a8905208c546e8c (patch)
tree630a624523bc15d1a9f59ab20693385eaf55befb /frontend/src/pages/ProjectStrategyTab.svelte
parent375584174fb7191a57c5423fea45924ad6d812d5 (diff)
downloadstrategy-lab-a7aab01f051487327e5ce52b6a8905208c546e8c.tar.gz
[变更性质] feat(frontend)
[新增功能] 12 页面组件全量 shadcn:Card/Button/Table/Tabs/Select/Alert;375 底部 Tab Bar 改用 Tabs 组件实现 [实现方案] 页面级手写 class 全部移除(.btn/.banner/table.data/.badge/.tabs/.metric-row/.m-tile/.hint 等改为 shadcn + tailwind 工具类);Modal/DatasetWizard/DatasetCard/DiffView/CodeEditor/Status/Layout 同步 [影响范围] src/pages/*.svelte、src/components/*.svelte
Diffstat (limited to 'frontend/src/pages/ProjectStrategyTab.svelte')
-rw-r--r--frontend/src/pages/ProjectStrategyTab.svelte182
1 files changed, 91 insertions, 91 deletions
diff --git a/frontend/src/pages/ProjectStrategyTab.svelte b/frontend/src/pages/ProjectStrategyTab.svelte
index 8614cc0..e4269e3 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 Table from '$lib/components/ui/table';
import { projectsStore } from '../lib/listsStore.svelte';
import {
getProject,
@@ -185,7 +186,7 @@
try {
const [a, b] = await Promise.all([getVersionCode(projectId, v.id), getVersionCode(projectId, prev.id)]);
comparing = {
- label: `${a.code ? '' : ''}${v.hash.slice(0, 8)}(新)对比 ${prev.hash.slice(0, 8)}(旧)`,
+ label: `${v.hash.slice(0, 8)}(新)对比 ${prev.hash.slice(0, 8)}(旧)`,
before: b.code,
after: a.code
};
@@ -261,29 +262,29 @@
}
</script>
-<div class="stack">
+<div class="flex flex-col gap-3">
<Status busy={initBusy} error={initErr} busyText="加载策略草稿…" empty={null} />
{#if !initBusy && !initErr}
- <Card.Root size="sm" class="stack">
- <div class="spread">
- <h2>策略代码(Backtrader Python)</h2>
- <span class="save-status" role="status">
+ <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">
{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="hint">
+ <p class="text-sm text-muted-foreground">
编辑器修改会自动保存(约 0.8 秒停顿后提交)。草稿代 {baseGeneration} · 回测与获取 AI 建议使用当前草稿快照
</p>
<CodeEditor value={code} onInput={onEditorInput} readOnly={false} label="策略源码" height={430} />
- <div class="spread wrap">
+ <div class="flex flex-wrap justify-between items-center gap-2">
<Input
bind:value={snapshotMessage}
placeholder="快照说明,例如:加入RSI过滤器(可选)"
- style="flex:2 1 240px"
+ class="flex-[2_1_240px] min-w-[200px]"
onkeydown={(ev) => {
if (ev.key === 'Enter') void snapshot();
}}
@@ -302,119 +303,118 @@
<Status busy={false} error={snapshotErr} empty={snapshotMsg} />
{#if conflictServer}
- <Alert variant="default" class="bg-[var(--warn-soft)] border-[#f0ddb4]">
+ <Alert variant="default">
<span aria-hidden="true">⚠</span>
<span>
服务器上的草稿已被其他修改更新(代 {conflictServer.generation}),你正在编辑的本地内容不会自动覆盖。
</span>
</Alert>
<DiffView before={conflictServer.code} after={code} beforeLabel="服务器" afterLabel="本机" />
- <div class="row">
+ <div class="flex items-center gap-2 flex-wrap">
<Button onclick={keepMine}>保留本机修改(覆盖服务器)</Button>
<Button variant="outline" onclick={loadServer}>采用服务器版本</Button>
</div>
{/if}
</Card.Root>
- <div class="stack">
- <h2>AI 助手</h2>
- <Card.Root size="sm" class="stack">
- <label for="ai-instruction">希望 AI 帮你做什么?<span class="hint">例如:把均线周期改为两个可调参数并解释影响</span></label>
- <textarea id="ai-instruction" rows="3" bind:value={instruction} placeholder="描述对策略的修改意图,AI 返回完整新源码与差异预览"></textarea>
- <div class="row">
- <Button disabled={aiBusy || !instruction.trim()} onclick={askAI}>
- {aiBusy ? 'AI 处理中…' : '获取建议'}
- </Button>
- </div>
- {#if aiErr}
- <Alert variant="destructive" class="bg-[var(--danger-soft)] border-[#f2c7c7]">
- <span aria-hidden="true">⚠</span>
- <span>{aiErr}</span>
- </Alert>
- {/if}
+ <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">
+ <Button class="w-fit" disabled={aiBusy || !instruction.trim()} onclick={askAI}>
+ {aiBusy ? 'AI 处理中…' : '获取建议'}
+ </Button>
+ </div>
+ {#if aiErr}
+ <Alert variant="destructive">
+ <span aria-hidden="true">⚠</span>
+ <span>{aiErr}</span>
+ </Alert>
+ {/if}
- {#if aiResult}
- <div class="stack">
- <div class="spread">
- <h3>建议解释 <span class="hint">{aiResult.model}</span></h3>
- <span class="hint">
- tokens {aiResult.usage?.input_tokens ?? '—'} 入 / {aiResult.usage?.output_tokens ?? '—'} 出
- </span>
- </div>
- <p>{aiResult.explanation}</p>
- <h3>完整建议源码</h3>
- <CodeEditor value={aiResult.proposed_code} onInput={() => {}} readOnly={true} label="AI 返回代码(只读)" height={280} />
- <h3>与当前草稿的差异</h3>
- <DiffView before={code} after={aiResult.proposed_code} beforeLabel="当前草稿" afterLabel="AI 建议" />
- <div class="row">
- <Button
- disabled={aiAcceptBusy}
- onclick={acceptAI}
- title={aiResult.base_generation !== baseGeneration ? '草稿代已变化,建议如需采用请重新获取' : ''}
- >
- {aiAcceptBusy ? '接受中…' : '接受为新版本'}
- </Button>
- <Button variant="ghost" disabled={aiAcceptBusy} onclick={discardAI}>放弃建议</Button>
- {#if aiResult.base_generation !== baseGeneration}
- <span class="text-[var(--warn)]">草稿代 {aiResult.base_generation} 与当前 {baseGeneration} 不一致,接受会失败;请重新发起说明</span>
- {/if}
- {#if aiAcceptErr}
- <Alert variant="destructive" class="bg-[var(--danger-soft)] border-[#f2c7c7]">{aiAcceptErr}</Alert>
- {/if}
- </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">
+ tokens {aiResult.usage?.input_tokens ?? '—'} 入 / {aiResult.usage?.output_tokens ?? '—'} 出
+ </span>
</div>
- {/if}
- {#if aiNote}
- <Alert variant="default" class="bg-[var(--brand-soft)] border-[#bfe9db]">{aiNote}</Alert>
- {/if}
- </Card.Root>
- </div>
+ <p class="m-0">{aiResult.explanation}</p>
+ <h3 class="text-base font-semibold">完整建议源码</h3>
+ <CodeEditor value={aiResult.proposed_code} onInput={() => {}} readOnly={true} label="AI 返回代码(只读)" height={280} />
+ <h3 class="text-base font-semibold">与当前草稿的差异</h3>
+ <DiffView before={code} after={aiResult.proposed_code} beforeLabel="当前草稿" afterLabel="AI 建议" />
+ <div class="flex items-center gap-2 flex-wrap">
+ <Button
+ class="w-fit"
+ disabled={aiAcceptBusy}
+ onclick={acceptAI}
+ title={aiResult.base_generation !== baseGeneration ? '草稿代已变化,建议如需采用请重新获取' : ''}
+ >
+ {aiAcceptBusy ? '接受中…' : '接受为新版本'}
+ </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>
+ {/if}
+ {#if aiAcceptErr}
+ <Alert variant="destructive">{aiAcceptErr}</Alert>
+ {/if}
+ </div>
+ </div>
+ {/if}
+ {#if aiNote}
+ <Alert variant="default">{aiNote}</Alert>
+ {/if}
+ </Card.Root>
- <div class="stack">
- <h2>版本历史</h2>
+ <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}
<Status busy={false} empty="还没有版本快照" emptyHint="保存第一个快照后可以在历史中查看完整代码与恢复" />
{/if}
{#if !versionsBusy && versions.length > 0}
- <div class="px-3 py-2 rounded-[var(--radius-lg)] border border-border bg-card">
- <table class="data">
- <thead>
- <tr>
- <th>时间</th>
- <th>摘要</th>
- <th>哈希</th>
- <th>来源</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
+ <div class="overflow-x-auto rounded-lg border border-border">
+ <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 versions as v, i (v.id)}
- <tr>
- <td>{fmtDateTime(v.created_at)}</td>
- <td>
+ <Table.Row>
+ <Table.Cell>{fmtDateTime(v.created_at)}</Table.Cell>
+ <Table.Cell>
{v.message?.trim() || '(无说明)'}
- <span class="hint">版本代 {v.hash?.slice(0, 8) || '—'}</span>
- </td>
- <td class="num">{v.hash?.slice(0, 12)}</td>
- <td><Badge variant="outline">{versionSourceLabel[v.source]}</Badge></td>
- <td>
- <div class="row">
+ <span class="text-sm 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>
+ <Table.Cell>
+ <div class="flex items-center gap-1 flex-wrap">
<Button variant="outline" size="sm" onclick={() => void compareWithDraft(v)}>对比当前</Button>
{#if i + 1 < versions.length}
<Button variant="outline" size="sm" onclick={() => void compareTwoVersions(i)}>与相邻旧版对比</Button>
{/if}
<Button size="sm" onclick={() => void restore(v)}>恢复为新草稿</Button>
</div>
- </td>
- </tr>
+ </Table.Cell>
+ </Table.Row>
{/each}
- </tbody>
- </table>
+ </Table.Body>
+ </Table.Root>
</div>
{/if}
- <p class="hint">版本为不可变记录,恢复均创建新草稿,从不改写历史。获取到最新草稿修改内容请刷新页面查看。</p>
- </div>
+ <p class="text-sm text-muted-foreground">版本为不可变记录,恢复均创建新草稿,从不改写历史。获取到最新草稿修改内容请刷新页面查看。</p>
+ </Card.Root>
{#if comparing}
<Modal title={comparing.label} onClose={() => (comparing = null)}>