feat(usage): add customizable fixed row height settings

- Add a display setting to enable or disable fixed record heights
- Allow users to adjust row height with a draggable slider
- Persist row height preferences across sessions
- Improve display settings layout and translations
This commit is contained in:
lzt404 committed 2026-10-06 13:41:35 +08:00
1 parent 8d3912f81b
commit 1d64a4535f
6 files changed
+211 -15

No files matched your search

+5 -1
View File
@@ -775,7 +775,7 @@ export const jaOverrides = {
'usage.analysis.shareLabel': '割合',
'usage.events.title': 'リクエストイベントログ',
'usage.events.description': '選択した期間のリクエストイベントを絞り込んで確認します。',
'usage.events.columns': '列',
'usage.events.columns': '表示する列',
'usage.events.exportPage': '絞り込み結果を CSV 出力',
'usage.events.exportDescription': '現在の絞り込み結果の全リクエスト項目を CSV ファイルに出力し、保存先を選択します',
'usage.events.exportDialogTitle': 'CSV の保存先を選択',
@@ -789,6 +789,10 @@ export const jaOverrides = {
'usage.events.dragHint': 'ヘッダーの境界をドラッグして列幅を調整',
'usage.events.columnSettings': '表示設定',
'usage.events.columnSettingsDescription': 'リクエスト詳細に表示する列を選択します。',
'usage.events.rowHeight.title': 'レコードの行高を固定',
'usage.events.rowHeight.description': 'すべてのレコードを同じ高さにします。オフにすると内容に合わせて調整します。',
'usage.events.rowHeight.fixed': '行高',
'usage.events.rowHeight.dragHint': 'スライダーをドラッグして調整',
'usage.events.columnsSelected': '{total} 列中 {selected} 列を選択',
'usage.events.selectAllColumns': 'すべて選択',
'usage.events.applyColumns': '適用',
+5 -1
View File
@@ -763,7 +763,7 @@ export const en: Record<MessageKey, string> = {
'usage.analysis.shareLabel': 'Share',
'usage.events.title': 'Request Event Log',
'usage.events.description': 'Filter and inspect request events in the selected time range.',
'usage.events.columns': 'Columns',
'usage.events.columns': 'Show columns',
'usage.events.exportPage': 'Export Filtered CSV',
'usage.events.exportDescription': 'Export all request fields matching the current filters and choose where to save the CSV',
'usage.events.exportDialogTitle': 'Choose where to save the CSV',
@@ -777,6 +777,10 @@ export const en: Record<MessageKey, string> = {
'usage.events.dragHint': 'Drag column header borders to resize',
'usage.events.columnSettings': 'Display Settings',
'usage.events.columnSettingsDescription': 'Choose which columns appear in request details.',
'usage.events.rowHeight.title': 'Fixed row height',
'usage.events.rowHeight.description': 'Use one height for every record; turn it off to fit content automatically.',
'usage.events.rowHeight.fixed': 'Row height',
'usage.events.rowHeight.dragHint': 'Drag the slider to adjust',
'usage.events.columnsSelected': '{selected} of {total} columns selected',
'usage.events.selectAllColumns': 'Select All',
'usage.events.applyColumns': 'Apply',
+5 -1
View File
@@ -762,7 +762,7 @@ export const zhCN = {
'usage.analysis.shareLabel': '占比',
'usage.events.title': '请求事件日志',
'usage.events.description': '筛选并查看所选时间范围内的请求事件。',
'usage.events.columns': '列',
'usage.events.columns': '显示列',
'usage.events.exportPage': '导出筛选结果 CSV',
'usage.events.exportDescription': '导出当前筛选结果中的全部请求字段,并选择保存位置',
'usage.events.exportDialogTitle': '选择 CSV 保存位置',
@@ -776,6 +776,10 @@ export const zhCN = {
'usage.events.dragHint': '可拖动表头边线调整每列宽度',
'usage.events.columnSettings': '显示设置',
'usage.events.columnSettingsDescription': '选择请求明细中需要显示的列。',
'usage.events.rowHeight.title': '固定记录行高',
'usage.events.rowHeight.description': '开启后每条记录使用统一高度;关闭时根据内容自动适应。',
'usage.events.rowHeight.fixed': '行高',
'usage.events.rowHeight.dragHint': '拖动滑块调整高度',
'usage.events.columnsSelected': '已选择 {selected} / {total} 列',
'usage.events.selectAllColumns': '选择全部',
'usage.events.applyColumns': '应用',
+16 -4
View File
@@ -50,16 +50,20 @@
background: var(--usage-log-surface);
color: var(--usage-log-muted);
box-shadow: none;
font-size: 11px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
cursor: pointer;
transition: background .15s ease, border-color .15s ease;
}
.usage-request-log .usage-col-reset-btn.icon-only {
width: 32px;
padding: 0;
.usage-request-log .usage-col-settings-btn svg {
width: 16px;
height: 16px;
}
.usage-request-log .usage-col-settings-btn {
color: var(--usage-log-text);
}
.usage-request-log .usage-col-settings-btn:hover,
@@ -131,6 +135,14 @@
line-height: 1.45;
}
.usage-request-log.usage-row-height-auto .usage-events-table td {
height: auto;
}
.usage-request-log.usage-row-height-fixed .usage-events-table td {
height: var(--usage-row-height, 68px);
}
.usage-request-log .usage-events-table tbody tr:hover td {
background: var(--usage-log-hover) !important;
}
+75 -8
View File
@@ -1,8 +1,8 @@
import { usagePreferences } from '../services/usagePreferences';
import { useEffect, useRef, useState, type KeyboardEvent } from 'react';
import { useEffect, useRef, useState, type KeyboardEvent, type CSSProperties } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { save } from '@tauri-apps/plugin-dialog';
import { ArrowDown, ArrowUp, Brain, ChevronLeft, ChevronRight, Columns3Cog, Database, DatabaseZap, Download, RotateCcw, TriangleAlert, X } from 'lucide-react';
import { ArrowDown, ArrowUp, Brain, ChevronLeft, ChevronRight, Columns3, Database, DatabaseZap, Download, RotateCcw, TriangleAlert, X } from 'lucide-react';
import { getCurrentLocale, useI18n } from '../i18n';
import type { MessageKey } from '../i18n/resources';
import { formatCacheReadRate, formatGenerationSpeed } from '../services/usageMetrics';
@@ -116,9 +116,26 @@ const EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v4';
const LEGACY_EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v3';
const EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v6';
const LEGACY_EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v5';
const EVENT_ROW_HEIGHT_ENABLED_STORAGE_KEY = 'cpa-gui.usage-events-row-height-enabled.v1';
const EVENT_ROW_HEIGHT_STORAGE_KEY = 'cpa-gui.usage-events-row-height.v1';
const DEFAULT_EVENT_ROW_HEIGHT = 68;
const MIN_EVENT_ROW_HEIGHT = 48;
const MAX_EVENT_ROW_HEIGHT = 140;
const getAllEventColumnKeys = () => EVENT_COLUMNS.map((column) => column.key);
const clampEventRowHeight = (value: number) => Math.min(
MAX_EVENT_ROW_HEIGHT,
Math.max(MIN_EVENT_ROW_HEIGHT, Math.round(value)),
);
const getInitialRowHeightEnabled = () => usagePreferences.getItem(EVENT_ROW_HEIGHT_ENABLED_STORAGE_KEY) === 'true';
const getInitialRowHeight = () => {
const value = Number(usagePreferences.getItem(EVENT_ROW_HEIGHT_STORAGE_KEY));
return Number.isFinite(value) ? clampEventRowHeight(value) : DEFAULT_EVENT_ROW_HEIGHT;
};
export const getInitialVisibleColumns = (): EventColumnKey[] => {
try {
const currentRaw = usagePreferences.getItem(EVENT_VISIBLE_COLS_STORAGE_KEY);
@@ -480,6 +497,8 @@ export function EventsView({
const [columnSettingsOpen, setColumnSettingsOpen] = useState(false);
const [draftVisibleColumnKeys, setDraftVisibleColumnKeys] = useState<EventColumnKey[]>(visibleColumnKeys);
const [resizingCol, setResizingCol] = useState<EventColumnKey | null>(null);
const [fixedRowHeight, setFixedRowHeight] = useState(getInitialRowHeightEnabled);
const [rowHeight, setRowHeight] = useState(getInitialRowHeight);
const [exporting, setExporting] = useState(false);
const columnDialogRef = useDialogFocusTrap<HTMLElement>({
@@ -543,6 +562,17 @@ export function EventsView({
setDraftVisibleColumnKeys(getAllEventColumnKeys());
};
const updateFixedRowHeight = (enabled: boolean) => {
setFixedRowHeight(enabled);
usagePreferences.setItem(EVENT_ROW_HEIGHT_ENABLED_STORAGE_KEY, String(enabled));
};
const updateRowHeight = (value: number) => {
const next = clampEventRowHeight(value);
setRowHeight(next);
usagePreferences.setItem(EVENT_ROW_HEIGHT_STORAGE_KEY, String(next));
};
const resetSingleColumn = (key: EventColumnKey, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -686,12 +716,12 @@ export function EventsView({
};
return (
<section className={`panel usage-events-panel usage-request-log${isCompactDefault ? ' usage-events-compact' : ''}`} aria-label={t('usage.events.title')} aria-busy={loading}>
<section className={`panel usage-events-panel usage-request-log${isCompactDefault ? ' usage-events-compact' : ''}${fixedRowHeight ? ' usage-row-height-fixed' : ' usage-row-height-auto'}`} aria-label={t('usage.events.title')} aria-busy={loading}>
{loading && events.items.length === 0 ? <div className="usage-empty" role="status"><Database size={20} aria-hidden="true" /><span>{t('usage.loading')}</span></div> : events.items.length ? (
<div ref={tableWrapRef} className="usage-table-wrap" tabIndex={0} role="region" aria-label={t('usage.events.title')}>
<table
className="usage-events-table"
style={{ width: `${totalTableWidth}px` }}
style={{ width: `${totalTableWidth}px`, '--usage-row-height': `${rowHeight}px` } as CSSProperties}
>
<colgroup>
{visibleColumns.map((col) => (
@@ -769,19 +799,22 @@ export function EventsView({
onClick={openColumnSettings}
title={t('usage.events.columnSettings')}
aria-label={t('usage.events.columnSettings')}
aria-haspopup="dialog"
aria-expanded={columnSettingsOpen}
>
<Columns3Cog size={14} aria-hidden="true" />
<Columns3 size={16} aria-hidden="true" />
<span>{t('usage.events.columns')}</span>
</button>
{isCustomized ? (
<button
type="button"
className="usage-col-reset-btn icon-only"
className="usage-col-reset-btn"
onClick={resetAllWidths}
title={t('usage.events.resetColumns')}
aria-label={t('usage.events.resetColumns')}
>
<RotateCcw size={13} aria-hidden="true" />
<RotateCcw size={14} aria-hidden="true" />
<span>{t('usage.events.resetColumns')}</span>
</button>
) : null}
<button type="button" className="usage-events-export-btn" disabled={loading || exporting || events.total === 0} onClick={() => void exportFilteredEvents()} title={t('usage.events.exportDescription')}>
@@ -820,7 +853,7 @@ export function EventsView({
>
<div className="usage-column-dialog-heading">
<div>
<Columns3Cog size={19} aria-hidden="true" />
<Columns3 size={19} aria-hidden="true" />
<h2 id="usage-column-dialog-title">{t('usage.events.columnSettings')}</h2>
</div>
<button
@@ -836,6 +869,40 @@ export function EventsView({
<p className="usage-column-dialog-description">
{t('usage.events.columnSettingsDescription')}
</p>
<section className="usage-row-height-settings" aria-labelledby="usage-row-height-title">
<div className="usage-row-height-heading">
<div>
<strong id="usage-row-height-title">{t('usage.events.rowHeight.title')}</strong>
<span>{t('usage.events.rowHeight.description')}</span>
</div>
<button
type="button"
className={`usage-settings-switch${fixedRowHeight ? ' active' : ''}`}
role="switch"
aria-checked={fixedRowHeight}
onClick={() => updateFixedRowHeight(!fixedRowHeight)}
>
<span aria-hidden="true" />
<span>{fixedRowHeight ? t('common.enabled') : t('common.disabled')}</span>
</button>
</div>
<div className="usage-row-height-control">
<label htmlFor="usage-row-height-range">{t('usage.events.rowHeight.fixed')}</label>
<input
id="usage-row-height-range"
type="range"
min={MIN_EVENT_ROW_HEIGHT}
max={MAX_EVENT_ROW_HEIGHT}
step="1"
value={rowHeight}
disabled={!fixedRowHeight}
onChange={(event) => updateRowHeight(Number(event.currentTarget.value))}
aria-label={t('usage.events.rowHeight.fixed')}
/>
<output htmlFor="usage-row-height-range">{rowHeight}px</output>
<span className="usage-row-height-hint">{t('usage.events.rowHeight.dragHint')}</span>
</div>
</section>
<div className="usage-column-options">
{EVENT_COLUMNS.map((column) => {
const checked = draftVisibleColumnKeys.includes(column.key);
+105
View File
@@ -9622,6 +9622,97 @@ textarea:focus-visible {
line-height: 1.5;
}
.usage-row-height-settings {
display: grid;
gap: 12px;
margin: 0 0 16px;
padding: 14px 16px;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--bg-tertiary);
}
.usage-row-height-heading,
.usage-row-height-control {
display: flex;
align-items: center;
gap: 12px;
}
.usage-row-height-heading {
justify-content: space-between;
}
.usage-row-height-heading > div {
display: grid;
gap: 3px;
}
.usage-row-height-heading strong,
.usage-row-height-control label {
color: var(--text-primary);
font-size: var(--font-size-body);
font-weight: var(--font-weight-semibold);
}
.usage-row-height-heading span,
.usage-row-height-hint {
color: var(--text-secondary);
font-size: var(--font-size-caption);
}
.usage-settings-switch {
display: inline-flex;
align-items: center;
gap: 7px;
border: 0;
background: transparent;
color: var(--text-secondary);
font-size: var(--font-size-caption);
cursor: pointer;
}
.usage-settings-switch > span:first-child {
position: relative;
width: 34px;
height: 20px;
border-radius: 999px;
background: var(--border-primary);
transition: background .15s ease;
}
.usage-settings-switch > span:first-child::after {
position: absolute;
top: 3px;
left: 3px;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
content: '';
transition: transform .15s ease;
}
.usage-settings-switch.active > span:first-child { background: var(--ui-accent); }
.usage-settings-switch.active > span:first-child::after { transform: translateX(14px); }
.usage-row-height-control {
flex-wrap: wrap;
}
.usage-row-height-control input[type='range'] {
flex: 1 1 180px;
accent-color: var(--ui-accent);
}
.usage-row-height-control output {
min-width: 42px;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
font-size: var(--font-size-caption);
text-align: right;
}
.usage-column-options {
display: grid;
min-height: 0;
@@ -10017,6 +10108,20 @@ body.table-col-resizing * {
grid-template-columns: 1fr;
}
.usage-row-height-heading {
align-items: flex-start;
flex-direction: column;
}
.usage-row-height-control {
align-items: flex-start;
flex-direction: column;
}
.usage-row-height-control input[type='range'] {
width: 100%;
}
.usage-column-dialog-footer {
align-items: stretch;
flex-direction: column;