From 61e5073d250af543b65989e29efcf060afeefb58 Mon Sep 17 00:00:00 2001 From: ssy <2090230619@qq.com> Date: Thu, 6 Aug 2026 12:41:23 +0800 Subject: [PATCH] feat(grid): support custom page navigation --- apps/desktop/src/components/grid/DataGrid.vue | 22 ++++++++ .../components/grid/DataGridPagination.vue | 55 ++++++++++++++++++- .../grid/__tests__/DataGridSurfaces.spec.ts | 43 +++++++++++++++ apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + 11 files changed, 126 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index f29f54de87..061b629837 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2543,6 +2543,11 @@ const canGoNextPage = computed(() => { allRowsLoaded: allRowsLoaded.value, }); }); +const maximumPage = computed(() => { + const total = hasKnownPaginationTotalRowCount.value ? paginationTotalRowCount.value : allRowsLoaded.value ? props.result.rows.length : undefined; + if (typeof total === "number" && Number.isFinite(total) && total >= 0) return Math.max(1, Math.ceil(total / pageSize.value)); + return canGoNextPage.value ? undefined : currentPage.value; +}); const canFetchNextInfiniteScrollSegment = computed(() => canFetchNextDataGridSegment({ hasMore: props.result.has_more, @@ -2713,6 +2718,21 @@ function nextPage() { emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy()); } +function jumpPage(page: number) { + if (gridSurfaceBusy.value || infiniteScrollEnabled.value || !Number.isSafeInteger(page) || page < 1) return; + const targetPage = Math.min(page, maximumPage.value ?? page); + if (targetPage === currentPage.value) return; + if (allRowsLoaded.value) { + currentPage.value = targetPage; + resetGridVerticalScroll(true); + return; + } + const offset = (targetPage - 1) * pageSize.value; + if (!Number.isSafeInteger(offset)) return; + resetGridVerticalScroll(true); + emit("paginate", offset, pageSize.value, currentWhereInput(), currentOrderBy()); +} + function infiniteScrollNextPage() { if (infiniteScrollLoading.value || props.loading) return; if (!canFetchNextInfiniteScrollSegment.value) { @@ -10014,6 +10034,7 @@ const gridContextMenuItems = computed(() => { :page-size-menu-items="pageSizeMenuItems" :export-menu-items="exportMenuItems" :current-page="currentPage" + :max-page="maximumPage" :can-go-next-page="canGoNextPage" :can-jump-last-page="canJumpLastPage" @select-page-size="selectPageSizeMenuItem" @@ -10021,6 +10042,7 @@ const gridContextMenuItems = computed(() => { @first-page="firstPage" @previous-page="prevPage" @next-page="nextPage" + @jump-page="jumpPage" @last-page="lastPage" @select-export="selectExportMenuItem" /> diff --git a/apps/desktop/src/components/grid/DataGridPagination.vue b/apps/desktop/src/components/grid/DataGridPagination.vue index 9f7bab43e0..ca1142c91c 100644 --- a/apps/desktop/src/components/grid/DataGridPagination.vue +++ b/apps/desktop/src/components/grid/DataGridPagination.vue @@ -1,5 +1,6 @@ diff --git a/apps/desktop/src/components/grid/__tests__/DataGridSurfaces.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridSurfaces.spec.ts index 4eb2629e44..c544a13264 100644 --- a/apps/desktop/src/components/grid/__tests__/DataGridSurfaces.spec.ts +++ b/apps/desktop/src/components/grid/__tests__/DataGridSurfaces.spec.ts @@ -222,6 +222,49 @@ describe("DataGridPagination", () => { await mounted.setProps({ loading: true }); const busyNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0"); expect(busyNavigation.map((node) => node.props.disabled)).toEqual([true, true, true, true]); + expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.jumpToPage").props.disabled).toBe(true); + }); + + it("jumps to an entered page and enforces page input boundaries", async () => { + const jumpPage = vi.fn(); + const mounted = mountComponent(DataGridPagination, { + selectionSummary: null, + selectionSummarySumText: "", + loading: false, + infiniteScrollEnabled: false, + infiniteScrollAllLoaded: false, + pageSize: 100, + customPageSizeInput: "", + pageSizeMenuItems: [], + exportMenuItems: [], + currentPage: 3, + maxPage: 12, + canGoNextPage: true, + canJumpLastPage: true, + onJumpPage: jumpPage, + }); + const pageInput = findOne(mounted.root, (node) => node.props["aria-label"] === "grid.jumpToPage"); + + expect(pageInput.props.modelValue).toBe("3"); + pageInput.props["onUpdate:modelValue"]("8"); + await nextTick(); + const enter = dispatch(pageInput, "keydown", { key: "Enter" }); + expect(enter.defaultPrevented).toBe(true); + expect(enter.propagationStopped).toBe(true); + expect(jumpPage).toHaveBeenLastCalledWith(8); + + pageInput.props["onUpdate:modelValue"]("99"); + await nextTick(); + dispatch(pageInput, "keydown", { key: "Enter" }); + expect(jumpPage).toHaveBeenLastCalledWith(12); + + pageInput.props["onUpdate:modelValue"]("0"); + await nextTick(); + dispatch(pageInput, "keydown", { key: "Enter" }); + await nextTick(); + const resetPageInput = findOne(mounted.root, (node) => node.props["aria-label"] === "grid.jumpToPage"); + expect(jumpPage).toHaveBeenCalledTimes(2); + expect(resetPageInput.props.modelValue).toBe("3"); }); it("hides pagination controls when the data source does not support paging", () => { diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 76125776a8..1bc3d9e971 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1377,6 +1377,7 @@ export default { filterAllRows: "All rows", filterChangedRows: "Changed", page: "Page {page}", + jumpToPage: "Jump to page", rowsPerPage: "Rows per page", customRowsPerPage: "Custom rows", applyPageSize: "Apply", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 6cfbb0e519..0092fe7df7 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1515,6 +1515,7 @@ export default withEnglishFallback({ xlsxHeaderPrompt: "Seleccione el formato de encabezado al exportar a Excel:", xlsxHeaderOriginal: "Encabezado usando nombres de campos", xlsxHeaderComment: "Encabezado usando comentarios", + jumpToPage: "Ir a la página", }, exportProgress: { streamingUnsupported: "La exportación en streaming no es compatible con esta consulta. Simplifíquela o use un controlador compatible.", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 25227713dd..73531a0069 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1513,6 +1513,7 @@ export default withEnglishFallback({ xlsxHeaderPrompt: "Seleziona il formato dell'intestazione da utilizzare per l'esportazione in Excel:", xlsxHeaderOriginal: "Intestazione con nome campo", xlsxHeaderComment: "Intestazione con commento", + jumpToPage: "Vai alla pagina", }, exportProgress: { streamingUnsupported: "L'esportazione in streaming non è supportata per questa query. Semplificala o usa un driver supportato.", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 3a6c22cb89..d8dbf8cf61 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1540,6 +1540,7 @@ export default withEnglishFallback({ searchModeFilter: "フィルター", searchModeHighlight: "ハイライト", emptyStringValue: "空文字", + jumpToPage: "ページ番号に移動", }, exportProgress: { streamingUnsupported: "このクエリはストリーミングエクスポートに対応していません。クエリを簡略化するか、対応しているドライバーを使用してください。", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index ddffafa647..6c20cd0343 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1282,6 +1282,7 @@ export default withEnglishFallback({ filterAllRows: "전체 행", filterChangedRows: "변경됨", page: "{page}페이지", + jumpToPage: "{page}페이지로 이동", rowsPerPage: "페이지당 행", customRowsPerPage: "사용자 지정 행", applyPageSize: "적용", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 9c518037e3..ad8ce2c284 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1515,6 +1515,7 @@ export default withEnglishFallback({ xlsxHeaderPrompt: "Selecione o formato do cabeçalho a ser usado ao exportar Excel:", xlsxHeaderOriginal: "Cabeçalho usa nome do campo", xlsxHeaderComment: "Cabeçalho usa comentário", + jumpToPage: "Ir para a página", }, exportProgress: { streamingUnsupported: "A exportação em streaming não é compatível com esta consulta. Simplifique-a ou use um driver compatível.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 9b0297008e..18f3285fbe 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1378,6 +1378,7 @@ export default withEnglishFallback({ filterAllRows: "全部行", filterChangedRows: "变更项", page: "第 {page} 页", + jumpToPage: "跳转到页码", rowsPerPage: "每页行数", customRowsPerPage: "自定义行数", applyPageSize: "应用", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 6fb1f873a7..6ac6635199 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1257,6 +1257,7 @@ export default withEnglishFallback({ filterAllRows: "全部列", filterChangedRows: "變更列", page: "第 {page} 頁", + jumpToPage: "跳至頁碼", rowsPerPage: "每頁筆數", customRowsPerPage: "自訂筆數", applyPageSize: "套用",