mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Stabilize compare board KPIs: fixed run overview + sheet strip slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
499a5fec2e
commit
fe34319d65
4 changed files with 224 additions and 115 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -329,6 +329,8 @@ const zh = {
|
|||
passRate: "通过率",
|
||||
passOk: "前后一致",
|
||||
passWarn: "存在差异",
|
||||
runOverview: "本次总览",
|
||||
sheetCurrent: "当前表",
|
||||
diffCountLabel: "差异条目",
|
||||
diffRate: "差异率",
|
||||
beforeCount: "操作前行数",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</aside>
|
||||
|
||||
<div className="bs-cmp-main">
|
||||
{/* Stable run overview — does not change when switching sheets */}
|
||||
<div
|
||||
className={`bs-cmp-strip${summary.ok ? " is-ok" : " is-warn"}`}
|
||||
className={`bs-cmp-overview${summary.ok ? " is-ok" : " is-warn"}`}
|
||||
>
|
||||
<span className="bs-cmp-overview__tag">{t("bizCompare.runOverview")}</span>
|
||||
<span className="bs-cmp-overview__pass">
|
||||
{t("bizCompare.passRate")}{" "}
|
||||
<b>{Number(summary.pass_rate ?? 0).toFixed(1)}%</b>
|
||||
</span>
|
||||
<span className="bs-cmp-overview__stat">
|
||||
Δ<b>{summary.diff_count ?? 0}</b>
|
||||
</span>
|
||||
<span className="bs-cmp-overview__stat">
|
||||
+<b>{summary.added ?? 0}</b>
|
||||
</span>
|
||||
<span className="bs-cmp-overview__stat">
|
||||
−<b>{summary.removed ?? 0}</b>
|
||||
</span>
|
||||
<span className="bs-cmp-overview__stat">
|
||||
~<b>{summary.changed ?? 0}</b>
|
||||
</span>
|
||||
<span className="bs-cmp-overview__stat">
|
||||
=<b>{summary.unchanged ?? 0}</b>
|
||||
</span>
|
||||
<span className="bs-cmp-overview__stat muted">
|
||||
{summary.before_count ?? 0}→{summary.after_count ?? 0}
|
||||
</span>
|
||||
<span className="bs-cmp-overview__time muted">
|
||||
{fmtTime(runDetail.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Sheet strip — fixed slots; only numbers/name swap */}
|
||||
<div
|
||||
className={`bs-cmp-strip${
|
||||
Number(activeSheetCard?.diff_count || 0) > 0 ? " is-warn" : " is-ok"
|
||||
}`}
|
||||
>
|
||||
<div className="bs-cmp-strip__sheet">
|
||||
<span className="bs-cmp-strip__sheet-tag">
|
||||
{t("bizCompare.sheetCurrent")}
|
||||
</span>
|
||||
<strong className="bs-cmp-strip__sheet-name" title={activeSheetCard?.metric_id || ""}>
|
||||
{activeSheetCard?.metric_id || "—"}
|
||||
</strong>
|
||||
<span className="muted bs-cmp-strip__sheet-mode">
|
||||
{activeSheetCard?.mode === "presence"
|
||||
? t("bizCompare.presenceShort")
|
||||
: t("bizCompare.modeFieldsShort")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bs-cmp-strip__pass">
|
||||
<span className="bs-cmp-strip__pass-label">{t("bizCompare.passRate")}</span>
|
||||
<span className="bs-cmp-strip__pass-value">
|
||||
{Number(summary.pass_rate ?? 0).toFixed(1)}%
|
||||
</span>
|
||||
<span className="bs-cmp-strip__pass-sub">
|
||||
{summary.ok ? t("bizCompare.passOk") : t("bizCompare.passWarn")}
|
||||
{Number(activeSheetCard?.pass_rate ?? 0).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="bs-cmp-strip__stats">
|
||||
<span>
|
||||
<b>{summary.diff_count ?? 0}</b> {t("bizCompare.diffCountLabel")}
|
||||
</span>
|
||||
<span>
|
||||
{summary.before_count ?? 0}→{summary.after_count ?? 0}
|
||||
</span>
|
||||
<span>
|
||||
{t("bizCompare.matchedRows")} {summary.matched_rows ?? 0}/
|
||||
{summary.total_rows ?? 0}
|
||||
</span>
|
||||
<span className="muted">{fmtTime(runDetail.created_at)}</span>
|
||||
</div>
|
||||
<div className="bs-cmp-strip__kinds">
|
||||
<div className="bs-cmp-strip__kinds" role="group">
|
||||
{(
|
||||
[
|
||||
["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]) => (
|
||||
<button
|
||||
|
|
@ -1466,37 +1493,11 @@ export function BizComparePage() {
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="bs-cmp-strip__range muted">
|
||||
{activeSheetCard?.before_count ?? 0}→{activeSheetCard?.after_count ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{activeSheetCard ? (
|
||||
<div className="bs-cmp-sheet-bar">
|
||||
<div className="bs-cmp-sheet-bar__title">
|
||||
<strong>{activeSheetCard.metric_id}</strong>
|
||||
<span className="muted">
|
||||
{activeSheetCard.mode === "presence"
|
||||
? t("bizCompare.presenceShort")
|
||||
: t("bizCompare.modeFieldsShort")}{" "}
|
||||
· {activeSheetCard.before_count ?? 0}→
|
||||
{activeSheetCard.after_count ?? 0} ·{" "}
|
||||
{Number(activeSheetCard.pass_rate ?? 0).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
{topChangedFields.length ? (
|
||||
<div className="bs-cmp-top-fields bs-cmp-top-fields--inline">
|
||||
<span className="bs-cmp-top-fields__label">
|
||||
{t("bizCompare.topChangedFields")}
|
||||
</span>
|
||||
{topChangedFields.slice(0, 6).map((f) => (
|
||||
<span key={f.field} className="bs-cmp-top-fields__chip">
|
||||
{f.field}
|
||||
<em>{f.count}</em>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="bs-cmp-filter-bar">
|
||||
<div className="bs-cmp-kind-pills" role="tablist">
|
||||
{(
|
||||
|
|
@ -1525,15 +1526,16 @@ export function BizComparePage() {
|
|||
onChange={(e) => setResultKw(e.target.value)}
|
||||
/>
|
||||
<span className="muted bs-sheet-count">
|
||||
{diffsLoading
|
||||
? "…"
|
||||
: `${pagedDiffs.length}/${resultTotal}`}
|
||||
{diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`}
|
||||
{resultColumns.presence ? ` · ${t("bizCompare.presenceShort")}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-list-table-wrap bs-sheet-table bs-cmp-result-table">
|
||||
<table className="data-table pt-list-table bs-cmp-diff-table">
|
||||
<div
|
||||
className={`pt-list-table-wrap bs-sheet-table bs-cmp-result-table${
|
||||
diffsLoading ? " is-loading" : ""
|
||||
}`}
|
||||
> <table className="data-table pt-list-table bs-cmp-diff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="bs-cmp-col-kind">{t("bizCompare.colKind")}</th>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue