diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index 478d12a..5246ade 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -511,6 +511,9 @@ const en = {
phaseQueued: "Queued",
phaseLoading: "Loading",
phaseSqlCount: "Counting",
+ phaseSqlProject: "Preparing tables",
+ phaseSqlJoin: "Joining",
+ phaseSqlFailFetch: "Fetching fails",
phaseComparing: "Comparing",
phasePersisting: "Writing",
phasePersistingFail: "Writing fails",
@@ -520,6 +523,11 @@ const en = {
phaseCancelled: "Cancelled",
sheetPending: "Pending",
colProgress: "Progress / result",
+ passRateScope: "all rows",
+ keyFiltersToggle: "Field filters",
+ hideDisplayCols: "Hide display cols",
+ showDisplayCols: "Show display cols",
+ hideDisplayColsHint: "Compact: hide display-only columns so keys and diffs stand out",
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 7369dfd..168cdf4 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -510,6 +510,9 @@ const zh = {
phaseQueued: "排队中",
phaseLoading: "加载数据",
phaseSqlCount: "库内统计",
+ phaseSqlProject: "准备比对表",
+ phaseSqlJoin: "库内比对",
+ phaseSqlFailFetch: "拉取失败行",
phaseComparing: "比对中",
phasePersisting: "写入结果",
phasePersistingFail: "写入失败行",
@@ -519,6 +522,11 @@ const zh = {
phaseCancelled: "已取消",
sheetPending: "等待中",
colProgress: "进度 / 结果",
+ passRateScope: "全表",
+ keyFiltersToggle: "字段筛选",
+ hideDisplayCols: "隐藏展示列",
+ showDisplayCols: "显示展示列",
+ hideDisplayColsHint: "紧凑模式:默认隐藏不参与比对的展示列,突出 Key 与差异",
ranWithDuration: "比对完成(耗时 {{s}} 秒)",
unchangedNotStored: "成功行仅统计数量未落库。可在任务配置将「成功行保存」改为抽样后重新比对(抽查用)。",
unchangedSampleHint: "成功共 {{total}} 条,明细抽样 {{listed}} 条(分层抽查)。要查任意路由请输入筛选条件——将按原表即时判定。通过率按全部 {{total}} 计。",
diff --git a/web/src/index.css b/web/src/index.css
index d3e61d5..2046460 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -12970,10 +12970,6 @@ body:has(.bs-cmp-board:fullscreen) {
vertical-align: middle;
}
-.bs-cmp-val-cell--diff {
- background: rgba(245, 158, 11, 0.08);
-}
-
.bs-cmp-pair {
display: flex;
flex-direction: column;
@@ -12981,6 +12977,10 @@ body:has(.bs-cmp-board:fullscreen) {
min-width: 4.5rem;
}
+.bs-cmp-pair--same {
+ min-width: 3rem;
+}
+
.bs-cmp-pair__row {
display: grid;
grid-template-columns: 1.6em minmax(0, 1fr);
@@ -13005,6 +13005,20 @@ body:has(.bs-cmp-board:fullscreen) {
color: #86efac;
}
+.bs-cmp-val-cell--pair.is-same {
+ background: transparent;
+}
+
+.bs-cmp-val--same {
+ color: #cbd5e1;
+ opacity: 0.92;
+}
+
+.bs-cmp-val-cell--diff {
+ background: rgba(245, 158, 11, 0.1);
+ box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.22);
+}
+
.bs-cmp-val {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
color: #e2e8f0;
@@ -13012,6 +13026,63 @@ body:has(.bs-cmp-board:fullscreen) {
line-height: 1.35;
}
+.bs-cmp-progress.is-compact {
+ padding: 6px 10px;
+ gap: 4px;
+}
+
+.bs-cmp-progress.is-compact .bs-cmp-progress__track {
+ height: 4px;
+}
+
+.bs-cmp-progress.is-compact .bs-cmp-progress__meta {
+ font-size: 0.75rem;
+}
+
+.bs-cmp-strip__pass-scope {
+ margin-left: 4px;
+ font-size: 9px;
+ font-weight: 600;
+ color: #64748b;
+ text-transform: none;
+}
+
+.bs-cmp-strip__toggle {
+ flex: 0 0 auto;
+ height: 28px;
+ padding: 0 8px;
+ border-radius: 6px;
+ border: 1px solid rgba(148, 163, 184, 0.28);
+ background: rgba(15, 23, 42, 0.45);
+ color: #cbd5e1;
+ font-size: 11px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.bs-cmp-strip__toggle:hover {
+ border-color: rgba(96, 165, 250, 0.45);
+ color: #f1f5f9;
+}
+
+.bs-cmp-strip__toggle.is-active {
+ border-color: rgba(59, 130, 246, 0.5);
+ background: rgba(37, 99, 235, 0.18);
+ color: #93c5fd;
+}
+
+.bs-cmp-strip__hint {
+ flex: 1 1 100%;
+ margin: 0;
+ font-size: 0.75rem;
+ line-height: 1.35;
+}
+
+.bs-cmp-strip .bs-cmp-key-filters {
+ flex: 1 1 100%;
+ margin-top: 0;
+}
+
.bs-cmp-val.is-empty {
color: #64748b;
font-style: italic;
diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx
index 5ed8927..aa3a018 100644
--- a/web/src/pages/network/BizComparePage.tsx
+++ b/web/src/pages/network/BizComparePage.tsx
@@ -196,6 +196,8 @@ function PairCell(props: {
const post = afterText || "—";
const isAdded = kind === "added";
const isRemoved = kind === "removed";
+ const same =
+ !mismatch && !isAdded && !isRemoved && String(beforeText || "") === String(afterText || "");
// Whole-row missing/extra: emphasize the present side; do not strike it out.
const preClass = [
"bs-cmp-val",
@@ -217,20 +219,31 @@ function PairCell(props: {
-
-
- {beforeLabel}
- {isAdded ? "—" : pre}
+ {same ? (
+
+ {pre || "—"}
-
- {afterLabel}
- {isRemoved ? "—" : post}
+ ) : (
+
+
+ {beforeLabel}
+ {isAdded ? "—" : pre}
+
+
+ {afterLabel}
+ {isRemoved ? "—" : post}
+
-
+ )}
{reason ? {reason} : null}
|
);
@@ -363,6 +376,9 @@ function localizeRunPhase(phase: string | undefined, t: (key: string) => string)
queued: "bizCompare.phaseQueued",
loading: "bizCompare.phaseLoading",
sql_count: "bizCompare.phaseSqlCount",
+ sql_project: "bizCompare.phaseSqlProject",
+ sql_join: "bizCompare.phaseSqlJoin",
+ sql_fail_fetch: "bizCompare.phaseSqlFailFetch",
comparing: "bizCompare.phaseComparing",
done: "bizCompare.phaseDone",
failed: "bizCompare.phaseFailed",
@@ -944,6 +960,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
const [resultKw, setResultKw] = useState("");
const debouncedResultKw = useDebouncedValue(resultKw, 300);
const [resultKeyFilters, setResultKeyFilters] = useState>({});
+ const [keyFiltersOpen, setKeyFiltersOpen] = useState(false);
+ const [hideDisplayCols, setHideDisplayCols] = useState(true);
const debouncedKeyFiltersJson = useDebouncedValue(JSON.stringify(resultKeyFilters), 300);
const debouncedKeyFilters = useMemo(() => {
try {
@@ -959,6 +977,10 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
}, [debouncedKeyFiltersJson]);
const hasResultSearch =
Boolean(debouncedResultKw.trim()) || Object.keys(debouncedKeyFilters).length > 0;
+ const activeKeyFilterCount = Object.keys(resultKeyFilters).filter((k) =>
+ String(resultKeyFilters[k] || "").trim(),
+ ).length;
+ const keyFiltersVisible = keyFiltersOpen || activeKeyFilterCount > 0;
const [resultPage, setResultPage] = useState(1);
const [resultPageSize, setResultPageSize] = useState(100);
const [resultTotal, setResultTotal] = useState(0);
@@ -1419,7 +1441,12 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
1 +
(showFailCol ? 1 : 0) +
resultColumns.keys.length +
- Math.max(resultColumns.extras.length, 0);
+ Math.max(
+ resultColumns.extras.filter(
+ (f) => resultColumns.compareSet.has(f) || !hideDisplayCols,
+ ).length,
+ 0,
+ );
const rememberTableScroll = useCallback(() => {
const wrap = tableScrollRef.current;
@@ -3404,7 +3431,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{runDetail && runIsActive ? (
-
+
{t("bizCompare.runStatusRunning")}
{runEngine === "sql" || runEngine === "python" ? (
@@ -3537,36 +3564,54 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{(sheetCards.length
? sheetCards
- : runSheets.map((s) => {
- const removed = Number(s.summary?.removed || 0);
- const changed = Number(s.summary?.changed || 0);
- const unchanged = Number(s.summary?.unchanged || 0);
- const fail = removed + changed;
- const judged = fail + unchanged;
- const st = String((s as any).status || "done");
- const pending = ["pending", "running", "queued"].includes(st);
- return {
- sheet_id: sheetIdentity(s),
- title: sheetLabel(s),
- metric_id: s.metric_id,
- mode: s.mode,
- status: st,
- added: s.summary?.added,
- removed,
- changed,
- unchanged,
- before_count: s.summary?.before_count,
- after_count: s.summary?.after_count,
- fail_count: fail,
- success_count: unchanged,
- diff_count: fail,
- pass_rate: pending
- ? null
- : judged
- ? Math.round((unchanged / judged) * 1000) / 10
- : 100,
- };
- })
+ : [...runSheets]
+ .map((s) => {
+ const removed = Number(s.summary?.removed || 0);
+ const changed = Number(s.summary?.changed || 0);
+ const unchanged = Number(s.summary?.unchanged || 0);
+ const fail = removed + changed;
+ const judged = fail + unchanged;
+ const st = String((s as any).status || "done");
+ const pending = ["pending", "running", "queued"].includes(st);
+ return {
+ sheet_id: sheetIdentity(s),
+ title: sheetLabel(s),
+ metric_id: s.metric_id,
+ mode: s.mode,
+ status: st,
+ added: s.summary?.added,
+ removed,
+ changed,
+ unchanged,
+ before_count: s.summary?.before_count,
+ after_count: s.summary?.after_count,
+ fail_count: fail,
+ success_count: unchanged,
+ diff_count: fail,
+ pass_rate: pending
+ ? null
+ : judged
+ ? Math.round((unchanged / judged) * 1000) / 10
+ : 100,
+ };
+ })
+ .sort((a, b) => {
+ const pa = ["pending", "running", "queued"].includes(
+ String(a.status || ""),
+ )
+ ? 1
+ : 0;
+ const pb = ["pending", "running", "queued"].includes(
+ String(b.status || ""),
+ )
+ ? 1
+ : 0;
+ if (pa !== pb) return pa - pb;
+ const da = Number(a.fail_count || 0);
+ const db = Number(b.fail_count || 0);
+ if (da !== db) return db - da;
+ return String(a.metric_id).localeCompare(String(b.metric_id));
+ })
).map((c) => {
const fail = sheetFailOf(c);
const ok = sheetSuccessOf(c);
@@ -3586,11 +3631,17 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
className={`bs-cmp-nav__item${active ? " is-active" : ""}${
pending ? " is-pending" : fail > 0 ? " has-diff" : " is-clean"
}`}
- onClick={() => setResultSheetId(id)}
+ onClick={() => {
+ setResultSheetId(id);
+ // Cutover default: land on fails when switching sheets
+ if (fail > 0 && kindFilter === "unchanged") {
+ setKindFilter("diff");
+ }
+ }}
title={
pending
? `${label} · ${t("bizCompare.sheetPending")}`
- : `${label} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate}%`
+ : `${label} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate ?? "…"}%`
}
>
@@ -3650,9 +3701,15 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
: ` · ${activeSheetCard?.before_count ?? 0}→${activeSheetCard?.after_count ?? 0}`}
-
+
{t("bizCompare.passRate")}
+
+ {t("bizCompare.passRateScope")}
+
{activeSheetPending || activePassRate === null
@@ -3708,9 +3765,31 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`}
+ {resultSearchKeyFields.length ? (
+
+ ) : null}
+
- {resultSearchKeyFields.length ? (
-
+ {resultSearchKeyFields.length && keyFiltersVisible ? (
+
{resultSearchKeyFields.map((f) => (
))}
- {Object.keys(resultKeyFilters).some((k) =>
- String(resultKeyFilters[k] || "").trim(),
- ) ? (
+ {activeKeyFilterCount ? (