diff --git a/frontend/src/components/DataGrid.layout.test.tsx b/frontend/src/components/DataGrid.layout.test.tsx index 3123658..e5b350c 100644 --- a/frontend/src/components/DataGrid.layout.test.tsx +++ b/frontend/src/components/DataGrid.layout.test.tsx @@ -104,6 +104,9 @@ describe('DataGrid layout', () => { expect(markup).toContain('data-grid-v2-page-chip="true"'); expect(markup).toContain('data-grid-v2-pagination-prev="true"'); expect(markup).toContain('data-grid-v2-pagination-next="true"'); + expect(markup).toContain('data-grid-pagination-jump="true"'); + expect(markup).toContain('跳页'); + expect(markup).toContain('跳转页码'); expect(markup).not.toContain('class="ant-pagination'); expect(markup).not.toContain('class="data-grid-pagination-kicker"'); expect(markup).toContain('当前页查找...'); @@ -545,9 +548,19 @@ describe('DataGrid layout', () => { expect(secondaryActionsSource.indexOf('{pageFindContent}')).toBeLessThan(secondaryActionsSource.indexOf('gn-v2-data-grid-status-center')); expect(css).toContain('width: 66px !important;'); expect(css).toContain('grid-template-columns: 160px 26px 26px !important;'); + expect(css).toContain('container-name: gn-v2-data-grid-statusbar;'); + expect(css).toContain('body[data-ui-version="v2"] .gn-v2-data-grid-status-right::-webkit-scrollbar'); + expect(css).toContain('body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap::-webkit-scrollbar'); + expect(css).toContain('@container gn-v2-data-grid-statusbar (max-width: 960px)'); + expect(css).toContain('@container gn-v2-data-grid-statusbar (max-width: 760px)'); expect(css).toContain('.data-grid-pagination-size-select.ant-select-focused .ant-select-selector'); expect(css).toContain('overflow-x: auto;'); expect(paginationBarSource).toContain("label: `${value}/页`"); + expect(paginationBarSource).toContain('const maxJumpPage = Math.max(1, paginationTotalPages);'); + expect(paginationBarSource).toContain('Math.min(maxJumpPage, Math.max(1, Math.trunc(Number(jumpPage))))'); + expect(paginationBarSource).toContain('onPressEnter={submitJumpPage}'); + expect(paginationBarSource).toContain('data-grid-pagination-jump="true"'); + expect(css).toContain('.data-grid-pagination-jump-input.ant-input-number-focused'); expect(css).toContain('background: transparent !important;'); }); diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 1f88ede..f35ad1f 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -2913,6 +2913,57 @@ const DataGrid: React.FC = ({ width: 100%; height: 100%; } + .${gridId} .data-grid-pagination-jump { + display: inline-flex; + align-items: center; + gap: 6px; + height: 34px; + color: ${paginationSecondaryTextColor}; + font-size: 12px; + font-weight: 600; + white-space: nowrap; + } + .${gridId} .data-grid-pagination-jump-label { + color: ${paginationSecondaryTextColor}; + font-variant-numeric: tabular-nums; + } + .${gridId} .data-grid-pagination-jump-input, + .${gridId} .data-grid-pagination-jump-input.ant-input-number { + width: 64px; + min-width: 64px; + height: 34px; + display: inline-flex; + align-items: stretch; + } + .${gridId} .data-grid-pagination-jump-input .ant-input-number-input-wrap, + .${gridId} .data-grid-pagination-jump-input .ant-input-number-input { + height: 100%; + } + .${gridId} .data-grid-pagination-jump-input .ant-input-number-input { + padding: 0 10px; + text-align: center; + color: ${paginationPrimaryTextColor}; + font-weight: 600; + font-variant-numeric: tabular-nums; + line-height: 34px; + } + .${gridId} .data-grid-pagination-jump-input.ant-input-number { + border-radius: 12px; + border: 1px solid ${paginationChipBorderColor}; + background: ${paginationChipBg}; + box-shadow: none; + } + .${gridId} .data-grid-pagination-jump-button.ant-btn { + height: 34px; + min-width: 34px; + padding: 0 10px; + border-radius: 12px; + border-color: ${paginationChipBorderColor}; + background: ${paginationChipBg}; + color: ${paginationPrimaryTextColor}; + font-weight: 700; + box-shadow: none; + } .${gridId} .data-grid-pagination-size-select { width: 72px; min-width: 72px; diff --git a/frontend/src/components/DataGridPaginationBar.tsx b/frontend/src/components/DataGridPaginationBar.tsx index 9bdb2e8..09ccbdd 100644 --- a/frontend/src/components/DataGridPaginationBar.tsx +++ b/frontend/src/components/DataGridPaginationBar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button, Pagination, Select } from 'antd'; +import { Button, InputNumber, Pagination, Select } from 'antd'; import { LeftOutlined, RightOutlined } from '@ant-design/icons'; interface DataGridPaginationState { @@ -38,10 +38,53 @@ const DataGridPaginationBar: React.FC = ({ onPageSizeChange, onV2PageStep, }) => { + const [jumpPage, setJumpPage] = React.useState(pagination?.current ?? null); + + React.useEffect(() => { + setJumpPage(pagination?.current ?? null); + }, [pagination?.current]); + if (!pagination) { return null; } + const maxJumpPage = Math.max(1, paginationTotalPages); + const normalizedJumpPage = Number.isFinite(Number(jumpPage)) && Number(jumpPage) > 0 + ? Math.min(maxJumpPage, Math.max(1, Math.trunc(Number(jumpPage)))) + : null; + const jumpDisabled = !onPageChange || normalizedJumpPage === null || normalizedJumpPage === pagination.current; + const submitJumpPage = () => { + if (!onPageChange || normalizedJumpPage === null) return; + if (normalizedJumpPage === pagination.current) return; + onPageChange(normalizedJumpPage, pagination.pageSize); + }; + const jumpPageControl = ( +
+ 跳页 + setJumpPage(typeof value === 'number' && Number.isFinite(value) ? value : null)} + onPressEnter={submitJumpPage} + className="data-grid-pagination-jump-input" + aria-label="跳转页码" + disabled={!onPageChange} + /> + +
+ ); + return (
= ({ disabled={!onPageChange || pagination.current >= paginationTotalPages} onClick={() => onV2PageStep('next')} /> + {jumpPageControl} span { + display: none; + } + + body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find, + body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-select, + body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-input-affix-wrapper, + body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-select-selector { + width: 140px !important; + min-width: 140px !important; + } + + body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find { + grid-template-columns: 140px 28px !important; + max-width: 180px; + } +} + body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select, body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select.ant-select, body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-size-select.ant-select-single,