From 83be9110c8b5a073c7700923ce6413df80044348 Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 21 Sep 2026 15:38:01 +0800 Subject: [PATCH] Fix compare board scroll sticky headers and CSS fullscreen interactions. Co-authored-by: Cursor --- web/src/index.css | 173 +++++++++++++------- web/src/pages/network/BizComparePage.tsx | 200 ++++++++++++++--------- 2 files changed, 238 insertions(+), 135 deletions(-) diff --git a/web/src/index.css b/web/src/index.css index 6694775..1580fe0 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11279,9 +11279,26 @@ html.login-page--paused .login-page__flare { max-width: min(96vw, 1600px) !important; } -.bs-cmp-board-modal .app-heroui-modal__dialog [data-slot="modal-body"], -.bs-cmp-board-modal .app-heroui-modal__dialog .modal__body { +.bs-cmp-board-modal .app-heroui-modal__dialog [data-slot="modal-body"].bs-cmp-job-body, +.bs-cmp-board-modal .app-heroui-modal__dialog .modal__body.bs-cmp-job-body { + display: flex; + flex-direction: column; + overflow: hidden; max-height: min(90vh, 980px); + min-height: min(64vh, 680px); +} + +.bs-cmp-board-modal .bs-cmp-job-body > .nm-config-modal__tabs { + flex: 0 0 auto; +} + +.bs-cmp-board-modal .app-heroui-modal__dialog [data-slot="modal-body"].bs-cmp-job-body > .bs-cmp-board, +.bs-cmp-board-modal .app-heroui-modal__dialog .modal__body.bs-cmp-job-body > .bs-cmp-board, +.bs-cmp-board-modal .bs-cmp-job-body > .bs-cmp-board { + flex: 1 1 auto !important; + flex-shrink: 1 !important; + min-height: 0 !important; + overflow: hidden; } .bs-cmp-board { @@ -11304,16 +11321,28 @@ html.login-page--paused .login-page__flare { rgba(8, 13, 24, 0.4); } -.bs-cmp-board.is-fullscreen, -.bs-cmp-board:fullscreen { +/* CSS immersive board — avoids native Fullscreen + modal focus-trap breaking clicks */ +.bs-cmp-board.is-fullscreen { --bs-cmp-surface: rgba(15, 23, 42, 0.92); - background: #0b1220; - color: #e2e8f0; + position: fixed; + inset: 0; + z-index: 12000; + width: 100vw; + height: 100vh; + max-height: 100vh; + min-height: 100vh; + margin: 0; + border-radius: 0; padding: 12px 14px; gap: 10px; overflow: hidden; - min-height: 100%; - height: 100%; + background: #0b1220; + color: #e2e8f0; + box-shadow: none; +} + +body:has(.bs-cmp-board.is-fullscreen) { + overflow: hidden; } .bs-cmp-board__toolbar { @@ -11478,6 +11507,7 @@ html.login-page--paused .login-page__flare { flex: 1 1 auto; min-height: 0; align-items: stretch; + overflow: hidden; transition: grid-template-columns 0.18s ease; } @@ -11726,6 +11756,12 @@ html.login-page--paused .login-page__flare { gap: 8px; min-width: 0; min-height: 0; + overflow: hidden; +} + +.bs-cmp-main > .pt-list-pager, +.bs-cmp-main > .pager { + flex: 0 0 auto; } /* Run-level overview: never changes when switching sheets */ @@ -12075,21 +12111,28 @@ html.login-page--paused .login-page__flare { .bs-cmp-result-table { flex: 1 1 auto; - min-height: 240px; + min-height: 0; max-height: none; + overflow: auto; border-color: var(--bs-cmp-border); background: rgba(8, 13, 24, 0.4); transition: opacity 0.15s ease; border-radius: 10px; + -webkit-overflow-scrolling: touch; +} + +.bs-cmp-result-table.bs-sheet-table { + max-height: none; + min-height: 0; } .bs-cmp-result-table.is-loading { opacity: 0.55; - pointer-events: none; + /* Keep header/filters clickable; only mute row hit-testing via opacity cue */ + pointer-events: auto; } -.bs-cmp-board.is-fullscreen .bs-cmp-result-table, -.bs-cmp-board:fullscreen .bs-cmp-result-table { +.bs-cmp-board.is-fullscreen .bs-cmp-result-table { max-height: none; min-height: 0; flex: 1 1 auto; @@ -12154,19 +12197,59 @@ html.login-page--paused .login-page__flare { .bs-cmp-diff-table { border-collapse: separate; border-spacing: 0; + --bs-cmp-group-h: 28px; } .bs-cmp-diff-table thead th { position: sticky; - top: 0; z-index: 2; background: rgba(15, 23, 42, 0.98); color: #94a3b8; box-shadow: 0 1px 0 rgba(148, 163, 184, 0.22); font-size: 12px; white-space: nowrap; + vertical-align: middle; + padding: 7px 10px; +} + +.bs-cmp-diff-table thead tr.bs-cmp-group-row th { + top: 0; + z-index: 3; + text-align: center; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + padding: 5px 10px; + height: var(--bs-cmp-group-h); + box-shadow: + 0 1px 0 rgba(148, 163, 184, 0.18), + inset 0 -1px 0 rgba(148, 163, 184, 0.08); +} + +.bs-cmp-diff-table thead tr.bs-cmp-field-row th { + top: var(--bs-cmp-group-h); + z-index: 2; vertical-align: bottom; - padding: 8px 10px; + padding-top: 6px; + padding-bottom: 8px; +} + +.bs-cmp-group--key { + background: rgba(37, 99, 235, 0.18) !important; + color: #93c5fd !important; + border-bottom: 2px solid rgba(59, 130, 246, 0.55); +} + +.bs-cmp-group--compare { + background: rgba(245, 158, 11, 0.16) !important; + color: #fcd34d !important; + border-bottom: 2px solid rgba(245, 158, 11, 0.55); +} + +.bs-cmp-group--display { + background: rgba(148, 163, 184, 0.12) !important; + color: #cbd5e1 !important; + border-bottom: 2px solid rgba(148, 163, 184, 0.35); } .bs-cmp-diff-table tbody td { @@ -12191,8 +12274,16 @@ html.login-page--paused .login-page__flare { } .bs-cmp-diff-table thead th.bs-cmp-sticky-kind { - z-index: 3; + z-index: 4; background: rgba(15, 23, 42, 0.98); + vertical-align: middle; + text-align: center; +} + +.bs-cmp-diff-table thead th.bs-cmp-col-fail { + z-index: 3; + text-align: center; + vertical-align: middle; } .bs-cmp-diff-table tbody tr:nth-child(even) td.bs-cmp-sticky-kind { @@ -12203,51 +12294,23 @@ html.login-page--paused .login-page__flare { background: rgba(30, 48, 80, 0.98); } -.bs-cmp-th { - display: inline-flex; - flex-direction: column; - align-items: flex-start; - gap: 3px; - min-width: 0; -} - -.bs-cmp-th__role { - display: inline-block; - padding: 0 5px; - border-radius: 3px; - font-size: 10px; - font-weight: 700; - line-height: 1.5; - letter-spacing: 0.02em; - min-height: 1.5em; -} - -.bs-cmp-th__role--spacer { - visibility: hidden; - padding: 0; - background: transparent; -} - -.bs-cmp-th__role--key { - background: rgba(59, 130, 246, 0.2); - color: #93c5fd; -} - -.bs-cmp-th__role--compare { - background: rgba(245, 158, 11, 0.22); - color: #fcd34d; -} - -.bs-cmp-th__role--display { - background: rgba(148, 163, 184, 0.16); - color: #cbd5e1; -} - .bs-cmp-th__name { color: #e2e8f0; font-weight: 600; } +.bs-cmp-diff-table thead tr.bs-cmp-field-row th.bs-cmp-col-key { + background: rgba(22, 36, 66, 0.98); +} + +.bs-cmp-diff-table thead tr.bs-cmp-field-row th.bs-cmp-col-compare { + background: rgba(42, 32, 16, 0.98); +} + +.bs-cmp-diff-table thead tr.bs-cmp-field-row th.bs-cmp-col-display { + background: rgba(15, 23, 42, 0.98); +} + .bs-cmp-col-key { background: rgba(59, 130, 246, 0.05); } diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 22454ed..f585ef4 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -1,5 +1,5 @@ import { Button, Input, Modal } from "@heroui/react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { ListPager } from "../../components/ListPager"; import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; @@ -829,6 +829,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage const [pagedDiffs, setPagedDiffs] = useState([]); const [diffsLoading, setDiffsLoading] = useState(false); const boardRef = useRef(null); + const tableScrollRef = useRef(null); + const tableScrollPosRef = useRef({ top: 0, left: 0 }); const [boardFs, setBoardFs] = useState(false); const [navCollapsed, setNavCollapsed] = useState(false); const tplImportRef = useRef(null); @@ -1207,24 +1209,38 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage (showFailCol ? 1 : 0) + resultColumns.keys.length + Math.max(resultColumns.extras.length, 0); - useEffect(() => { - const syncFs = () => { - const el = boardRef.current; - setBoardFs(Boolean(el && document.fullscreenElement === el)); - }; - document.addEventListener("fullscreenchange", syncFs); - return () => document.removeEventListener("fullscreenchange", syncFs); + + const rememberTableScroll = useCallback(() => { + const wrap = tableScrollRef.current; + if (!wrap) return; + tableScrollPosRef.current = { top: wrap.scrollTop, left: wrap.scrollLeft }; }, []); - const toggleBoardFullscreen = async () => { - const el = boardRef.current; - if (!el) return; - try { - if (document.fullscreenElement === el) await document.exitFullscreen(); - else await el.requestFullscreen(); - } catch (e) { - showError(formatErr(e)); - } + useEffect(() => { + if (!boardFs) return; + const onKey = (e: KeyboardEvent) => { + if (e.key !== "Escape") return; + e.preventDefault(); + e.stopPropagation(); + rememberTableScroll(); + setBoardFs(false); + }; + document.addEventListener("keydown", onKey, true); + return () => document.removeEventListener("keydown", onKey, true); + }, [boardFs, rememberTableScroll]); + + // Keep table scroll across fullscreen enter/exit (layout swap otherwise jumps to top). + useLayoutEffect(() => { + const wrap = tableScrollRef.current; + if (!wrap) return; + const { top, left } = tableScrollPosRef.current; + wrap.scrollTop = top; + wrap.scrollLeft = left; + }, [boardFs]); + + const toggleBoardFullscreen = () => { + rememberTableScroll(); + setBoardFs((v) => !v); }; const downloadRunTables = async () => { @@ -1689,6 +1705,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage }; const closeJob = () => { + setBoardFs(false); setJobId(""); setRuns([]); setRunDetail(null); @@ -2657,6 +2674,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage @@ -2811,7 +2829,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage size="sm" variant="secondary" isDisabled={!runDetail} - onPress={() => void toggleBoardFullscreen()} + onPress={() => toggleBoardFullscreen()} > {boardFs ? t("bizCompare.exitFullscreen") : t("bizCompare.fullscreen")} @@ -3020,76 +3038,98 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
- - - {showFailCol ? ( - - ) : null} - {resultColumns.keys.map((k, ki) => ( - - ))} - {resultColumns.extras.map((f, fi) => { - const isCmp = resultColumns.compareSet.has(f); - const prev = resultColumns.extras[fi - 1]; - const prevCmp = prev - ? resultColumns.compareSet.has(prev) - : null; - const showRole = fi === 0 || prevCmp !== isCmp; - return ( - + + {showFailCol ? ( + + ) : null} + {keyCols.length ? ( + - ); - })} - + {t("bizCompare.compareFields")} + + ) : null} + {displayCols.length ? ( + + ) : null} + + ) : ( + + + {showFailCol ? ( + + ) : null} + + )} + {hasGroups ? ( + + {keyCols.map((k) => ( + + ))} + {compareCols.map((f) => ( + + ))} + {displayCols.map((f) => ( + + ))} + + ) : null} + + ); + })()} {pagedDiffs.map((d, i) => {
- {t("bizCompare.colKind")} - {t("bizCompare.colFailFields")} - - {ki === 0 ? ( - - {t("bizCompare.keyFields")} - - ) : ( - -   - - )} - {k} - - - - {showRole ? ( - { + const keyCols = resultColumns.keys; + const compareCols = resultColumns.extras.filter((f) => + resultColumns.compareSet.has(f), + ); + const displayCols = resultColumns.extras.filter( + (f) => !resultColumns.compareSet.has(f), + ); + const hasGroups = + keyCols.length + compareCols.length + displayCols.length > 0; + return ( + <> + {hasGroups ? ( +
+ {t("bizCompare.colKind")} + + {t("bizCompare.colFailFields")} + - {isCmp - ? t("bizCompare.compareFields") - : t("bizCompare.displayField")} - - ) : ( - + ) : null} + {compareCols.length ? ( + -   - - )} - {f} - -
+ {t("bizCompare.displayField")} +
+ {t("bizCompare.colKind")} + + {t("bizCompare.colFailFields")} +
+ {k} + + {f} + + {f} +