Show before/after values for all non-key compare result columns.

Key stays single-value; other fields always render a paired 前/后 cell so cutover diffs are readable at a glance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-18 09:27:30 +08:00
parent 157188bd3e
commit 97796478b0
4 changed files with 102 additions and 50 deletions

View file

@ -381,6 +381,8 @@ const en = {
failFields: "Failed fields", failFields: "Failed fields",
failFieldsEmpty: "—", failFieldsEmpty: "—",
failWholeRow: "Whole row", failWholeRow: "Whole row",
pairBefore: "Before",
pairAfter: "After",
sheetAllPass: "All match", sheetAllPass: "All match",
filterFailField: "Failed compare field", filterFailField: "Failed compare field",
filterFailFieldAll: "Any failed field", filterFailFieldAll: "Any failed field",

View file

@ -381,6 +381,8 @@ const zh = {
failFields: "失败项", failFields: "失败项",
failFieldsEmpty: "—", failFieldsEmpty: "—",
failWholeRow: "整行", failWholeRow: "整行",
pairBefore: "前",
pairAfter: "后",
sheetAllPass: "全部一致", sheetAllPass: "全部一致",
filterFailField: "失败比对项", filterFailField: "失败比对项",
filterFailFieldAll: "全部失败项", filterFailFieldAll: "全部失败项",

View file

@ -10735,8 +10735,47 @@ html.login-page--paused .login-page__flare {
background: rgba(245, 158, 11, 0.08); background: rgba(245, 158, 11, 0.08);
} }
.bs-cmp-pair {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 4.5rem;
}
.bs-cmp-pair__row {
display: grid;
grid-template-columns: 1.6em minmax(0, 1fr);
gap: 6px;
align-items: baseline;
}
.bs-cmp-pair__tag {
flex: 0 0 auto;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.02em;
color: #64748b;
line-height: 1.3;
}
.bs-cmp-val-cell--diff .bs-cmp-pair__row:first-child .bs-cmp-pair__tag {
color: #fca5a5;
}
.bs-cmp-val-cell--diff .bs-cmp-pair__row:last-child .bs-cmp-pair__tag {
color: #86efac;
}
.bs-cmp-val { .bs-cmp-val {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
color: #e2e8f0;
word-break: break-all;
line-height: 1.35;
}
.bs-cmp-val.is-empty {
color: #64748b;
font-style: italic;
} }
.bs-cmp-val--pre { .bs-cmp-val--pre {

View file

@ -138,6 +138,51 @@ function failFieldNames(d: DiffRow): string[] {
return Object.keys(d.changes || {}); return Object.keys(d.changes || {});
} }
/** Non-key fields: always show before/after pair for cutover review. */
function PairCell(props: {
beforeText: string;
afterText: string;
mismatch: boolean;
reason?: string;
beforeLabel: string;
afterLabel: string;
}) {
const { beforeText, afterText, mismatch, reason, beforeLabel, afterLabel } = props;
const pre = beforeText || "—";
const post = afterText || "—";
return (
<td
className={`bs-cmp-val-cell bs-cmp-val-cell--pair${
mismatch ? " bs-cmp-val-cell--diff" : ""
}`}
>
<div className="bs-cmp-pair">
<div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{beforeLabel}</span>
<span
className={`bs-cmp-val${mismatch ? " bs-cmp-val--pre" : ""}${
!beforeText ? " is-empty" : ""
}`}
>
{pre}
</span>
</div>
<div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{afterLabel}</span>
<span
className={`bs-cmp-val${mismatch ? " bs-cmp-val--post" : ""}${
!afterText ? " is-empty" : ""
}`}
>
{post}
</span>
</div>
</div>
{reason ? <div className="bs-cmp-val-reason muted">{reason}</div> : null}
</td>
);
}
function toggleInList(list: string[], name: string, on: boolean): string[] { function toggleInList(list: string[], name: string, on: boolean): string[] {
if (on) return list.includes(name) ? list : [...list, name]; if (on) return list.includes(name) ? list : [...list, name];
return list.filter((x) => x !== name); return list.filter((x) => x !== name);
@ -2263,58 +2308,22 @@ export function BizComparePage() {
{resultColumns.extras.map((f) => { {resultColumns.extras.map((f) => {
const pv = cellText(pre[f]); const pv = cellText(pre[f]);
const av = cellText(post[f]); const av = cellText(post[f]);
const isCmp = resultColumns.compareSet.has(f);
const ch = d.changes?.[f]; const ch = d.changes?.[f];
if (!isCmp) { const isCmp = resultColumns.compareSet.has(f);
const show = // Compare fields: engine mismatch; display fields: value differ / side missing
d.kind === "removed" ? pv || "—" : av || pv || "—"; const mismatch = isCmp
return ( ? Boolean(ch) || d.kind === "added" || d.kind === "removed"
<td key={f} className="bs-cmp-val-cell bs-cmp-val-cell--ctx"> : pv !== av;
<span className="bs-cmp-val">{show}</span>
</td>
);
}
if (d.kind === "added") {
return (
<td key={f} className="bs-cmp-val-cell">
<span className="bs-cmp-val bs-cmp-val--post">
{av || "—"}
</span>
</td>
);
}
if (d.kind === "removed") {
return (
<td key={f} className="bs-cmp-val-cell">
<span className="bs-cmp-val bs-cmp-val--pre">
{pv || "—"}
</span>
</td>
);
}
const mismatch = Boolean(ch);
if (!mismatch) {
return (
<td key={f} className="bs-cmp-val-cell">
<span className="bs-cmp-val">{pv || av || "—"}</span>
</td>
);
}
return ( return (
<td key={f} className="bs-cmp-val-cell bs-cmp-val-cell--diff"> <PairCell
<span className="bs-cmp-val bs-cmp-val--pre"> key={f}
{pv || "—"} beforeText={pv}
</span> afterText={av}
<span className="bs-cmp-val-arrow" aria-hidden> mismatch={mismatch}
→ reason={ch?.reason}
</span> beforeLabel={t("bizCompare.pairBefore")}
<span className="bs-cmp-val bs-cmp-val--post"> afterLabel={t("bizCompare.pairAfter")}
{av || "—"} />
</span>
{ch?.reason ? (
<div className="bs-cmp-val-reason muted">{ch.reason}</div>
) : null}
</td>
); );
})} })}
</tr> </tr>