Skip to content

Commit 374404f

Browse files
committed
feat: 更新列设置,支持列宽自适应和可见性调整
1 parent 1e523f9 commit 374404f

2 files changed

Lines changed: 169 additions & 48 deletions

File tree

app/records/page.tsx

Lines changed: 163 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ type ColumnKey =
7070
type ColumnSetting = {
7171
key: ColumnKey;
7272
visible: boolean;
73-
width: number;
73+
width: number | null;
7474
};
7575

7676
const PAGE_SIZE = 60;
77-
const COLUMN_SETTINGS_STORAGE_KEY = "records-column-settings-v1";
77+
const COLUMN_SETTINGS_STORAGE_KEY = "records-column-settings-v2";
7878
const DEFAULT_COLUMN_ORDER: ColumnKey[] = [
7979
"occurredAt",
8080
"model",
@@ -106,23 +106,37 @@ const COLUMN_LABELS: Record<ColumnKey, string> = {
106106
};
107107

108108
const DEFAULT_COLUMN_WIDTHS: Record<ColumnKey, number> = {
109-
occurredAt: 160,
110-
model: 240,
111-
route: 210,
112-
credentialName: 225,
113-
provider: 150,
114-
totalTokens: 130,
115-
inputTokens: 110,
116-
outputTokens: 110,
117-
reasoningTokens: 110,
118-
cachedTokens: 110,
119-
cost: 130,
120-
isError: 100
109+
occurredAt: 140,
110+
model: 210,
111+
route: 180,
112+
credentialName: 180,
113+
provider: 130,
114+
totalTokens: 115,
115+
inputTokens: 95,
116+
outputTokens: 95,
117+
reasoningTokens: 95,
118+
cachedTokens: 95,
119+
cost: 110,
120+
isError: 90
121121
};
122122

123+
const FIXED_WIDTH_COLUMNS = new Set<ColumnKey>(["model", "route", "credentialName"]);
124+
123125
const COLUMN_MIN_WIDTH = 80;
124126
const COLUMN_MAX_WIDTH = 420;
125127

128+
const NON_FIXED_CONTENT_MIN_WIDTHS: Record<Exclude<ColumnKey, "model" | "route" | "credentialName">, number> = {
129+
occurredAt: 150,
130+
provider: 120,
131+
totalTokens: 120,
132+
inputTokens: 96,
133+
outputTokens: 96,
134+
reasoningTokens: 96,
135+
cachedTokens: 96,
136+
cost: 105,
137+
isError: 88
138+
};
139+
126140
const SORT_FIELD_BY_COLUMN: Partial<Record<ColumnKey, SortField>> = {
127141
occurredAt: "occurredAt",
128142
model: "model",
@@ -139,7 +153,11 @@ const SORT_FIELD_BY_COLUMN: Partial<Record<ColumnKey, SortField>> = {
139153

140154
function normalizeColumnSettings(raw: unknown): ColumnSetting[] {
141155
if (!Array.isArray(raw)) {
142-
return DEFAULT_COLUMN_ORDER.map((key) => ({ key, visible: true, width: DEFAULT_COLUMN_WIDTHS[key] }));
156+
return DEFAULT_COLUMN_ORDER.map((key) => ({
157+
key,
158+
visible: true,
159+
width: FIXED_WIDTH_COLUMNS.has(key) ? DEFAULT_COLUMN_WIDTHS[key] : null
160+
}));
143161
}
144162

145163
const seen = new Set<ColumnKey>();
@@ -155,7 +173,9 @@ function normalizeColumnSettings(raw: unknown): ColumnSetting[] {
155173
const parsedWidth = Number(item?.width);
156174
const width = Number.isFinite(parsedWidth)
157175
? Math.min(COLUMN_MAX_WIDTH, Math.max(COLUMN_MIN_WIDTH, Math.round(parsedWidth)))
158-
: DEFAULT_COLUMN_WIDTHS[key as ColumnKey];
176+
: FIXED_WIDTH_COLUMNS.has(key as ColumnKey)
177+
? DEFAULT_COLUMN_WIDTHS[key as ColumnKey]
178+
: null;
159179
ordered.push({
160180
key: key as ColumnKey,
161181
visible: item?.visible !== false,
@@ -165,7 +185,7 @@ function normalizeColumnSettings(raw: unknown): ColumnSetting[] {
165185

166186
for (const key of DEFAULT_COLUMN_ORDER) {
167187
if (!seen.has(key)) {
168-
ordered.push({ key, visible: true, width: DEFAULT_COLUMN_WIDTHS[key] });
188+
ordered.push({ key, visible: true, width: FIXED_WIDTH_COLUMNS.has(key) ? DEFAULT_COLUMN_WIDTHS[key] : null });
169189
}
170190
}
171191

@@ -290,11 +310,17 @@ export default function RecordsPage() {
290310
const [sortField, setSortField] = useState<SortField>("occurredAt");
291311
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
292312
const [columnSettings, setColumnSettings] = useState<ColumnSetting[]>(
293-
DEFAULT_COLUMN_ORDER.map((key) => ({ key, visible: true, width: DEFAULT_COLUMN_WIDTHS[key] }))
313+
DEFAULT_COLUMN_ORDER.map((key) => ({
314+
key,
315+
visible: true,
316+
width: FIXED_WIDTH_COLUMNS.has(key) ? DEFAULT_COLUMN_WIDTHS[key] : null
317+
}))
294318
);
295319
const [columnSettingsReady, setColumnSettingsReady] = useState(false);
296320
const [columnPanelOpen, setColumnPanelOpen] = useState(false);
321+
const [tableContainerWidth, setTableContainerWidth] = useState(0);
297322
const columnPanelRef = useRef<HTMLDivElement | null>(null);
323+
const tableWrapperRef = useRef<HTMLDivElement | null>(null);
298324
const resizingColumnRef = useRef<{ key: ColumnKey; startX: number; startWidth: number } | null>(null);
299325
const draggingColumnRef = useRef<ColumnKey | null>(null);
300326
const [dragIndicator, setDragIndicator] = useState<{ key: ColumnKey; position: "before" | "after" } | null>(null);
@@ -315,7 +341,9 @@ export default function RecordsPage() {
315341
setColumnSettings((prev) => {
316342
const next = prev.map((item) => (item.key === key ? { ...item, visible: !item.visible } : item));
317343
if (next.filter((item) => item.visible).length === 0) return prev;
318-
return next;
344+
345+
// 每次列显隐变更后,非关键列重置为 auto,触发一次自适应宽度。
346+
return next.map((item) => (FIXED_WIDTH_COLUMNS.has(item.key) ? item : { ...item, width: null }));
319347
});
320348
}, []);
321349

@@ -370,22 +398,84 @@ export default function RecordsPage() {
370398
setColumnSettings((prev) => prev.map((item) => (item.key === key ? { ...item, width: nextWidth } : item)));
371399
}, []);
372400

373-
const getColumnWidth = useCallback(
374-
(key: ColumnKey) => columnSettingMap.get(key)?.width ?? DEFAULT_COLUMN_WIDTHS[key],
401+
const getBaseColumnWidth = useCallback(
402+
(key: ColumnKey) => {
403+
const width = columnSettingMap.get(key)?.width;
404+
if (width !== undefined && width !== null) return width;
405+
return DEFAULT_COLUMN_WIDTHS[key];
406+
},
375407
[columnSettingMap]
376408
);
377409

410+
const adaptiveWidthMap = useMemo(() => {
411+
const result = new Map<ColumnKey, number>();
412+
if (visibleColumns.length === 0) return result;
413+
414+
const fixedColumns = visibleColumns.filter((key) => FIXED_WIDTH_COLUMNS.has(key));
415+
const nonFixedColumns = visibleColumns.filter((key) => !FIXED_WIDTH_COLUMNS.has(key));
416+
417+
let usedWidth = 0;
418+
419+
for (const key of fixedColumns) {
420+
const width = getBaseColumnWidth(key);
421+
result.set(key, width);
422+
usedWidth += width;
423+
}
424+
425+
const manualNonFixed: ColumnKey[] = [];
426+
const autoNonFixed: Array<Exclude<ColumnKey, "model" | "route" | "credentialName">> = [];
427+
428+
for (const key of nonFixedColumns) {
429+
const setting = columnSettingMap.get(key);
430+
if (setting?.width !== null && setting?.width !== undefined) {
431+
manualNonFixed.push(key);
432+
} else {
433+
autoNonFixed.push(key as Exclude<ColumnKey, "model" | "route" | "credentialName">);
434+
}
435+
}
436+
437+
for (const key of manualNonFixed) {
438+
const minRequired = NON_FIXED_CONTENT_MIN_WIDTHS[key as Exclude<ColumnKey, "model" | "route" | "credentialName">];
439+
const width = Math.max(getBaseColumnWidth(key), minRequired);
440+
result.set(key, width);
441+
usedWidth += width;
442+
}
443+
444+
if (autoNonFixed.length === 0) {
445+
return result;
446+
}
447+
448+
const available = tableContainerWidth > 0 ? tableContainerWidth - 8 : 0;
449+
const minTotal = autoNonFixed.reduce((sum, key) => sum + NON_FIXED_CONTENT_MIN_WIDTHS[key], 0);
450+
const remaining = Math.max(0, available - usedWidth);
451+
452+
if (remaining <= minTotal || available <= 0) {
453+
for (const key of autoNonFixed) {
454+
result.set(key, NON_FIXED_CONTENT_MIN_WIDTHS[key]);
455+
}
456+
return result;
457+
}
458+
459+
const extraPerColumn = Math.floor((remaining - minTotal) / autoNonFixed.length);
460+
for (const key of autoNonFixed) {
461+
result.set(key, NON_FIXED_CONTENT_MIN_WIDTHS[key] + extraPerColumn);
462+
}
463+
464+
return result;
465+
}, [visibleColumns, getBaseColumnWidth, columnSettingMap, tableContainerWidth]);
466+
378467
const beginResizeColumn = useCallback(
379468
(key: ColumnKey, event: ReactMouseEvent<HTMLDivElement>) => {
380469
event.preventDefault();
381470
event.stopPropagation();
382471
const current = columnSettingMap.get(key);
383472
if (!current) return;
473+
const currentRenderedWidth = Math.round(event.currentTarget.parentElement?.getBoundingClientRect().width ?? DEFAULT_COLUMN_WIDTHS[key]);
384474

385475
resizingColumnRef.current = {
386476
key,
387477
startX: event.clientX,
388-
startWidth: current.width
478+
startWidth: currentRenderedWidth
389479
};
390480

391481
const onMouseMove = (moveEvent: MouseEvent) => {
@@ -793,7 +883,13 @@ export default function RecordsPage() {
793883
setColumnSettings(normalizeColumnSettings(parsed));
794884
}
795885
} catch {
796-
setColumnSettings(DEFAULT_COLUMN_ORDER.map((key) => ({ key, visible: true, width: DEFAULT_COLUMN_WIDTHS[key] })));
886+
setColumnSettings(
887+
DEFAULT_COLUMN_ORDER.map((key) => ({
888+
key,
889+
visible: true,
890+
width: FIXED_WIDTH_COLUMNS.has(key) ? DEFAULT_COLUMN_WIDTHS[key] : null
891+
}))
892+
);
797893
} finally {
798894
setColumnSettingsReady(true);
799895
}
@@ -816,6 +912,23 @@ export default function RecordsPage() {
816912
return () => document.removeEventListener("mousedown", onDocClick);
817913
}, [columnPanelOpen]);
818914

915+
useEffect(() => {
916+
const element = tableWrapperRef.current;
917+
if (!element) return;
918+
919+
const updateWidth = () => {
920+
setTableContainerWidth(element.clientWidth);
921+
};
922+
923+
updateWidth();
924+
925+
if (typeof ResizeObserver === "undefined") return;
926+
927+
const observer = new ResizeObserver(() => updateWidth());
928+
observer.observe(element);
929+
return () => observer.disconnect();
930+
}, []);
931+
819932
return (
820933
<main className="min-h-screen bg-slate-900 px-6 py-8 text-slate-100">
821934
<header className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
@@ -1038,17 +1151,13 @@ export default function RecordsPage() {
10381151
onDragOver={(event) => onColumnDragOver(column.key, event)}
10391152
onDrop={(event) => onColumnDrop(column.key, event)}
10401153
onDragEnd={onColumnDragEnd}
1041-
className={`flex items-center justify-between rounded-lg border px-2 py-1.5 ${
1042-
dragIndicator?.key === column.key
1043-
? "border-indigo-400/70 bg-indigo-500/10"
1044-
: "border-slate-800 bg-slate-950/60"
1045-
} relative`}
1154+
className="relative flex items-center justify-between rounded-lg border border-slate-800 bg-slate-950/60 px-2 py-1.5"
10461155
>
10471156
{dragIndicator?.key === column.key && dragIndicator.position === "before" ? (
1048-
<span className="pointer-events-none absolute left-2 right-2 -top-1.5 h-px bg-indigo-400" />
1157+
<span className="pointer-events-none absolute left-2 right-2 top-0 h-px -translate-y-1/2 bg-indigo-300/90 shadow-[0_0_6px_rgba(129,140,248,0.45)]" />
10491158
) : null}
10501159
{dragIndicator?.key === column.key && dragIndicator.position === "after" ? (
1051-
<span className="pointer-events-none absolute left-2 right-2 -bottom-1.5 h-px bg-indigo-400" />
1160+
<span className="pointer-events-none absolute left-2 right-2 bottom-0 h-px translate-y-1/2 bg-indigo-300/90 shadow-[0_0_6px_rgba(129,140,248,0.45)]" />
10521161
) : null}
10531162
<label className="inline-flex items-center gap-2 text-sm text-slate-200">
10541163
<span className="cursor-grab text-slate-500 active:cursor-grabbing" title="按住拖拽排序">
@@ -1062,7 +1171,7 @@ export default function RecordsPage() {
10621171
className="h-4 w-4 rounded border-slate-600 bg-slate-800 text-indigo-500"
10631172
/>
10641173
<span>{COLUMN_LABELS[column.key]}</span>
1065-
<span className="text-xs text-slate-500">{column.width}px</span>
1174+
<span className="text-xs text-slate-500">{column.width ? `${column.width}px` : "auto"}</span>
10661175
</label>
10671176
</div>
10681177
))}
@@ -1076,27 +1185,28 @@ export default function RecordsPage() {
10761185

10771186
<section className={`mt-5 rounded-2xl bg-slate-800/40 p-4 shadow-sm ring-1 ring-slate-700 ${loadingEmpty ? "min-h-[100vh]" : ""}`}>
10781187
{!loadingEmpty ? (
1079-
<div className="overflow-auto">
1080-
<table className="min-w-[1460px] w-[99%] mx-auto table-fixed border-separate border-spacing-y-2">
1188+
<div ref={tableWrapperRef} className="overflow-auto">
1189+
<table className="min-w-full w-full table-fixed border-separate border-spacing-y-2">
10811190
<thead className="sticky top-0 z-10">
10821191
<tr className="text-left text-[13px] uppercase tracking-wide text-slate-400">
10831192
{visibleColumns.map((columnKey) => {
1084-
const width = getColumnWidth(columnKey);
1193+
const width = adaptiveWidthMap.get(columnKey) ?? DEFAULT_COLUMN_WIDTHS[columnKey];
10851194
return (
10861195
<th
10871196
key={columnKey}
1088-
className="relative px-3 py-2"
1089-
style={{ width: `${width}px`, minWidth: `${width}px` }}
1197+
className="group/col relative px-3 py-2"
1198+
style={width ? { width: `${width}px`, minWidth: `${width}px` } : undefined}
10901199
>
10911200
{renderHeaderByColumn(columnKey)}
10921201
<div
10931202
role="separator"
10941203
aria-orientation="vertical"
10951204
onMouseDown={(event) => beginResizeColumn(columnKey, event)}
1096-
className="group absolute right-0 top-0 h-full w-2 cursor-col-resize select-none"
1205+
className="group absolute right-0 top-0 h-full w-3 cursor-col-resize select-none opacity-0 pointer-events-none transition-opacity duration-250 group-hover/col:opacity-100 group-hover/col:pointer-events-auto"
10971206
title="拖拽调整列宽"
10981207
>
1099-
<span className="absolute right-[3px] top-1 bottom-1 w-px rounded bg-slate-500/70 transition-colors group-hover:bg-indigo-400/80" />
1208+
<span className="absolute right-[4px] top-2 bottom-2 w-px rounded bg-gradient-to-b from-transparent via-slate-400/35 to-transparent opacity-70 transition-all duration-150 group-hover:via-indigo-300/60 group-hover:opacity-100" />
1209+
<span className="pointer-events-none absolute right-[3px] top-1/2 h-3 w-[3px] -translate-y-1/2 rounded-full bg-slate-400/35 transition-colors duration-150 group-hover:bg-indigo-300/65" />
11001210
</div>
11011211
</th>
11021212
);
@@ -1107,15 +1217,25 @@ export default function RecordsPage() {
11071217
{records.map((row) => (
11081218
<tr
11091219
key={row.id}
1110-
className="rounded-lg bg-slate-900/70 text-slate-100 shadow-sm ring-1 ring-slate-800 transition hover:ring-1.5 hover:ring-indigo-400/40 hover:shadow-[0_0_24px_rgba(99,102,241,0.18)] h-13"
1220+
className="group h-13 rounded-lg bg-slate-900/70 text-slate-100 shadow-sm ring-1 ring-slate-800 transition hover:shadow-[0_0_24px_rgba(99,102,241,0.18)]"
11111221
>
1112-
{visibleColumns.map((columnKey) => {
1113-
const width = getColumnWidth(columnKey);
1222+
{visibleColumns.map((columnKey, index) => {
1223+
const width = adaptiveWidthMap.get(columnKey) ?? DEFAULT_COLUMN_WIDTHS[columnKey];
1224+
const isFirst = index === 0;
1225+
const isLast = index === visibleColumns.length - 1;
11141226
return (
11151227
<td
11161228
key={`${row.id}-${columnKey}`}
1117-
className="px-3 py-3 whitespace-nowrap"
1118-
style={{ width: `${width}px`, minWidth: `${width}px` }}
1229+
className={`whitespace-nowrap border-y border-transparent px-3 py-3 transition group-hover:border-indigo-400/40 ${
1230+
isFirst
1231+
? "rounded-l-lg border-l border-l-transparent group-hover:border-l-indigo-400/40 group-hover:shadow-[-10px_0_16px_-10px_rgba(99,102,241,0.48)]"
1232+
: ""
1233+
} ${
1234+
isLast
1235+
? "rounded-r-lg border-r border-r-transparent group-hover:border-r-indigo-400/40 group-hover:shadow-[10px_0_16px_-10px_rgba(99,102,241,0.48)]"
1236+
: ""
1237+
}`}
1238+
style={width ? { width: `${width}px`, minWidth: `${width}px` } : undefined}
11191239
>
11201240
{renderCellByColumn(columnKey, row)}
11211241
</td>

lib/queries/records.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export async function getUsageRecords(input: {
161161
}
162162

163163
if (input.source) {
164-
whereParts.push(eq(usageRecords.source, input.source));
164+
whereParts.push(sql`${CREDENTIAL_NAME_EXPR} = ${input.source}`);
165165
}
166166

167167
const sortExpr = (() => {
@@ -281,17 +281,18 @@ export async function getUsageRecords(input: {
281281
.orderBy(usageRecords.route)
282282
.limit(200),
283283
db
284-
.select({ source: usageRecords.source })
284+
.select({ source: CREDENTIAL_NAME_EXPR })
285285
.from(usageRecords)
286+
.leftJoin(authFileMappings, eq(usageRecords.authIndex, authFileMappings.authId))
286287
.where(where)
287-
.groupBy(usageRecords.source)
288-
.orderBy(usageRecords.source)
288+
.groupBy(CREDENTIAL_NAME_EXPR)
289+
.orderBy(CREDENTIAL_NAME_EXPR)
289290
.limit(200),
290291
]);
291292
filters = {
292293
models: modelRows.map((row) => row.model),
293294
routes: routeRows.map((row) => row.route),
294-
sources: sourceRows.map((row) => row.source).filter(Boolean)
295+
sources: sourceRows.map((row) => row.source).filter((name): name is string => Boolean(name) && name !== "-")
295296
};
296297
}
297298

0 commit comments

Comments
 (0)