From 327b824ef5007a30ae133273db6df797516409bd Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sat, 19 Sep 2026 20:10:03 +0800 Subject: [PATCH] fix: smooth request table scrolling and clarify logging guidance - Prevent dropped scroll updates and position jumps - Keep scroll ranges aligned when resizing or toggling columns - Update logging guidance across all supported languages - Add browser regression coverage for table scrolling --- src/i18n/ja.ts | 2 +- src/i18n/locales/en.ts | 2 +- src/i18n/locales/zh-CN.ts | 2 +- src/i18n/resources.ts | 1 + src/pages/ConfigPanel.tsx | 6 +- src/pages/UsageRecordsPage.tsx | 84 ++++++++----------- src/styles.css | 5 ++ tests/fixtures/usage-layout.tsx | 41 ++++++++- tests/usage-scroll-ui.cjs | 143 ++++++++++++++++++++++++++++++++ 9 files changed, 230 insertions(+), 56 deletions(-) create mode 100644 tests/usage-scroll-ui.cjs diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 2be7222..3151318 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -615,7 +615,7 @@ export const jaOverrides = { 'config.notice.sessionAffinityDisabled': 'セッションアフィニティを無効にしました', 'config.notice.sessionTtlUpdated': 'セッションアフィニティ TTL を保存しました', 'config.diagnostics.title': 'ログと診断', - 'config.diagnostics.description': 'デバッグ出力、ファイルログ、使用量統計を CPA WebUI と同じ項目で設定します。', + 'config.diagnostics.description': '有効にすると、呼び出しでエラーが発生したリクエストのみを記録し、成功したリクエストは記録しません。ストレージ容量を過度に消費しないよう、不要になったら速やかに「**ログファイルへ書き込む**」を無効にしてください。', 'config.diagnostics.debug.title': 'デバッグモード', 'config.diagnostics.debug.description': '問題調査用の詳細なデバッグログを出力します。', 'config.diagnostics.commercial.title': '商用モード', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index a1192f4..72cd73e 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -646,7 +646,7 @@ export const en: Record = { 'config.software.behavior.minimize': 'Minimize to Tray', 'config.software.behavior.exit': 'Exit App', 'config.diagnostics.title': 'Logging & Diagnostics', - 'config.diagnostics.description': 'Configure debug output, file logging, and usage statistics with the same controls available in the CPA WebUI.', + 'config.diagnostics.description': 'When enabled, only failed requests are logged; successful requests are not logged. Turn off "**Write logs to files**" promptly when no longer needed to avoid excessive storage use.', 'config.diagnostics.debug.title': 'Debug mode', 'config.diagnostics.debug.description': 'Emit detailed debug-level logs to help diagnose runtime issues.', 'config.diagnostics.commercial.title': 'Commercial mode', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 47bbf44..761afe8 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -645,7 +645,7 @@ export const zhCN = { 'config.software.behavior.minimize': '最小化到托盘', 'config.software.behavior.exit': '退出程序', 'config.diagnostics.title': '日志与诊断', - 'config.diagnostics.description': '配置调试输出、文件日志及用量统计。所有字段与 CPA WebUI 的日志设置保持一致。', + 'config.diagnostics.description': '启用后将记录调用报错的请求,成功的请求不会记录,请及时关闭"**写入日志文件**",避免占用过多存储空间。', 'config.diagnostics.debug.title': '调试模式', 'config.diagnostics.debug.description': '输出更详细的调试级日志,便于定位运行异常。', 'config.diagnostics.commercial.title': '商业模式', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 79a982c..55c0b75 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -9,6 +9,7 @@ export type MessageVariables = Record; export const zhTW: Record = { ...createTraditionalMessages(zhCN), + 'config.diagnostics.description': '啟用後將記錄呼叫出錯的請求,成功的請求不會記錄,請及時關閉「**寫入日誌檔案**」,避免占用過多儲存空間。', 'appUpdate.notes.title': '軟體更新說明', 'appUpdate.notes.expand': '展開', 'appUpdate.notes.collapse': '收合', diff --git a/src/pages/ConfigPanel.tsx b/src/pages/ConfigPanel.tsx index e72f70f..b433dd2 100644 --- a/src/pages/ConfigPanel.tsx +++ b/src/pages/ConfigPanel.tsx @@ -1248,7 +1248,11 @@ export function ConfigPanelPage() { -

{t('config.diagnostics.description')}

+

+ {t('config.diagnostics.description').split('**').map((part, index) => ( + index % 2 === 1 ? {part} : part + ))} +

diff --git a/src/pages/UsageRecordsPage.tsx b/src/pages/UsageRecordsPage.tsx index 9b6c25b..3a58062 100644 --- a/src/pages/UsageRecordsPage.tsx +++ b/src/pages/UsageRecordsPage.tsx @@ -1644,93 +1644,79 @@ const getInitialColumnWidths = (): Record => { function TableTopScrollbar({ tableWrapRef, - totalWidth, - visibleColumnKeys, }: { tableWrapRef: React.RefObject; - totalWidth: number; - visibleColumnKeys: EventColumnKey[]; }) { const scrollbarRef = useRef(null); - const [hasOverflow, setHasOverflow] = useState(false); - const [scrollWidth, setScrollWidth] = useState(totalWidth); + const trackRef = useRef(null); useEffect(() => { const scrollbar = scrollbarRef.current; + const track = trackRef.current; const tableWrap = tableWrapRef.current; - if (!scrollbar || !tableWrap) return; + if (!scrollbar || !track || !tableWrap) return; - let syncing = false; + // Remember the positions we applied, rather than locking a whole frame. + // This ignores delayed programmatic/vertical scroll events without dropping + // newer drag or trackpad input on either surface. + let lastScrollbarLeft = scrollbar.scrollLeft; + let lastTableLeft = tableWrap.scrollLeft; const syncTable = () => { - if (syncing) return; - syncing = true; - tableWrap.scrollLeft = scrollbar.scrollLeft; - window.requestAnimationFrame(() => { - syncing = false; - }); + const left = scrollbar.scrollLeft; + if (left === lastScrollbarLeft) return; + lastScrollbarLeft = left; + tableWrap.scrollLeft = left; + lastTableLeft = tableWrap.scrollLeft; }; const syncScrollbar = () => { - if (syncing) return; - syncing = true; - scrollbar.scrollLeft = tableWrap.scrollLeft; - window.requestAnimationFrame(() => { - syncing = false; - }); + const left = tableWrap.scrollLeft; + if (left === lastTableLeft) return; + lastTableLeft = left; + scrollbar.scrollLeft = left; + lastScrollbarLeft = scrollbar.scrollLeft; }; const updateLayout = () => { const clientWidth = tableWrap.clientWidth; - const wrapScrollWidth = tableWrap.scrollWidth; - const maxScroll = Math.max(0, wrapScrollWidth - clientWidth); - const isOverflowing = maxScroll > 1; + const maxScroll = Math.max(0, tableWrap.scrollWidth - clientWidth); + const left = Math.min(tableWrap.scrollLeft, maxScroll); - setHasOverflow(isOverflowing); - - if (isOverflowing) { - const scrollbarClientWidth = scrollbar.clientWidth || clientWidth; - const targetInnerWidth = scrollbarClientWidth + maxScroll; - setScrollWidth(targetInnerWidth); - - if (tableWrap.scrollLeft > maxScroll) { - tableWrap.scrollLeft = maxScroll; - } - scrollbar.scrollLeft = tableWrap.scrollLeft; - } else { - tableWrap.scrollLeft = 0; - scrollbar.scrollLeft = 0; - setScrollWidth(clientWidth); - } + // Commit the range before the position. A deferred React width update can + // clamp the thumb to its old range and then rewind the table via scroll. + scrollbar.classList.toggle('is-hidden', maxScroll <= 1); + track.style.width = `${(scrollbar.clientWidth || clientWidth) + maxScroll}px`; + tableWrap.scrollLeft = left; + scrollbar.scrollLeft = left; + lastTableLeft = tableWrap.scrollLeft; + lastScrollbarLeft = scrollbar.scrollLeft; }; updateLayout(); - const frameId = window.requestAnimationFrame(updateLayout); - scrollbar.addEventListener('scroll', syncTable, { passive: true }); tableWrap.addEventListener('scroll', syncScrollbar, { passive: true }); - const resizeObserver = new ResizeObserver(() => { - updateLayout(); - }); + const resizeObserver = new ResizeObserver(updateLayout); resizeObserver.observe(tableWrap); resizeObserver.observe(scrollbar); + // Column resizing changes the table's width without resizing its viewport. + if (tableWrap.firstElementChild) resizeObserver.observe(tableWrap.firstElementChild); return () => { - window.cancelAnimationFrame(frameId); scrollbar.removeEventListener('scroll', syncTable); tableWrap.removeEventListener('scroll', syncScrollbar); resizeObserver.disconnect(); }; - }, [tableWrapRef, totalWidth, visibleColumnKeys]); + }, [tableWrapRef]); return (