diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index cb51630..2c0ea31 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -330,6 +330,8 @@ const en = { passRate: "Pass rate", passOk: "In sync", passWarn: "Has diffs", + runOverview: "Run overview", + sheetCurrent: "Current sheet", diffCountLabel: "Diff rows", diffRate: "Diff rate", beforeCount: "Before rows", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 0384b3f..eed8048 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -329,6 +329,8 @@ const zh = { passRate: "通过率", passOk: "前后一致", passWarn: "存在差异", + runOverview: "本次总览", + sheetCurrent: "当前表", diffCountLabel: "差异条目", diffRate: "差异率", beforeCount: "操作前行数", diff --git a/web/src/index.css b/web/src/index.css index 73dfafe..c3925b4 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -10213,11 +10213,76 @@ html.login-page--paused .login-page__flare { min-height: 0; } -.bs-cmp-strip { +/* Run-level overview: never changes when switching sheets */ +.bs-cmp-overview { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; + align-items: center; + gap: 10px 14px; + min-height: 34px; + padding: 6px 12px; + border-radius: 8px; + border: 1px solid var(--bs-cmp-border); + background: rgba(15, 23, 42, 0.45); + font-size: 12px; + font-variant-numeric: tabular-nums; + color: #cbd5e1; + overflow: hidden; + flex: 0 0 auto; +} + +.bs-cmp-overview.is-ok { + border-color: rgba(34, 197, 94, 0.28); +} + +.bs-cmp-overview.is-warn { + border-color: rgba(245, 158, 11, 0.32); +} + +.bs-cmp-overview__tag { + flex: 0 0 auto; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--bs-cmp-muted); + padding: 2px 6px; + border-radius: 4px; + background: rgba(148, 163, 184, 0.12); +} + +.bs-cmp-overview__pass b { + color: #f1f5f9; + font-weight: 800; + margin-left: 2px; +} + +.bs-cmp-overview.is-ok .bs-cmp-overview__pass b { + color: #86efac; +} + +.bs-cmp-overview.is-warn .bs-cmp-overview__pass b { + color: #fbbf24; +} + +.bs-cmp-overview__stat b { + color: #e2e8f0; + font-weight: 700; + margin-left: 1px; +} + +.bs-cmp-overview__time { + margin-left: auto; + white-space: nowrap; +} + +/* Sheet strip: fixed geometry so only digits/name swap */ +.bs-cmp-strip { + display: grid; + grid-template-columns: minmax(140px, 1.2fr) 88px minmax(240px, 1.4fr) auto; gap: 10px 14px; align-items: center; + min-height: 48px; padding: 8px 12px; border-radius: 10px; border: 1px solid var(--bs-cmp-border); @@ -10233,24 +10298,57 @@ html.login-page--paused .login-page__flare { border-color: rgba(245, 158, 11, 0.4); } -.bs-cmp-strip__pass { +.bs-cmp-strip__sheet { display: flex; - align-items: baseline; + align-items: center; gap: 8px; min-width: 0; } -.bs-cmp-strip__pass-label { +.bs-cmp-strip__sheet-tag { + flex: 0 0 auto; + font-size: 10px; + font-weight: 700; + color: #93c5fd; + padding: 2px 6px; + border-radius: 4px; + background: rgba(37, 99, 235, 0.18); +} + +.bs-cmp-strip__sheet-name { + font-size: 13px; + color: #f8fafc; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.bs-cmp-strip__sheet-mode { + flex: 0 0 auto; font-size: 11px; +} + +.bs-cmp-strip__pass { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} + +.bs-cmp-strip__pass-label { + font-size: 10px; color: var(--bs-cmp-muted); + line-height: 1.2; } .bs-cmp-strip__pass-value { - font-size: 22px; + font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; - line-height: 1; + line-height: 1.1; + min-width: 4.5ch; } .bs-cmp-strip.is-ok .bs-cmp-strip__pass-value { @@ -10261,31 +10359,11 @@ html.login-page--paused .login-page__flare { color: #fbbf24; } -.bs-cmp-strip__pass-sub { - font-size: 11px; - color: var(--bs-cmp-muted); -} - -.bs-cmp-strip__stats { - display: flex; - flex-wrap: wrap; - gap: 10px 14px; - font-size: 12px; - color: #cbd5e1; - font-variant-numeric: tabular-nums; - flex: 1 1 auto; -} - -.bs-cmp-strip__stats b { - color: #f1f5f9; - font-weight: 700; -} - .bs-cmp-strip__kinds { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; - margin-left: auto; + min-width: 0; } .bs-cmp-strip__kind { @@ -10294,15 +10372,20 @@ html.login-page--paused .login-page__flare { border-radius: 6px; background: rgba(15, 23, 42, 0.55); color: #cbd5e1; - padding: 3px 8px; + padding: 4px 6px; font-size: 11px; cursor: pointer; line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .bs-cmp-strip__kind b { font-variant-numeric: tabular-nums; margin-left: 2px; + display: inline-block; + min-width: 1.5ch; } .bs-cmp-strip__kind:hover { @@ -10331,26 +10414,11 @@ html.login-page--paused .login-page__flare { color: #94a3b8; } -.bs-cmp-sheet-bar { - display: flex; - flex-wrap: wrap; - gap: 8px 12px; - align-items: center; - justify-content: space-between; - flex: 0 0 auto; -} - -.bs-cmp-sheet-bar__title { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: baseline; - min-width: 0; -} - -.bs-cmp-sheet-bar__title strong { - font-size: 14px; - color: #f8fafc; +.bs-cmp-strip__range { + font-size: 11px; + font-variant-numeric: tabular-nums; + white-space: nowrap; + justify-self: end; } .bs-cmp-top-fields { @@ -10399,6 +10467,7 @@ html.login-page--paused .login-page__flare { gap: 8px; align-items: center; flex: 0 0 auto; + min-height: 36px; } .bs-cmp-kind-pills { @@ -10437,6 +10506,12 @@ html.login-page--paused .login-page__flare { max-height: none; border-color: var(--bs-cmp-border); background: rgba(8, 13, 24, 0.35); + transition: opacity 0.15s ease; +} + +.bs-cmp-result-table.is-loading { + opacity: 0.55; + pointer-events: none; } .bs-cmp-board.is-fullscreen .bs-cmp-result-table, @@ -10446,6 +10521,34 @@ html.login-page--paused .login-page__flare { flex: 1 1 auto; } +@media (max-width: 1100px) { + .bs-cmp-strip { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + } + + .bs-cmp-strip__pass { + justify-self: end; + } + + .bs-cmp-strip__kinds { + grid-column: 1 / -1; + } + + .bs-cmp-strip__range { + display: none; + } + + .bs-cmp-overview { + flex-wrap: wrap; + row-gap: 4px; + } + + .bs-cmp-overview__time { + margin-left: 0; + } +} + @media (max-width: 960px) { .bs-cmp-board__body { grid-template-columns: 1fr; diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 75b3c8d..036c72f 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -323,7 +323,7 @@ export function BizComparePage() { [runSheets, resultSheetId], ); - // Reset page when sheet / filter / page size changes + // Reset page when sheet / filter / page size changes useEffect(() => { setResultPage(1); }, [resultSheetId, kindFilter, debouncedResultKw, resultPageSize, runDetail?.id]); @@ -358,8 +358,7 @@ export function BizComparePage() { if (resultPage > pages) setResultPage(pages); } catch (e) { if (!cancelled) { - setPagedDiffs([]); - setResultTotal(0); + // Keep previous rows to avoid strip/table jump; only clear on hard empty run showError(formatErr(e)); } } finally { @@ -421,10 +420,6 @@ export function BizComparePage() { }); }, [summary.sheet_cards]); const sheetFailCount = sheetCards.filter((c) => Number(c.diff_count || 0) > 0).length; - const topChangedFields = (summary.top_changed_fields || []) as Array<{ - field: string; - count: number; - }>; const activeSheetCard = sheetCards.find((c) => c.metric_id === resultSheetId) || sheetCards[0]; useEffect(() => { @@ -1420,38 +1415,70 @@ export function BizComparePage() {
+ {/* Stable run overview — does not change when switching sheets */}
+ {t("bizCompare.runOverview")} + + {t("bizCompare.passRate")}{" "} + {Number(summary.pass_rate ?? 0).toFixed(1)}% + + + Δ{summary.diff_count ?? 0} + + + +{summary.added ?? 0} + + + −{summary.removed ?? 0} + + + ~{summary.changed ?? 0} + + + ={summary.unchanged ?? 0} + + + {summary.before_count ?? 0}→{summary.after_count ?? 0} + + + {fmtTime(runDetail.created_at)} + +
+ + {/* Sheet strip — fixed slots; only numbers/name swap */} +
0 ? " is-warn" : " is-ok" + }`} + > +
+ + {t("bizCompare.sheetCurrent")} + + + {activeSheetCard?.metric_id || "—"} + + + {activeSheetCard?.mode === "presence" + ? t("bizCompare.presenceShort") + : t("bizCompare.modeFieldsShort")} + +
{t("bizCompare.passRate")} - {Number(summary.pass_rate ?? 0).toFixed(1)}% - - - {summary.ok ? t("bizCompare.passOk") : t("bizCompare.passWarn")} + {Number(activeSheetCard?.pass_rate ?? 0).toFixed(1)}%
-
- - {summary.diff_count ?? 0} {t("bizCompare.diffCountLabel")} - - - {summary.before_count ?? 0}→{summary.after_count ?? 0} - - - {t("bizCompare.matchedRows")} {summary.matched_rows ?? 0}/ - {summary.total_rows ?? 0} - - {fmtTime(runDetail.created_at)} -
-
+
{( [ - ["added", summary.added ?? 0, "added"], - ["removed", summary.removed ?? 0, "removed"], - ["changed", summary.changed ?? 0, "changed"], - ["unchanged", summary.unchanged ?? 0, "unchanged"], + ["added", activeSheetCard?.added ?? 0, "added"], + ["removed", activeSheetCard?.removed ?? 0, "removed"], + ["changed", activeSheetCard?.changed ?? 0, "changed"], + ["unchanged", activeSheetCard?.unchanged ?? 0, "unchanged"], ] as const ).map(([id, n, cls]) => (
+ + {activeSheetCard?.before_count ?? 0}→{activeSheetCard?.after_count ?? 0} +
- {activeSheetCard ? ( -
-
- {activeSheetCard.metric_id} - - {activeSheetCard.mode === "presence" - ? t("bizCompare.presenceShort") - : t("bizCompare.modeFieldsShort")}{" "} - · {activeSheetCard.before_count ?? 0}→ - {activeSheetCard.after_count ?? 0} ·{" "} - {Number(activeSheetCard.pass_rate ?? 0).toFixed(1)}% - -
- {topChangedFields.length ? ( -
- - {t("bizCompare.topChangedFields")} - - {topChangedFields.slice(0, 6).map((f) => ( - - {f.field} - {f.count} - - ))} -
- ) : null} -
- ) : null} -
{( @@ -1525,15 +1526,16 @@ export function BizComparePage() { onChange={(e) => setResultKw(e.target.value)} /> - {diffsLoading - ? "…" - : `${pagedDiffs.length}/${resultTotal}`} + {diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`} {resultColumns.presence ? ` · ${t("bizCompare.presenceShort")}` : ""}
-
- +
{t("bizCompare.colKind")}