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", sheetCount: "Sheets",
sheetNavTitle: "Checks", sheetNavTitle: "Checks",
sheetNavHint: "{{n}} checks · click to switch", sheetNavHint: "{{n}} checks · click to switch",
collapseSheetNav: "Collapse checks",
expandSheetNav: "Expand checks",
sheetFailCount: "{{n}} failed", sheetFailCount: "{{n}} failed",
topChangedFields: "Top failed fields", topChangedFields: "Top failed fields",
exportTables: "Download tables", exportTables: "Download tables",

View file

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

View file

@ -11327,6 +11327,15 @@ html.login-page--paused .login-page__flare {
padding: 2px 0; 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 { .bs-cmp-board__run-select {
flex: 0 1 280px; flex: 0 1 280px;
min-width: 140px; min-width: 140px;
@ -11355,7 +11364,7 @@ html.login-page--paused .login-page__flare {
align-items: center; align-items: center;
gap: 6px 8px; gap: 6px 8px;
min-width: 0; min-width: 0;
flex: 1 1 320px; flex: 1 1 280px;
height: 32px; height: 32px;
padding: 0 8px; padding: 0 8px;
border-radius: 8px; border-radius: 8px;
@ -11440,9 +11449,12 @@ html.login-page--paused .login-page__flare {
margin-left: 0; margin-left: 0;
} }
.bs-cmp-board__meta {
flex: 1 1 100%;
}
.bs-cmp-sides { .bs-cmp-sides {
flex: 1 1 100%; flex: 1 1 100%;
order: 3;
height: auto; height: auto;
min-height: 32px; min-height: 32px;
padding: 4px 8px; padding: 4px 8px;
@ -11452,6 +11464,11 @@ html.login-page--paused .login-page__flare {
.bs-cmp-sides__name { .bs-cmp-sides__name {
max-width: none; max-width: none;
} }
.bs-cmp-board__run-select {
flex: 1 1 200px;
max-width: none;
}
} }
.bs-cmp-board__body { .bs-cmp-board__body {
@ -11461,6 +11478,11 @@ html.login-page--paused .login-page__flare {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
align-items: stretch; 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 { .bs-cmp-nav {
@ -11475,15 +11497,44 @@ html.login-page--paused .login-page__flare {
.bs-cmp-nav__head { .bs-cmp-nav__head {
display: grid; display: grid;
grid-template-columns: 8px minmax(0, 1fr) auto; grid-template-columns: 28px minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 0 8px; gap: 0 6px;
padding: 8px 10px; padding: 6px 8px;
border-bottom: 1px solid var(--bs-cmp-border); border-bottom: 1px solid var(--bs-cmp-border);
flex: 0 0 auto; flex: 0 0 auto;
min-height: 36px; 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 { .bs-cmp-nav__dot--spacer {
visibility: hidden; visibility: hidden;
} }
@ -11597,6 +11648,34 @@ html.login-page--paused .login-page__flare {
transition: background 0.12s ease, border-color 0.12s ease; 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 { .bs-cmp-nav__item:hover {
background: rgba(30, 41, 59, 0.75); background: rgba(30, 41, 59, 0.75);
} }
@ -12042,10 +12121,18 @@ html.login-page--paused .login-page__flare {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.bs-cmp-board__body.is-nav-collapsed {
grid-template-columns: 1fr;
}
.bs-cmp-nav { .bs-cmp-nav {
max-height: 180px; max-height: 180px;
} }
.bs-cmp-nav.is-collapsed {
max-height: 52px;
}
.bs-cmp-nav__list { .bs-cmp-nav__list {
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -12053,6 +12140,10 @@ html.login-page--paused .login-page__flare {
overflow-y: hidden; overflow-y: hidden;
} }
.bs-cmp-nav.is-collapsed .bs-cmp-nav__list {
display: none;
}
.bs-cmp-nav__item { .bs-cmp-nav__item {
flex: 0 0 auto; flex: 0 0 auto;
min-width: 180px; min-width: 180px;

View file

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