feat(HEL-566): 去掉主站行情管理入口并修好模型列表收起
主站行情管理并入数据中枢
- 数据中枢数据源配置页新增「主站行情任务」卡:交易时段后台刷新开关、
手动后台刷新、历史区间回补(任务提交 + 轮询主站 job 记录亮灯),
数据与调度仍归主站,控制台只经桥接代操作
- 新增桥接端点 /api/hub-admin/market/{refresh,backfill};回补改为
market.backfill 后台任务(jobs.config.json 注册,锁独立,超时 30 分钟),
桥接调用秒回,不再阻塞
- 主站桌面端删除顶栏「行情管理」按钮与整个行情管理对话框,清理随之
失效的 CSS;移动端删除 system/admin 页与入口,管理员专区只留数据中枢
- Tushare Token / iFinD 凭证编辑沿用数据源页既有凭证区,无功能缺失
模型池收起修复
- 拉出模型清单后按钮切换为「收起列表」,收起只留一行摘要;再次点击
重新拉取并展开;勾选添加完成后清单自动收起(原逻辑保留)
- 交互全部沿用 HEL-558 已确认样式的既有按钮与提示组件,未新增视觉
自测
- verify_baseline 通过;pytest 492 项通过;数据中枢 240 项通过
- verify_datahub_console 新增 [11b] 行情任务桥接端到端段;UI 自测新增
行情任务卡开关往返、模型清单展开/收起/再展开/添加自动收起,日夜主题
与 1030 窄屏复验通过
- Playwright e2e 102/103:唯一失败项在基线提交上同样失败(本机字体度量
导致的头部溢出,与本卡无关)
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -1,54 +1,3 @@
|
||||
async function backfillData() {
|
||||
const button = document.querySelector("#backfillButton");
|
||||
button.disabled = true;
|
||||
setLoading(true, "正在回补历史交易日");
|
||||
try {
|
||||
const payload = await apiRequest("/api/backfill", "POST", {
|
||||
start_date: document.querySelector("#backfillStart").value,
|
||||
end_date: document.querySelector("#backfillEnd").value,
|
||||
});
|
||||
const failed = (payload.failed_count || 0);
|
||||
const skipped = (payload.skipped_non_trading_days || []).length;
|
||||
const suffix = failed
|
||||
? `,失败 ${failed} 个`
|
||||
: skipped
|
||||
? `,跳过 ${skipped} 个非交易日`
|
||||
: "";
|
||||
showToast(`历史回补完成,共处理 ${payload.results.length} 个交易日${suffix}`);
|
||||
state.sentimentHistory = null;
|
||||
state.sentimentHistoryKey = "";
|
||||
if (state.activeView === "sentimentCycleView") {
|
||||
await loadSentimentHistory(true);
|
||||
}
|
||||
await openAdminSettings(true);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function openAdminSettings(refreshOnly = false) {
|
||||
if (state.user?.role !== "admin") return;
|
||||
if (!refreshOnly) openModalDialog(elements.adminDialog);
|
||||
const status = document.querySelector("#adminConnectionStatus");
|
||||
status.textContent = "正在读取系统状态";
|
||||
try {
|
||||
const payload = await apiRequest("/api/admin/settings");
|
||||
const data = payload.data || {};
|
||||
const ifind = data.ifind || {};
|
||||
status.textContent = `数据中枢 ${data.configured ? "已连接" : "未连接"} · iFinD ${ifind.configured ? "已配置" : "未配置"} · ${number(data.snapshot_dates)} 个交易日`;
|
||||
status.classList.toggle("connected", Boolean(data.configured));
|
||||
setText("systemDataStatus", data.background_refresh_enabled ? "后台刷新已启用" : "后台刷新已暂停");
|
||||
renderDatahubRouteStatus(data.datahub || {});
|
||||
document.querySelector("#systemBackgroundRefresh").checked = Boolean(data.background_refresh_enabled);
|
||||
} catch (error) {
|
||||
status.textContent = error.message || "系统配置读取失败";
|
||||
}
|
||||
}
|
||||
|
||||
function dataHubConsoleUrl() {
|
||||
if (window.XIAOBAI_DATAHUB_URL) return String(window.XIAOBAI_DATAHUB_URL);
|
||||
// HEL-564: 固定内网地址。域名只反代 8765,按主机名推导会拼出打不开的
|
||||
@@ -61,49 +10,9 @@ function openDataHubConsole() {
|
||||
window.open(dataHubConsoleUrl(), "_blank", "noopener");
|
||||
}
|
||||
|
||||
function renderDatahubRouteStatus(hub) {
|
||||
const box = document.querySelector("#datahubRouteStatus");
|
||||
if (!box) return;
|
||||
const label = box.querySelector("span");
|
||||
const enabled = number(hub.enabled_reads);
|
||||
const total = number(hub.total_reads) || enabled;
|
||||
const fallbacks = hub.fallback_labels || [];
|
||||
if (fallbacks.length) {
|
||||
box.dataset.tone = "warning";
|
||||
if (label) label.textContent = `数据中枢主线路 ${enabled}/${total} · 备用 ${fallbacks.length} 类:${fallbacks.join("、")}`;
|
||||
return;
|
||||
}
|
||||
box.dataset.tone = hub.configured ? "success" : "idle";
|
||||
if (label) {
|
||||
label.textContent = hub.configured
|
||||
? `数据中枢主线路 ${enabled}/${total},当前无备用`
|
||||
: "数据中枢未配置,网站只保留已有真实快照";
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMarketSettings(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget.querySelector("button[type='submit']");
|
||||
button.disabled = true;
|
||||
try {
|
||||
await apiRequest("/api/admin/settings", "POST", {
|
||||
background_refresh_enabled: document.querySelector("#systemBackgroundRefresh").checked,
|
||||
});
|
||||
showToast("行情刷新设置已保存");
|
||||
await openAdminSettings(true);
|
||||
} catch (error) {
|
||||
showToast(error.message || "系统配置保存失败");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* HEL-566: 主站「行情管理」已整体并入数据中枢(数据源配置页的
|
||||
主站行情任务卡:后台刷新开关 / 手动刷新 / 历史回补),主站只保留
|
||||
顶栏「数据中枢」入口。 */
|
||||
function bindAdminEvents() {
|
||||
document.querySelector("#settingsButton").addEventListener("click", openDataHubConsole);
|
||||
document.querySelector("#marketAdminButton").addEventListener("click", () => openAdminSettings());
|
||||
document.querySelector("#closeAdminDialog").addEventListener("click", () => elements.adminDialog.close());
|
||||
document.querySelector("#backfillButton").addEventListener("click", backfillData);
|
||||
document.querySelector("#systemMarketForm").addEventListener("submit", saveMarketSettings);
|
||||
document.querySelector("#adminRefreshButton").addEventListener("click", startAdminRefresh);
|
||||
}
|
||||
|
||||
+2
-109
@@ -121,23 +121,6 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.admin-dialog {
|
||||
width: min(1060px, -24px + 100vw);
|
||||
}
|
||||
|
||||
.admin-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-inline-actions {
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-dialog > form,
|
||||
.settings-section > form {
|
||||
padding: 0px;
|
||||
}
|
||||
@@ -160,22 +143,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 9px;
|
||||
|
||||
margin-top: 13px;
|
||||
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
margin: 18px 18px 0px;
|
||||
|
||||
@@ -200,8 +167,7 @@
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.settings-dialog .settings-section > form,
|
||||
.settings-dialog.admin-dialog > form {
|
||||
.settings-dialog .settings-section > form {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -245,22 +211,6 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.backfill-controls {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.admin-inline-actions {
|
||||
align-items: stretch;
|
||||
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin: 10px 0px 0px;
|
||||
|
||||
@@ -271,32 +221,6 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.admin-refresh-status {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.admin-refresh-status svg {
|
||||
flex: 0 0 auto;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.admin-refresh-status[data-tone="running"] { color: var(--primary); }
|
||||
.admin-refresh-status[data-tone="success"] { color: var(--down); }
|
||||
.admin-refresh-status[data-tone="warning"] { color: var(--warning); }
|
||||
.admin-refresh-status[data-tone="failure"] { color: var(--up); }
|
||||
|
||||
.account-button > span {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -359,8 +283,7 @@
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.settings-dialog .settings-section > form,
|
||||
.settings-dialog.admin-dialog > form {
|
||||
.settings-dialog .settings-section > form {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -368,10 +291,6 @@
|
||||
.settings-dialog .form-hint {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.backfill-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.account-menu-chevron {
|
||||
@@ -1297,32 +1216,6 @@ button.account-role-badge:focus-visible {
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.settings-dialog.admin-dialog {
|
||||
width: min(900px, -28px + 100vw);
|
||||
}
|
||||
|
||||
.admin-dialog .connection-status {
|
||||
margin: 12px 20px 0px;
|
||||
|
||||
padding: 8px 10px;
|
||||
|
||||
border-radius: 7px;
|
||||
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.admin-dialog .admin-panel {
|
||||
max-width: 860px;
|
||||
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.admin-dialog .settings-section + .settings-section {
|
||||
border-top: 1px solid var(--dialog-line);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.account-settings-dialog .membership-status-grid {
|
||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||
|
||||
@@ -212,7 +212,6 @@ const elements = {
|
||||
entityDetailDialog: document.querySelector("#entityDetailDialog"),
|
||||
entityDetailChart: document.querySelector("#entityDetailChart"),
|
||||
settingsDialog: document.querySelector("#settingsDialog"),
|
||||
adminDialog: document.querySelector("#adminDialog"),
|
||||
priceChart: document.querySelector("#priceChart"),
|
||||
stockPreview: document.querySelector("#stockPreview"),
|
||||
stockPreviewBackdrop: document.querySelector("#stockPreviewBackdrop"),
|
||||
|
||||
@@ -38,22 +38,18 @@ async function loadDashboard(force = false, background = false, showOverlay = tr
|
||||
}
|
||||
|
||||
async function startAdminRefresh() {
|
||||
const buttons = [document.querySelector("#syncButton"), document.querySelector("#adminRefreshButton")].filter(Boolean);
|
||||
const buttons = [document.querySelector("#syncButton")].filter(Boolean);
|
||||
buttons.forEach((button) => { button.disabled = true; });
|
||||
const requestedDate = elements.tradeDate.value;
|
||||
setAdminRefreshStatus("running", `正在刷新 ${requestedDate} 的行情,请稍候…`, "loader-circle");
|
||||
try {
|
||||
const payload = await apiRequest("/api/admin/refresh", "POST", { trade_date: requestedDate });
|
||||
if (!payload.started || !payload.job_key) {
|
||||
setAdminRefreshStatus("warning", "已有刷新任务正在运行,请稍后再试。", "clock-3");
|
||||
showToast(payload.message || "已有后台刷新任务正在运行");
|
||||
return;
|
||||
}
|
||||
setStatus(`正在刷新 ${requestedDate} 的行情`);
|
||||
const job = await waitForAdminRefresh(payload.job_key);
|
||||
if (job.status === "failed") {
|
||||
const reason = job.message || job.error_code || "数据源未返回结果";
|
||||
setAdminRefreshStatus("failure", `刷新失败:${reason}`, "circle-x");
|
||||
setStatus("后台刷新失败");
|
||||
showToast("后台刷新失败");
|
||||
return;
|
||||
@@ -68,25 +64,20 @@ async function startAdminRefresh() {
|
||||
const updated = formatTimestamp(meta.updated_at);
|
||||
const freshness = dashboardFreshnessMessage(meta);
|
||||
if (meta.realtime && actualCompact === requestedCompact && !meta.carried_forward) {
|
||||
setAdminRefreshStatus("success", `刷新成功:已获取 ${actualDate} 的盘中行情,更新时间 ${updated}`, "circle-check");
|
||||
showToast(`刷新成功:已获取 ${actualDate} 的盘中行情`);
|
||||
return;
|
||||
}
|
||||
if (freshness || actualCompact !== requestedCompact || meta.carried_forward || meta.limit_data_source === "derived") {
|
||||
setAdminRefreshStatus("warning", freshness || `部分正式数据尚未到齐,当前展示 ${actualDate || "最近可用数据"}`, "triangle-alert");
|
||||
setStatus(freshness || "部分正式数据尚未到齐,当前展示最近可用数据");
|
||||
return;
|
||||
}
|
||||
if (meta.notice) {
|
||||
setAdminRefreshStatus("warning", `已刷新到 ${actualDate}(${updated}),但数据源提示:${meta.notice}`, "triangle-alert");
|
||||
showToast(`已刷新到 ${actualDate},请留意数据源提示`);
|
||||
} else {
|
||||
setAdminRefreshStatus("success", `刷新成功:已获取 ${actualDate} 的最新行情,更新时间 ${updated}`, "circle-check");
|
||||
showToast(`刷新成功:已获取 ${actualDate} 的最新行情`);
|
||||
showToast(`刷新成功:已获取 ${actualDate} 的最新行情,更新时间 ${updated}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.message || "后台刷新失败";
|
||||
setAdminRefreshStatus("failure", `刷新失败:${message}`, "circle-x");
|
||||
setStatus("后台刷新失败");
|
||||
showToast(message);
|
||||
} finally {
|
||||
@@ -94,14 +85,6 @@ async function startAdminRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
function setAdminRefreshStatus(tone, message, icon = "circle-dot") {
|
||||
const status = document.querySelector("#adminRefreshStatus");
|
||||
if (!status) return;
|
||||
status.dataset.tone = tone;
|
||||
status.innerHTML = `<i data-lucide="${icon}"></i><span>${escapeHtml(message)}</span>`;
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
async function waitForAdminRefresh(jobKey) {
|
||||
for (let attempt = 0; attempt < 120; attempt += 1) {
|
||||
const payload = await apiRequest("/api/admin/settings");
|
||||
|
||||
@@ -48,7 +48,6 @@ async function applyAuthenticatedSession(session) {
|
||||
const isAdmin = session.user?.role === "admin";
|
||||
updateAccountIdentityBadges(session.user?.membership || {});
|
||||
document.querySelector("#settingsButton").hidden = !isAdmin;
|
||||
document.querySelector("#marketAdminButton").hidden = !isAdmin;
|
||||
document.querySelector("#syncButton").hidden = !isAdmin;
|
||||
document.querySelector("#reasonForm").hidden = !isAdmin;
|
||||
document.querySelector("#sectorPhaseManager").hidden = !isAdmin;
|
||||
|
||||
Reference in New Issue
Block a user