feat: add dataset catalog specification for single dataset research and Alpha template input
- Introduced a comprehensive specification for a dataset catalog focused on single dataset research. - Defined user stories, implementation decisions, and testing strategies to enhance the research workflow. - Established clear constraints and visual components to align with Lark design principles. - Outlined synchronization and persistence strategies for dataset and field management.
This commit is contained in:
@@ -0,0 +1,312 @@
|
|||||||
|
<!--
|
||||||
|
scope_sketch: Existing inline prototype style pass. Preserve one dataset with all fields, top actions, no visible panel titles, 75% field drawer and 30% detail drawer.
|
||||||
|
lark_style_recipe: Prescribed light surfaces, fixed #f9f9f9 sidebar and #1f23290d selection, restrained blue actions, neutral tags, 4px grid, 32px controls, light borders and no surface shadows.
|
||||||
|
ud_control_coverage: Local style update retains the existing semantic native Button/Input/Select/Table/checkbox/dialog implementation with UD-like sizing, hover/focus/disabled/selected states; no host fragment framework migration.
|
||||||
|
layout_signature_usage: side-nav-primary, 224px expanded navigation, responsive white workspace; other navigation items remain static prototype context.
|
||||||
|
top_nav_policy: Compact breadcrumb and sample marker. Business actions remain at top of the task area.
|
||||||
|
section_header_policy: No visible page or panel titles per the approved user requirement.
|
||||||
|
table_policy: Single-line 14px/22px/400 body, stable 56px rows, ID in detail and tooltip, child category in detail and existing filters. Five rows per page.
|
||||||
|
emphasis_budget: Body/actions/rows 400; selected navigation/table headers/detail object names 500. No Hero or decorative media.
|
||||||
|
icon_plan: Catalog exact matches: icon_close_outlined.43b3fbb2, icon_sort_outlined.a9ba7d09, icon_left_outlined.aa3e47e0, icon_right_outlined.598846b1, icon_down_outlined.8d68f3dd, icon_search_outlined.eac3ce55. All outlined/non-v2. URL prefix https://cdn-tos-cn.bytedance.net/obj/archi/ee/es-design-base/svgs/ and suffix .svg. Resource requests failed with TLS errors through urllib and curl. Area fallback: icon-free text actions and native Select affordances. No drawn replacement icons.
|
||||||
|
media_decision: media_needed=false; table-first research workflow.
|
||||||
|
verification_plan: Static checker, browser controls, drawer widths, desktop and compact screenshots, console checks.
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
#dataset-table-mock{color-scheme:light;--dt-paper:#ffffff;--dt-ink:#1f2329;--dt-muted:#646a73;--dt-line:#dee0e3;--dt-control:#d0d3d6;--dt-accent:#1456f0;--dt-soft:#1456f00a;--dt-head:#f8f9fa;--dt-ok:#25863b;--dt-warn:#a65b00;--dt-mask:#1f232933;--dt-page-x:32px;--dt-module-gap:16px;--dt-section-gap:40px;--dt-control-height:32px;font:400 14px/22px -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--dt-ink);width:100%}
|
||||||
|
#dataset-table-mock *{box-sizing:border-box}
|
||||||
|
#dataset-table-mock [hidden]{display:none!important}
|
||||||
|
#dataset-table-mock button,#dataset-table-mock input,#dataset-table-mock select,#dataset-table-mock textarea{font:inherit;color:inherit}
|
||||||
|
#dataset-table-mock button{cursor:pointer;font-weight:400}
|
||||||
|
#dataset-table-mock button:disabled{cursor:not-allowed;color:#8f959e;background:#f5f6f7;border-color:var(--dt-line)}
|
||||||
|
#dataset-table-mock :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dt-accent);outline-offset:2px}
|
||||||
|
#dataset-table-mock .dt-stage{position:relative;overflow:hidden;background:var(--dt-paper);min-height:640px;border:0.5px solid var(--dt-line)}
|
||||||
|
#dataset-table-mock .dt-shell{display:grid;grid-template-columns:224px minmax(0,1fr);min-height:640px}
|
||||||
|
#dataset-table-mock .dt-sidebar{background:#f9f9f9;padding:24px 12px;border-right:0.5px solid var(--dt-line)}
|
||||||
|
#dataset-table-mock .dt-brand{font-size:16px;line-height:24px;font-weight:500;letter-spacing:1px;padding:0 12px 32px;color:var(--dt-ink)}
|
||||||
|
#dataset-table-mock .dt-nav{display:flex;align-items:center;min-height:40px;padding:8px 12px;color:var(--dt-muted);border-radius:6px;margin:2px 0}
|
||||||
|
#dataset-table-mock .dt-nav.active{color:var(--dt-ink);background:#1f23290d;font-weight:500}
|
||||||
|
#dataset-table-mock .dt-main{min-width:0;background:var(--dt-paper)}
|
||||||
|
#dataset-table-mock .dt-topbar{min-height:56px;padding:16px var(--dt-page-x);border-bottom:0.5px solid var(--dt-line);display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;color:var(--dt-muted)}
|
||||||
|
#dataset-table-mock .dt-topbar>span:last-child{font-size:12px;line-height:20px;color:#8f959e}
|
||||||
|
#dataset-table-mock .dt-page{padding:24px var(--dt-page-x)}
|
||||||
|
#dataset-table-mock h3{font-size:16px;line-height:24px;font-weight:500;margin:0}
|
||||||
|
#dataset-table-mock .dt-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:var(--dt-control-height);padding:4px 12px;border:1px solid var(--dt-control);border-radius:6px;background:var(--dt-paper);white-space:nowrap}
|
||||||
|
#dataset-table-mock .dt-button:hover:not(:disabled){background:#f5f6f7}
|
||||||
|
#dataset-table-mock .dt-button:active:not(:disabled){background:#eff0f1}
|
||||||
|
#dataset-table-mock .dt-button.primary{background:var(--dt-accent);border-color:var(--dt-accent);color:#ffffff}
|
||||||
|
#dataset-table-mock .dt-button.primary:hover:not(:disabled){background:#336df4;border-color:#336df4}
|
||||||
|
#dataset-table-mock .dt-button.primary:active:not(:disabled){background:#0442d2;border-color:#0442d2}
|
||||||
|
#dataset-table-mock .dt-button.primary:disabled{background:#bacefd;border-color:#bacefd;color:#ffffff}
|
||||||
|
#dataset-table-mock .dt-link{padding:0;border:0;background:transparent;color:var(--dt-accent);text-align:left;white-space:nowrap}
|
||||||
|
#dataset-table-mock .dt-link:hover{color:#336df4;text-decoration:underline;text-underline-offset:4px}
|
||||||
|
#dataset-table-mock .dt-iconbutton{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:6px;background:transparent;padding:4px;color:var(--dt-muted);min-width:40px;height:32px;flex:none}
|
||||||
|
#dataset-table-mock .dt-iconbutton:hover{background:#1f23290a}
|
||||||
|
#dataset-table-mock input[type=search],#dataset-table-mock select,#dataset-table-mock textarea{background-color:var(--dt-paper);border:1px solid var(--dt-control);border-radius:6px;min-height:var(--dt-control-height);padding:4px 12px;max-width:100%;min-width:0}
|
||||||
|
#dataset-table-mock :is(input[type=search],select,textarea):hover:not(:disabled){border-color:#8f959e}
|
||||||
|
#dataset-table-mock input::placeholder,#dataset-table-mock textarea::placeholder{color:#8f959e}
|
||||||
|
#dataset-table-mock select{cursor:pointer}
|
||||||
|
#dataset-table-mock select:disabled{background-color:#f5f6f7;color:#8f959e;cursor:not-allowed}
|
||||||
|
#dataset-table-mock input[type=search]{padding-left:12px}
|
||||||
|
#dataset-table-mock input[type=radio],#dataset-table-mock input[type=checkbox]{accent-color:var(--dt-accent);width:16px;height:16px;vertical-align:middle;margin:0;cursor:pointer}
|
||||||
|
#dataset-table-mock .dt-scope{display:flex;align-items:center;gap:var(--dt-module-gap);flex-wrap:wrap;margin-bottom:24px}
|
||||||
|
#dataset-table-mock .dt-scope label{display:flex;align-items:center;gap:8px}
|
||||||
|
#dataset-table-mock .dt-scope label span{color:var(--dt-muted)}
|
||||||
|
#dataset-table-mock .dt-scope select{min-width:80px}
|
||||||
|
#dataset-table-mock .dt-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:var(--dt-module-gap)}
|
||||||
|
#dataset-table-mock .dt-toolbar input[type=search]{width:240px;flex:0 1 240px}
|
||||||
|
#dataset-table-mock .dt-toolbar select{max-width:160px;flex:none}
|
||||||
|
#dataset-table-mock .dt-tablewrap{width:100%;overflow-x:auto;border:0.5px solid var(--dt-line);border-radius:8px;background:var(--dt-paper)}
|
||||||
|
#dataset-table-mock table{border-collapse:collapse;width:100%;min-width:616px;text-align:left;font-size:14px;line-height:22px;font-weight:400;table-layout:fixed}
|
||||||
|
#dataset-table-mock th{background:var(--dt-head);color:var(--dt-muted);font-size:14px;font-weight:500;height:44px}
|
||||||
|
#dataset-table-mock th,#dataset-table-mock td{padding:12px 16px;border-bottom:0.5px solid var(--dt-line);vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
#dataset-table-mock td{height:56px;font-weight:400}
|
||||||
|
#dataset-table-mock tbody tr:last-child td{border-bottom:0}
|
||||||
|
#dataset-table-mock tr[data-select]:hover td,#dataset-table-mock tr[data-fieldrow]:hover td{background:#f5f6f7}
|
||||||
|
#dataset-table-mock tr.dt-selected td{background:var(--dt-soft)}
|
||||||
|
#dataset-table-mock th:nth-child(2){width:28%}
|
||||||
|
#dataset-table-mock th:nth-child(3){width:16%}
|
||||||
|
#dataset-table-mock .dt-checkcol{width:56px;text-align:center;padding:12px}
|
||||||
|
#dataset-table-mock .dt-number{text-align:right;font-variant-numeric:tabular-nums}
|
||||||
|
#dataset-table-mock .dt-cellname{font-size:14px;font-weight:400;display:block;width:100%;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
#dataset-table-mock .dt-id{display:block;font:400 12px/20px ui-monospace,SFMono-Regular,monospace;color:var(--dt-muted);overflow-wrap:anywhere;margin-top:8px}
|
||||||
|
#dataset-table-mock .dt-status{color:var(--dt-muted);white-space:nowrap}
|
||||||
|
#dataset-table-mock .dt-status.pending{color:var(--dt-warn)}
|
||||||
|
#dataset-table-mock .dt-sort{font:inherit;color:var(--dt-muted);border:0;background:transparent;padding:0;display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
|
||||||
|
#dataset-table-mock .dt-sort:hover,#dataset-table-mock .dt-sort[aria-pressed=true]{color:var(--dt-accent)}
|
||||||
|
#dataset-table-mock .dt-pagination{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;padding:var(--dt-module-gap) 0;color:var(--dt-muted)}
|
||||||
|
#dataset-table-mock .dt-pages{display:flex;align-items:center;gap:8px}
|
||||||
|
#dataset-table-mock .dt-pages .dt-button{min-width:48px;padding:4px 8px;border-color:transparent}
|
||||||
|
#dataset-table-mock .dt-inputbar{margin-bottom:var(--dt-section-gap);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;min-height:32px}
|
||||||
|
#dataset-table-mock .dt-inputsummary{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||||
|
#dataset-table-mock .dt-badge{padding:0 8px;background:#f2f3f5;color:var(--dt-muted);border-radius:4px;font-size:12px;line-height:24px;white-space:nowrap}
|
||||||
|
#dataset-table-mock .dt-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
#dataset-table-mock .dt-empty{text-align:center;padding:40px;color:var(--dt-muted)}
|
||||||
|
#dataset-table-mock .dt-live{font-size:12px;line-height:20px;color:var(--dt-muted);margin-top:8px}
|
||||||
|
#dataset-table-mock .dt-live:empty{display:none}
|
||||||
|
#dataset-table-mock .dt-layer{position:absolute;inset:0;z-index:10;display:flex;justify-content:flex-end;background:var(--dt-mask)}
|
||||||
|
#dataset-table-mock .dt-backdrop{position:absolute;inset:0}
|
||||||
|
#dataset-table-mock .dt-drawer{position:relative;width:var(--dt-drawer-width,432px);max-width:100%;background:var(--dt-paper);border-left:0.5px solid var(--dt-line);display:flex;flex-direction:column;min-width:0;animation:dt-slide .18s ease-out}
|
||||||
|
#dataset-table-mock #dt-layer{z-index:20}
|
||||||
|
#dataset-table-mock #df-drawer{width:75%}
|
||||||
|
#dataset-table-mock #dt-drawer[data-kind="field"]{width:30%}
|
||||||
|
#dataset-table-mock #df-scope{margin:0 0 24px;color:var(--dt-muted);font-size:14px}
|
||||||
|
#dataset-table-mock .dt-drawer-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;flex:1}
|
||||||
|
#dataset-table-mock #df-drawer-actions{justify-content:space-between;gap:12px}
|
||||||
|
#dataset-table-mock .dt-drawerhead>.dt-iconbutton{flex:none;align-self:flex-start}
|
||||||
|
#dataset-table-mock .dt-drawerhead{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;gap:8px;border-bottom:0.5px solid var(--dt-line)}
|
||||||
|
#dataset-table-mock .dt-drawerbody{padding:24px;flex:1;overflow:auto;min-height:0}
|
||||||
|
#dataset-table-mock .dt-drawerbody p{margin:16px 0;font-size:14px;line-height:22px}
|
||||||
|
#dataset-table-mock #dt-drawer[data-kind="field"] .dt-drawerhead,#dataset-table-mock #dt-drawer[data-kind="field"] .dt-drawerbody{padding:16px}
|
||||||
|
#dataset-table-mock .dt-facts{display:grid;grid-template-columns:80px minmax(0,1fr);gap:16px 12px;margin:24px 0;font-size:14px}
|
||||||
|
#dataset-table-mock #dt-drawer[data-kind="field"] .dt-facts{grid-template-columns:64px minmax(0,1fr);gap:16px 8px}
|
||||||
|
#dataset-table-mock .dt-facts dt{color:var(--dt-muted)}
|
||||||
|
#dataset-table-mock .dt-facts dd{margin:0;overflow-wrap:anywhere}
|
||||||
|
#dataset-table-mock .dt-section{padding-top:24px;margin-top:24px;border-top:0.5px solid var(--dt-line)}
|
||||||
|
#dataset-table-mock .dt-formfield{display:flex;flex-direction:column;gap:8px;margin-bottom:24px}
|
||||||
|
#dataset-table-mock .dt-formfield label{font-size:14px;font-weight:500}
|
||||||
|
#dataset-table-mock .dt-formfield select{width:100%}
|
||||||
|
#dataset-table-mock .dt-result{padding:16px;background:#f5f6f7;border-radius:8px;line-height:24px;font-size:14px}
|
||||||
|
#dataset-table-mock.compact td{height:44px;padding-top:8px;padding-bottom:8px}
|
||||||
|
@keyframes dt-slide{from{transform:translateX(24px)}to{transform:translateX(0)}}
|
||||||
|
@media(prefers-reduced-motion:reduce){#dataset-table-mock .dt-drawer{animation:none}}
|
||||||
|
@media(max-width:1023px){#dataset-table-mock{--dt-page-x:24px}#dataset-table-mock .dt-shell{grid-template-columns:160px minmax(0,1fr)}}
|
||||||
|
@media(max-width:700px){#dataset-table-mock #df-drawer,#dataset-table-mock #dt-drawer[data-kind="field"]{width:100%}}
|
||||||
|
@media(max-width:599px){#dataset-table-mock{--dt-page-x:16px}#dataset-table-mock .dt-shell{grid-template-columns:minmax(0,1fr)}#dataset-table-mock .dt-sidebar{display:none}#dataset-table-mock .dt-scope{gap:12px}#dataset-table-mock .dt-scope label{gap:8px}#dataset-table-mock .dt-toolbar input[type=search]{width:100%;flex:1 1 100%}#dataset-table-mock input[type=search],#dataset-table-mock select,#dataset-table-mock textarea{font-size:16px}#dataset-table-mock .dt-drawerhead,#dataset-table-mock .dt-drawerbody{padding:16px}#dataset-table-mock .dt-stage,#dataset-table-mock .dt-shell{min-height:704px}#dataset-table-mock .dt-pagination{font-size:12px}#dataset-table-mock .dt-pagination select{font-size:14px}}
|
||||||
|
@media(pointer:coarse){#dataset-table-mock button,#dataset-table-mock select,#dataset-table-mock input[type=search]{min-height:44px}#dataset-table-mock .dt-iconbutton{width:44px;height:44px}#dataset-table-mock .dt-checkcol label{display:flex;align-items:center;justify-content:center;min-height:44px}}
|
||||||
|
</style>
|
||||||
|
<div id="dataset-table-mock" aria-label="单数据集 Alpha 模板流程">
|
||||||
|
<div class="dt-stage">
|
||||||
|
<div class="dt-shell" id="dt-base">
|
||||||
|
<aside class="dt-sidebar" aria-label="工作空间导航"><div class="dt-brand">ALPHA</div><div class="dt-nav">Alpha 管理</div><div class="dt-nav active" aria-current="page">数据集</div><div class="dt-nav">个人信息</div></aside>
|
||||||
|
<div class="dt-main">
|
||||||
|
<div class="dt-topbar"><span id="dt-breadcrumb">工作空间 / 数据集</span><span>原型 · 示例数据</span></div>
|
||||||
|
<main class="dt-page">
|
||||||
|
<div class="dt-inputbar"><div class="dt-inputsummary" id="dt-inputsummary">选择一个数据集</div><div class="dt-actions"><button type="button" class="dt-link" id="dt-restore-all" hidden>恢复全选</button><button type="button" class="dt-button primary" id="dt-template" disabled>用于 Alpha 模板</button></div></div>
|
||||||
|
<div class="dt-scope">
|
||||||
|
<label><span>Region</span><select id="dt-region" aria-label="Region"><option>USA</option><option>EUR</option></select></label>
|
||||||
|
<label><span>Universe</span><select id="dt-universe" aria-label="Universe"><option>TOP3000</option><option>TOP1000</option></select></label>
|
||||||
|
<label><span>Delay</span><select id="dt-delay" aria-label="Delay"><option>1</option><option>0</option></select></label>
|
||||||
|
</div>
|
||||||
|
<div class="dt-toolbar">
|
||||||
|
<input type="search" id="dt-search" aria-label="搜索数据集" placeholder="搜索名称或 ID">
|
||||||
|
<select id="dt-category" aria-label="分类"><option value="">全部分类</option><option value="fundamental">基本面</option><option value="analyst">分析师</option><option value="news">新闻</option><option value="price">价量</option></select>
|
||||||
|
<select id="dt-subcategory" aria-label="子分类" disabled><option value="">全部子分类</option></select>
|
||||||
|
<select id="dt-type" aria-label="字段类型" hidden><option value="">全部类型</option><option>MATRIX</option><option>VECTOR</option></select>
|
||||||
|
<select id="dt-coverage" aria-label="最低覆盖率" hidden><option value="0">全部覆盖率</option><option value="80">覆盖率 ≥ 80%</option><option value="90">覆盖率 ≥ 90%</option></select>
|
||||||
|
<button type="button" class="dt-button" id="dt-reset">重置</button>
|
||||||
|
</div>
|
||||||
|
<div class="dt-tablewrap"><table id="dt-table" aria-label="数据集列表"><thead id="dt-thead"></thead><tbody id="dt-tbody"></tbody></table></div>
|
||||||
|
<div class="dt-pagination"><span id="dt-count"></span><div class="dt-pages"><select id="dt-pagesize" aria-label="每页条数"><option value="5">5 条 / 页</option><option value="10">10 条 / 页</option></select><button type="button" class="dt-button" id="dt-prev" aria-label="上一页">‹</button><span id="dt-page-count"></span><button type="button" class="dt-button" id="dt-next" aria-label="下一页">›</button></div></div>
|
||||||
|
<div class="dt-live" id="dt-live" role="status" aria-live="polite"></div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dt-layer" id="df-layer" hidden>
|
||||||
|
<div class="dt-backdrop" id="df-backdrop" aria-hidden="true"></div>
|
||||||
|
<section class="dt-drawer" id="df-drawer" role="dialog" aria-modal="true" aria-label="数据字段">
|
||||||
|
<div class="dt-drawerhead"><div class="dt-drawer-actions" id="df-drawer-actions"><div class="dt-inputsummary" id="df-inputsummary"></div><div class="dt-actions"><button type="button" class="dt-link" id="df-restore-all" hidden>恢复全选</button><button type="button" class="dt-button primary" id="df-template">用于 Alpha 模板</button></div></div><button type="button" id="df-close" class="dt-iconbutton" aria-label="关闭字段列表">×</button></div>
|
||||||
|
<div class="dt-drawerbody" id="df-body">
|
||||||
|
<div id="df-scope"></div>
|
||||||
|
<div class="dt-toolbar"><input type="search" id="df-search" aria-label="搜索字段" placeholder="搜索字段名称或 ID"><select id="df-type" aria-label="字段类型"><option value="">全部类型</option><option>MATRIX</option><option>VECTOR</option></select><select id="df-coverage" aria-label="最低覆盖率"><option value="0">全部覆盖率</option><option value="80">覆盖率 ≥ 80%</option><option value="90">覆盖率 ≥ 90%</option></select><button type="button" class="dt-button" id="df-reset">重置</button></div>
|
||||||
|
<div class="dt-tablewrap"><table id="df-table" aria-label="字段列表"><thead id="df-thead"></thead><tbody id="df-tbody"></tbody></table></div>
|
||||||
|
<div class="dt-pagination"><span id="df-count"></span><div class="dt-pages"><select id="df-pagesize" aria-label="每页条数"><option value="5">5 条 / 页</option><option value="10">10 条 / 页</option></select><button type="button" class="dt-button" id="df-prev" aria-label="上一页">‹</button><span id="df-page-count"></span><button type="button" class="dt-button" id="df-next" aria-label="下一页">›</button></div></div>
|
||||||
|
<div class="dt-live" id="df-live" role="status" aria-live="polite"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="dt-layer" id="dt-layer" hidden><div class="dt-backdrop" id="dt-backdrop" aria-hidden="true"></div><section class="dt-drawer" id="dt-drawer" role="dialog" aria-modal="true" aria-label="详情"><div class="dt-drawerhead"><div class="dt-drawer-actions" id="dt-drawer-actions"></div><button type="button" id="dt-close" class="dt-iconbutton" aria-label="关闭抽屉">×</button></div><div class="dt-drawerbody" id="dt-drawer-body"></div></section></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const root=document.getElementById('dataset-table-mock');
|
||||||
|
const el=id=>root.querySelector('#'+id);
|
||||||
|
for(const prefix of ['dt','df']){el(prefix+'-close').textContent='关闭';el(prefix+'-prev').textContent='上页';el(prefix+'-next').textContent='下页';}
|
||||||
|
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
const categories={fundamental:{label:'基本面',subs:{financial:'财务报表',ratios:'财务比率'}},analyst:{label:'分析师',subs:{estimates:'盈利预期'}},news:{label:'新闻',subs:{sentiment:'新闻情绪'}},price:{label:'价量',subs:{daily:'日频行情'}}};
|
||||||
|
const datasets=[
|
||||||
|
{id:'demo_financials',name:'公司财务报表',category:'fundamental',subcategory:'financial',description:'Company financial statements, including income, cash flow and balance sheet items.',count:12,vector:0,complete:true},
|
||||||
|
{id:'demo_estimates',name:'分析师盈利预期',category:'analyst',subcategory:'estimates',description:'Analyst earnings estimates and revisions.',count:8,vector:3,complete:true},
|
||||||
|
{id:'demo_ratios',name:'公司财务比率',category:'fundamental',subcategory:'ratios',description:'Profitability, leverage and valuation ratios.',count:9,vector:0,complete:true},
|
||||||
|
{id:'demo_news',name:'新闻情绪',category:'news',subcategory:'sentiment',description:'News sentiment observations.',count:7,vector:7,complete:false},
|
||||||
|
{id:'demo_prices',name:'股票日频行情',category:'price',subcategory:'daily',description:'Daily price and volume observations.',count:10,vector:0,complete:true},
|
||||||
|
{id:'demo_balance',name:'资产负债表',category:'fundamental',subcategory:'financial',description:'Assets, liabilities and equity items.',count:6,vector:0,complete:true}
|
||||||
|
];
|
||||||
|
const labels=[['operating_profit','营业利润','Operating profit.'],['operating_cashflow','经营现金流','Cash flow from operating activities.'],['revenue','营业收入','Revenue reported by the company.'],['net_income','净利润','Net income.'],['total_assets','总资产','Total assets.'],['total_liabilities','总负债','Total liabilities.'],['equity','股东权益','Total shareholders equity.'],['cash','现金及等价物','Cash and cash equivalents.'],['gross_profit','毛利润','Gross profit.'],['capex','资本开支','Capital expenditures.'],['receivables','应收账款','Accounts receivable.'],['inventory','存货','Inventories.']];
|
||||||
|
const otherLabels={demo_estimates:['盈利预测均值','盈利预测中位数','目标价均值','预测调整幅度','覆盖分析师数','盈利预测明细','目标价明细','评级明细'],demo_ratios:['资产收益率','净资产收益率','毛利率','净利率','资产负债率','流动比率','现金流比率','收入增长率','盈利增长率'],demo_news:['新闻情绪分值','正面新闻分值','负面新闻分值','新闻热度','事件强度','新闻相关性','新闻置信度'],demo_prices:['收盘价','开盘价','最高价','最低价','成交量','成交额','日收益率','换手率','流通市值','均价'],demo_balance:['总资产','总负债','股东权益','流动资产','流动负债','现金及等价物']};
|
||||||
|
const fields=Object.fromEntries(datasets.map(d=>[d.id,Array.from({length:d.count},(_,i)=>({id:d.id==='demo_financials'?'demo_'+labels[i][0]:d.id+'_field_'+(i+1),name:d.id==='demo_financials'?labels[i][1]:otherLabels[d.id][i],description:d.id==='demo_financials'?labels[i][2]:d.description,type:i>=d.count-d.vector?'VECTOR':'MATRIX',coverage:Number((94.8-i*1.7).toFixed(1)),users:285-i*13,alphas:1800-i*97,dataset:d.id}))]));
|
||||||
|
const state={view:'datasets',selected:'',dataset:'',page:1,size:5,sort:'',direction:1,query:'',category:'',subcategory:'',type:'',coverage:0,exclusions:{},snapshots:{},notes:{},drawer:null,template:'timeseries',bound:[],compact:false,drawerWidth:432,catalog:null};
|
||||||
|
let focusBeforeDrawer=null;
|
||||||
|
const ui=id=>el(state.view==='fields'?id.replace(/^dt-/,'df-'):id);
|
||||||
|
const dataset=id=>datasets.find(d=>d.id===id);
|
||||||
|
const scope=()=>({instrumentType:'EQUITY',region:el('dt-region').value,universe:el('dt-universe').value,delay:Number(el('dt-delay').value)});
|
||||||
|
const scopeText=()=>`${el('dt-region').value} · ${el('dt-universe').value} · Delay ${el('dt-delay').value}`;
|
||||||
|
const key=id=>[id,...Object.values(scope())].join('|');
|
||||||
|
const excludes=id=>state.exclusions[key(id)]||(state.exclusions[key(id)]=new Set());
|
||||||
|
const selectedFields=id=>fields[id].filter(f=>!excludes(id).has(f.id));
|
||||||
|
const complete=id=>state.snapshots[key(id)]??(dataset(id).complete&&scope().region==='USA'&&scope().universe==='TOP3000'&&scope().delay===1);
|
||||||
|
function say(message){ui('dt-live').textContent=message;}
|
||||||
|
function resetPage(){state.page=1;render();}
|
||||||
|
function filterRows(){
|
||||||
|
const q=state.query.toLowerCase().trim();
|
||||||
|
if(state.view==='fields'&&!complete(state.dataset))return [];
|
||||||
|
let rows=state.view==='datasets'?datasets.filter(d=>(!state.category||d.category===state.category)&&(!state.subcategory||d.subcategory===state.subcategory)&&(!q||[d.name,d.id,d.description].join(' ').toLowerCase().includes(q))):fields[state.dataset].filter(f=>(!state.type||f.type===state.type)&&f.coverage>=state.coverage&&(!q||[f.name,f.id,f.description].join(' ').toLowerCase().includes(q)));
|
||||||
|
if(state.sort)rows=[...rows].sort((a,b)=>{const av=a[state.sort],bv=b[state.sort];return (typeof av==='number'?av-bv:String(av).localeCompare(String(bv),'zh'))*state.direction;});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
function sortButton(label,name){return `<button type="button" class="dt-sort" data-sort="${name}" aria-pressed="${state.sort===name}" data-tooltip="${state.sort===name?(state.direction===1?'当前升序,点击降序':'当前降序,点击升序'):'点击排序'}" aria-label="${label},${state.sort===name?(state.direction===1?'升序':'降序'):'排序'}">${label}</button>`;}
|
||||||
|
function render(){
|
||||||
|
root.classList.toggle('compact',state.compact);root.style.setProperty('--dt-drawer-width',state.drawerWidth+'px');
|
||||||
|
const isFields=state.view==='fields';const activeId=isFields?state.dataset:state.selected;const active=dataset(activeId);
|
||||||
|
if(isFields){el('df-drawer').setAttribute('aria-label',dataset(state.dataset).name+' / 数据字段');el('df-scope').textContent=dataset(state.dataset).name+' · '+scopeText();}
|
||||||
|
else{el('dt-category').value=state.category;el('dt-subcategory').disabled=!state.category;el('dt-subcategory').innerHTML='<option value="">全部子分类</option>'+(state.category?Object.entries(categories[state.category].subs).map(([id,label])=>`<option value="${id}">${label}</option>`).join(''):'');el('dt-subcategory').value=state.subcategory;}
|
||||||
|
ui('dt-pagesize').value=String(state.size);
|
||||||
|
const rows=filterRows();const pages=Math.max(1,Math.ceil(rows.length/state.size));state.page=Math.min(state.page,pages);const visible=rows.slice((state.page-1)*state.size,state.page*state.size);
|
||||||
|
ui('dt-table').setAttribute('aria-label',isFields?'字段列表':'数据集列表');
|
||||||
|
ui('dt-thead').innerHTML=isFields?`<tr><th class="dt-checkcol"><label><input type="checkbox" id="dt-select-all" aria-label="选择本数据集全部字段"></label></th><th>${sortButton('字段','name')}</th><th>类型</th><th class="dt-number">${sortButton('覆盖率','coverage')}</th><th class="dt-number">${sortButton('用户数','users')}</th><th class="dt-number">${sortButton('Alpha 数','alphas')}</th></tr>`:`<tr><th class="dt-checkcol">选中</th><th>${sortButton('数据集','name')}</th><th>分类</th><th class="dt-number">${sortButton('字段数','count')}</th><th>同步状态</th><th>操作</th></tr>`;
|
||||||
|
ui('dt-tbody').innerHTML=visible.length?(isFields?visible.map(f=>`<tr data-fieldrow="${f.id}"><td class="dt-checkcol"><label><input type="checkbox" data-field="${f.id}" aria-label="选择${f.name}" ${excludes(state.dataset).has(f.id)?'':'checked'}></label></td><td><button type="button" class="dt-link dt-cellname" data-detail-field="${f.id}" data-tooltip="${f.id}">${f.name}</button></td><td>${f.type}</td><td class="dt-number">${complete(state.dataset)?f.coverage+'%':'—'}</td><td class="dt-number">${complete(state.dataset)?f.users:'—'}</td><td class="dt-number">${complete(state.dataset)?f.alphas:'—'}</td></tr>`).join(''):visible.map(d=>`<tr data-select="${d.id}" class="${state.selected===d.id?'dt-selected':''}"><td class="dt-checkcol"><label><input type="radio" name="dt-dataset" value="${d.id}" aria-label="选择${d.name}" ${state.selected===d.id?'checked':''}></label></td><td><button type="button" class="dt-link dt-cellname" data-detail-dataset="${d.id}" data-tooltip="${d.id}">${d.name}</button></td><td>${categories[d.category].label}</td><td class="dt-number">${d.count}</td><td><span class="dt-status ${complete(d.id)?'':'pending'}">${complete(d.id)?'已同步':'待同步'}</span></td><td class="dt-nowrap"><button type="button" class="dt-link" data-open-fields="${d.id}">查看字段</button></td></tr>`).join('')):`<tr><td colspan="6" class="dt-empty">暂无结果</td></tr>`;
|
||||||
|
for(const button of ui('dt-thead').querySelectorAll('[data-sort]'))button.closest('th').setAttribute('aria-sort',button.dataset.sort===state.sort?(state.direction===1?'ascending':'descending'):'none');
|
||||||
|
if(isFields){const check=el('dt-select-all');const n=selectedFields(state.dataset).length;check.checked=n===dataset(state.dataset).count;check.indeterminate=n>0&&n<dataset(state.dataset).count;check.disabled=!complete(state.dataset);if(!complete(state.dataset))ui('dt-tbody').innerHTML='<tr><td colspan="6" class="dt-empty">尚未同步字段</td></tr>';}
|
||||||
|
ui('dt-count').textContent=isFields?`匹配 ${rows.length} / 本集 ${dataset(state.dataset).count} 个字段`:`共 ${rows.length} 个数据集`;
|
||||||
|
ui('dt-page-count').textContent=`${state.page} / ${pages}`;ui('dt-prev').disabled=state.page===1;ui('dt-next').disabled=state.page>=pages;
|
||||||
|
const n=active?selectedFields(activeId).length:0;const all=active&&n===active.count;
|
||||||
|
ui('dt-inputsummary').innerHTML=active?`${isFields?'':`<span>${active.name}</span>`}<span class="dt-badge">${all?'全部 '+n+' 个字段':n+' / '+active.count+' 个字段'}</span>${complete(activeId)?'':`<span class="dt-status pending">待同步</span>`}`:'选择一个数据集';
|
||||||
|
ui('dt-restore-all').hidden=!active||all;ui('dt-template').disabled=!active||n===0;ui('dt-template').textContent=active&&!complete(activeId)?'同步全部字段':'用于 Alpha 模板';
|
||||||
|
}
|
||||||
|
function openFields(id){
|
||||||
|
if(state.view==='fields'&&state.dataset===id){closeDrawer(false);el('df-close').focus();return;}
|
||||||
|
closeDrawer(false);
|
||||||
|
if(state.view==='datasets'){state.selected=id;render();state.catalog={query:state.query,category:state.category,subcategory:state.subcategory,page:state.page,size:state.size,sort:state.sort,direction:state.direction};}
|
||||||
|
state.view='fields';state.dataset=id;state.selected=id;state.query='';state.type='';state.coverage=0;state.sort='';state.page=1;
|
||||||
|
el('df-search').value='';el('df-type').value='';el('df-coverage').value='0';el('df-layer').hidden=false;el('df-drawer').inert=false;el('df-drawer').setAttribute('aria-modal','true');el('dt-base').inert=true;render();el('df-body').scrollTop=0;el('df-close').focus();say('');
|
||||||
|
}
|
||||||
|
function closeFields(){
|
||||||
|
if(state.drawer){closeDrawer();return;}
|
||||||
|
const id=state.dataset;el('df-layer').hidden=true;el('dt-base').inert=false;state.view='datasets';state.dataset='';Object.assign(state,state.catalog||{query:'',category:'',subcategory:'',page:1,size:5,sort:''});render();
|
||||||
|
(el('dt-base').querySelector(`[data-open-fields="${id}"]`)||el('dt-search')).focus();
|
||||||
|
}
|
||||||
|
function openDrawer(kind,id){
|
||||||
|
if(!state.drawer)focusBeforeDrawer=document.activeElement;
|
||||||
|
state.drawer={kind,id};el('dt-drawer').dataset.kind=kind;el('dt-layer').hidden=false;el('dt-base').inert=true;el('df-drawer').inert=true;el('df-drawer').setAttribute('aria-modal','false');renderDrawer();el('dt-close').focus();
|
||||||
|
}
|
||||||
|
function closeDrawer(restore=true){
|
||||||
|
state.drawer=null;el('dt-layer').hidden=true;el('dt-base').inert=state.view==='fields';el('df-drawer').inert=false;el('df-drawer').setAttribute('aria-modal','true');if(restore){const target=focusBeforeDrawer?.isConnected&&!focusBeforeDrawer.disabled?focusBeforeDrawer:ui('dt-search');target.focus();}
|
||||||
|
}
|
||||||
|
function facts(values){return `<dl class="dt-facts">${values.map(([label,value])=>`<dt>${label}</dt><dd>${value}</dd>`).join('')}</dl>`;}
|
||||||
|
function renderDrawer(){
|
||||||
|
if(!state.drawer)return;const {kind,id}=state.drawer;let body='',actions='';
|
||||||
|
if(kind==='dataset'){
|
||||||
|
const d=dataset(id);el('dt-drawer').setAttribute('aria-label','数据集详情');
|
||||||
|
body=`<h3>${d.name}</h3><span class="dt-id">${d.id}</span><p>${d.description}</p>${facts([['分类',categories[d.category].label+' / '+categories[d.category].subs[d.subcategory]],['研究范围',scopeText()],['字段数',d.count],['字段类型',`${d.count-d.vector} MATRIX${d.vector?' · '+d.vector+' VECTOR':''}`],['同步状态',complete(id)?'已同步':'待同步']])}<div class="dt-section"><label for="dt-note">研究备注</label><textarea id="dt-note" rows="3" style="width:100%;margin-top:12px" placeholder="添加备注">${esc(state.notes[id]||'')}</textarea></div>`;
|
||||||
|
actions=`<button type="button" class="dt-button" data-open-fields="${id}">查看字段</button><button type="button" class="dt-button primary" data-use="${id}">${complete(id)?'全部字段用于模板':'同步全部字段'}</button>`;
|
||||||
|
}else if(kind==='field'){
|
||||||
|
const f=fields[state.dataset].find(f=>f.id===id);el('dt-drawer').setAttribute('aria-label','字段详情');
|
||||||
|
body=`<h3>${f.name}</h3><span class="dt-id">${f.id}</span><p>${f.description}</p>${facts([['数据集',dataset(f.dataset).name],['字段类型',f.type],['覆盖率',complete(f.dataset)?f.coverage+'%':'未提供'],['用户数',complete(f.dataset)?f.users:'未提供'],['Alpha 数',complete(f.dataset)?f.alphas:'未提供'],['数据单位','未提供']])}<div class="dt-section"><label for="dt-note">研究备注</label><textarea id="dt-note" rows="3" style="width:100%;margin-top:12px" placeholder="添加备注">${esc(state.notes[id]||'')}</textarea></div>`;
|
||||||
|
actions=`<button type="button" class="dt-button" data-toggle-field="${id}">${excludes(state.dataset).has(id)?'加入模板输入':'从模板输入排除'}</button><button type="button" class="dt-button primary" data-save-note="${id}">保存备注</button>`;
|
||||||
|
}else if(kind==='template'){
|
||||||
|
const d=dataset(id),chosen=selectedFields(id),matrix=chosen.filter(f=>f.type==='MATRIX').length,vector=chosen.length-matrix;
|
||||||
|
el('dt-drawer').setAttribute('aria-label','用于 Alpha 模板');
|
||||||
|
body=`${facts([['数据集',`${d.name}<span class="dt-id">${d.id}</span>`],['研究范围',scopeText()],['字段范围',chosen.length===d.count?`本集全部 ${chosen.length} 个字段`:`${chosen.length} / ${d.count} 个字段`],['字段类型',`${matrix} MATRIX${vector?' · '+vector+' VECTOR':''}`]])}<div class="dt-section"><div class="dt-formfield"><label for="dt-template-choice">Alpha 模板</label><select id="dt-template-choice"><option value="timeseries">单字段时序研究</option><option value="crosssection">单字段截面研究</option></select></div><div class="dt-result">1 个数据集 · ${chosen.length} 个字段输入</div></div>`;
|
||||||
|
actions='<button type="button" class="dt-button" data-close>取消</button><button type="button" class="dt-button primary" id="dt-bind">绑定到模板</button>';
|
||||||
|
}else if(kind==='bound'){
|
||||||
|
const draft=state.bound[state.bound.length-1];el('dt-drawer').setAttribute('aria-label','模板输入已绑定');
|
||||||
|
body=`<div class="dt-result">${dataset(draft.datasetId).name} · ${draft.fieldIds.length} 个字段</div>${facts([['Alpha 模板',draft.template==='timeseries'?'单字段时序研究':'单字段截面研究'],['研究范围',`${draft.scope.region} · ${draft.scope.universe} · Delay ${draft.scope.delay}`],['字段范围',draft.mode==='all'?'本集全部字段':`${draft.fieldIds.length} / ${dataset(draft.datasetId).count} 个字段`],['数据集数',1]])}`;
|
||||||
|
actions=`<button type="button" class="dt-button" data-open-fields="${draft.datasetId}">查看输入字段</button><button type="button" class="dt-button primary" data-close>完成</button>`;
|
||||||
|
}
|
||||||
|
el('dt-drawer-body').innerHTML=body;el('dt-drawer-actions').innerHTML=actions;
|
||||||
|
if(kind==='template')el('dt-template-choice').value=state.template;
|
||||||
|
}
|
||||||
|
function useDataset(id){
|
||||||
|
state.selected=id;
|
||||||
|
if(!complete(id)){state.snapshots[key(id)]=true;render();if(state.drawer)renderDrawer();say(`${dataset(id).name}:${dataset(id).count} 个字段已同步(示例)`);return;}
|
||||||
|
if(!selectedFields(id).length){say('至少选择一个字段');return;}
|
||||||
|
render();openDrawer('template',id);
|
||||||
|
}
|
||||||
|
root.addEventListener('click',event=>{
|
||||||
|
const button=event.target.closest('button');
|
||||||
|
if(button){
|
||||||
|
if(button.dataset.detailDataset)openDrawer('dataset',button.dataset.detailDataset);
|
||||||
|
else if(button.dataset.detailField)openDrawer('field',button.dataset.detailField);
|
||||||
|
else if(button.dataset.openFields)openFields(button.dataset.openFields);
|
||||||
|
else if(button.dataset.use){state.exclusions[key(button.dataset.use)]=new Set();useDataset(button.dataset.use);}
|
||||||
|
else if(button.hasAttribute('data-close'))closeDrawer();
|
||||||
|
else if(button.dataset.toggleField){const x=excludes(state.dataset);x.has(button.dataset.toggleField)?x.delete(button.dataset.toggleField):x.add(button.dataset.toggleField);render();renderDrawer();}
|
||||||
|
else if(button.dataset.saveNote){state.notes[button.dataset.saveNote]=el('dt-note').value;say('备注已保存(示例)');closeDrawer();}
|
||||||
|
else if(button.dataset.sort){state.direction=state.sort===button.dataset.sort?-state.direction:1;state.sort=button.dataset.sort;render();}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row=event.target.closest('[data-select]');if(row&&!event.target.closest('input')){state.selected=row.dataset.select;render();}
|
||||||
|
});
|
||||||
|
root.addEventListener('change',event=>{
|
||||||
|
if(event.target.name==='dt-dataset'){state.selected=event.target.value;render();}
|
||||||
|
if(event.target.dataset.field){const x=excludes(state.dataset);event.target.checked?x.delete(event.target.dataset.field):x.add(event.target.dataset.field);render();}
|
||||||
|
if(event.target.id==='dt-select-all'){state.exclusions[key(state.dataset)]=event.target.checked?new Set():new Set(fields[state.dataset].map(f=>f.id));render();}
|
||||||
|
if(event.target.id==='dt-template-choice')state.template=event.target.value;
|
||||||
|
});
|
||||||
|
root.addEventListener('input',event=>{if(event.target.id==='dt-note'&&state.drawer)state.notes[state.drawer.id]=event.target.value;});
|
||||||
|
el('dt-category').addEventListener('change',()=>{state.category=el('dt-category').value;state.subcategory='';el('dt-subcategory').disabled=!state.category;el('dt-subcategory').innerHTML='<option value="">全部子分类</option>'+(state.category?Object.entries(categories[state.category].subs).map(([id,label])=>`<option value="${id}">${label}</option>`).join(''):'');resetPage();});
|
||||||
|
el('dt-subcategory').addEventListener('change',()=>{state.subcategory=el('dt-subcategory').value;resetPage();});
|
||||||
|
for(const prefix of ['dt','df']){
|
||||||
|
const control=name=>el(prefix+'-'+name);
|
||||||
|
control('search').addEventListener('input',()=>{state.query=control('search').value;resetPage();});
|
||||||
|
control('type').addEventListener('change',()=>{state.type=control('type').value;resetPage();});
|
||||||
|
control('coverage').addEventListener('change',()=>{state.coverage=Number(control('coverage').value);resetPage();});
|
||||||
|
control('reset').addEventListener('click',()=>{state.query='';state.type='';state.coverage=0;if(state.view==='datasets'){state.category='';state.subcategory='';}control('search').value='';control('type').value='';control('coverage').value='0';resetPage();});
|
||||||
|
control('pagesize').addEventListener('change',()=>{state.size=Number(control('pagesize').value);resetPage();});
|
||||||
|
control('prev').addEventListener('click',()=>{state.page--;render();});control('next').addEventListener('click',()=>{state.page++;render();});
|
||||||
|
control('restore-all').addEventListener('click',()=>{state.exclusions[key(state.view==='fields'?state.dataset:state.selected)]=new Set();render();});
|
||||||
|
control('template').addEventListener('click',()=>useDataset(state.view==='fields'?state.dataset:state.selected));
|
||||||
|
}
|
||||||
|
function changedScope(){if(state.view==='datasets')state.selected='';state.page=1;render();say('研究范围已切换');}
|
||||||
|
el('dt-region').addEventListener('change',()=>{const options=el('dt-region').value==='USA'?['TOP3000','TOP1000']:['TOP2500','TOP1200'];el('dt-universe').innerHTML=options.map(v=>`<option>${v}</option>`).join('');changedScope();});
|
||||||
|
el('dt-universe').addEventListener('change',changedScope);el('dt-delay').addEventListener('change',changedScope);
|
||||||
|
el('df-close').addEventListener('click',()=>closeFields());el('df-backdrop').addEventListener('click',()=>closeFields());
|
||||||
|
el('dt-close').addEventListener('click',()=>closeDrawer());el('dt-backdrop').addEventListener('click',()=>closeDrawer());
|
||||||
|
el('dt-drawer-actions').addEventListener('click',event=>{if(event.target.id==='dt-bind'){
|
||||||
|
const id=state.drawer.id;const chosen=selectedFields(id);state.bound.push({datasetId:id,scope:scope(),mode:chosen.length===dataset(id).count?'all':'explicit',fieldIds:chosen.map(f=>f.id),template:state.template});openDrawer('bound',id);say(`${dataset(id).name} · ${chosen.length} 个字段已绑定(示例)`);
|
||||||
|
}});
|
||||||
|
root.addEventListener('keydown',event=>{if(!state.drawer&&state.view!=='fields')return;if(event.key==='Escape'){event.preventDefault();state.drawer?closeDrawer():closeFields();}else if(event.key==='Tab'){const panel=state.drawer?el('dt-drawer'):el('df-drawer');const controls=[...panel.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled)')].filter(c=>c.getClientRects().length);const first=controls[0],last=controls[controls.length-1];if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}}});
|
||||||
|
render();
|
||||||
|
if(globalThis.Tweak){const tweak=new Tweak({container:root,onChange:render});tweak.addToggle(state,'compact',{label:'紧凑表格'});}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# 数据集与数据字段:单数据集研究及 Alpha 模板输入
|
||||||
|
|
||||||
|
Status: ready-for-agent
|
||||||
|
|
||||||
|
日期:2026-09-07
|
||||||
|
类型:功能规格
|
||||||
|
范围:数据集目录、数据字段、详情抽屉、本地研究备注、可靠同步、模板输入交接。
|
||||||
|
依据:本次对话中确认的流程及 Lark 风格原型。测试边界已由用户确认。
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
研究员通常先选定一个数据集,再集中研究该数据集中的字段,并将整集字段提供给 Alpha 模板。以跨数据集搜索、逐个收集字段或命名字段池为主的流程,会增加准备步骤,也容易无意间混入其他数据集。
|
||||||
|
|
||||||
|
研究员需要在表格中按研究范围和分类筛选数据集,逐层查看字段和详情,同时保留列表上下文。搜索、筛选、排序或翻页只是帮助查看,不应悄悄缩小模板输入。只有研究员主动取消字段选择时,输入范围才发生变化。
|
||||||
|
|
||||||
|
当前系统已有账户连接、Alpha 管理、本地研究记录和持久化同步任务,尚无数据集、字段及模板输入能力。旧项目提供功能参考,不迁移旧库,也不继承字段池和跨数据集组合的默认工作流。
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
增加以单数据集为中心的数据目录。研究员设置 Region、Universe、Delay,按分类和子分类筛选,在 Table 中选定一个数据集。默认将该数据集的全部字段作为模板输入。
|
||||||
|
|
||||||
|
“查看字段”打开占整个工作区宽度 75% 的右抽屉,抽屉内仍使用 Table。点击字段打开占工作区宽度约 30% 的第二层右抽屉。关闭字段详情后保留第一层抽屉的搜索、筛选、页码和勾选;关闭字段列表后恢复原数据集列表。数据集自身详情也使用右抽屉。
|
||||||
|
|
||||||
|
页面和抽屉的功能操作区统一放在顶部,不设置重复的页面或抽屉标题,不在底部再放一组功能按钮。分页属于 Table 交互,可保留在表格下方。用选中数据集、字段数和研究范围表达当前上下文,避免大段功能说明及辅助小字。
|
||||||
|
|
||||||
|
界面采用已确认的 Lark 风格:白色主工作区、中性浅色侧栏、蓝色主操作、单行常规字重表格、轻边框及统一间距。正式实现沿用现有应用框架和控件体系,不直接把演示数据或原型脚本接入生产。
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
1. 作为研究员,我希望从工作空间进入数据集目录,以便围绕一个数据集开展研究。
|
||||||
|
2. 作为研究员,我希望按 Region 选择研究地区,以便看到该地区可用的数据集。
|
||||||
|
3. 作为研究员,我希望 Universe 与 Region 联动,以便避免组合不兼容的研究范围。
|
||||||
|
4. 作为研究员,我希望设置 Delay,以便目录、字段和模板输入使用一致的研究条件。
|
||||||
|
5. 作为研究员,我希望搜索数据集名称或 ID,以便快速定位已知数据集。
|
||||||
|
6. 作为研究员,我希望按分类筛选,以便集中查看基本面、分析师、新闻或价量等类型的数据。
|
||||||
|
7. 作为研究员,我希望子分类随分类联动,以便进一步缩小范围而不产生无效条件。
|
||||||
|
8. 作为研究员,我希望重置目录筛选时保留明确设置的研究范围,以便重新浏览同一研究环境。
|
||||||
|
9. 作为研究员,我希望在可排序、可分页的 Table 中查看数据集,以便高效比较名称、分类、字段数和同步状态。
|
||||||
|
10. 作为研究员,我希望一次只选中一个数据集,以便默认构建单数据集 Alpha。
|
||||||
|
11. 作为研究员,我希望选中数据集后默认包含整集字段,以便省去逐个勾选的步骤。
|
||||||
|
12. 作为研究员,我希望直接从数据集使用全部字段,以便无需先打开字段列表才能准备模板输入。
|
||||||
|
13. 作为研究员,我希望在右抽屉查看数据集说明、ID、分类、子分类和研究范围,以便了解数据含义且不离开列表。
|
||||||
|
14. 作为研究员,我希望在 75% 宽的右抽屉查看数据字段,以便获得足够的表格空间并保留数据集背景。
|
||||||
|
15. 作为研究员,我希望搜索当前数据集内的字段名称或 ID,以便定位关注的字段。
|
||||||
|
16. 作为研究员,我希望按字段类型和覆盖率筛选,以便比较符合当前研究条件的字段。
|
||||||
|
17. 作为研究员,我希望字段表格支持排序和分页,以便浏览较大的数据集。
|
||||||
|
18. 作为研究员,我希望筛选和翻页不改变模板输入,以便“全部字段”始终指当前数据集的完整字段集合。
|
||||||
|
19. 作为研究员,我希望通过取消勾选排除个别字段,以便保留整集为主的研究方式并处理例外。
|
||||||
|
20. 作为研究员,我希望表头全选操作作用于整个数据集,以便不会把当前页或当前搜索结果误当成全集。
|
||||||
|
21. 作为研究员,我希望顶部展示全部字段数或已选数/总数,以便随时确认模板输入范围。
|
||||||
|
22. 作为研究员,我希望能恢复全选,以便快速撤销排除并回到整集研究。
|
||||||
|
23. 作为研究员,我希望排除全部字段后无法提交输入,以便避免创建空的研究任务。
|
||||||
|
24. 作为研究员,我希望在约 30% 宽的第二层抽屉查看字段详情,以便对照当前字段列表理解含义。
|
||||||
|
25. 作为研究员,我希望字段详情显示 ID、所属数据集、类型、说明及平台实际提供的指标,以便判断字段是否适合研究。
|
||||||
|
26. 作为研究员,我希望缺失指标和单位显示为未提供,以便不把未知值误认为零或确定事实。
|
||||||
|
27. 作为研究员,我希望能在字段详情中排除或重新加入字段,以便判断后直接更新模板输入。
|
||||||
|
28. 作为研究员,我希望逐层关闭抽屉时保留搜索、页码和勾选,以便继续刚才的研究。
|
||||||
|
29. 作为研究员,我希望按钮统一放在顶部且界面少说明文字,以便把注意力留给数据。
|
||||||
|
30. 作为研究员,我希望能用键盘操作列表和抽屉,以便完成选择、查看和逐层返回。
|
||||||
|
31. 作为研究员,我希望窄屏下抽屉和顶部操作仍可用,以便在较小窗口中继续查看。
|
||||||
|
32. 作为研究员,我希望查看同步进度、失败原因、重试与取消,以便知道字段是否已经完整取得。
|
||||||
|
33. 作为研究员,我希望同步失败或重启后能继续,以便不必反复从头下载大数据集。
|
||||||
|
34. 作为研究员,我希望不完整同步不会被当成全部字段,以便模板不会遗漏尚未获取的数据。
|
||||||
|
35. 作为研究员,我希望为数据集和字段保存本地研究备注,以便记录理解与研究假设。
|
||||||
|
36. 作为研究员,我希望平台同步不会覆盖本地备注,以便长期积累研究记录。
|
||||||
|
37. 作为研究员,我希望模板输入明确记录数据集、研究范围和字段集合,以便后续模板消费时不靠猜测恢复条件。
|
||||||
|
38. 作为研究员,我希望已保存的输入不随后台同步自动变化,以便能复现一次研究准备结果。
|
||||||
|
39. 作为研究员,我希望模板尚未接入时能明确保存输入草稿,以便先完成准备且不会误以为已经开始回测。
|
||||||
|
40. 作为研究员,我希望已有账户、Alpha 和 AI 助手功能继续正常工作,以便新增数据目录不破坏现有研究流程。
|
||||||
|
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### 已确认的交互约束
|
||||||
|
|
||||||
|
- 入口以数据集为中心,不以跨数据集字段检索为默认入口。数据集单选,不提供多数据集输入篮子。
|
||||||
|
- 数据集和字段列表均采用 Table,包含筛选、稳定排序、分页和明确的选中状态。
|
||||||
|
- 数据集列表默认列为选择、数据集名称、分类、字段数、同步状态、查看字段。字段列表默认列为选择、字段名称、类型、覆盖率、用户数、Alpha 数。字段指标仅在上游确实提供时展示。
|
||||||
|
- 单元格保持单行、14px/22px/400;较长名称省略并可进入详情查看。ID 放在详情和可选的悬停提示中,子分类保留在筛选和详情中,不重新堆叠成双行小字。
|
||||||
|
- 字段抽屉宽度为完整工作区的 75%,字段详情抽屉宽度为完整工作区的 30%,不是父抽屉宽度的 30%。工作区指应用整体承载区域,包含侧栏;不按剩余表格宽度计算。
|
||||||
|
- 字段详情覆盖字段抽屉右部,不推动父抽屉或重建父列表。普通数据集详情、模板输入面板沿用常规右抽屉,不强行套用字段详情的 30%。
|
||||||
|
- 所有主要功能操作均在各自页面或抽屉顶部。保留关闭入口和无障碍名称,不恢复重复标题。详情正文中的对象名称是业务内容,不属于重复面板标题。
|
||||||
|
- Esc、关闭按钮及遮罩点击逐层关闭;背景不可交互,焦点限制在当前最上层抽屉。关闭时优先返回原触发控件,原控件不存在时回到有效的列表入口。
|
||||||
|
- 查看字段、打开详情、逐层返回不丢失父列表状态。重新打开一个已完全关闭的字段列表可重置浏览条件,但显式字段排除应按当前研究会话和范围保留。
|
||||||
|
- 与现有 AI 助手共享遮罩与焦点管理:不能同时出现两个可操作的模态层。窄屏沿用现有聊天展开时暂时隐藏业务详情、收起后恢复的规则;不得修改 75%/30% 的计算基准来挤出聊天空间。
|
||||||
|
|
||||||
|
### 视觉与组件
|
||||||
|
|
||||||
|
- 正式界面沿用现有 React、TypeScript、Semi Design 控件体系,实现 Lark/UD 风格的 Table、Button、Input、Select、Drawer、Tag 和反馈状态。原型的原生 HTML 控件是演示实现,不是正式组件选型。
|
||||||
|
- 主工作区白色或近白;侧栏直接使用 `#f9f9f9`,选中侧栏背景直接使用 `#1f23290d`,选中文字字重 500。蓝色用于主操作、链接、焦点和当前状态,不用作普通分类装饰。
|
||||||
|
- 间距以 4px 为基准,统一页面留白;控件圆角约 6px,表格容器约 8px。无渐变、普通内容无阴影。详情及表格正文以常规字重为主。
|
||||||
|
- 保留列表、研究范围、操作区这几个主要内容组,不增加 KPI 墙、Hero、推荐区或解释性侧栏。
|
||||||
|
- 原型每页 5 条用于展示,不作为产品分页上限;正式分页复用现有 25/50/100 条偏好。完整字段输入不得受分页上限影响。
|
||||||
|
- 宽度不足时工具栏换行或折叠筛选;字段抽屉在紧凑视口铺满可用工作区。表格可局部横向滚动,不能导致整页横向溢出。列表表体滚动、操作区可达,分页保持在列表可用区域内。
|
||||||
|
- 图标按指定 skill 的目录语义选择;正式资源可用时使用同组一致的图标。原型因图标资源不可用采用文字按钮,这是允许的降级,不要求复制字符图标或引入额外图标库。
|
||||||
|
|
||||||
|
### 领域边界与模块职责
|
||||||
|
|
||||||
|
- 沿用“平台快照、本地研究记录、同步任务”的既有分离原则,新增数据目录业务能力,统一由共用业务层提供查询、备注修改、同步控制和输入准备。
|
||||||
|
- WorldQuant 集成负责真实平台协议、认证、分页、退避和数据归一化;业务层负责研究范围、快照完整性、字段归属和输入约束;页面只处理交互状态并消费业务契约。
|
||||||
|
- 研究范围包含 instrument type、Region、Universe、Delay。首版页面以 EQUITY 为基础,不新增只有一个选项的品种选择器;契约显式保留该维度。
|
||||||
|
- 数据集/字段的可用性和指标按研究范围隔离。不能仅以字段 ID 建立跨范围唯一性,也不能从某个字段或模板表达式猜测 Region、Universe、Delay。
|
||||||
|
- 分类及子分类来自平台实际数据或已同步元数据;原型中的分类名称和示例 ID 不硬编码为完整生产枚举。未知分类仍可展示,缺失分类有明确空值处理。
|
||||||
|
- 平台字段类型按原值保留,已知 MATRIX/VECTOR 可筛选;未知类型不强制映射为已有类型。覆盖率的原始单位在集成层核对,显示与筛选使用同一归一化口径。
|
||||||
|
- 本地数据集备注与字段备注独立于平台原始响应保存,并按对象和研究范围建立身份关联。沿用本地研究记录版本检查,冲突时提示而不覆盖较新的内容;后台刷新不能覆盖未保存草稿。
|
||||||
|
|
||||||
|
### 同步和持久化
|
||||||
|
|
||||||
|
- 数据集目录按研究范围同步,字段按选定数据集与研究范围同步。读取页面不隐式触发全平台下载,不预先下载所有数据集的字段。
|
||||||
|
- 首次无目录数据时提供明确同步入口;已有缓存时可查看缓存及最后同步时间。未取得字段全集时,“用于 Alpha 模板”先转为同步动作,不允许用部分数据完成输入准备。
|
||||||
|
- 扩展现有持久化任务执行器与任务面板,继续使用任务 ID、查询进度、取消、失败重试和检查点恢复。保持单管理员、单平台账户、单后端进程约束,不引入第二套队列。
|
||||||
|
- 现有任务明细以 Alpha ID 为目标,新任务应使用明确的数据集/研究范围目标契约;不能将数据集 ID 伪装为 Alpha ID。保持旧任务 API 与历史记录可读。
|
||||||
|
- 每页字段与检查点同事务落库;重试幂等去重,并遵守上游 Retry-After。断开连接、人工验证和重启沿用已有任务状态语义。
|
||||||
|
- 字段集合记录同步批次、完成状态、实际去重数量与来源时间。只有一次成功完成的完整枚举才可用于“全部字段”;上游总数不可靠或分页异常时不得仅凭当前页数量宣称完整。
|
||||||
|
- 后续刷新在完成前不替换上一版可用字段集合。失败或取消保留上一版及本次进度;首次同步未完成时保持不可绑定。
|
||||||
|
- 单次未出现的字段不直接删除其历史快照或本地研究记录。一次成功刷新可形成新的字段集合版本,旧输入仍指向旧版本;这不意味着平台提供了严格的时间点一致性快照。
|
||||||
|
- 新增持久化结构覆盖范围化数据集、字段快照/集合版本、研究备注及模板输入记录。使用增量迁移,不改写已发布迁移,不清空 Alpha、账户、AI 数据或现有研究记录。
|
||||||
|
|
||||||
|
### 选择模型与模板输入交接
|
||||||
|
|
||||||
|
- 选择状态由单个数据集、研究范围和显式排除集合决定,浏览筛选独立保存。默认排除集合为空;搜索、类型筛选、覆盖率筛选、排序和翻页不得修改排除集合。
|
||||||
|
- 表头勾选作用于整个已完成字段集合;部分排除时显示半选。取消全选后为零选择,绑定不可用;“恢复全选”清空排除集合。
|
||||||
|
- 切换数据集或研究范围不沿用另一对象的排除集合。切换范围后重新读取该范围目录及字段同步状态,清理当前不适用的选中目标。
|
||||||
|
- 来自原型的核心不变量为:有效字段等于当前完整字段集合减去显式排除;与列表当前匹配结果和当前页无关。正文不要求保留原型内部状态变量或组件结构。
|
||||||
|
- 输入准备由服务端解析全部字段,不依赖浏览器已加载页数。服务端检查字段归属、范围、集合版本与非空约束,拒绝跨数据集字段、未知字段或不完整集合。
|
||||||
|
- 输入记录至少包含数据集 ID、研究范围、完整字段集合版本、选择意图(全部/显式子集)、实际字段 ID 集合、创建时间;接入真实模板后关联模板标识及必要版本。
|
||||||
|
- 即使选择意图为“全部”,保存时也固定实际字段集合及来源版本。后续同步新增或移除字段,不静默改变已保存输入;再次准备输入才消费新的完整版本。
|
||||||
|
- 页面显示总数与输入记录中的实际字段数一致。准备过程中集合版本变化时返回冲突,要求重新读取范围,不在后台悄悄改变结果。
|
||||||
|
- 本期交付可持久化的模板输入准备/交接能力,不扩展完整模板编辑器。模板消费方尚未接入时,只显示“保存输入草稿”及真实草稿状态,不展示虚构可用模板,也不提示已绑定真实模板。原型中的两个模板名称是演示数据。
|
||||||
|
- 消费方必须使用显式研究范围及字段类型,不猜测默认 Region/Delay,不在数据目录中静默加入 winsorize、backfill 或 VECTOR 聚合。具体表达式生成、参数规则与类型处理由后续模板规格定义。
|
||||||
|
|
||||||
|
### 对外契约和错误行为
|
||||||
|
|
||||||
|
- 数据集查询接受研究范围、查询词、分类、子分类、排序及分页,返回 items、total、分页参数和同步元数据。分类变化清空旧子分类;无匹配结果正常返回空列表。
|
||||||
|
- 字段查询还接受所属数据集、类型与最低覆盖率;返回列表匹配数量、完整集合总数和集合版本,明确区分“匹配数”与“本集总数”。
|
||||||
|
- 数据集详情和字段详情返回身份、范围、来源时间、平台描述、可用指标及独立的本地研究记录;未知指标保留 null,不伪装成零。字段 ID 不直接拼接成未经校验的上游请求。
|
||||||
|
- 备注更新包含读取时的记录版本;输入准备包含数据集、范围、集合版本与选择意图。写入成功才更新保存状态,异常不显示成功反馈。
|
||||||
|
- 同步创建异步返回任务 ID;查询和重试沿用已有任务契约。具体 URL 和任务 kind 名称在实现时与现有 API 命名保持一致,并通过 OpenAPI 描述,不在规格中绑定文件组织。
|
||||||
|
- 所有业务接口复用系统会话、写请求来源校验和账户隔离。沿用既有 401/403/404/409/422 等错误语义,冲突或无效范围不能造成部分写入。
|
||||||
|
- WorldQuant 只读边界保持,认证除外;数据集同步不回测、不检查、不提交、不修改平台属性。公开错误不得包含凭据、认证正文或 Cookie。
|
||||||
|
|
||||||
|
## Testing Decisions
|
||||||
|
|
||||||
|
用户已确认:以“筛选数据集 → 查看字段与详情 → 整集字段绑定到模板”的完整流程为主,只在 WorldQuant 外部接口边界使用模拟数据,并补充同步失败、断点恢复等接口测试。
|
||||||
|
|
||||||
|
1. 优先使用现有浏览器验收环境,将真实页面、API、业务层、数据库和任务执行器串起来。外部平台 HTTP 是主测试替换点,不再逐层 mock 查询服务、选择状态或组件内部函数。
|
||||||
|
2. 好的测试断言用户可见结果与公开契约,例如字段数、绑定集合、备注保留、错误状态和恢复后的结果;不锁定组件树、内部变量、SQL 调用次数或 CSS 类名。
|
||||||
|
3. 复用现有工作空间端到端测试先例:登录、连接模拟平台、多页同步、保存研究记录、重同步后记录保留、轮询任务完成和浏览器无运行异常。数据目录新增同层级流程,不独立搭建另一套浏览器服务。
|
||||||
|
4. 复用现有 API 测试的隔离数据库、ASGI 客户端和上游 HTTP 模拟方式,覆盖查询、范围校验、输入准备及备注版本冲突。复用现有同步测试的检查点、重试、取消和重启恢复用例设计;涉及新协议的测试尽量仍在 HTTP 边界替换。
|
||||||
|
5. 主路径:同步目录,按分类/子分类筛选,选择数据集,取得多页字段,确认默认全选;搜索到两个字段并打开第二层详情;关闭后保留筛选;准备输入仍包含整集所有字段。模板未接入时,以真实持久化输入草稿与同一交接契约为验收终点,不伪造一个成功的模板消费者。
|
||||||
|
6. 选择例外:在非第一页排除字段,筛选与排序后排除仍生效;恢复全选还原全集;表头取消全选使主操作禁用;切换数据集及研究范围不串选。
|
||||||
|
7. 完整性:使用超过单页大小且包含重复 ID 的合成字段。验证多页去重、失败不标记完成、重试不丢进度、刷新失败保留旧版本、缺失总数按实际完整枚举处理;不将已下载页当成全部字段。
|
||||||
|
8. 固定输入:绑定后刷新字段集合,原输入 ID 集合保持不变;新准备使用新版本;准备时版本冲突返回明确错误。跨数据集、跨范围、空集合、未知字段不能产生输入记录。
|
||||||
|
9. 备注:数据集和字段备注保存后经重新同步及页面刷新仍存在;并发版本冲突不覆盖新内容;后台更新不清空未保存草稿。
|
||||||
|
10. 数据质量:缺失分类、说明、覆盖率、单位及未知字段类型有可理解的展示;覆盖率筛选与显示口径一致,null 不作为零参与数值条件。
|
||||||
|
11. 交互验收:75% / 30% 宽度按同一工作区测量;顶部操作可见、底部无重复功能区、表格单行常规字重;Esc/遮罩逐层关闭、背景隔离和焦点恢复正确。
|
||||||
|
12. 响应式:覆盖桌面、窄窗口和手机宽度;只允许表格容器局部横向滚动,操作按钮、筛选和关闭入口不被遮挡。验证与现有 AI 助手开合时的状态恢复,不新增数据集 AI 工具。
|
||||||
|
13. 无匹配、无缓存、同步中、失败、取消、登录失效、等待连接及人工验证均有反馈。已有账户和 Alpha 的主要验收流程继续通过。
|
||||||
|
14. 生产存储使用 PostgreSQL;涉及集合版本和事务迁移时,应在隔离 PostgreSQL 验证升级与数据保留。SQLite 测试不能替代生产数据库迁移验收。
|
||||||
|
15. 自动化禁止使用真实凭据、正式数据库、付费模型或真实平台写操作。真实 WorldQuant 数据集 schema、分类选项及权限仍需后续只读联调,模拟测试通过不等于真实平台兼容性已证实。
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- 跨数据集组合、跨目录字段购物篮、默认逐字段收集、命名字段池和自定义数据集编辑器。
|
||||||
|
- 完整 Alpha 模板管理、模板编辑、表达式生成、AST 校验、参数搜索、批次队列、实验去重与回测执行。
|
||||||
|
- 数据集/字段的 AI 查询工具、AI 自动选字段、自动解释数据、自动生成研究假设和自动向模型发送字段信息。
|
||||||
|
- 平台回写、运行检查、提交 Alpha、修改属性或任何真实交易操作。
|
||||||
|
- 旧数据库迁移、旧 API 兼容、多用户/多平台账户、分布式队列、Redis、额外服务与大规模架构重构。
|
||||||
|
- 本次文档交付不实施业务代码、不执行数据库迁移、不部署、不提交 Git,不触发真实平台同步。
|
||||||
|
- 原型样例数据量、虚构模板名称、显示宽度中的固定像素和降级文字按钮不成为生产业务数据或不可调整的技术实现。
|
||||||
|
|
||||||
|
## Further Notes
|
||||||
|
|
||||||
|
- [交互原型](prototype.html)是本次已评审样式与交互的留档,使用明确标识的合成数据。原型中的同步、备注和绑定均为浏览器内演示,不能当作已实现的后端能力。
|
||||||
|
- 用户明确确认的核心约束:数据集优先、单数据集 Alpha、默认整集字段、Table、分类联动、75%/30% 双层抽屉、功能区在顶部、顶部无重复标题、少描述小字及 Lark 风格。
|
||||||
|
- 本规格中的快照版本、服务端完整性校验、输入草稿交接及既有 AI 面板兼容属于为实现上述流程作出的工程约定;不代表本轮已经实现或验证生产行为。
|
||||||
|
- 原型历史核验已覆盖分类联动、筛选后仍保持整集输入、字段排除/恢复全选、逐层返回、宽度比例和顶部按钮;Lark 静态检查通过,浏览器未发现运行错误。正式实现仍须执行本规格的验收,不能沿用原型结果宣称功能完成。
|
||||||
|
- 图标目录已检索,但当时资源服务连接失败,原型按 skill 允许的文字方案降级。实现时可重新接入可用官方目录资源,不要求绕过网络或证书校验。
|
||||||
|
- 项目依据:[总体方案](../../docs/project-plan.md)、[现有验证记录](../../docs/verification.md)、[本地任务规范](../../docs/agents/issue-tracker.md)。
|
||||||
|
- 测试确认记录:用户答复“符合,按这个边界”。规格标记 ready-for-agent 表示可供后续实现,不表示用户本轮要求开始开发。
|
||||||
Reference in New Issue
Block a user