summaryrefslogtreecommitdiff
path: root/frontend/src/components/DatasetCard.svelte
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
commit5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch)
tree948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /frontend/src/components/DatasetCard.svelte
downloadstrategy-lab-5c0ba37eda80d39e6ceca59bb1d5f4942f858995.tar.gz
chore: establish Strategy Lab source baseline (development, not release)
Diffstat (limited to 'frontend/src/components/DatasetCard.svelte')
-rw-r--r--frontend/src/components/DatasetCard.svelte157
1 files changed, 157 insertions, 0 deletions
diff --git a/frontend/src/components/DatasetCard.svelte b/frontend/src/components/DatasetCard.svelte
new file mode 100644
index 0000000..7b38874
--- /dev/null
+++ b/frontend/src/components/DatasetCard.svelte
@@ -0,0 +1,157 @@
+<script lang="ts">
+ import Status from './Status.svelte';
+ import { getDataset, getDatasetPreview } from '../lib/client';
+ import { extractStatusMessage } from '../lib/state';
+ import { adjustmentLabel, assetTypeLabel, instrumentLabel, datasetStatusLabel, fmtDate } from '../lib/format';
+ import { summarizeCoverage } from '../lib/coverage';
+ import type { Dataset, DatasetPreview } from '../lib/types';
+
+ interface Props {
+ dsId: string;
+ }
+ let { dsId }: Props = $props();
+
+ let ds = $state<Dataset | null>(null);
+ let error: string | null = $state(null);
+ let busy = $state(true);
+ let showPreview = $state(false);
+ let preview = $state<DatasetPreview | null>(null);
+ let previewBusy = $state(false);
+ let previewError: string | null = $state(null);
+ let pollTimer: ReturnType<typeof setInterval> | null = null;
+
+ function stopPolling() {
+ if (pollTimer) {
+ clearInterval(pollTimer);
+ pollTimer = null;
+ }
+ }
+
+ $effect(() => {
+ void load();
+ return stopPolling;
+ });
+
+ async function load() {
+ error = null;
+ try {
+ ds = await getDataset(dsId);
+ if (ds.status === 'pending' || ds.status === 'running') {
+ if (!pollTimer) {
+ pollTimer = setInterval(() => {
+ void load();
+ }, 2000);
+ }
+ } else {
+ stopPolling();
+ }
+ } catch (e) {
+ error = extractStatusMessage(e);
+ } finally {
+ busy = false;
+ }
+ }
+
+ async function togglePreview() {
+ showPreview = !showPreview;
+ if (showPreview && !preview && ds?.status === 'ready') {
+ previewBusy = true;
+ previewError = null;
+ try {
+ preview = await getDatasetPreview(dsId);
+ } catch (e) {
+ previewError = `预览加载失败:${extractStatusMessage(e)}`;
+ } finally {
+ previewBusy = false;
+ }
+ }
+ }
+</script>
+
+<div class="card">
+ {#if ds}
+ <div class="spread">
+ <div>
+ <strong>{ds.name ?? '未命名数据集'}</strong>
+ {#if ds.manifest_hash}<span class="hint mono">manifest {ds.manifest_hash.slice(0, 10)}…</span>{/if}
+ {#if ds.cache_hit}<span class="badge teal">缓存命中 · 复用现有物化对象</span>{/if}
+ </div>
+ <span class="badge {ds.status === 'ready' ? 'ok' : ds.status === 'failed' ? 'danger' : 'warn'}">
+ {datasetStatusLabel[ds.status]}
+ </span>
+ </div>
+ <table class="data kv">
+ <tbody>
+ <tr><th>请求标的</th><td>{(ds.request.instruments ?? []).map(instrumentLabel).join(';')}</td></tr>
+ <tr><th>范围</th><td>{ds.request.start_date} ~ {ds.request.end_date} · {ds.request.frequency} · {adjustmentLabel[ds.request.adjustment] ?? ds.request.adjustment}</td></tr>
+ <tr><th>字段</th><td>{(ds.request.fields ?? []).join(', ')}</td></tr>
+ <tr><th>创建时间</th><td>{fmtDate(ds.created_at)}</td></tr>
+ </tbody>
+ </table>
+ {#if ds.status === 'failed'}
+ <div class="banner error" role="alert"><span aria-hidden="true">✕</span><span>{ds.error ?? '数据获取失败'}</span></div>
+ {/if}
+ {#if ds.status !== 'ready'}
+ <p class="hint">{ds.status === 'pending' || ds.status === 'running' ? '获取中,完成后可预览覆盖情况' : ''}</p>
+ {/if}
+ {#if ds.status === 'ready'}
+ <div>
+ <button class="btn small" aria-expanded={showPreview} onclick={togglePreview}>
+ {showPreview ? '收起预览' : '查看数据覆盖预览'}
+ </button>
+ </div>
+ {#if showPreview}
+ <div class="preview-box">
+ <Status busy={previewBusy} busyText="加载预览…" error={previewError} empty={null} />
+ {#if preview}
+ {@const cov = summarizeCoverage(preview.coverage ?? [])}
+ {#if cov.warnings.length > 0}
+ <div class="banner warn"><span aria-hidden="true">⚠</span><div>
+ <div class="cov-caption">覆盖差异(共有区间 {cov.common.start ?? '—'} ~ {cov.common.end ?? '—'}):</div>
+ <ul class="cov-list">
+ {#each cov.warnings as w, i (i)}<li>{w}</li>{/each}
+ </ul>
+ {#if (preview.coverage ?? []).some((c) => c.warnings)}
+ <ul class="cov-list">
+ {#each preview.coverage.filter((c) => c.warnings) as c (c.instrument)}
+ <li>{c.instrument}:{(c.warnings ?? []).join(';')}</li>
+ {/each}
+ </ul>
+ {/if}
+ </div></div>
+ {:else if cov.common.start}
+ <div class="banner info">
+ <span aria-hidden="true">✓</span>
+ <span>覆盖平稳:共有可用区间 {cov.common.start} ~ {cov.common.end}。可以直接用于回测。</span>
+ </div>
+ {/if}
+ <table class="data scrollx">
+ <thead>
+ <tr>{#each preview.columns as c (c)}<th>{c}</th>{/each}</tr>
+ </thead>
+ <tbody>
+ {#each preview.rows as row, i (i)}
+ <tr>{#each preview.columns as c (c)}<td class="num">{row[c]}</td>{/each}</tr>
+ {/each}
+ </tbody>
+ </table>
+ {#if preview.truncated}
+ <p class="hint">预览仅展示部分行</p>
+ {/if}
+ {/if}
+ </div>
+ {/if}
+ {/if}
+ {:else}
+ <Status busy={busy} error={error} busyText="载入数据集…" empty={null} />
+ {/if}
+</div>
+
+<style>
+ .kv th { width: 110px; color: var(--text-2); font-weight: 500; min-width: 110px; }
+ .mono { font-family: var(--mono); margin-left: 0.5rem; }
+ .preview-box { margin-top: 0.5rem; display: grid; gap: 0.5rem; }
+ .scrollx { display: block; overflow-x: auto; }
+ .cov-caption { font-weight: 600; }
+ .cov-list { margin: 0.2rem 0 0 1.1rem; padding: 0; }
+</style>