summaryrefslogtreecommitdiff
path: root/frontend/src/components/DatasetWizard.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/components/DatasetWizard.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/components/DatasetWizard.svelte')
-rw-r--r--frontend/src/components/DatasetWizard.svelte223
1 files changed, 110 insertions, 113 deletions
diff --git a/frontend/src/components/DatasetWizard.svelte b/frontend/src/components/DatasetWizard.svelte
index d9e923a..e6ec105 100644
--- a/frontend/src/components/DatasetWizard.svelte
+++ b/frontend/src/components/DatasetWizard.svelte
@@ -3,6 +3,11 @@
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { Alert } from '$lib/components/ui/alert';
+ import { Input } from '$lib/components/ui/input';
+ import { Label } from '$lib/components/ui/label';
+ import * as Card from '$lib/components/ui/card';
+ import * as Select from '$lib/components/ui/select';
+ import * as Table from '$lib/components/ui/table';
import { getCapabilities, searchInstruments, createDataset } from '../lib/client';
import { extractStatusMessage } from '../lib/state';
import { adjustmentLabel, assetTypeLabel, itemSourceLabel } from '../lib/format';
@@ -180,17 +185,16 @@
}
</script>
-<div class="card stack">
+<Card.Root class="p-4 flex flex-col gap-3">
<h2>新建数据集</h2>
<Status busy={!caps && !loadErr} busyText="加载能力配置…" error={loadErr} empty={null} />
{#if caps}
- <section class="stack">
- <h3 class="step-h">1) 选择标的 <span class="hint">最多 {maxSymbols} 个 · 日频 POC</span></h3>
- <div class="row">
- <input
- style="flex:2 1 200px"
+ <section class="flex flex-col gap-2">
+ <h3 class="text-base font-semibold m-0">1) 选择标的 <span class="text-sm text-muted-foreground">最多 {maxSymbols} 个 · 日频 POC</span></h3>
+ <div class="flex items-center gap-2">
+ <Input class="flex-[2_1_200px]"
placeholder="搜索代码或名称,如 600000、沪深300ETF"
bind:value={query}
oninput={onSearchInput}
@@ -208,75 +212,83 @@
<Button variant="outline" size="sm" onclick={() => doSearch()}>重试</Button>
{/if}
{#if searchItems.length > 0}
- <table class="data">
- <thead>
- <tr><th>代码</th><th>类型</th><th>名称</th><th>来源</th><th></th></tr>
- </thead>
- <tbody>
- {#each searchItems as inst (inst.market + '.' + inst.symbol + inst.asset_type + '.' + (inst.source ?? 'unknown'))}
- <tr>
- <td class="num">{inst.market}.<strong>{inst.symbol}</strong></td>
- <td>{assetTypeLabel[inst.asset_type] ?? inst.asset_type}</td>
- <td>{inst.name ?? '—'}</td>
- <td class="hint">{itemSourceLabel(inst.source, searchSource)}</td>
- <td>
- <Button variant="outline" size="sm" onclick={() => addSelection(inst)} disabled={inst.asset_type === 'index' && selected.some((s) => sameInstrument(s, inst))}>
- {inst.asset_type === 'index' ? '加入(基准研究)' : '加入'}
- </Button>
- </td>
- </tr>
- {/each}
- </tbody>
- </table>
+ <div class="overflow-x-auto">
+ <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 searchItems as inst (inst.market + '.' + inst.symbol + inst.asset_type + '.' + (inst.source ?? 'unknown'))}
+ <Table.Row>
+ <Table.Cell class="tabular-nums">{inst.market}.<strong>{inst.symbol}</strong></Table.Cell>
+ <Table.Cell>{assetTypeLabel[inst.asset_type] ?? inst.asset_type}</Table.Cell>
+ <Table.Cell>{inst.name ?? '—'}</Table.Cell>
+ <Table.Cell class="text-sm text-muted-foreground">{itemSourceLabel(inst.source, searchSource)}</Table.Cell>
+ <Table.Cell>
+ <Button variant="outline" size="sm" onclick={() => addSelection(inst)} disabled={inst.asset_type === 'index' && selected.some((s) => sameInstrument(s, inst))}>
+ {inst.asset_type === 'index' ? '加入(基准研究)' : '加入'}
+ </Button>
+ </Table.Cell>
+ </Table.Row>
+ {/each}
+ </Table.Body>
+ </Table.Root>
+ </div>
{/if}
<details>
<summary>手动录入标的(目录未收录时)</summary>
- <div class="card stack manual">
- <p class="hint">仅接受确切标识,不会推断市场或类型。请与供应商页面核对后再录入。</p>
- <div class="row wrap">
- <label style="flex:1 1 130px">
- 代码
- <input bind:value={symbol} placeholder="600000 / 510300 / 000300" />
- </label>
- <label style="flex:0 0 110px">
- 市场
- <select bind:value={market}>
- {#each ['SH', 'SZ', 'BJ'] as m (m)}
- <option value={m}>{m}</option>
- {/each}
- </select>
- </label>
- <label style="flex:0 0 120px">
- 类型
- <select bind:value={assetType}>
- {#each caps.asset_types as t (t)}
- <option value={t}>{assetTypeLabel[t] ?? t}</option>
- {/each}
- </select>
- </label>
- <label style="flex:1 1 160px">
- 名称(可选)
- <input bind:value={manualName} placeholder="与供应商页面一致" />
- </label>
+ <Card.Root class="p-3 flex flex-col gap-2 my-1">
+ <p class="text-sm text-muted-foreground">仅接受确切标识,不会推断市场或类型。请与供应商页面核对后再录入。</p>
+ <div class="flex flex-wrap items-end gap-2">
+ <div class="flex-1 min-w-[130px]">
+ <Label for="manual-symbol">代码</Label>
+ <Input id="manual-symbol" bind:value={symbol} placeholder="600000 / 510300 / 000300" />
+ </div>
+ <div class="w-[110px]">
+ <Label for="manual-market">市场</Label>
+ <Select.Root type="single" value={market} onValueChange={(v) => (market = v)}>
+ <Select.Trigger id="manual-market">{market}</Select.Trigger>
+ <Select.Content>
+ {#each ['SH', 'SZ', 'BJ'] as m (m)}
+ <Select.Item value={m}>{m}</Select.Item>
+ {/each}
+ </Select.Content>
+ </Select.Root>
+ </div>
+ <div class="w-[120px]">
+ <Label for="manual-atype">类型</Label>
+ <Select.Root type="single" value={assetType} onValueChange={(v) => (assetType = v)}>
+ <Select.Trigger id="manual-atype">{assetTypeLabel[assetType] ?? assetType}</Select.Trigger>
+ <Select.Content>
+ {#each caps.asset_types as t (t)}
+ <Select.Item value={t}>{assetTypeLabel[t] ?? t}</Select.Item>
+ {/each}
+ </Select.Content>
+ </Select.Root>
+ </div>
+ <div class="flex-1 min-w-[160px]">
+ <Label for="manual-name">名称(可选)</Label>
+ <Input id="manual-name" bind:value={manualName} placeholder="与供应商页面一致" />
+ </div>
</div>
- <div class="row">
- <Button onclick={addManual}>加入所选</Button>
+ <div class="flex items-center gap-2">
+ <Button class="w-fit" onclick={addManual}>加入所选</Button>
</div>
- </div>
+ </Card.Root>
</details>
</section>
<section>
- <h3 class="step-h">2) 已选标的 <span class="hint">{selected.length}/{maxSymbols}</span></h3>
+ <h3 class="text-base font-semibold m-0">2) 已选标的 <span class="text-sm text-muted-foreground">{selected.length}/{maxSymbols}</span></h3>
{#if selected.length === 0}
- <p class="hint">尚未选择标的。指数仅可作研究中枢,不可直接交易。</p>
+ <p class="text-sm text-muted-foreground">尚未选择标的。指数仅可作研究中枢,不可直接交易。</p>
{:else}
- <ul class="chip-list">
+ <ul class="flex flex-wrap gap-1.5 p-0 m-0 list-none">
{#each selected as s, i (s.market + '.' + s.symbol + s.asset_type)}
- <li>
- <span class="num">{s.market}.{s.symbol}</span>
- {#if s.name}<span class="hint">{s.name}</span>{/if}
+ <li class="inline-flex items-center gap-1 border border-border rounded-full px-2 py-0.5 bg-secondary">
+ <span class="tabular-nums">{s.market}.{s.symbol}</span>
+ {#if s.name}<span class="text-sm text-muted-foreground">{s.name}</span>{/if}
<Badge variant="outline">{assetTypeLabel[s.asset_type] ?? s.asset_type}</Badge>
<Button variant="ghost" size="icon-sm" aria-label={`移除 ${s.symbol}`} onclick={() => selected.splice(i, 1)}>✕</Button>
</li>
@@ -285,67 +297,72 @@
{/if}
</section>
- <section class="grid3 wrap-3">
- <label>
- 起始日期
- <input type="date" bind:value={startDate} />
- </label>
- <label>
- 结束日期
- <input type="date" bind:value={endDate} />
- </label>
- <label style="min-width:160px">
- 复权
- <select bind:value={adjustment}>
- {#each caps.adjustments as a (a.code)}
- <option value={a.code}>{a.label}</option>
- {/each}
- </select>
+ <section class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-start">
+ <div>
+ <Label for="ds-start">起始日期</Label>
+ <Input id="ds-start" type="date" bind:value={startDate} />
+ </div>
+ <div>
+ <Label for="ds-end">结束日期</Label>
+ <Input id="ds-end" type="date" bind:value={endDate} />
+ </div>
+ <div class="min-w-40">
+ <Label for="ds-adj">复权</Label>
+ <Select.Root type="single" value={adjustment} onValueChange={(v) => (adjustment = v as typeof adjustment)}>
+ <Select.Trigger id="ds-adj" class="w-full">
+ {caps.adjustments.find((a) => a.code === adjustment)?.label ?? adjustment}
+ </Select.Trigger>
+ <Select.Content>
+ {#each caps.adjustments as a (a.code)}
+ <Select.Item value={a.code}>{a.label}</Select.Item>
+ {/each}
+ </Select.Content>
+ </Select.Root>
{#if adjustment !== 'none'}
- <p class="hint">{adjustmentLabel[adjustment]}:以复权口径返回</p>
+ <p class="text-sm text-muted-foreground">{adjustmentLabel[adjustment]}:以复权口径返回</p>
{/if}
- <p class="hint">日频 · 周期上限 {maxYears} 年</p>
- </label>
+ <p class="text-sm text-muted-foreground">日频 · 周期上限 {maxYears} 年</p>
+ </div>
</section>
<section>
- <label for="ds-name">数据集名称(可选,留空自动生成)</label>
- <input id="ds-name" bind:value={dsName} placeholder="例如:沪深300 2019-2024 前复权" />
+ <Label for="ds-name">数据集名称(可选,留空自动生成)</Label>
+ <Input id="ds-name" bind:value={dsName} placeholder="例如:沪深300 2019-2024 前复权" />
</section>
<details>
<summary>附加原始字段(可选)</summary>
- <p class="hint">OHLCV 必需字段由引擎要求固定获取;附加字段保持原始口径,未请求的字段不会返回。</p>
- <ul class="field-list">
+ <p class="text-sm text-muted-foreground">OHLCV 必需字段由引擎要求固定获取;附加字段保持原始口径,未请求的字段不会返回。</p>
+ <ul class="grid gap-1 p-0 m-0 list-none grid-cols-[repeat(auto-fit,minmax(220px,1fr))]">
{#each rawFieldChoices as f (f.code)}
<li>
- <label class="check">
+ <label class="flex items-center gap-1.5 text-sm m-0">
<input type="checkbox" checked={chosenRaw.includes(f.code)} onchange={() => toggleRawField(f.code)} />
- {f.label} <span class="hint">(未请求不返回)</span>
+ {f.label} <span class="text-sm text-muted-foreground">(未请求不返回)</span>
</label>
</li>
{/each}
</ul>
{#if caps.fields.some((f) => !f.raw)}
- <p class="hint">必需字段:{caps.fields.filter((f) => !f.raw).map((f) => f.label).join(' / ')}</p>
+ <p class="text-sm text-muted-foreground">必需字段:{caps.fields.filter((f) => !f.raw).map((f) => f.label).join(' / ')}</p>
{/if}
</details>
{#if formErr.length > 0}
- <Alert variant="destructive" class="bg-[var(--danger-soft)] border-[#f2c7c7]" role="alert">
+ <Alert variant="destructive" role="alert">
<span aria-hidden="true">⚠</span>
- <ul class="err-list">
+ <ul class="list-disc pl-5 m-0">
{#each formErr as msg, i (i)}<li>{msg}</li>{/each}
</ul>
</Alert>
{/if}
{#if submitErr}
- <Alert variant="destructive" class="bg-[var(--danger-soft)] border-[#f2c7c7]" role="alert">
+ <Alert variant="destructive" role="alert">
<span aria-hidden="true">⚠</span>
<span>数据请求提交失败:{submitErr}</span>
</Alert>
{/if}
- <p class="hint formhint">数据请求会进入队列异步获取。配置仅保存你填写的请求,不预填演示数据。</p>
+ <p class="text-sm text-muted-foreground">数据请求会进入队列异步获取。配置仅保存你填写的请求,不预填演示数据。</p>
<div>
<Button disabled={submitting || selected.length === 0} onclick={submit}>
{submitting ? '提交中…' : '提交数据请求'}
@@ -354,24 +371,4 @@
{/if}
<Status busy={false} error={null} empty={formOk} />
-</div>
-
-<style>
- .manual { padding: 0.8rem; margin: 0.4rem 0; }
- .manual input, .manual select { margin-top: 0.25rem; }
- .chip-list { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.4rem; }
- .chip-list li {
- display: inline-flex;
- align-items: center;
- gap: 0.4rem;
- border: 1px solid var(--border-strong);
- padding: 0.25rem 0.5rem;
- border-radius: 999px;
- background: var(--surface-2);
- }
- .field-list { list-style: none; padding: 0; margin: 0.3rem 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.2rem; }
- .check { font-size: 0.88rem; margin: 0; display: flex; gap: 0.35rem; align-items: center; }
- .step-h { font-size: 0.95rem; }
- .err-list { padding-left: 1.1rem; margin: 0; list-style: disc; }
- .wrap-3 { align-items: start; }
-</style>
+</Card.Root>