blob: 7b3887450f2a3e5ac0625cb81948d19274e482f7 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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>
|