From 0632c5242c18b972eb85102bde5f0b52d3c6ebe0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 17 Jun 2026 09:49:15 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(oceanbase/data-grid):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Oracle=20=E6=97=B6=E9=97=B4=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=98=BE=E7=A4=BA=E7=BC=96=E8=BE=91=E4=B8=8E=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E8=A7=86=E5=9B=BE=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 OceanBase Oracle DATE 与 TIMESTAMP 的解码、展示和编辑精度丢失问题 - 修复查询结果与数据视图的行号显示、分页页数和日期列展示口径 - 打通 Oracle 与 OceanBase 会话执行链路的扫描方言透传 - 补齐 DBQuery、DataGrid temporal 和 OceanBase 结果链路回归测试 --- .../src/components/DataGrid.layout.test.tsx | 95 ++++ frontend/src/components/DataGrid.tsx | 208 +++++++-- .../src/components/DataGridPaginationBar.tsx | 20 +- .../DataViewer.primary-key.test.tsx | 32 ++ frontend/src/components/DataViewer.tsx | 4 +- .../src/components/dataGridTemporal.test.ts | 43 +- frontend/src/components/dataGridTemporal.ts | 116 ++++- .../src/utils/dataSourceCapabilities.test.ts | 10 +- frontend/src/utils/dataSourceCapabilities.ts | 7 + internal/app/methods_db.go | 358 ++++++++++++++ internal/app/methods_db_conn_test.go | 8 + .../app/methods_db_create_statement_test.go | 31 ++ .../app/methods_db_metadata_retry_test.go | 149 +++++- internal/db/database.go | 23 +- internal/db/database_session_test.go | 87 ++++ internal/db/oceanbase_impl.go | 143 +++++- internal/db/oceanbase_impl_test.go | 77 +++ internal/db/oracle_applychanges_test.go | 54 ++- internal/db/oracle_get_tables_test.go | 114 +++++ internal/db/oracle_impl.go | 437 +++++++++++++----- internal/db/query_value.go | 337 +++++++++++++- internal/db/query_value_test.go | 167 +++++++ internal/db/scan_rows_test.go | 336 ++++++++++++++ internal/mcpserver/service.go | 4 +- internal/mcpserver/service_test.go | 40 ++ 25 files changed, 2702 insertions(+), 198 deletions(-) create mode 100644 internal/db/database_session_test.go diff --git a/frontend/src/components/DataGrid.layout.test.tsx b/frontend/src/components/DataGrid.layout.test.tsx index 97e9269..2338def 100644 --- a/frontend/src/components/DataGrid.layout.test.tsx +++ b/frontend/src/components/DataGrid.layout.test.tsx @@ -222,7 +222,9 @@ describe('DataGrid layout', () => { paginationSummaryText="当前 24 条 / 共 24 条" paginationControlTotal={24} paginationTotalPages={1} + paginationPageText="第 1 / 1 页" paginationPageSizeOptions={['100', '200']} + showKnownPageCount onPageChange={() => {}} onPageSizeChange={() => {}} onV2PageStep={() => {}} @@ -233,6 +235,37 @@ describe('DataGrid layout', () => { expect(markup).not.toContain('第 1 / 1 页'); }); + it('keeps unknown-total pagination in sequential mode instead of pretending total pages are known', () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('第 3 页'); + expect(markup).not.toContain('3/4'); + expect(markup).not.toContain('data-grid-pagination-jump="true"'); + }); + it('renders the v2 DataGrid toolbar using the redesigned topbar hooks', () => { const markup = renderToStaticMarkup( { expect(markup).toContain('AI 洞察'); }); + it('renders a non-data row number column when enabled', () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('aria-label="行号"'); + expect(markup).toContain('#'); + expect(markup).not.toContain('>行号<'); + expect(markup).toContain('data-grid-row-number-title="true"'); + expect(markup).toContain('data-grid-column-title-single-line="true"'); + expect(markup).toContain('justify-content:center'); + expect(markup).toContain('align-items:center'); + expect(markup).toContain('min-height:var(--gonavi-header-min-height, 40px)'); + expect(markup).toContain('text-align:center'); + expect(markup).toContain('padding-inline:0'); + expect(markup).toContain('vertical-align:middle'); + expect(markup).toContain('data-grid-row-number="true"'); + expect(markup).toContain('51'); + }); + it('clears modified cell markers when refreshing the grid', () => { const source = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8'); expect(source).toMatch(/const handleRefreshGrid = useCallback\(\(\) => \{[\s\S]*setModifiedColumns\(\{\}\);[\s\S]*if \(onReload\) onReload\(\);[\s\S]*\}, \[clearAutoCommitTimer, onReload\]\);/); }); + it('routes temporal inline editors through the current connection config', () => { + const source = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8'); + + expect(source).toContain('const pickerType = getTemporalPickerType(columnType, dbType, connectionConfig);'); + expect(source).toContain('const pickerType = getTemporalPickerType(columnType, dbType, currentConnConfig);'); + expect(source).toContain('cellProps.connectionConfig = currentConnConfig;'); + expect(source).toContain('format={getTemporalPickerFormat(pickerType)}'); + }); + it('renders a cell-level undo action in the v2 context menu for modified cells', () => { const markup = renderToStaticMarkup( { expect(formatCellDisplayText('2026-05-10T09:12:33.456+08:00')).toBe('2026-05-10 09:12:33.456'); }); + it('collapses OceanBase Oracle DATE midnight values to date-only text', () => { + const oceanBaseOracleConfig = { + type: 'oceanbase', + oceanBaseProtocol: 'oracle', + } as any; + + expect(formatCellDisplayText('2026-06-16T00:00:00Z', 'DATE', oceanBaseOracleConfig)).toBe('2026-06-16'); + expect(formatCellDisplayText('2026-06-16 00:00:00', 'DATE', oceanBaseOracleConfig)).toBe('2026-06-16'); + expect(formatCellDisplayText('2026-06-16T13:14:15Z', 'DATE', oceanBaseOracleConfig)).toBe('2026-06-16 13:14:15'); + expect(formatCellDisplayText('2026-06-16T00:00:00Z', 'DATE', { type: 'oracle' } as any)).toBe('2026-06-16 00:00:00'); + }); + it('renders bit column hex values as decimal flags', () => { expect(formatCellDisplayText('0x00', 'bit(1)')).toBe('0'); expect(formatCellDisplayText('0x01', 'bit(1)')).toBe('1'); diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 86fb37f..2fad341 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -31,12 +31,13 @@ import { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, escapeLiteral, import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance'; import { getDataSourceCapabilities, resolveDataSourceType } from '../utils/dataSourceCapabilities'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; +import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol'; import { getDensityParams, resolveDataTableColumnWidth, resolveDataTableVerticalBorderColor, } from '../utils/dataGridDisplay'; -import { resolvePaginationSummaryText, resolvePaginationTotalForControl } from '../utils/dataGridPagination'; +import { resolvePaginationPageText, resolvePaginationSummaryText, resolvePaginationTotalForControl } from '../utils/dataGridPagination'; import { resolveGridSortInfoFromTableSorter } from '../utils/dataGridSort'; import { calculateExternalHorizontalScrollInnerWidth, @@ -71,10 +72,12 @@ import { DEFAULT_SHORTCUT_OPTIONS, getShortcutPlatform, resolveShortcutDisplay } import { TEMPORAL_FORMATS, formatFromDayjs, + getTemporalPickerFormat, getTemporalPickerType, isTemporalColumnType, parseToDayjs, resolveTemporalEditorSaveValue, + type TemporalConnectionLike, type TemporalPickerType, } from './dataGridTemporal'; import { @@ -182,6 +185,7 @@ class DataGridErrorBoundary extends React.Component< // 内部行标识字段:避免与真实业务字段(如 `key` 列)冲突。 export const GONAVI_ROW_KEY = '__gonavi_row_key__'; +export const GONAVI_ROW_NUMBER_COLUMN_KEY = '__gonavi_row_number__'; // Cell key helpers for batch selection/fill. // Use a control character separator to avoid collisions with rowKey/columnName contents (e.g. `new-123`). @@ -189,6 +193,7 @@ const CELL_KEY_SEP = '\u0001'; const CELL_SELECTION_DRAG_THRESHOLD_PX = 4; const DATE_TIME_CACHE_LIMIT = 2000; const TABLE_CELL_PREVIEW_MAX_CHARS = 240; +const ROW_NUMBER_COLUMN_WIDTH = 58; const DATA_EDIT_AUTO_COMMIT_DELAY_OPTIONS = [ { value: 3000, label: '3 秒' }, { value: 5000, label: '5 秒' }, @@ -281,7 +286,46 @@ const normalizeBitHexDisplayText = (val: any, columnType?: string): string | nul } }; -export const formatCellDisplayText = (val: any, columnType?: string): string => { +type CellDisplayConnectionLike = TemporalConnectionLike; + +const isDateOnlyColumnType = (columnType?: string): boolean => { + const normalized = String(columnType || '').trim().toLowerCase(); + if (!normalized) return false; + const base = normalized.split(/[ (]/)[0]; + return base === 'date' || base === 'newdate'; +}; + +const isOceanBaseOracleDisplayConnection = (connectionConfig?: CellDisplayConnectionLike): boolean => { + if (!connectionConfig) return false; + const type = String(connectionConfig.type || '').trim().toLowerCase(); + const driver = String(connectionConfig.driver || '').trim().toLowerCase(); + return (type === 'oceanbase' || driver === 'oceanbase') + && normalizeOceanBaseProtocol(connectionConfig.oceanBaseProtocol) === 'oracle'; +}; + +const normalizeOceanBaseOracleDateDisplayText = ( + val: string, + columnType?: string, + connectionConfig?: CellDisplayConnectionLike, +): string | null => { + if (!isDateOnlyColumnType(columnType) || !isOceanBaseOracleDisplayConnection(connectionConfig)) { + return null; + } + const trimmed = String(val || '').trim(); + if (!trimmed) return trimmed; + const match = trimmed.match( + /^(\d{4}-\d{2}-\d{2})(?:[T ](\d{2}:\d{2}:\d{2})(\.\d+)?(?:\s*(?:Z|[+-]\d{2}:?\d{2})(?:\s+[A-Za-z_\/+-]+)?)?)?$/ + ); + if (!match) return null; + const [, datePart, timePart, fractionPart] = match; + if (!timePart) return datePart; + if (timePart === '00:00:00' && (!fractionPart || /^\.0+$/.test(fractionPart))) { + return datePart; + } + return null; +}; + +export const formatCellDisplayText = (val: any, columnType?: string, connectionConfig?: CellDisplayConnectionLike): string => { try { if (val === null) return 'NULL'; const bitText = normalizeBitHexDisplayText(val, columnType); @@ -310,6 +354,10 @@ export const formatCellDisplayText = (val: any, columnType?: string): string => } } if (typeof val === 'string') { + const oceanBaseDateOnly = normalizeOceanBaseOracleDateDisplayText(val, columnType, connectionConfig); + if (oceanBaseDateOnly !== null) { + return oceanBaseDateOnly.length > TABLE_CELL_PREVIEW_MAX_CHARS ? `${oceanBaseDateOnly.slice(0, TABLE_CELL_PREVIEW_MAX_CHARS)}…` : oceanBaseDateOnly; + } const normalized = normalizeDateTimeString(val); return normalized.length > TABLE_CELL_PREVIEW_MAX_CHARS ? `${normalized.slice(0, TABLE_CELL_PREVIEW_MAX_CHARS)}…` : normalized; } @@ -320,12 +368,16 @@ export const formatCellDisplayText = (val: any, columnType?: string): string => } }; -const formatClipboardCellText = (val: any, columnType?: string): string => { +const formatClipboardCellText = (val: any, columnType?: string, connectionConfig?: CellDisplayConnectionLike): string => { try { if (val === null || val === undefined) return 'NULL'; const bitText = normalizeBitHexDisplayText(val, columnType); if (bitText !== null) return bitText; - if (typeof val === 'string') return normalizeDateTimeString(val); + if (typeof val === 'string') { + const oceanBaseDateOnly = normalizeOceanBaseOracleDateDisplayText(val, columnType, connectionConfig); + if (oceanBaseDateOnly !== null) return oceanBaseDateOnly; + return normalizeDateTimeString(val); + } if (typeof val === 'object') { try { return JSON.stringify(val); @@ -346,6 +398,7 @@ const buildClipboardTsv = ( rows: Array>, columnNames: string[], getColumnType?: (columnName: string) => string | undefined, + connectionConfig?: CellDisplayConnectionLike, ): string => { if (!Array.isArray(rows) || rows.length === 0 || !Array.isArray(columnNames) || columnNames.length === 0) { return ''; @@ -353,7 +406,7 @@ const buildClipboardTsv = ( const header = columnNames.map(normalizeClipboardTsvCell).join('\t'); const lines = rows.map((row) => ( columnNames - .map((columnName) => normalizeClipboardTsvCell(formatClipboardCellText(row?.[columnName], getColumnType?.(columnName)))) + .map((columnName) => normalizeClipboardTsvCell(formatClipboardCellText(row?.[columnName], getColumnType?.(columnName), connectionConfig))) .join('\t') )); return [header, ...lines].join('\n'); @@ -382,8 +435,8 @@ const renderHighlightedCellText = (text: string, query: string): React.ReactNode return <>{nodes}; }; -const renderCellDisplayValue = (val: any, query: string, columnType?: string): React.ReactNode => { - const text = formatCellDisplayText(val, columnType); +const renderCellDisplayValue = (val: any, query: string, columnType?: string, connectionConfig?: CellDisplayConnectionLike): React.ReactNode => { + const text = formatCellDisplayText(val, columnType, connectionConfig); const content = renderHighlightedCellText(text, query); if (val === null) return {content}; return content; @@ -778,6 +831,7 @@ interface EditableCellProps { focusCell?: (record: Item, dataIndex: string, title: React.ReactNode) => void; columnType?: string; dbType?: string; + connectionConfig?: CellDisplayConnectionLike; inputCellPadding?: React.CSSProperties; as?: any; modifiedColumns?: Record>; @@ -828,6 +882,9 @@ const areEditableCellPropsEqual = (prevProps: EditableCellProps, nextProps: Edit if (prevProps.title !== nextProps.title) return false; if (prevProps.columnType !== nextProps.columnType) return false; if (prevProps.dbType !== nextProps.dbType) return false; + if ((prevProps.connectionConfig?.type ?? null) !== (nextProps.connectionConfig?.type ?? null)) return false; + if ((prevProps.connectionConfig?.driver ?? null) !== (nextProps.connectionConfig?.driver ?? null)) return false; + if ((prevProps.connectionConfig?.oceanBaseProtocol ?? null) !== (nextProps.connectionConfig?.oceanBaseProtocol ?? null)) return false; if (prevProps.darkMode !== nextProps.darkMode) return false; if (prevProps.as !== nextProps.as) return false; if (prevProps.handleSave !== nextProps.handleSave) return false; @@ -866,6 +923,7 @@ const EditableCell: React.FC = React.memo(({ focusCell, columnType, dbType, + connectionConfig, inputCellPadding, as: Component = 'td', modifiedColumns, @@ -953,7 +1011,7 @@ const EditableCell: React.FC = React.memo(({ let childNode = children; - const pickerType = getTemporalPickerType(columnType, dbType); + const pickerType = getTemporalPickerType(columnType, dbType, connectionConfig); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[dataIndex] || ''))); const isRowDeleted = deletedRowKeys && rowKeyStr && record?.[GONAVI_ROW_KEY] !== undefined @@ -988,7 +1046,7 @@ const EditableCell: React.FC = React.memo(({ style={{ width: '100%' }} showTime showNow={false} - format={TEMPORAL_FORMATS[pickerType]} + format={getTemporalPickerFormat(pickerType)} renderExtraFooter={() => ( void; onSort?: (field: string, order: string) => void; onPageChange?: (page: number, size: number) => void; @@ -1499,7 +1558,7 @@ const DataGrid: React.FC = ({ resultExportAllSql, onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter, appliedFilterConditions, quickWhereCondition, onApplyQuickWhereCondition, - scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions + scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions, showRowNumberColumn = false }) => { const connections = useStore(state => state.connections); const addTab = useStore(state => state.addTab); @@ -4448,7 +4507,7 @@ const DataGrid: React.FC = ({ } const columnType = (columnMetaMap[dataIndex] || columnMetaMapByLowerName[dataIndex.toLowerCase()])?.type; - const pickerType = getTemporalPickerType(columnType, dbType); + const pickerType = getTemporalPickerType(columnType, dbType, currentConnConfig); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(raw || ''))); const fieldName = getCellFieldName(record, dataIndex); if (isDateTimeField) { @@ -4463,7 +4522,7 @@ const DataGrid: React.FC = ({ title, columnType, }); - }, [canModifyData, columnMetaMap, columnMetaMapByLowerName, dbType, form, openCellEditor, rowKeyStr]); + }, [canModifyData, columnMetaMap, columnMetaMapByLowerName, currentConnConfig, dbType, form, openCellEditor, rowKeyStr]); const handleVirtualCellActivate = useCallback((record: Item, dataIndex: string, title: React.ReactNode) => { if (!canModifyData) return; @@ -4593,7 +4652,7 @@ const DataGrid: React.FC = ({ return; } - const pickerType = getTemporalPickerType(editingCell.columnType, dbType); + const pickerType = getTemporalPickerType(editingCell.columnType, dbType, currentConnConfig); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[editingCell.dataIndex] || ''))); const fieldName = getCellFieldName(record, editingCell.dataIndex); try { @@ -4612,22 +4671,30 @@ const DataGrid: React.FC = ({ closeVirtualInlineEditor(); } } - }, [closeVirtualInlineEditor, dbType, form, handleCellSave, virtualEditingCell]); + }, [closeVirtualInlineEditor, currentConnConfig, dbType, form, handleCellSave, virtualEditingCell]); const pageFindMatches = useMemo(() => collectDataGridFindMatches( mergedDisplayData, displayColumnNames, normalizedPageFindText, - (value, _row, columnName) => formatCellDisplayText(value, (columnMetaMap[columnName] || columnMetaMapByLowerName[columnName.toLowerCase()])?.type), + (value, _row, columnName) => formatCellDisplayText( + value, + (columnMetaMap[columnName] || columnMetaMapByLowerName[columnName.toLowerCase()])?.type, + currentConnConfig, + ), (row, rowIndex) => String(row?.[GONAVI_ROW_KEY] ?? `row-${rowIndex}`), - ), [mergedDisplayData, displayColumnNames, normalizedPageFindText, columnMetaMap, columnMetaMapByLowerName]); + ), [mergedDisplayData, displayColumnNames, normalizedPageFindText, columnMetaMap, columnMetaMapByLowerName, currentConnConfig]); const pageFindSummary = useMemo(() => summarizeDataGridFindMatches( mergedDisplayData, displayColumnNames, normalizedPageFindText, - (value, _row, columnName) => formatCellDisplayText(value, (columnMetaMap[columnName] || columnMetaMapByLowerName[columnName.toLowerCase()])?.type), - ), [mergedDisplayData, displayColumnNames, normalizedPageFindText, columnMetaMap, columnMetaMapByLowerName]); + (value, _row, columnName) => formatCellDisplayText( + value, + (columnMetaMap[columnName] || columnMetaMapByLowerName[columnName.toLowerCase()])?.type, + currentConnConfig, + ), + ), [mergedDisplayData, displayColumnNames, normalizedPageFindText, columnMetaMap, columnMetaMapByLowerName, currentConnConfig]); useEffect(() => { setActivePageFindMatchIndex(-1); @@ -4734,7 +4801,7 @@ const DataGrid: React.FC = ({ displayMap[col] = toFormText(displayVal); // 日期时间类型: 将字符串值转为 dayjs 对象供 DatePicker 使用 const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()]; - const rowPickerType = getTemporalPickerType(colMeta?.type, dbType); + const rowPickerType = getTemporalPickerType(colMeta?.type, dbType, currentConnConfig); if (rowPickerType && displayVal !== null && displayVal !== undefined) { const dVal = parseToDayjs(displayVal, rowPickerType); formMap[col] = dVal; @@ -4751,7 +4818,7 @@ const DataGrid: React.FC = ({ nullCols, formValues: formMap, }); - }, [canModifyData, mergedDisplayData, data, addedRows, visibleColumnNames, rowKeyStr, columnMetaMap, columnMetaMapByLowerName, dbType, openRowEditor]); + }, [addedRows, canModifyData, columnMetaMap, columnMetaMapByLowerName, currentConnConfig, data, dbType, mergedDisplayData, openRowEditor, rowKeyStr, visibleColumnNames]); const openCurrentViewRowEditor = useCallback(() => { if (!canModifyData) return; @@ -4916,7 +4983,7 @@ const DataGrid: React.FC = ({ if (!isWritableResultColumn(col, effectiveEditLocator)) return; if (val && dayjs.isDayjs(val)) { const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()]; - const rowPickerType = getTemporalPickerType(colMeta?.type, dbType); + const rowPickerType = getTemporalPickerType(colMeta?.type, dbType, currentConnConfig); convertedValues[col] = formatFromDayjs(val as dayjs.Dayjs, rowPickerType); } else { convertedValues[col] = val; @@ -4935,7 +5002,7 @@ const DataGrid: React.FC = ({ // 日期时间类型: 将 dayjs 对象转回格式化字符串 if (nextVal && dayjs.isDayjs(nextVal)) { const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()]; - const rowPickerType = getTemporalPickerType(colMeta?.type, dbType); + const rowPickerType = getTemporalPickerType(colMeta?.type, dbType, currentConnConfig); nextVal = formatFromDayjs(nextVal as dayjs.Dayjs, rowPickerType); } const baseVal = baseRawMap[col]; @@ -4950,7 +5017,7 @@ const DataGrid: React.FC = ({ }); closeRowEditor(); - }, [rowEditorRowKey, rowEditorForm, addedRows, visibleColumnNames, rowKeyStr, closeRowEditor, effectiveEditLocator, columnMetaMap, columnMetaMapByLowerName, dbType]); + }, [addedRows, closeRowEditor, columnMetaMap, columnMetaMapByLowerName, currentConnConfig, dbType, effectiveEditLocator, rowEditorForm, rowEditorRowKey, rowKeyStr, visibleColumnNames]); const enableVirtual = isTableSurfaceActive; @@ -4985,7 +5052,7 @@ const DataGrid: React.FC = ({ sortOrder: (sortInfo.find(s => s.columnKey === key && s.enabled !== false)?.order || null) as SortOrder | undefined, editable: canModifyData && isWritableResultColumn(key, effectiveEditLocator), render: (text: any) => { - const renderedContent = renderCellDisplayValue(text, normalizedPageFindText, displayColumnTypeMap[key]); + const renderedContent = renderCellDisplayValue(text, normalizedPageFindText, displayColumnTypeMap[key], currentConnConfig); if (enableVirtual) { return renderedContent; } @@ -5037,7 +5104,7 @@ const DataGrid: React.FC = ({ }, }), })); - }, [displayColumnNames, columnWidths, sortInfo, handleResizeStart, handleResizeAutoFit, isV2Ui, showColumnHeaderContextMenu, canModifyData, onSort, renderColumnTitle, dataTableDensity, normalizedPageFindText, displayColumnTypeMap, enableVirtual, showColumnComment, showColumnType]); + }, [canModifyData, columnWidths, currentConnConfig, dataTableDensity, displayColumnNames, displayColumnTypeMap, enableVirtual, handleResizeAutoFit, handleResizeStart, isV2Ui, normalizedPageFindText, onSort, renderColumnTitle, showColumnComment, showColumnHeaderContextMenu, showColumnType, sortInfo]); const mergedColumns = useMemo(() => columns.map((col): ColumnType => { const dataIndex = String(col.dataIndex); @@ -5067,6 +5134,7 @@ const DataGrid: React.FC = ({ cellProps.focusCell = openCellEditor; cellProps.columnType = displayColumnTypeMap[dataIndex]; cellProps.dbType = dbType; + cellProps.connectionConfig = currentConnConfig; cellProps.inputCellPadding = inputCellPadding; cellProps.modifiedColumns = modifiedColumns; cellProps.rowKeyStr = rowKeyStr; @@ -5097,7 +5165,7 @@ const DataGrid: React.FC = ({ : undefined; const shouldUsePlainVirtualContent = isV2Ui && !modifiedStyle; if (enableVirtual && enableInlineEditableCell) { - const pickerType = getTemporalPickerType(columnType, dbType); + const pickerType = getTemporalPickerType(columnType, dbType, currentConnConfig); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[dataIndex] || ''))); const virtualCellStyle = modifiedStyle ? { ...virtualCellWrapperStyle, ...modifiedStyle } : virtualCellWrapperStyle; const virtualEditable = !!col.editable && !rowDeletedForRender; @@ -5126,7 +5194,7 @@ const DataGrid: React.FC = ({ style={{ width: '100%' }} showTime showNow={false} - format={TEMPORAL_FORMATS[pickerType]} + format={getTemporalPickerFormat(pickerType)} renderExtraFooter={() => ( = ({ return originalRenderContent; } }; - }), [columns, useInlineEditableBodyCell, enableInlineEditableCell, enableVirtual, handleCellSave, openCellEditor, handleVirtualCellActivate, handleSharedCellContextMenu, displayColumnTypeMap, dbType, inputCellPadding, virtualCellWrapperStyle, modifiedColumns, rowKeyStr, deletedRowKeys, darkMode, virtualEditingCell, form, saveVirtualInlineEditor, lockVirtualInlineTableScroll, closeVirtualInlineEditor, updateFocusedCell]); + }), [closeVirtualInlineEditor, columns, currentConnConfig, darkMode, dbType, deletedRowKeys, displayColumnTypeMap, enableInlineEditableCell, enableVirtual, form, handleCellSave, handleSharedCellContextMenu, handleVirtualCellActivate, inputCellPadding, lockVirtualInlineTableScroll, modifiedColumns, openCellEditor, rowKeyStr, saveVirtualInlineEditor, updateFocusedCell, useInlineEditableBodyCell, virtualCellWrapperStyle, virtualEditingCell]); + + const rowNumberColumn = useMemo>(() => ({ + title: ( +
+ # +
+ ), + key: GONAVI_ROW_NUMBER_COLUMN_KEY, + dataIndex: GONAVI_ROW_NUMBER_COLUMN_KEY, + width: ROW_NUMBER_COLUMN_WIDTH, + className: 'data-grid-row-number-cell', + align: 'center', + onHeaderCell: () => ({ + style: { + textAlign: 'center' as const, + paddingInline: 0, + verticalAlign: 'middle' as const, + }, + }), + render: (_value: unknown, _record: Item, index: number) => { + const currentPage = Math.max(1, Number(pagination?.current) || 1); + const pageSize = Math.max(1, Number(pagination?.pageSize) || 0); + const offset = pageSize > 0 ? (currentPage - 1) * pageSize : 0; + return ( + + {offset + index + 1} + + ); + }, + }), [pagination?.current, pagination?.pageSize]); + + const tableColumns = useMemo( + () => (showRowNumberColumn ? [rowNumberColumn, ...mergedColumns] : mergedColumns), + [mergedColumns, rowNumberColumn, showRowNumberColumn] + ); const handleAddRow = () => { const newKey = `new-${Date.now()}`; @@ -5529,10 +5648,10 @@ const DataGrid: React.FC = ({ const columnType = (columnMetaMap[normalizedColumnName] || columnMetaMapByLowerName[normalizedColumnName.toLowerCase()])?.type; const text = mergedDisplayData - .map((row) => normalizeClipboardTsvCell(formatClipboardCellText(row?.[normalizedColumnName], columnType))) + .map((row) => normalizeClipboardTsvCell(formatClipboardCellText(row?.[normalizedColumnName], columnType, currentConnConfig))) .join('\n'); copyToClipboard(text); - }, [columnMetaMap, columnMetaMapByLowerName, copyToClipboard, displayOutputColumnNames, mergedDisplayData]); + }, [columnMetaMap, columnMetaMapByLowerName, copyToClipboard, currentConnConfig, displayOutputColumnNames, mergedDisplayData]); const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeaderContextMenuActionKey) => { const columnName = resolveContextMenuFieldName(cellContextMenu.dataIndex, cellContextMenu.title); @@ -5872,13 +5991,14 @@ const DataGrid: React.FC = ({ rows, columns, (columnName) => (columnMetaMap[columnName] || columnMetaMapByLowerName[columnName.toLowerCase()])?.type, + currentConnConfig, ); if (!text) { void message.info('当前行没有可复制内容'); return; } copyToClipboard(text); - }, [columnMetaMap, columnMetaMapByLowerName, copyToClipboard, displayOutputColumnNames, getContextMenuTargetRows]); + }, [columnMetaMap, columnMetaMapByLowerName, copyToClipboard, currentConnConfig, displayOutputColumnNames, getContextMenuTargetRows]); const buildConnConfig = useCallback(() => { if (!connectionId) return null; @@ -6435,7 +6555,7 @@ const DataGrid: React.FC = ({ const rowPropsFactory = useCallback((record: any) => ({ record } as any), []); - const totalWidth = columns.reduce((sum: number, col: any) => sum + (Number(col.width) || densityParams.defaultColumnWidth), 0) + selectionColumnWidth; + const totalWidth = tableColumns.reduce((sum: number, col: any) => sum + (Number(col.width) || densityParams.defaultColumnWidth), 0) + selectionColumnWidth; const useContextMenuRow = false; const tableScrollX = useMemo(() => { // rc-table 在 scroll.x 小于容器宽度时会把实际列宽按视口补齐。 @@ -7330,6 +7450,14 @@ const DataGrid: React.FC = ({ }); }, [pagination, supportsApproximateTotalPages]); + const paginationHasKnownTotalPages = useMemo(() => { + if (!pagination) return false; + if (pagination.totalKnown !== false) return true; + if (!supportsApproximateTotalPages || !pagination.totalApprox) return false; + const approximateTotal = Number(pagination.approximateTotal); + return Number.isFinite(approximateTotal) && approximateTotal > 0; + }, [pagination, supportsApproximateTotalPages]); + const paginationTotalPages = useMemo(() => { if (!pagination) return 1; if (!Number.isFinite(paginationControlTotal) || paginationControlTotal <= 0) { @@ -7361,6 +7489,14 @@ const DataGrid: React.FC = ({ supportsApproximateTotalPages, ]); + const paginationPageText = useMemo(() => { + if (!pagination) return ''; + return resolvePaginationPageText({ + pagination, + supportsApproximateTotalPages, + }); + }, [pagination, supportsApproximateTotalPages]); + const handlePageSizeChange = useCallback((value: string) => { if (!pagination || !onPageChange) return; const nextSize = Number(value); @@ -7412,7 +7548,7 @@ const DataGrid: React.FC = ({ ref={tableRef} components={tableComponents} dataSource={tableRenderData} - columns={mergedColumns} + columns={tableColumns} {...(enableVirtual && typeof virtualListItemHeight === 'number' ? { listItemHeight: virtualListItemHeight } : {})} @@ -7502,7 +7638,9 @@ const DataGrid: React.FC = ({ paginationSummaryText={paginationSummaryText} paginationControlTotal={paginationControlTotal} paginationTotalPages={paginationTotalPages} + paginationPageText={paginationPageText} paginationPageSizeOptions={paginationPageSizeOptions} + showKnownPageCount={paginationHasKnownTotalPages} onPageChange={onPageChange} onPageSizeChange={handlePageSizeChange} onV2PageStep={handleV2PageStep} @@ -7516,7 +7654,7 @@ const DataGrid: React.FC = ({ const isJson = looksLikeJsonText(sample); const useTextArea = isJson || sample.includes('\n') || sample.length >= 160; const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()]; - const pickerType = getTemporalPickerType(colMeta?.type, dbType); + const pickerType = getTemporalPickerType(colMeta?.type, dbType, currentConnConfig); const isTemporalValue = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(sample || ''))); const isWritable = isWritableResultColumn(col, effectiveEditLocator); return { @@ -7530,7 +7668,7 @@ const DataGrid: React.FC = ({ isWritable, }; }) - ), [displayColumnNames, columnMetaMap, columnMetaMapByLowerName, dbType, effectiveEditLocator, rowEditorOpen, rowEditorRowKey]); + ), [columnMetaMap, columnMetaMapByLowerName, currentConnConfig, dbType, displayColumnNames, effectiveEditLocator, rowEditorOpen, rowEditorRowKey]); const handleRefreshGrid = useCallback(() => { clearAutoCommitTimer(); diff --git a/frontend/src/components/DataGridPaginationBar.tsx b/frontend/src/components/DataGridPaginationBar.tsx index 09ccbdd..4cb0faa 100644 --- a/frontend/src/components/DataGridPaginationBar.tsx +++ b/frontend/src/components/DataGridPaginationBar.tsx @@ -20,7 +20,9 @@ export interface DataGridPaginationBarProps { paginationSummaryText: string; paginationControlTotal: number; paginationTotalPages: number; + paginationPageText: string; paginationPageSizeOptions: string[]; + showKnownPageCount: boolean; onPageChange?: (page: number, size: number) => void; onPageSizeChange: (value: string) => void; onV2PageStep: (direction: 'previous' | 'next') => void; @@ -33,7 +35,9 @@ const DataGridPaginationBar: React.FC = ({ paginationSummaryText, paginationControlTotal, paginationTotalPages, + paginationPageText, paginationPageSizeOptions, + showKnownPageCount, onPageChange, onPageSizeChange, onV2PageStep, @@ -58,7 +62,7 @@ const DataGridPaginationBar: React.FC = ({ if (normalizedJumpPage === pagination.current) return; onPageChange(normalizedJumpPage, pagination.pageSize); }; - const jumpPageControl = ( + const jumpPageControl = showKnownPageCount ? (
跳页 = ({ 跳
- ); + ) : null; return (
= ({ onClick={() => onV2PageStep('previous')} />
- {pagination.current} - / - {paginationTotalPages} + {showKnownPageCount ? ( + <> + {pagination.current} + / + {paginationTotalPages} + + ) : ( + {paginationPageText} + )}