- 基于第七版稳定基线(be647bb)重构 admin 前端为模块化架构:
tokens.css(设计token唯一来源) + shared.css(公共组件) + core.js(状态/API/事件总线/轮询/路由)
+ main.js(入口) + layouts/{flowline,ledger,strata}.{css,js}(三方向独立布局)
+ layouts/shared.js(三方向共用数据视图与操作绑定)
- 三个页面(A装配线/B值班台账/C地层剖面)通过 ?layout= 查询参数独立可达、可刷新、可前进后退导航,
共享登录态、真实后端数据(overview/sources/jobs/batches/datasets/audit)、错误处理与主题
- 六大板块(总览/数据源/调度任务/盘后发布/数据集/审计)在三个方向均可查看与操作(探测/触发/回滚/补数)
- 修正健康状态语义:CircuitBreaker closed/half_open/open 与适配器 ok/error/empty/unconfigured/unknown
统一归一化为 ok/warn/error/unconfigured/unknown 供三方向一致展示
- 事件驱动动效:A 水平接力光梭、B 行级高亮+实时调用跑马灯、C 纵向贯穿光点+分层标记;
统一使用 transform/opacity/WAAPI,避免 transition:all,支持 prefers-reduced-motion 静态降级
- 修复响应式布局在 1024/1280 断点因 CSS Grid 默认 min-width:auto 被内部宽表格撑爆轨道的问题
- 修复布局切换/前进后退时残留 setTimeout 回调在 unmount 后访问 null root 导致的报错(mounted 标志位+
safeTimeout+统一清理定时器)
- 自测:Playwright 全断点(1440/1280/1024)x 双主题矩阵截图、90 次连续布局切换+前进后退压力测试无报错、
探测/触发/回滚danger操作流程验证、reduced-motion 验证;后端 pytest 全量 133 用例通过,无回归
- 未改动:登录/鉴权契约、后端 API、'问天'冻结区、生产数据/权限
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
137 lines
4.7 KiB
JavaScript
137 lines
4.7 KiB
JavaScript
"use strict";
|
|
/* ==========================================================================
|
|
三方向共用的数据整形 + 操作绑定。A/B/C 各自的 mount() 只管 DOM 结构和动效编排,
|
|
"字段从哪来、按钮点了调什么接口" 统一走这里,禁止在布局文件里重写业务判断。
|
|
========================================================================== */
|
|
window.HubShared = (function () {
|
|
const C = window.Core;
|
|
const { esc, timeShort } = C;
|
|
|
|
function overviewView() {
|
|
const d = C.state.data.overview;
|
|
if (!d) return null;
|
|
const eod = d.eod_status || {}, rev = d.revision_status || {};
|
|
return {
|
|
tradeDate: d.trade_date,
|
|
phase: C.PHASE_LABELS[d.session_phase] || d.session_phase,
|
|
pubCount: d.publications.length,
|
|
eodState: eod.state, eodLabel: C.EOD_LABELS[eod.state] || eod.state || "-", eod,
|
|
revState: rev.state, revLabel: C.REV_LABELS[rev.state] || rev.state || "-", rev,
|
|
anomalies: d.anomalies.length,
|
|
recentCalls: d.recent_calls || [],
|
|
sourceCount: d.source_count,
|
|
};
|
|
}
|
|
|
|
function sourcesView() {
|
|
const d = C.state.data.sources;
|
|
if (!d) return null;
|
|
const live = d.items.filter((it) => C.FLOW_PROVIDERS.includes(it.provider));
|
|
const reserved = d.items.filter((it) => !C.FLOW_PROVIDERS.includes(it.provider));
|
|
return { live, reserved, all: d.items };
|
|
}
|
|
|
|
function jobsView() {
|
|
const d = C.state.data.jobs;
|
|
if (!d) return null;
|
|
const runs = d.runs || [];
|
|
const latestByJob = new Map();
|
|
for (const r of runs) if (!latestByJob.has(r.job_id)) latestByJob.set(r.job_id, r);
|
|
return {
|
|
jobs: d.jobs || [], runs, latest: runs[0], latestByJob,
|
|
failedRecent: runs.slice(0, 20).filter((r) => r.state === "failed").length,
|
|
fresh: C.state.freshJobRuns || [],
|
|
};
|
|
}
|
|
|
|
function releaseView() {
|
|
const d = C.state.data.batches;
|
|
if (!d) return null;
|
|
const pubs = d.publications || [];
|
|
const batches = d.batches || [];
|
|
return {
|
|
tradeDate: d.trade_date, pubs, batches,
|
|
rollbacks: pubs.filter((p) => p.prev_batch).length,
|
|
failedBatches: batches.filter((b) => b.state === "failed").length,
|
|
};
|
|
}
|
|
|
|
function datasetsView() {
|
|
const d = C.state.data.datasets;
|
|
if (!d) return null;
|
|
return { tradeDate: d.trade_date, pubs: d.publications || [], diffs: d.diff_reports || [] };
|
|
}
|
|
|
|
function auditView() {
|
|
const d = C.state.data.audit;
|
|
if (!d) return null;
|
|
return { items: d.items || [] };
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- 操作绑定 */
|
|
function bindProbeButtons(root, onDone) {
|
|
root.querySelectorAll("[data-probe]").forEach((btn) => {
|
|
btn.addEventListener("click", async () => {
|
|
const provider = btn.dataset.probe;
|
|
btn.disabled = true;
|
|
const original = btn.textContent;
|
|
btn.textContent = "探测中…";
|
|
try {
|
|
const result = await C.probeSource(provider);
|
|
btn.textContent = "已探测";
|
|
setTimeout(() => { btn.textContent = original; }, 1200);
|
|
if (onDone) onDone(result, provider);
|
|
} catch (err) {
|
|
alert(err.message);
|
|
btn.textContent = original;
|
|
} finally {
|
|
setTimeout(() => { btn.disabled = false; }, 400);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindRunButtons(root, onDone) {
|
|
root.querySelectorAll("[data-run]").forEach((btn) => {
|
|
btn.addEventListener("click", async () => {
|
|
const date = prompt("交易日 YYYYMMDD(可留空=今天)", "") || "";
|
|
btn.disabled = true;
|
|
try {
|
|
const result = await C.runJob(btn.dataset.run, date);
|
|
if (onDone) onDone(result);
|
|
} catch (err) {
|
|
alert(err.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindRollbackButtons(root, onDone) {
|
|
root.querySelectorAll("[data-rollback]").forEach((btn) => {
|
|
btn.addEventListener("click", () => C.dangerous("rollback", btn.dataset.rollback, onDone));
|
|
});
|
|
}
|
|
|
|
function bindBackfillButton(root, selector, onDone) {
|
|
const el = root.querySelector(selector);
|
|
if (el) el.addEventListener("click", () => C.dangerous("backfill", null, onDone));
|
|
}
|
|
|
|
function bindReleaseDateReload(root, inputSel, btnSel, onDone) {
|
|
const btn = root.querySelector(btnSel);
|
|
if (!btn) return;
|
|
btn.addEventListener("click", async () => {
|
|
C.state.releaseDate = root.querySelector(inputSel).value.trim();
|
|
await C.pollBatches();
|
|
if (onDone) onDone();
|
|
});
|
|
}
|
|
|
|
return {
|
|
overviewView, sourcesView, jobsView, releaseView, datasetsView, auditView,
|
|
bindProbeButtons, bindRunButtons, bindRollbackButtons, bindBackfillButton, bindReleaseDateReload,
|
|
};
|
|
})();
|