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
This commit is contained in:
lzt404 committed 2026-09-19 20:42:30 +08:00
1 parent bac67c1524
commit 327b824ef5
9 files changed
+230 -56

No files matched your search

+1 -1
View File
@@ -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': '商用モード',
+1 -1
View File
@@ -646,7 +646,7 @@ export const en: Record<MessageKey, string> = {
'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',
+1 -1
View File
@@ -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': '商业模式',
+1
View File
@@ -9,6 +9,7 @@ export type MessageVariables = Record<string, string | number>;
export const zhTW: Record<MessageKey, string> = {
...createTraditionalMessages(zhCN),
'config.diagnostics.description': '啟用後將記錄呼叫出錯的請求,成功的請求不會記錄,請及時關閉「**寫入日誌檔案**」,避免占用過多儲存空間。',
'appUpdate.notes.title': '軟體更新說明',
'appUpdate.notes.expand': '展開',
'appUpdate.notes.collapse': '收合',
+5 -1
View File
@@ -1248,7 +1248,11 @@ export function ConfigPanelPage() {
</div>
</div>
<p className="config-diagnostics-intro">{t('config.diagnostics.description')}</p>
<p className="config-diagnostics-intro">
{t('config.diagnostics.description').split('**').map((part, index) => (
index % 2 === 1 ? <strong key={index}>{part}</strong> : part
))}
</p>
<div className="config-diagnostics-toggle-grid">
<div className="config-diagnostics-setting">
+34 -50
View File
@@ -1644,93 +1644,79 @@ const getInitialColumnWidths = (): Record<EventColumnKey, number> => {
function TableTopScrollbar({
tableWrapRef,
totalWidth,
visibleColumnKeys,
}: {
tableWrapRef: React.RefObject<HTMLDivElement | null>;
totalWidth: number;
visibleColumnKeys: EventColumnKey[];
}) {
const scrollbarRef = useRef<HTMLDivElement | null>(null);
const [hasOverflow, setHasOverflow] = useState(false);
const [scrollWidth, setScrollWidth] = useState(totalWidth);
const trackRef = useRef<HTMLDivElement | null>(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 (
<div
ref={scrollbarRef}
className={`usage-table-top-scrollbar ${hasOverflow ? '' : 'is-hidden'}`}
className="usage-table-top-scrollbar"
aria-hidden="true"
>
<div style={{ width: `${scrollWidth}px`, height: '1px' }} />
<div ref={trackRef} style={{ height: '1px' }} />
</div>
);
}
@@ -2080,8 +2066,6 @@ function EventsView({
{events.items.length > 0 ? (
<TableTopScrollbar
tableWrapRef={tableWrapRef}
totalWidth={totalTableWidth}
visibleColumnKeys={visibleColumnKeys}
/>
) : null}
+5
View File
@@ -9343,6 +9343,11 @@ textarea:focus-visible {
transition: background-color 0.15s ease, height 0.15s ease, width 0.15s ease, opacity 0.15s ease;
}
/* Keep the final resize handle from adding overflow when all columns fit. */
.usage-events-table th:last-child .usage-col-resizer {
right: 0;
}
.usage-events-table th:hover .usage-col-resizer::after {
width: 2px;
height: 78%;