Make compare sheet nav collapsible and hide run picker in fullscreen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-21 15:16:53 +08:00
parent 4c3c034c4c
commit 28aa7cd87e
4 changed files with 267 additions and 140 deletions

View file

@ -491,6 +491,8 @@ const en = {
sheetCount: "Sheets",
sheetNavTitle: "Checks",
sheetNavHint: "{{n}} checks · click to switch",
collapseSheetNav: "Collapse checks",
expandSheetNav: "Expand checks",
sheetFailCount: "{{n}} failed",
topChangedFields: "Top failed fields",
exportTables: "Download tables",

View file

@ -488,6 +488,8 @@ const zh = {
sheetCount: "监控表",
sheetNavTitle: "检查项",
sheetNavHint: "共 {{n}} 项 · 点击切换",
collapseSheetNav: "折叠检查项",
expandSheetNav: "展开检查项",
sheetFailCount: "{{n}} 项失败",
topChangedFields: "失败字段 Top",
exportTables: "下载表格",

View file

@ -11327,6 +11327,15 @@ html.login-page--paused .login-page__flare {
padding: 2px 0;
}
.bs-cmp-board__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1 1 auto;
}
.bs-cmp-board__run-select {
flex: 0 1 280px;
min-width: 140px;
@ -11355,7 +11364,7 @@ html.login-page--paused .login-page__flare {
align-items: center;
gap: 6px 8px;
min-width: 0;
flex: 1 1 320px;
flex: 1 1 280px;
height: 32px;
padding: 0 8px;
border-radius: 8px;
@ -11440,9 +11449,12 @@ html.login-page--paused .login-page__flare {
margin-left: 0;
}
.bs-cmp-board__meta {
flex: 1 1 100%;
}
.bs-cmp-sides {
flex: 1 1 100%;
order: 3;
height: auto;
min-height: 32px;
padding: 4px 8px;
@ -11452,6 +11464,11 @@ html.login-page--paused .login-page__flare {
.bs-cmp-sides__name {
max-width: none;
}
.bs-cmp-board__run-select {
flex: 1 1 200px;
max-width: none;
}
}
.bs-cmp-board__body {
@ -11461,6 +11478,11 @@ html.login-page--paused .login-page__flare {
flex: 1 1 auto;
min-height: 0;
align-items: stretch;
transition: grid-template-columns 0.18s ease;
}
.bs-cmp-board__body.is-nav-collapsed {
grid-template-columns: 44px minmax(0, 1fr);
}
.bs-cmp-nav {
@ -11475,15 +11497,44 @@ html.login-page--paused .login-page__flare {
.bs-cmp-nav__head {
display: grid;
grid-template-columns: 8px minmax(0, 1fr) auto;
grid-template-columns: 28px minmax(0, 1fr) auto;
align-items: center;
gap: 0 8px;
padding: 8px 10px;
gap: 0 6px;
padding: 6px 8px;
border-bottom: 1px solid var(--bs-cmp-border);
flex: 0 0 auto;
min-height: 36px;
}
.bs-cmp-nav.is-collapsed .bs-cmp-nav__head {
grid-template-columns: 1fr;
justify-items: center;
padding: 6px 4px;
}
.bs-cmp-nav__toggle {
appearance: none;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 6px;
background: rgba(15, 23, 42, 0.55);
color: #93c5fd;
font-size: 14px;
font-weight: 700;
line-height: 1;
cursor: pointer;
padding: 0;
}
.bs-cmp-nav__toggle:hover {
background: rgba(37, 99, 235, 0.22);
border-color: rgba(59, 130, 246, 0.45);
}
.bs-cmp-nav__dot--spacer {
visibility: hidden;
}
@ -11597,6 +11648,34 @@ html.login-page--paused .login-page__flare {
transition: background 0.12s ease, border-color 0.12s ease;
}
.bs-cmp-nav.is-collapsed .bs-cmp-nav__item {
grid-template-columns: 1fr;
justify-items: center;
padding: 8px 4px;
border-radius: 6px;
}
.bs-cmp-nav.is-collapsed .bs-cmp-nav__name,
.bs-cmp-nav.is-collapsed .bs-cmp-nav__stats {
display: none;
}
.bs-cmp-nav.is-collapsed .bs-cmp-nav__list {
padding: 4px;
gap: 2px;
align-items: stretch;
}
.bs-cmp-nav.is-collapsed .bs-cmp-nav__dot {
width: 8px;
height: 8px;
}
.bs-cmp-nav.is-collapsed .bs-cmp-nav__item.is-active {
box-shadow: none;
background: rgba(37, 99, 235, 0.28);
}
.bs-cmp-nav__item:hover {
background: rgba(30, 41, 59, 0.75);
}
@ -12042,10 +12121,18 @@ html.login-page--paused .login-page__flare {
grid-template-columns: 1fr;
}
.bs-cmp-board__body.is-nav-collapsed {
grid-template-columns: 1fr;
}
.bs-cmp-nav {
max-height: 180px;
}
.bs-cmp-nav.is-collapsed {
max-height: 52px;
}
.bs-cmp-nav__list {
flex-direction: row;
flex-wrap: nowrap;
@ -12053,6 +12140,10 @@ html.login-page--paused .login-page__flare {
overflow-y: hidden;
}
.bs-cmp-nav.is-collapsed .bs-cmp-nav__list {
display: none;
}
.bs-cmp-nav__item {
flex: 0 0 auto;
min-width: 180px;

View file

@ -830,6 +830,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
const [diffsLoading, setDiffsLoading] = useState(false);
const boardRef = useRef<HTMLDivElement | null>(null);
const [boardFs, setBoardFs] = useState(false);
const [navCollapsed, setNavCollapsed] = useState(false);
const tplImportRef = useRef<HTMLInputElement | null>(null);
const refresh = useCallback(async () => {
@ -2693,109 +2694,119 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
className={`bs-cmp-board${boardFs ? " is-fullscreen" : ""}`}
>
<div className="bs-cmp-board__toolbar">
<select
className="ui-field__select bs-cmp-board__run-select"
aria-label={t("bizCompare.pickBatchRun")}
value={runDetail?.id || ""}
onChange={(e) => {
const id = e.target.value;
if (id) void loadRun(id);
}}
>
<option value="">{t("bizCompare.pickRun")}</option>
{runs.map((r) => {
const before = enrichSide(
(r as any).before,
beforeTaskId,
tasks,
beforeBatches,
);
const after = enrichSide(
(r as any).after,
afterTaskId || beforeTaskId,
tasks,
afterBatches.length ? afterBatches : beforeBatches,
);
const bl = sideDeviceName(before);
const al = sideDeviceName(after);
const when = formatSystemTime((r as any).created_at) || "";
return (
<option key={r.id} value={r.id}>
{when ? `${when} · ` : ""}
{bl} {sideCollectTime(before)} → {al} {sideCollectTime(after)}
</option>
);
})}
</select>
{runs.length ? (
<span className="muted bs-cmp-board__run-count">
{t("bizCompare.runCount", { n: String(runs.length) })}
</span>
) : null}
{runDetail ? (
<div className="bs-cmp-sides" aria-label={t("bizCompare.sidesTitle")}>
{(() => {
const before = enrichSide(
runDetail.before,
beforeTaskId,
tasks,
beforeBatches,
);
const after = enrichSide(
runDetail.after,
afterTaskId || beforeTaskId,
tasks,
afterBatches.length ? afterBatches : beforeBatches,
);
const beforeName = sideDeviceName(before);
const afterName = sideDeviceName(after);
const beforeTime = sideCollectTime(before);
const afterTime = sideCollectTime(after);
return (
<>
<div className="bs-cmp-sides__side is-before">
<span className="bs-cmp-sides__tag">{t("bizCompare.sideBefore")}</span>
<strong className="bs-cmp-sides__name" title={`${beforeName} ${beforeTime}`}>
{beforeName}
</strong>
<span className="bs-cmp-sides__meta" title={beforeTime}>
{beforeTime}
<div className="bs-cmp-board__meta">
{runDetail ? (
<div className="bs-cmp-sides" aria-label={t("bizCompare.sidesTitle")}>
{(() => {
const before = enrichSide(
runDetail.before,
beforeTaskId,
tasks,
beforeBatches,
);
const after = enrichSide(
runDetail.after,
afterTaskId || beforeTaskId,
tasks,
afterBatches.length ? afterBatches : beforeBatches,
);
const beforeName = sideDeviceName(before);
const afterName = sideDeviceName(after);
const beforeTime = sideCollectTime(before);
const afterTime = sideCollectTime(after);
return (
<>
<div className="bs-cmp-sides__side is-before">
<span className="bs-cmp-sides__tag">{t("bizCompare.sideBefore")}</span>
<strong className="bs-cmp-sides__name" title={`${beforeName} ${beforeTime}`}>
{beforeName}
</strong>
<span className="bs-cmp-sides__meta" title={beforeTime}>
{beforeTime}
</span>
</div>
<span className="bs-cmp-sides__arrow" aria-hidden>
→
</span>
</div>
<span className="bs-cmp-sides__arrow" aria-hidden>
→
</span>
<div className="bs-cmp-sides__side is-after">
<span className="bs-cmp-sides__tag">{t("bizCompare.sideAfter")}</span>
<strong className="bs-cmp-sides__name" title={`${afterName} ${afterTime}`}>
{afterName}
</strong>
<span className="bs-cmp-sides__meta" title={afterTime}>
{afterTime}
</span>
</div>
</>
);
})()}
</div>
) : null}
<div className="bs-cmp-sides__side is-after">
<span className="bs-cmp-sides__tag">{t("bizCompare.sideAfter")}</span>
<strong className="bs-cmp-sides__name" title={`${afterName} ${afterTime}`}>
{afterName}
</strong>
<span className="bs-cmp-sides__meta" title={afterTime}>
{afterTime}
</span>
</div>
</>
);
})()}
</div>
) : null}
{!boardFs ? (
<>
<select
className="ui-field__select bs-cmp-board__run-select"
aria-label={t("bizCompare.pickBatchRun")}
value={runDetail?.id || ""}
onChange={(e) => {
const id = e.target.value;
if (id) void loadRun(id);
}}
>
<option value="">{t("bizCompare.pickRun")}</option>
{runs.map((r) => {
const before = enrichSide(
(r as any).before,
beforeTaskId,
tasks,
beforeBatches,
);
const after = enrichSide(
(r as any).after,
afterTaskId || beforeTaskId,
tasks,
afterBatches.length ? afterBatches : beforeBatches,
);
const bl = sideDeviceName(before);
const al = sideDeviceName(after);
const when = formatSystemTime((r as any).created_at) || "";
return (
<option key={r.id} value={r.id}>
{when ? `${when} · ` : ""}
{bl} {sideCollectTime(before)} → {al} {sideCollectTime(after)}
</option>
);
})}
</select>
{runs.length ? (
<span className="muted bs-cmp-board__run-count">
{t("bizCompare.runCount", { n: String(runs.length) })}
</span>
) : null}
</>
) : null}
</div>
<div className="btn-row bs-cmp-board__actions">
<Button
size="sm"
variant="secondary"
isDisabled={busy || !runDetail?.id}
onPress={() => void downloadRunTables()}
>
{t("bizCompare.exportTables")}
</Button>
<Button
size="sm"
variant="danger"
isDisabled={busy || !runDetail?.id}
onPress={() => void removeRun(String(runDetail?.id || ""))}
>
{t("bizCompare.deleteRun")}
</Button>
{!boardFs ? (
<>
<Button
size="sm"
variant="secondary"
isDisabled={busy || !runDetail?.id}
onPress={() => void downloadRunTables()}
>
{t("bizCompare.exportTables")}
</Button>
<Button
size="sm"
variant="danger"
isDisabled={busy || !runDetail?.id}
onPress={() => void removeRun(String(runDetail?.id || ""))}
>
{t("bizCompare.deleteRun")}
</Button>
</>
) : null}
<Button
size="sm"
variant="secondary"
@ -2808,41 +2819,62 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
</div>
{runDetail ? (
<div className="bs-cmp-board__body">
<aside className="bs-cmp-nav" aria-label={t("bizCompare.sheetNavTitle")}>
<div
className={`bs-cmp-board__body${navCollapsed ? " is-nav-collapsed" : ""}`}
>
<aside
className={`bs-cmp-nav${navCollapsed ? " is-collapsed" : ""}`}
aria-label={t("bizCompare.sheetNavTitle")}
>
<div className="bs-cmp-nav__head">
<span className="bs-cmp-nav__dot bs-cmp-nav__dot--spacer" aria-hidden />
<span className="bs-cmp-nav__head-main">
<span className="bs-cmp-nav__title">{t("bizCompare.sheetNavTitle")}</span>
<span
className="bs-cmp-nav__count"
title={t("bizCompare.sheetNavHint", {
n: String(sheetCards.length || runSheets.length),
})}
>
{sheetCards.length || runSheets.length}
</span>
</span>
<span className="bs-cmp-nav__legend" aria-hidden>
<span
className="bs-cmp-nav__num bs-cmp-nav__num--fail is-hot"
title={t("bizCompare.kindFail")}
>
{t("bizCompare.kindFail")}
</span>
<span
className="bs-cmp-nav__num bs-cmp-nav__num--ok is-hot"
title={t("bizCompare.kindSuccess")}
>
{t("bizCompare.kindSuccess")}
</span>
<span
className="bs-cmp-nav__num bs-cmp-nav__num--rate"
title={t("bizCompare.passRateShort")}
>
%
</span>
</span>
<button
type="button"
className="bs-cmp-nav__toggle"
aria-expanded={!navCollapsed}
title={
navCollapsed
? t("bizCompare.expandSheetNav")
: t("bizCompare.collapseSheetNav")
}
onClick={() => setNavCollapsed((v) => !v)}
>
{navCollapsed ? "»" : "«"}
</button>
{!navCollapsed ? (
<>
<span className="bs-cmp-nav__head-main">
<span className="bs-cmp-nav__title">{t("bizCompare.sheetNavTitle")}</span>
<span
className="bs-cmp-nav__count"
title={t("bizCompare.sheetNavHint", {
n: String(sheetCards.length || runSheets.length),
})}
>
{sheetCards.length || runSheets.length}
</span>
</span>
<span className="bs-cmp-nav__legend" aria-hidden>
<span
className="bs-cmp-nav__num bs-cmp-nav__num--fail is-hot"
title={t("bizCompare.kindFail")}
>
{t("bizCompare.kindFail")}
</span>
<span
className="bs-cmp-nav__num bs-cmp-nav__num--ok is-hot"
title={t("bizCompare.kindSuccess")}
>
{t("bizCompare.kindSuccess")}
</span>
<span
className="bs-cmp-nav__num bs-cmp-nav__num--rate"
title={t("bizCompare.passRateShort")}
>
%
</span>
</span>
</>
) : null}
</div>
<div className="bs-cmp-nav__list" role="tablist">
{(sheetCards.length