diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index a72a130..478d12a 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -500,13 +500,26 @@ const en = { runStatusFailed: "Failed", runStatusDoneSec: "Done {{s}}s", runProgress: "{{phase}} · sheet {{i}}/{{n}} · {{sheet}} · {{s}}s elapsed", + runSheetProgress: "sheet {{i}}/{{n}}", + runElapsed: "{{s}}s elapsed", runRowsLoaded: "Loaded {{side}} {{n}} rows", runRowsSqlCount: "DB count {{side}} {{n}} rows", runPersisting: "Wrote {{done}}/{{total}} rows", runEngineSql: "engine SQL", runEnginePython: "engine Python", runEngineNote: "reason {{note}}", + phaseQueued: "Queued", + phaseLoading: "Loading", + phaseSqlCount: "Counting", + phaseComparing: "Comparing", + phasePersisting: "Writing", + phasePersistingFail: "Writing fails", + phasePersistingOk: "Writing passes", + phaseDone: "Done", + phaseFailed: "Failed", + phaseCancelled: "Cancelled", sheetPending: "Pending", + colProgress: "Progress / result", ranWithDuration: "Compare finished ({{s}}s)", unchangedNotStored: "Success rows were counted but not stored. Set “Store success rows” to sample and re-run for spot-check.", unchangedSampleHint: "{{total}} success rows total; browsing a stratified sample of {{listed}}. Search any route for live source lookup. Pass rate uses all {{total}}.", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 5247a88..7369dfd 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -499,13 +499,26 @@ const zh = { runStatusFailed: "失败", runStatusDoneSec: "完成 {{s}}s", runProgress: "{{phase}} · 表 {{i}}/{{n}} · {{sheet}} · 已用 {{s}}s", + runSheetProgress: "表 {{i}}/{{n}}", + runElapsed: "已用 {{s}}s", runRowsLoaded: "已加载 {{side}} {{n}} 行", runRowsSqlCount: "库内统计 {{side}} {{n}} 行", runPersisting: "已写入 {{done}}/{{total}} 行", runEngineSql: "引擎 SQL", runEnginePython: "引擎 Python", runEngineNote: "原因 {{note}}", + phaseQueued: "排队中", + phaseLoading: "加载数据", + phaseSqlCount: "库内统计", + phaseComparing: "比对中", + phasePersisting: "写入结果", + phasePersistingFail: "写入失败行", + phasePersistingOk: "写入成功行", + phaseDone: "完成", + phaseFailed: "失败", + phaseCancelled: "已取消", sheetPending: "等待中", + colProgress: "进度 / 结果", ranWithDuration: "比对完成(耗时 {{s}} 秒)", unchangedNotStored: "成功行仅统计数量未落库。可在任务配置将「成功行保存」改为抽样后重新比对(抽查用)。", unchangedSampleHint: "成功共 {{total}} 条,明细抽样 {{listed}} 条(分层抽查)。要查任意路由请输入筛选条件——将按原表即时判定。通过率按全部 {{total}} 计。", diff --git a/web/src/index.css b/web/src/index.css index d70fc9d..d3e61d5 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11700,10 +11700,9 @@ html.login-page--paused .login-page__flare { .bs-cmp-progress { display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 8px 14px; - padding: 8px 12px; + flex-direction: column; + gap: 6px; + padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(59, 130, 246, 0.35); background: rgba(37, 99, 235, 0.12); @@ -11716,11 +11715,147 @@ html.login-page--paused .login-page__flare { background: rgba(239, 68, 68, 0.12); } +.bs-cmp-progress.is-cancelled { + border-color: rgba(245, 158, 11, 0.4); + background: rgba(245, 158, 11, 0.1); +} + +.bs-cmp-progress__head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; +} + +.bs-cmp-progress__actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 6px; +} + +.bs-cmp-progress__engine, +.bs-cmp-progress__elapsed { + font-size: 0.8125rem; +} + +.bs-cmp-progress__track, +.bs-cmp-run-prog__track { + height: 6px; + border-radius: 999px; + background: rgba(148, 163, 184, 0.28); + overflow: hidden; +} + +.bs-cmp-progress__fill, +.bs-cmp-run-prog__fill { + height: 100%; + width: 0; + border-radius: inherit; + background: linear-gradient(90deg, #2563eb, #60a5fa); + transition: width 0.35s ease; +} + +.bs-cmp-progress__fill.is-indeterminate, +.bs-cmp-run-prog__fill.is-indeterminate { + width: 36%; + animation: bs-cmp-prog-slide 1.25s ease-in-out infinite; +} + +@keyframes bs-cmp-prog-slide { + 0% { + transform: translateX(-120%); + } + 100% { + transform: translateX(320%); + } +} + +.bs-cmp-progress__meta { + font-size: 0.8125rem; + color: var(--bs-cmp-muted, #94a3b8); + line-height: 1.4; + word-break: break-word; +} + .bs-cmp-progress__msg { flex: 1 1 100%; font-size: 0.8125rem; } +/* Compare batch list (job detail → 对比批次) */ +.bs-cmp-runs { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 260px; +} + +.bs-cmp-runs__toolbar { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 10px 14px; +} + +.bs-cmp-runs__hint { + margin: 0; + flex: 1 1 220px; + line-height: 1.45; +} + +.bs-cmp-runs__table tbody tr.is-selected td { + background: rgba(37, 99, 235, 0.08); +} + +.bs-cmp-runs__elapsed { + margin-top: 4px; + font-size: 0.75rem; + color: var(--muted, #64748b); + font-variant-numeric: tabular-nums; +} + +.bs-cmp-runs__sides { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 2px 6px; + font-size: 0.8125rem; + line-height: 1.35; + max-width: 22rem; +} + +.bs-cmp-runs__arrow { + color: var(--muted, #94a3b8); + flex: 0 0 auto; +} + +.bs-cmp-runs__summary { + font-size: 0.8125rem; + line-height: 1.4; + color: var(--ink, #334155); + max-width: 18rem; +} + +.bs-cmp-run-prog { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 160px; + max-width: 22rem; +} + +.bs-cmp-run-prog__meta { + font-size: 0.75rem; + line-height: 1.35; + color: var(--muted, #64748b); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + /* Native :fullscreen + CSS immersive fallback (class only when FS API blocked) */ .bs-cmp-board.is-fullscreen, .bs-cmp-board:fullscreen { diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 4ebbf6d..5ed8927 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -336,6 +336,106 @@ function sheetLabel(s: { title?: string; sheet_id?: string; metric_id?: string } return String(s?.title || s?.sheet_id || s?.metric_id || "").trim() || "—"; } +type RunProgressInfo = { + phase?: string; + sheet_index?: number; + sheet_total?: number; + sheet_title?: string; + sheet_id?: string; + elapsed_ms?: number; + load_side?: string; + rows_loaded?: number; + engine?: string; + engine_note?: string; + persisted?: number; + persist_total?: number; +}; + +function localizeRunPhase(phase: string | undefined, t: (key: string) => string): string { + const p = String(phase || "").trim().toLowerCase(); + if (!p || p === "…") return "…"; + if (p.startsWith("persisting")) { + if (p.includes("fail")) return t("bizCompare.phasePersistingFail"); + if (p.includes("ok")) return t("bizCompare.phasePersistingOk"); + return t("bizCompare.phasePersisting"); + } + const map: Record = { + queued: "bizCompare.phaseQueued", + loading: "bizCompare.phaseLoading", + sql_count: "bizCompare.phaseSqlCount", + comparing: "bizCompare.phaseComparing", + done: "bizCompare.phaseDone", + failed: "bizCompare.phaseFailed", + cancelled: "bizCompare.phaseCancelled", + }; + const key = map[p]; + return key ? t(key) : String(phase); +} + +function runProgressPercent(prog: RunProgressInfo): number { + const total = Number(prog.sheet_total || 0); + const idx = Number(prog.sheet_index || 0); + if (total <= 0) return 8; + const phase = String(prog.phase || "").toLowerCase(); + if (phase === "queued") return 4; + if (phase === "done") return 100; + let base = Math.max(0, Math.min(idx, total)) / total; + const persistTotal = Number(prog.persist_total || 0); + const persisted = Number(prog.persisted || 0); + if (persistTotal > 0 && phase.startsWith("persisting")) { + const within = Math.min(1, persisted / persistTotal) * (1 / total); + base = Math.max(0, (idx - 1) / total) + within; + } else if (phase === "loading" || phase === "sql_count" || phase === "comparing") { + base = Math.max(0, (idx - 1) / total) + 0.35 / total; + } + return Math.max(4, Math.min(99, Math.round(base * 100))); +} + +function runProgressMetaLine( + prog: RunProgressInfo, + t: (key: string, vars?: Record) => string, +): string { + const parts: string[] = []; + const phase = localizeRunPhase(prog.phase, t); + if (phase && phase !== "…") parts.push(phase); + const total = Number(prog.sheet_total || 0); + if (total > 0) { + parts.push( + t("bizCompare.runSheetProgress", { + i: String(prog.sheet_index || 0), + n: String(total), + }), + ); + } + const title = String(prog.sheet_title || prog.sheet_id || "").trim(); + if (title) parts.push(title); + const rows = Number(prog.rows_loaded || 0); + if (rows > 0) { + const eng = String(prog.engine || "").toLowerCase(); + parts.push( + eng === "sql" + ? t("bizCompare.runRowsSqlCount", { + side: String(prog.load_side || "—"), + n: String(rows), + }) + : t("bizCompare.runRowsLoaded", { + side: String(prog.load_side || "—"), + n: String(rows), + }), + ); + } + const persistTotal = Number(prog.persist_total || 0); + if (String(prog.phase || "").startsWith("persisting") && persistTotal > 0) { + parts.push( + t("bizCompare.runPersisting", { + done: String(prog.persisted || 0), + total: String(persistTotal), + }), + ); + } + return parts.join(" · "); +} + function templateSheets(tpl?: Template | null): MetricSheet[] { if (!tpl) return []; if (tpl.metrics?.length) return tpl.metrics; @@ -1937,25 +2037,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage setBusy(false); } }; - const runProgress = (runDetail?.summary?.progress || {}) as { - phase?: string; - sheet_index?: number; - sheet_total?: number; - sheet_title?: string; - sheet_id?: string; - elapsed_ms?: number; - before_count?: number; - after_count?: number; - diff_rows?: number; - load_side?: string; - rows_loaded?: number; - engine?: string; - engine_note?: string; - persisted?: number; - persist_total?: number; - fail_rows?: number; - ok_rows?: number; - }; + const runProgress = (runDetail?.summary?.progress || {}) as RunProgressInfo; const runEngine = String(runProgress.engine || "").toLowerCase(); // Poll active compare runs so the modal can be closed and reopened safely. @@ -3009,10 +3091,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage renderJobForm() ) : jobDetailTab === "runs" ? (
-
-

- {t("bizCompare.runsHint")} -

+
+

{t("bizCompare.runsHint")}

) : ( -
- +
+
- + @@ -3059,17 +3139,11 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage Number(sum.removed || 0) + Number(sum.changed || 0); const ok = Number(sum.unchanged || 0); const durMs = Number(sum.duration_ms || 0); - const prog = ((r as any).progress || {}) as { - phase?: string; - sheet_index?: number; - sheet_total?: number; - sheet_title?: string; - rows_loaded?: number; - load_side?: string; - engine?: string; - engine_note?: string; - }; - const eng = String(prog.engine || "").toLowerCase(); + const prog = ((r as any).progress || + (r as any).summary?.progress || + {}) as RunProgressInfo; + const pct = active ? runProgressPercent(prog) : 100; + const elapsedSec = Math.round(Number(prog.elapsed_ms || 0) / 1000); const stLabel = st === "running" || st === "queued" ? t("bizCompare.runStatusRunning") @@ -3080,62 +3154,76 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage : t("bizCompare.runStatusDoneSec", { s: String(durMs > 0 ? Math.round(durMs / 1000) : 0), }); + const chipColor = active + ? "accent" + : st === "cancelled" + ? "warning" + : jobChipColor(st); + const meta = active ? runProgressMetaLine(prog, t) : ""; return ( - +
{t("bizCompare.colStatus")} {t("bizCompare.colTime")} {t("bizCompare.sidesTitle")}{t("bizCompare.colResult")}{t("bizCompare.colProgress")} {t("bizCompare.colActions")}
- {stLabel} - {active && (prog.phase || eng) ? ( -
- {eng === "sql" - ? t("bizCompare.runEngineSql") - : eng === "python" - ? t("bizCompare.runEnginePython") - : ""} - {eng && prog.engine_note - ? ` · ${prog.engine_note}` - : ""} - {prog.phase ? `${eng ? " · " : ""}${prog.phase}` : ""} - {prog.sheet_total - ? ` · ${prog.sheet_index || 0}/${prog.sheet_total}` - : ""} - {prog.sheet_title ? ` · ${prog.sheet_title}` : ""} - {Number(prog.rows_loaded || 0) > 0 - ? eng === "sql" - ? ` · ${prog.load_side || ""} ${prog.rows_loaded}` - : ` · ${prog.load_side || ""} ${prog.rows_loaded}` - : ""} -
- ) : null} - {(r as any).message ? ( -
- {String((r as any).message).slice(0, 80)} + {stLabel} + {active && elapsedSec > 0 ? ( +
+ {t("bizCompare.runElapsed", { s: String(elapsedSec) })}
) : null}
{when}{when} - {sideDeviceName(before)} {sideCollectTime(before)} - {" → "} - {sideDeviceName(after)} {sideCollectTime(after)} +
+ {sideDeviceName(before)} + {sideCollectTime(before)} + + → + + {sideDeviceName(after)} + {sideCollectTime(after)} +
- {active - ? "…" - : t("bizCompare.runBatchSummary", { - fail: String(fail), - ok: String(ok), - added: String(Number(sum.added || 0)), - })} + {active ? ( +
+
+
0 + ? { width: `${pct}%` } + : undefined + } + /> +
+
+ {meta || "…"} +
+
+ ) : ( +
+ {st === "failed" || st === "cancelled" + ? String((r as any).message || stLabel).slice(0, 96) + : t("bizCompare.runBatchSummary", { + fail: String(fail), + ok: String(ok), + added: String(Number(sum.added || 0)), + })} +
+ )}
-
+
+
+
+
+
0 + ? { width: `${runProgressPercent(runProgress)}%` } + : undefined + } + /> +
+
+ {runProgressMetaLine(runProgress, t)} +
) : null} {runDetail && runStatus === "failed" ? (
- {t("bizCompare.runStatusFailed")} - {String(runDetail.message || "")} +
+ {t("bizCompare.runStatusFailed")} +
+
{String(runDetail.message || "")}
+
+ ) : null} + {runDetail && runStatus === "cancelled" ? ( +
+
+ {t("bizCompare.runStatusCancelled")} +
+
+ {String(runDetail.message || t("bizCompare.runCancelled"))} +
) : null} {runDetail ? (