From 6517284d952f7cce8f6f5d42fea596f288e7d2b3 Mon Sep 17 00:00:00 2001
From: xianghu908 <76929699+xianghu908@users.noreply.github.com>
Date: Thu, 24 Sep 2026 12:11:58 +0800
Subject: [PATCH] feat(data-grid): make the row number column show original row
numbers
---
apps/desktop/src/components/grid/DataGrid.vue | 12 +++-
.../src/components/layout/ContentArea.vue | 60 ++++++++++++++++++-
apps/desktop/src/i18n/locales/az.ts | 4 ++
apps/desktop/src/i18n/locales/en.ts | 4 ++
apps/desktop/src/i18n/locales/es.ts | 4 ++
apps/desktop/src/i18n/locales/it.ts | 4 ++
apps/desktop/src/i18n/locales/ja.ts | 4 ++
apps/desktop/src/i18n/locales/ko.ts | 4 ++
apps/desktop/src/i18n/locales/pt-BR.ts | 4 ++
apps/desktop/src/i18n/locales/tr.ts | 4 ++
apps/desktop/src/i18n/locales/zh-CN.ts | 4 ++
apps/desktop/src/i18n/locales/zh-TW.ts | 4 ++
.../__tests__/dataGridRowNumber.spec.ts | 31 ++++++++++
.../lib/dataGrid/canvasDataGridRenderer.ts | 22 ++++---
.../src/lib/dataGrid/dataGridRowNumber.ts | 25 ++++++++
.../stores/__tests__/settingsStore.spec.ts | 6 ++
apps/desktop/src/stores/settingsStore.ts | 10 ++++
17 files changed, 196 insertions(+), 10 deletions(-)
create mode 100644 apps/desktop/src/lib/dataGrid/__tests__/dataGridRowNumber.spec.ts
create mode 100644 apps/desktop/src/lib/dataGrid/dataGridRowNumber.ts
diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue
index 3d71c07adf..3c7c48a9ca 100644
--- a/apps/desktop/src/components/grid/DataGrid.vue
+++ b/apps/desktop/src/components/grid/DataGrid.vue
@@ -231,6 +231,7 @@ import {
} from "@/lib/dataGrid/dataGridInfiniteScroll";
import { resolveDataGridWheelScroll } from "@/lib/dataGrid/dataGridWheel";
import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, resolveCanvasCellTextLayout, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
+import { resolveDataGridRowNumberLabel } from "@/lib/dataGrid/dataGridRowNumber";
import { resolveCrosshairTarget, type CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme";
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
@@ -757,6 +758,7 @@ const columnIndexMap = computed(() => buildColumnIndexMap(indexes.value, primary
const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.compactColumnHeaderActions);
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
+const dataGridRowNumberMode = computed(() => settingsStore.editorSettings.dataGridRowNumberMode);
const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
const splitDataGridToolbar = computed(() => settingsStore.editorSettings.dataGridToolbarLayout === "split");
const responsiveDataGridToolbarActionCount = computed(() => dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
@@ -6387,8 +6389,13 @@ function rowNumberPageOffset(): number {
function rowNumberText(item: RowItem | undefined): string {
if (!item) return "";
- if (item.isDraft) return "*";
- return String(item.displayIndex + 1 + rowNumberPageOffset());
+ return resolveDataGridRowNumberLabel({
+ displayIndex: item.displayIndex,
+ sourceIndex: item.sourceIndex,
+ isDraft: item.isDraft,
+ sourceRowNumbers: dataGridRowNumberMode.value === "source",
+ pageOffset: rowNumberPageOffset(),
+ });
}
const quickEntryDraftPlaceholder = computed(() => t("grid.quickEntryDraftPlaceholder"));
@@ -7286,6 +7293,7 @@ function drawCanvasGrid() {
booleanDisplayMode: booleanDisplayMode.value,
flatteningMultiLineEnabled: flatteningMultiLineEnabled.value,
showWhitespace: showWhitespaceEnabled.value,
+ rowNumberMode: dataGridRowNumberMode.value,
});
if (!drawn) return;
flipCanvasSurface();
diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue
index d2716ce54c..2e234c2416 100644
--- a/apps/desktop/src/components/layout/ContentArea.vue
+++ b/apps/desktop/src/components/layout/ContentArea.vue
@@ -34,6 +34,7 @@ import {
Pin,
Pencil,
Rows3,
+ Hash,
SquareDashed,
Minus,
Plus,
@@ -136,7 +137,7 @@ const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryCh
import { useQueryStore } from "@/stores/queryStore";
import { useConnectionStore } from "@/stores/connectionStore";
import type { ContentAreaSurfaceEmits, ContentAreaSurfaceProps } from "@/components/layout/querySurfaces";
-import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode, type ResultRunDisplayMode } from "@/stores/settingsStore";
+import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridRowNumberMode, type DataGridSearchMode, type ResultRunDisplayMode } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { canCancelQueryExecution, isActiveResultLoading, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
@@ -328,6 +329,7 @@ const dataToolbarCompact = computed(() => dataToolbarTier.value >= 1);
const showDataColumnsChip = computed(() => dataToolbarTier.value < 2);
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
+const dataGridRowNumberMode = computed(() => settingsStore.editorSettings.dataGridRowNumberMode);
const resultRunDisplayMode = computed(() => settingsStore.editorSettings.resultRunDisplayMode);
const columnWidthDensity = computed(() => settingsStore.editorSettings.columnWidthDensity);
const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize);
@@ -398,6 +400,10 @@ function setDataGridSearchMode(value: DataGridSearchMode) {
settingsStore.updateEditorSettings({ dataGridSearchMode: value });
}
+function setDataGridRowNumberMode(value: DataGridRowNumberMode) {
+ settingsStore.updateEditorSettings({ dataGridRowNumberMode: value });
+}
+
function setResultRunDisplayMode(value: ResultRunDisplayMode) {
settingsStore.updateEditorSettings({ resultRunDisplayMode: value });
}
@@ -1821,6 +1827,32 @@ defineExpose({
+
diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts
index 985583b035..6f9c444487 100644
--- a/apps/desktop/src/i18n/locales/az.ts
+++ b/apps/desktop/src/i18n/locales/az.ts
@@ -2063,6 +2063,10 @@ export default withEnglishFallback({
searchModeHint: "Ctrl+F düymələrinin uyğun sətirləri süzməsini, yoxsa bütün sətirləri göstərib uyğunluqları vurğulamasını seçin.",
searchModeFilter: "Süzgəc",
searchModeHighlight: "Vurğulama",
+ rowNumberMode: "Sətir nömrəsinin göstərilməsi",
+ rowNumberModeHint: "Sətir nömrəsi sütununun cari görünüş mövqeyini, yoxsa filtrdən əvvəlki orijinal sətir nömrəsini göstərəcəyini seçin. Orijinal sətir nömrələri sətirlər filtrləndikdə dəyişməz qalır.",
+ rowNumberModeView: "Görünüş mövqeyi",
+ rowNumberModeSource: "Orijinal nömrə",
colorizeDataTypes: "Verilən növlərinin rəngləri",
numericColumnAlign: "Ədədi sütunun düzləndirilməsi",
numericColumnAlignLeft: "Sola",
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index ad76ec4c96..588e7992ee 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -2301,6 +2301,10 @@ export default {
searchModeHint: "Choose whether Ctrl+F filters matching rows or keeps all rows visible and highlights matches.",
searchModeFilter: "Filter",
searchModeHighlight: "Highlight",
+ rowNumberMode: "Row number display",
+ rowNumberModeHint: "Choose whether the row number column shows the current view position or the original row number before filtering. Original row numbers stay stable when rows are filtered out.",
+ rowNumberModeView: "View position",
+ rowNumberModeSource: "Original number",
colorizeDataTypes: "Data type colors",
numericColumnAlign: "Numeric Column Align",
numericColumnAlignLeft: "Left",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index a6434c0a31..7265775667 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -2550,6 +2550,10 @@ export default withEnglishFallback({
},
cachedResultUnavailable: "Resultado en caché faltante o incompatible.",
reexecuteQuery: "Volver a ejecutar consulta",
+ rowNumberMode: "Visualización del número de fila",
+ rowNumberModeHint: "Elige si la columna de números de fila muestra la posición actual en la vista o el número de fila original antes de filtrar. Los números de fila originales no cambian al filtrar filas.",
+ rowNumberModeView: "Posición en la vista",
+ rowNumberModeSource: "Número original",
colorizeDataTypes: "Colores de tipos de datos",
numericColumnAlign: "Alineación de columna numérica",
numericColumnAlignLeft: "Alineación izquierda",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 0ea54d8fc0..e06cd1e713 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -2547,6 +2547,10 @@ export default withEnglishFallback({
},
cachedResultUnavailable: "Risultato in cache mancante o non compatibile.",
reexecuteQuery: "Esegui di nuovo la query",
+ rowNumberMode: "Visualizzazione numero di riga",
+ rowNumberModeHint: "Scegli se la colonna dei numeri di riga mostra la posizione corrente nella vista o il numero di riga originale prima del filtro. I numeri di riga originali restano invariati quando le righe vengono filtrate.",
+ rowNumberModeView: "Posizione nella vista",
+ rowNumberModeSource: "Numero originale",
colorizeDataTypes: "Colori dei tipi di dato",
numericColumnAlign: "Allineamento colonna numerica",
numericColumnAlignLeft: "Allineamento a sinistra",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index be91a66376..cd1f1d676f 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -2601,6 +2601,10 @@ export default withEnglishFallback({
searchModeHint: "Ctrl+F 検索時に一致する行をフィルタリングするか、全行を表示したまま一致箇所をハイライトするかを選択します。",
searchModeFilter: "フィルター",
searchModeHighlight: "ハイライト",
+ rowNumberMode: "行番号の表示",
+ rowNumberModeHint: "行番号欄に現在の表示位置とフィルター前の元の行番号のどちらを表示するか選択します。元の行番号は絞り込み後も変わりません。",
+ rowNumberModeView: "表示位置",
+ rowNumberModeSource: "元の行番号",
emptyStringValue: "空文字",
jumpToPage: "ページ番号に移動",
importBinaryValue: "ファイルからインポート",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index e4483feebc..4c2531b9c0 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -2140,6 +2140,10 @@ export default withEnglishFallback({
searchModeHint: "Ctrl+F가 일치하는 행을 필터링할지, 모든 행을 표시하고 일치 항목을 강조할지 선택하세요.",
searchModeFilter: "필터",
searchModeHighlight: "강조",
+ rowNumberMode: "행 번호 표시",
+ rowNumberModeHint: "행 번호 열에 현재 보기 위치와 필터링 전 원래 행 번호 중 무엇을 표시할지 선택하세요. 원래 행 번호는 필터링 후에도 바뀌지 않습니다.",
+ rowNumberModeView: "보기 위치",
+ rowNumberModeSource: "원래 행 번호",
colorizeDataTypes: "데이터 타입 색상",
numericColumnAlign: "숫자 컬럼 정렬",
numericColumnAlignLeft: "왼쪽",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 27a382fd8f..2c0146ba5a 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -2549,6 +2549,10 @@ export default withEnglishFallback({
},
cachedResultUnavailable: "Resultado em cache ausente ou incompatível.",
reexecuteQuery: "Reexecutar consulta",
+ rowNumberMode: "Exibição do número da linha",
+ rowNumberModeHint: "Escolha se a coluna de números de linha mostra a posição atual na visualização ou o número de linha original antes da filtragem. Os números de linha originais permanecem inalterados quando linhas são filtradas.",
+ rowNumberModeView: "Posição na visualização",
+ rowNumberModeSource: "Número original",
colorizeDataTypes: "Cores por tipo de dado",
numericColumnAlign: "Alinhamento de colunas numéricas",
numericColumnAlignLeft: "Alinhamento à esquerda",
diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts
index c191c161d9..cabbea2bbb 100644
--- a/apps/desktop/src/i18n/locales/tr.ts
+++ b/apps/desktop/src/i18n/locales/tr.ts
@@ -2058,6 +2058,10 @@ export default withEnglishFallback({
searchModeHint: "Ctrl+F'nin eşleşen satırları filtrelemesini mi yoksa tüm satırları görünür tutup eşleşmeleri vurgulamasını mı istediğinizi seçin.",
searchModeFilter: "Filtre",
searchModeHighlight: "Vurgula",
+ rowNumberMode: "Satır numarası gösterimi",
+ rowNumberModeHint: "Satır numarası sütununda geçerli görünüm konumunun mu yoksa filtrelemeden önceki özgün satır numarasının mı gösterileceğini seçin. Özgün satır numaraları filtrelemeden sonra değişmez.",
+ rowNumberModeView: "Görünüm konumu",
+ rowNumberModeSource: "Özgün numara",
colorizeDataTypes: "Veri türü renkleri",
numericColumnAlign: "Sayısal Sütun Hizalaması",
numericColumnAlignLeft: "Sol",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 9d108a5cc3..3f49415086 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -2214,6 +2214,10 @@ export default withEnglishFallback({
searchModeHint: "选择 Ctrl+F 搜索时过滤匹配行,或保留全部行并高亮定位命中项。",
searchModeFilter: "过滤",
searchModeHighlight: "高亮",
+ rowNumberMode: "行号显示方式",
+ rowNumberModeHint: "选择行号栏显示当前视图序号,或筛选前的原始行号;原始行号在筛选、搜索后保持不变,便于定位数据在完整结果中的位置。",
+ rowNumberModeView: "视图序号",
+ rowNumberModeSource: "原始行号",
colorizeDataTypes: "数据类型着色",
numericColumnAlign: "数值列对齐",
numericColumnAlignLeft: "左对齐",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index d9bbdf92d2..18f07423f4 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -2552,6 +2552,10 @@ export default withEnglishFallback({
},
cachedResultUnavailable: "快取結果缺失或不相容。",
reexecuteQuery: "重新執行查詢",
+ rowNumberMode: "行號顯示方式",
+ rowNumberModeHint: "選擇行號欄顯示目前檢視序號,或篩選前的原始行號;原始行號在篩選、搜尋後保持不變,便於定位資料在完整結果中的位置。",
+ rowNumberModeView: "檢視序號",
+ rowNumberModeSource: "原始行號",
colorizeDataTypes: "資料類型著色",
numericColumnAlign: "數值列對齊",
numericColumnAlignLeft: "左對齊",
diff --git a/apps/desktop/src/lib/dataGrid/__tests__/dataGridRowNumber.spec.ts b/apps/desktop/src/lib/dataGrid/__tests__/dataGridRowNumber.spec.ts
new file mode 100644
index 0000000000..3111d12c2e
--- /dev/null
+++ b/apps/desktop/src/lib/dataGrid/__tests__/dataGridRowNumber.spec.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from "vitest";
+import { resolveDataGridRowNumberLabel } from "@/lib/dataGrid/dataGridRowNumber";
+
+describe("data grid row number labels", () => {
+ it("keeps the current view position by default", () => {
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 0, sourceRowNumbers: false, pageOffset: 0 })).toBe("1");
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 41, sourceIndex: 137, sourceRowNumbers: false, pageOffset: 0 })).toBe("42");
+ });
+
+ it("adds the page offset in paged results", () => {
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 0, sourceRowNumbers: false, pageOffset: 100 })).toBe("101");
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 0, sourceIndex: 0, sourceRowNumbers: true, pageOffset: 100 })).toBe("101");
+ });
+
+ it("survives filtering by reporting the original row number", () => {
+ // 第 137 行被筛成视图里的第 3 行:原始行号模式下仍应显示 137
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 2, sourceIndex: 136, sourceRowNumbers: true, pageOffset: 0 })).toBe("137");
+ // 分页场景下 sourceIndex 是页内下标,需要叠加页偏移
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 2, sourceIndex: 36, sourceRowNumbers: true, pageOffset: 100 })).toBe("137");
+ });
+
+ it("falls back to the view position for rows without a source index", () => {
+ // 新增行还没有落库位置,sourceIndex 为 undefined
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 7, sourceRowNumbers: true, pageOffset: 0 })).toBe("8");
+ });
+
+ it("keeps the draft placeholder regardless of the mode", () => {
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 7, isDraft: true, sourceRowNumbers: false, pageOffset: 0 })).toBe("*");
+ expect(resolveDataGridRowNumberLabel({ displayIndex: 7, sourceIndex: 3, isDraft: true, sourceRowNumbers: true, pageOffset: 100 })).toBe("*");
+ });
+});
diff --git a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts
index 717466d54b..5ccea1a845 100644
--- a/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts
+++ b/apps/desktop/src/lib/dataGrid/canvasDataGridRenderer.ts
@@ -7,6 +7,7 @@ import type { CellSelectionRange } from "@/lib/dataGrid/gridSelection";
import type { RowStatus } from "@/lib/dataGrid/gridRowStatus";
import { DATA_GRID_DARK_SEARCH_COLORS, dataGridTypeForeground, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
import type { CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
+import { resolveDataGridRowNumberLabel } from "@/lib/dataGrid/dataGridRowNumber";
export const CANVAS_DATA_GRID_ROW_HEIGHT = 26;
export const MAX_CANVAS_DATA_GRID_PIXEL_RATIO = 4;
@@ -102,6 +103,8 @@ export interface DrawCanvasDataGridOptions {
booleanDisplayMode?: "checkbox" | "dropdown";
flatteningMultiLineEnabled: boolean;
showWhitespace?: boolean;
+ /** 行号栏取值:`view` = 当前视图序号(默认,与筛选前一致),`source` = 筛选前的原始行号 */
+ rowNumberMode?: "view" | "source";
}
type NumericCanvasContext = CanvasRenderingContext2D & {
@@ -365,6 +368,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions): boolean
booleanDisplayMode = "dropdown",
flatteningMultiLineEnabled,
showWhitespace = false,
+ rowNumberMode = "view",
} = options;
// 框选热路径:整次绘制只判断一次。常见情况(单矩形 / 多列且每段都是多格)可跳过逐格 kind 查询
const paintSelectionOuterFrame = dataGridSelectionUsesOuterFrame(selectionFrames);
@@ -485,13 +489,17 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions): boolean
ctx.beginPath();
ctx.rect(0, y, rowNumberWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
ctx.clip();
- if (item.isDraft) {
- ctx.fillText("*", rowNumberTextX, textY);
- } else if (infiniteScrollEnabled) {
- ctx.fillText(String(item.displayIndex + 1), rowNumberTextX, textY);
- } else {
- ctx.fillText(String(item.displayIndex + 1 + pageOffset), rowNumberTextX, textY);
- }
+ ctx.fillText(
+ resolveDataGridRowNumberLabel({
+ displayIndex: item.displayIndex,
+ sourceIndex: item.sourceIndex,
+ isDraft: item.isDraft,
+ sourceRowNumbers: rowNumberMode === "source",
+ pageOffset: infiniteScrollEnabled ? 0 : pageOffset,
+ }),
+ rowNumberTextX,
+ textY,
+ );
ctx.restore();
ctx.font = normalFont;
diff --git a/apps/desktop/src/lib/dataGrid/dataGridRowNumber.ts b/apps/desktop/src/lib/dataGrid/dataGridRowNumber.ts
new file mode 100644
index 0000000000..b54127be02
--- /dev/null
+++ b/apps/desktop/src/lib/dataGrid/dataGridRowNumber.ts
@@ -0,0 +1,25 @@
+/**
+ * 行号栏取值。
+ *
+ * `displayIndex` 是当前视图里的压缩序号:一旦应用筛选(Ctrl+F 过滤模式、列头筛选、
+ * 行状态筛选),被隐藏的行后面的行号会整体前移,无法再对应回完整结果里的位置。
+ * `sourceIndex` 是筛选前的原始下标,一直保留在行数据里,因此 `source` 模式直接用它,
+ * 让筛选前后同一个行号始终指向同一条数据。分页模式下原始下标是页内下标,
+ * 仍需叠加页偏移才能得到完整结果中的位置。
+ */
+export interface DataGridRowNumberInput {
+ displayIndex: number;
+ sourceIndex?: number;
+ isDraft?: boolean;
+ /** true = 显示原始行号(sourceIndex),false = 显示当前视图序号(displayIndex) */
+ sourceRowNumbers: boolean;
+ /** 已加载行之前的行数:分页页码偏移;无限滚动 / 单页时为 0 */
+ pageOffset: number;
+}
+
+export function resolveDataGridRowNumberLabel(input: DataGridRowNumberInput): string {
+ // 待保存的草稿行没有落库位置,保持原有的 "*" 占位
+ if (input.isDraft) return "*";
+ const index = input.sourceRowNumbers && input.sourceIndex !== undefined ? input.sourceIndex : input.displayIndex;
+ return String(index + 1 + input.pageOffset);
+}
diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
index d32bf1b358..6b159d9155 100644
--- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
+++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
@@ -371,6 +371,12 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ dataGridSearchMode: "invalid" as any }).dataGridSearchMode).toBe("filter");
});
+ it("defaults the data grid row number column to the view position and preserves original row numbers", () => {
+ expect(normalizeEditorSettings({}).dataGridRowNumberMode).toBe("view");
+ expect(normalizeEditorSettings({ dataGridRowNumberMode: "source" }).dataGridRowNumberMode).toBe("source");
+ expect(normalizeEditorSettings({ dataGridRowNumberMode: "invalid" as any }).dataGridRowNumberMode).toBe("view");
+ });
+
it("defaults the global data grid copy preference and preserves valid choices", () => {
expect(normalizeEditorSettings({}).dataGridCopyExtractor).toBe("smart");
expect(normalizeEditorSettings({ dataGridCopyExtractor: "smart" }).dataGridCopyExtractor).toBe("smart");
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index 44a600a85c..c6633a4285 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -663,6 +663,8 @@ const DATA_GRID_RENDER_MODES = ["dom", "canvas"] as const;
export type DataGridRenderMode = (typeof DATA_GRID_RENDER_MODES)[number];
const DATA_GRID_SEARCH_MODES = ["filter", "highlight"] as const;
export type DataGridSearchMode = (typeof DATA_GRID_SEARCH_MODES)[number];
+const DATA_GRID_ROW_NUMBER_MODES = ["view", "source"] as const;
+export type DataGridRowNumberMode = (typeof DATA_GRID_ROW_NUMBER_MODES)[number];
export type DataGridFilterEditorView = "quick" | "conditions" | "text";
export type DataGridToolbarLayout = "single" | "split";
const RESULT_RUN_DISPLAY_MODES = ["tabs", "list"] as const;
@@ -852,6 +854,7 @@ export interface EditorSettings {
localFilterPopoverWidth: number;
dataGridRenderMode: DataGridRenderMode;
dataGridSearchMode: DataGridSearchMode;
+ dataGridRowNumberMode: DataGridRowNumberMode;
dataGridCopyExtractor: DataGridCopyPreference;
dataGridExtractorOptions: DataGridExtractorOptions;
dataGridExtractorOptionsMigrationVersion: number;
@@ -1127,6 +1130,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
localFilterPopoverWidth: 360,
dataGridRenderMode: "canvas",
dataGridSearchMode: "filter",
+ dataGridRowNumberMode: "view",
dataGridCopyExtractor: "smart",
dataGridExtractorOptions: normalizeDataGridExtractorOptions(DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS),
dataGridExtractorOptionsMigrationVersion: DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION,
@@ -1300,6 +1304,10 @@ function normalizeDataGridSearchMode(value: unknown): DataGridSearchMode {
return DATA_GRID_SEARCH_MODES.includes(value as DataGridSearchMode) ? (value as DataGridSearchMode) : DEFAULT_EDITOR_SETTINGS.dataGridSearchMode;
}
+function normalizeDataGridRowNumberMode(value: unknown): DataGridRowNumberMode {
+ return DATA_GRID_ROW_NUMBER_MODES.includes(value as DataGridRowNumberMode) ? (value as DataGridRowNumberMode) : DEFAULT_EDITOR_SETTINGS.dataGridRowNumberMode;
+}
+
function normalizeDataGridFilterEditorView(value: unknown): DataGridFilterEditorView {
return value === "conditions" || value === "text" ? value : DEFAULT_EDITOR_SETTINGS.dataGridFilterEditorView;
}
@@ -1675,6 +1683,7 @@ export function normalizeEditorSettings(settings: Partial, exist
localFilterPopoverWidth: normalizeDrawerWidth(settings.localFilterPopoverWidth, 240, DEFAULT_EDITOR_SETTINGS.localFilterPopoverWidth),
dataGridRenderMode: normalizeDataGridRenderMode(settings.dataGridRenderMode),
dataGridSearchMode: normalizeDataGridSearchMode(settings.dataGridSearchMode),
+ dataGridRowNumberMode: normalizeDataGridRowNumberMode(settings.dataGridRowNumberMode),
dataGridCopyExtractor: normalizeDataGridCopyPreference(settings.dataGridCopyExtractor),
dataGridExtractorOptions,
dataGridExtractorOptionsMigrationVersion: DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION,
@@ -2499,6 +2508,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.localFilterPopoverWidth !== undefined) editorSettings.value.localFilterPopoverWidth = normalizeDrawerWidth(partial.localFilterPopoverWidth, 240, DEFAULT_EDITOR_SETTINGS.localFilterPopoverWidth);
if (partial.dataGridRenderMode !== undefined) editorSettings.value.dataGridRenderMode = normalizeDataGridRenderMode(partial.dataGridRenderMode);
if (partial.dataGridSearchMode !== undefined) editorSettings.value.dataGridSearchMode = normalizeDataGridSearchMode(partial.dataGridSearchMode);
+ if (partial.dataGridRowNumberMode !== undefined) editorSettings.value.dataGridRowNumberMode = normalizeDataGridRowNumberMode(partial.dataGridRowNumberMode);
if (partial.dataGridCopyExtractor !== undefined) editorSettings.value.dataGridCopyExtractor = normalizeDataGridCopyPreference(partial.dataGridCopyExtractor);
if (partial.dataGridExtractorOptions !== undefined) editorSettings.value.dataGridExtractorOptions = normalizeDataGridExtractorOptions(partial.dataGridExtractorOptions);
if (partial.dataGridExtractorOptionsMigrationVersion !== undefined) editorSettings.value.dataGridExtractorOptionsMigrationVersion = DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION;