diff options
Diffstat (limited to 'frontend/src/components/DatasetWizard.svelte')
| -rw-r--r-- | frontend/src/components/DatasetWizard.svelte | 223 |
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> |
