From 51f87976b47fa6e43dd03d52f5ea3a72b67a96df Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 17 Jul 2026 08:39:16 +0800 Subject: [PATCH] fix(grid): keep column names fully visible --- apps/desktop/src/components/grid/DataGrid.vue | 14 +++++++ .../__tests__/useDataGridColumnResize.spec.ts | 40 +++++++++++++++---- .../composables/useDataGridColumnResize.ts | 8 +++- .../src/lib/dataGrid/dataGridColumnWidth.ts | 13 +++--- .../app-tests/dataGridColumnWidth.test.ts | 24 +++++++++++ 5 files changed, 84 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 098683e033..c6ebbf30ef 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -1774,12 +1774,26 @@ function scrollToColumnIndex(columnIndex: number) { // --- Column resize composable --- const columnWidthDensity = computed(() => settingsStore.editorSettings.columnWidthDensity); +let columnHeaderMeasureContext: CanvasRenderingContext2D | null | undefined; + +function measureColumnHeaderText(text: string): number | undefined { + if (typeof document === "undefined") return undefined; + if (columnHeaderMeasureContext === undefined) columnHeaderMeasureContext = document.createElement("canvas").getContext("2d"); + if (!columnHeaderMeasureContext) return undefined; + // Match the rendered semibold header font instead of estimating proportional glyphs by character count. + const fontFamily = getComputedStyle(gridRef.value ?? document.body).fontFamily || "sans-serif"; + columnHeaderMeasureContext.font = `600 ${tableFontSize.value}px ${fontFamily}`; + return Math.ceil(columnHeaderMeasureContext.measureText(text).width); +} + const { initColumnWidths, onResizeStart, autoFitColumn, renderedColumnWidths, totalWidth, columnVars, getIsResizing } = useDataGridColumnResize({ columns: visibleColumns, sourceRows: computed(() => props.result.rows), columnIndexes: visibleColumnIndexes, density: columnWidthDensity, compactColumnHeaderActions, + measureHeaderText: measureColumnHeaderText, + headerMeasurementKey: tableFontSize, }); const gridStyle = computed(() => ({ ...columnVars.value, diff --git a/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts index 8e274b46ee..4611c3f0fa 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts @@ -3,20 +3,28 @@ import { describe, expect, it } from "vitest"; import { DATA_GRID_COL_AUTO_FIT_MAX_WIDTH, DATA_GRID_COL_MIN_WIDTH } from "@/lib/dataGrid/dataGridColumnWidth"; import { DATA_GRID_ROW_NUM_WIDTH, resizeDataGridColumnWidth, useDataGridColumnResize } from "@/composables/useDataGridColumnResize"; -function createResizeState(options: { columns: string[]; rows: Array>; columnIndexes?: number[]; density?: "compact" | "standard" | "comfortable"; compactColumnHeaderActions?: boolean }) { +function createResizeState(options: { columns: string[]; rows: Array>; columnIndexes?: number[]; density?: "compact" | "standard" | "comfortable"; compactColumnHeaderActions?: boolean; headerTextWidth?: number }) { const compact = ref(options.compactColumnHeaderActions ?? true); + const headerTextWidth = ref(options.headerTextWidth); + const headerMeasurementKey = ref(0); const state = useDataGridColumnResize({ columns: computed(() => options.columns), sourceRows: computed(() => options.rows), columnIndexes: computed(() => options.columnIndexes ?? options.columns.map((_, index) => index)), density: ref(options.density ?? "standard"), compactColumnHeaderActions: computed(() => compact.value), + measureHeaderText: () => headerTextWidth.value, + headerMeasurementKey, }); return { ...state, setCompact(v: boolean) { compact.value = v; }, + setHeaderTextWidth(width: number) { + headerTextWidth.value = width; + headerMeasurementKey.value += 1; + }, }; } @@ -109,7 +117,25 @@ describe("useDataGridColumnResize", () => { expect(widthNonCompact).toBeGreaterThan(widthCompact); }); - it("compact mode bases width on field name only, capping long names", () => { + it("recalculates column widths when rendered header font metrics change", async () => { + const state = createResizeState({ + columns: ["AMOUNT"], + rows: [[1]], + density: "comfortable", + compactColumnHeaderActions: true, + headerTextWidth: 54, + }); + + state.initColumnWidths(); + expect(state.columnWidths.value[0]).toBe(113); + + state.setHeaderTextWidth(70); + await nextTick(); + + expect(state.columnWidths.value[0]).toBe(129); + }); + + it("compact mode bases width on field name without truncating long names", () => { // 短字段名:列宽=字段名宽度,值不参与撑宽 const short = createResizeState({ columns: ["id"], @@ -132,16 +158,16 @@ describe("useDataGridColumnResize", () => { // 9×7+45=108 expect(mid.columnWidths.value[0]).toBe(108); - // 过长字段名:截断到边界 - const capped = createResizeState({ + // 过长字段名:字段名仍完整展示,不受内容宽度上限影响 + const longName = createResizeState({ columns: ["x".repeat(70)], rows: [["a"]], density: "compact", compactColumnHeaderActions: true, }); - capped.initColumnWidths(); - // 70×7+45=535 > maxWidth 480 → 480 - expect(capped.columnWidths.value[0]).toBe(480); + longName.initColumnWidths(); + // 70×7+45=535,紧凑模式只限制内容,不限制字段名 + expect(longName.columnWidths.value[0]).toBe(535); }); it("comfortable mode uses percentile to ignore outlier values", () => { diff --git a/apps/desktop/src/composables/useDataGridColumnResize.ts b/apps/desktop/src/composables/useDataGridColumnResize.ts index 67a7bd132f..008760bad4 100644 --- a/apps/desktop/src/composables/useDataGridColumnResize.ts +++ b/apps/desktop/src/composables/useDataGridColumnResize.ts @@ -16,10 +16,12 @@ export interface UseDataGridColumnResizeOptions { columnIndexes: ComputedRef; density: Ref; compactColumnHeaderActions: ComputedRef; + measureHeaderText?: (text: string) => number | undefined; + headerMeasurementKey?: Ref; } export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) { - const { columns, sourceRows, columnIndexes, density, compactColumnHeaderActions } = options; + const { columns, sourceRows, columnIndexes, density, compactColumnHeaderActions, measureHeaderText } = options; const columnWidths = ref([]); let isResizing = false; @@ -55,6 +57,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) sampleValues: sampleColumnValues(colIdx), density: density.value, compactColumnHeaderActions: compactColumnHeaderActions.value, + headerTextWidth: measureHeaderText?.(colName), }); }); } @@ -114,6 +117,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) density: density.value, compactColumnHeaderActions: compactColumnHeaderActions.value, includeValues: true, + headerTextWidth: measureHeaderText?.(colName), }); } @@ -139,7 +143,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) () => columnIndexes.value.join("\0"), () => initColumnWidths(), ); - watch([density, compactColumnHeaderActions], () => initColumnWidths(true)); + watch([density, compactColumnHeaderActions, () => options.headerMeasurementKey?.value], () => initColumnWidths(true)); return { columnWidths, diff --git a/apps/desktop/src/lib/dataGrid/dataGridColumnWidth.ts b/apps/desktop/src/lib/dataGrid/dataGridColumnWidth.ts index 2179f5b432..424abe7602 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridColumnWidth.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridColumnWidth.ts @@ -91,17 +91,18 @@ export function percentileValue(values: number[], percentile: number): number { return sorted[idx]; } -export function calculateDataGridColumnWidth(options: { columnName: string; sampleValues: readonly CellValue[]; maxWidth?: number; valueTextLimit?: number; density?: ColumnWidthDensity; compactColumnHeaderActions?: boolean; includeValues?: boolean }): number { +export function calculateDataGridColumnWidth(options: { columnName: string; sampleValues: readonly CellValue[]; maxWidth?: number; valueTextLimit?: number; density?: ColumnWidthDensity; compactColumnHeaderActions?: boolean; includeValues?: boolean; headerTextWidth?: number }): number { const density = options.density ?? "standard"; const preset = COLUMN_WIDTH_DENSITY_PRESETS[density]; const maxAllowedWidth = options.maxWidth ?? preset.maxWidth; const valueTextLimit = options.valueTextLimit ?? preset.valueTextLimit; const headerControl = options.compactColumnHeaderActions ? preset.headerControlWidthCompact : preset.headerControlWidth; - const headerWidth = estimateTextWidth(options.columnName, headerControl, preset.charWidth); + const headerTextWidth = options.headerTextWidth ?? estimateTextWidth(options.columnName, 0, preset.charWidth); + const headerWidth = headerTextWidth + headerControl; - // Compact defaults to header-only sizing, while explicit auto-fit still measures values. + // Density limits cell content, never the column name and its header controls. if (density === "compact" && !options.includeValues) { - return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.min(maxAllowedWidth, Math.round(headerWidth))); + return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.round(headerWidth)); } const valueWidths: number[] = []; @@ -113,7 +114,7 @@ export function calculateDataGridColumnWidth(options: { columnName: string; samp } const valueWidth = percentileValue(valueWidths, preset.valueWidthPercentile); - const maxContentWidth = Math.max(headerWidth, valueWidth); + const maxContentWidth = Math.max(headerWidth, Math.min(maxAllowedWidth, valueWidth)); - return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.min(maxAllowedWidth, Math.round(maxContentWidth))); + return Math.max(DATA_GRID_COL_MIN_WIDTH, Math.round(maxContentWidth)); } diff --git a/packages/app-tests/dataGridColumnWidth.test.ts b/packages/app-tests/dataGridColumnWidth.test.ts index b76c8f31f5..6e0d198621 100644 --- a/packages/app-tests/dataGridColumnWidth.test.ts +++ b/packages/app-tests/dataGridColumnWidth.test.ts @@ -32,3 +32,27 @@ test("auto-fit data grid column width stays bounded for very long values", () => expect(width).toBe(DATA_GRID_COL_AUTO_FIT_MAX_WIDTH); }); + +test("data grid column width uses measured header text as an uncapped minimum", () => { + const width = calculateDataGridColumnWidth({ + columnName: "AMOUNT", + sampleValues: ["1"], + density: "comfortable", + compactColumnHeaderActions: true, + headerTextWidth: 54, + }); + + expect(width).toBe(113); +}); + +test("compact data grid columns keep long field names complete", () => { + const width = calculateDataGridColumnWidth({ + columnName: "x".repeat(100), + sampleValues: ["1"], + density: "compact", + compactColumnHeaderActions: true, + headerTextWidth: 700, + }); + + expect(width).toBe(745); +});