From 8006844b9fa2f618dbd260ddcbdd4d194439f2af Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 11 Jun 2026 18:27:44 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(datagrid):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20Oracle=20DATE=20=E7=BC=96=E8=BE=91=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Oracle-like DATE 字段按日期时间编辑,保留时分秒 - 普通 date 字段维持纯日期编辑行为 - 补充时间编辑器回归测试 --- frontend/src/components/DataGrid.tsx | 36 ++++++++++--------- .../src/components/dataGridTemporal.test.ts | 18 +++++++++- frontend/src/components/dataGridTemporal.ts | 13 +++---- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index dec0811..7bc71d6 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -777,6 +777,7 @@ interface EditableCellProps { handleSave: (record: Item) => void; focusCell?: (record: Item, dataIndex: string, title: React.ReactNode) => void; columnType?: string; + dbType?: string; inputCellPadding?: React.CSSProperties; as?: any; modifiedColumns?: Record>; @@ -826,6 +827,7 @@ const areEditableCellPropsEqual = (prevProps: EditableCellProps, nextProps: Edit if (prevProps.dataIndex !== nextProps.dataIndex) return false; if (prevProps.title !== nextProps.title) return false; if (prevProps.columnType !== nextProps.columnType) return false; + if (prevProps.dbType !== nextProps.dbType) return false; if (prevProps.darkMode !== nextProps.darkMode) return false; if (prevProps.as !== nextProps.as) return false; if (prevProps.handleSave !== nextProps.handleSave) return false; @@ -863,6 +865,7 @@ const EditableCell: React.FC = React.memo(({ handleSave, focusCell, columnType, + dbType, inputCellPadding, as: Component = 'td', modifiedColumns, @@ -950,7 +953,7 @@ const EditableCell: React.FC = React.memo(({ let childNode = children; - const pickerType = getTemporalPickerType(columnType); + const pickerType = getTemporalPickerType(columnType, dbType); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[dataIndex] || ''))); const isRowDeleted = deletedRowKeys && rowKeyStr && record?.[GONAVI_ROW_KEY] !== undefined @@ -2334,7 +2337,7 @@ const DataGrid: React.FC = ({ if (value === undefined) return undefined; const normalizedName = String(columnName || '').trim(); const meta = columnMetaMap[normalizedName] || columnMetaMapByLowerName[normalizedName.toLowerCase()]; - const temporal = isTemporalColumnType(meta?.type); + const temporal = isTemporalColumnType(meta?.type, dbType); if (!temporal) { return value; @@ -2355,7 +2358,7 @@ const DataGrid: React.FC = ({ return value; }, - [columnMetaMap, columnMetaMapByLowerName] + [columnMetaMap, columnMetaMapByLowerName, dbType] ); const openForeignKeyTarget = useCallback((target: ForeignKeyTarget) => { @@ -4376,7 +4379,7 @@ const DataGrid: React.FC = ({ } const columnType = (columnMetaMap[dataIndex] || columnMetaMapByLowerName[dataIndex.toLowerCase()])?.type; - const pickerType = getTemporalPickerType(columnType); + const pickerType = getTemporalPickerType(columnType, dbType); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(raw || ''))); const fieldName = getCellFieldName(record, dataIndex); if (isDateTimeField) { @@ -4391,7 +4394,7 @@ const DataGrid: React.FC = ({ title, columnType, }); - }, [canModifyData, columnMetaMap, columnMetaMapByLowerName, form, openCellEditor, rowKeyStr]); + }, [canModifyData, columnMetaMap, columnMetaMapByLowerName, dbType, form, openCellEditor, rowKeyStr]); const handleVirtualCellActivate = useCallback((record: Item, dataIndex: string, title: React.ReactNode) => { if (!canModifyData) return; @@ -4521,7 +4524,7 @@ const DataGrid: React.FC = ({ return; } - const pickerType = getTemporalPickerType(editingCell.columnType); + const pickerType = getTemporalPickerType(editingCell.columnType, dbType); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[editingCell.dataIndex] || ''))); const fieldName = getCellFieldName(record, editingCell.dataIndex); try { @@ -4540,7 +4543,7 @@ const DataGrid: React.FC = ({ closeVirtualInlineEditor(); } } - }, [closeVirtualInlineEditor, form, handleCellSave, virtualEditingCell]); + }, [closeVirtualInlineEditor, dbType, form, handleCellSave, virtualEditingCell]); const pageFindMatches = useMemo(() => collectDataGridFindMatches( mergedDisplayData, @@ -4662,7 +4665,7 @@ const DataGrid: React.FC = ({ displayMap[col] = toFormText(displayVal); // 日期时间类型: 将字符串值转为 dayjs 对象供 DatePicker 使用 const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()]; - const rowPickerType = getTemporalPickerType(colMeta?.type); + const rowPickerType = getTemporalPickerType(colMeta?.type, dbType); if (rowPickerType && displayVal !== null && displayVal !== undefined) { const dVal = parseToDayjs(displayVal, rowPickerType); formMap[col] = dVal; @@ -4679,7 +4682,7 @@ const DataGrid: React.FC = ({ nullCols, formValues: formMap, }); - }, [canModifyData, mergedDisplayData, data, addedRows, visibleColumnNames, rowKeyStr, columnMetaMap, columnMetaMapByLowerName, openRowEditor]); + }, [canModifyData, mergedDisplayData, data, addedRows, visibleColumnNames, rowKeyStr, columnMetaMap, columnMetaMapByLowerName, dbType, openRowEditor]); const openCurrentViewRowEditor = useCallback(() => { if (!canModifyData) return; @@ -4844,7 +4847,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); + const rowPickerType = getTemporalPickerType(colMeta?.type, dbType); convertedValues[col] = formatFromDayjs(val as dayjs.Dayjs, rowPickerType); } else { convertedValues[col] = val; @@ -4863,7 +4866,7 @@ const DataGrid: React.FC = ({ // 日期时间类型: 将 dayjs 对象转回格式化字符串 if (nextVal && dayjs.isDayjs(nextVal)) { const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()]; - const rowPickerType = getTemporalPickerType(colMeta?.type); + const rowPickerType = getTemporalPickerType(colMeta?.type, dbType); nextVal = formatFromDayjs(nextVal as dayjs.Dayjs, rowPickerType); } const baseVal = baseRawMap[col]; @@ -4878,7 +4881,7 @@ const DataGrid: React.FC = ({ }); closeRowEditor(); - }, [rowEditorRowKey, rowEditorForm, addedRows, visibleColumnNames, rowKeyStr, closeRowEditor, effectiveEditLocator, columnMetaMap, columnMetaMapByLowerName]); + }, [rowEditorRowKey, rowEditorForm, addedRows, visibleColumnNames, rowKeyStr, closeRowEditor, effectiveEditLocator, columnMetaMap, columnMetaMapByLowerName, dbType]); const enableVirtual = isTableSurfaceActive; @@ -4994,6 +4997,7 @@ const DataGrid: React.FC = ({ cellProps.handleSave = handleCellSave; cellProps.focusCell = openCellEditor; cellProps.columnType = displayColumnTypeMap[dataIndex]; + cellProps.dbType = dbType; cellProps.inputCellPadding = inputCellPadding; cellProps.modifiedColumns = modifiedColumns; cellProps.rowKeyStr = rowKeyStr; @@ -5024,7 +5028,7 @@ const DataGrid: React.FC = ({ : undefined; const shouldUsePlainVirtualContent = isV2Ui && !modifiedStyle; if (enableVirtual && enableInlineEditableCell) { - const pickerType = getTemporalPickerType(columnType); + const pickerType = getTemporalPickerType(columnType, dbType); const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[dataIndex] || ''))); const virtualCellStyle = modifiedStyle ? { ...virtualCellWrapperStyle, ...modifiedStyle } : virtualCellWrapperStyle; const virtualEditable = !!col.editable && !rowDeletedForRender; @@ -5138,7 +5142,7 @@ const DataGrid: React.FC = ({ return originalRenderContent; } }; - }), [columns, useInlineEditableBodyCell, enableInlineEditableCell, enableVirtual, handleCellSave, openCellEditor, handleVirtualCellActivate, handleSharedCellContextMenu, displayColumnTypeMap, inputCellPadding, virtualCellWrapperStyle, modifiedColumns, rowKeyStr, deletedRowKeys, darkMode, virtualEditingCell, form, saveVirtualInlineEditor, lockVirtualInlineTableScroll, closeVirtualInlineEditor, updateFocusedCell]); + }), [columns, useInlineEditableBodyCell, enableInlineEditableCell, enableVirtual, handleCellSave, openCellEditor, handleVirtualCellActivate, handleSharedCellContextMenu, displayColumnTypeMap, dbType, inputCellPadding, virtualCellWrapperStyle, modifiedColumns, rowKeyStr, deletedRowKeys, darkMode, virtualEditingCell, form, saveVirtualInlineEditor, lockVirtualInlineTableScroll, closeVirtualInlineEditor, updateFocusedCell]); const handleAddRow = () => { const newKey = `new-${Date.now()}`; @@ -7439,7 +7443,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); + const pickerType = getTemporalPickerType(colMeta?.type, dbType); const isTemporalValue = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(sample || ''))); const isWritable = isWritableResultColumn(col, effectiveEditLocator); return { @@ -7453,7 +7457,7 @@ const DataGrid: React.FC = ({ isWritable, }; }) - ), [displayColumnNames, columnMetaMap, columnMetaMapByLowerName, effectiveEditLocator, rowEditorOpen, rowEditorRowKey]); + ), [displayColumnNames, columnMetaMap, columnMetaMapByLowerName, dbType, effectiveEditLocator, rowEditorOpen, rowEditorRowKey]); const handleRefreshGrid = useCallback(() => { clearAutoCommitTimer(); diff --git a/frontend/src/components/dataGridTemporal.test.ts b/frontend/src/components/dataGridTemporal.test.ts index 18ae576..99a8e9c 100644 --- a/frontend/src/components/dataGridTemporal.test.ts +++ b/frontend/src/components/dataGridTemporal.test.ts @@ -1,10 +1,26 @@ import dayjs from 'dayjs'; import { describe, expect, it } from 'vitest'; -import { resolveTemporalEditorSaveValue } from './dataGridTemporal'; +import { getTemporalPickerType, resolveTemporalEditorSaveValue } from './dataGridTemporal'; describe('dataGridTemporal helpers', () => { it('prefers the picker selected date when form store has not caught up yet', () => { expect(resolveTemporalEditorSaveValue(undefined, dayjs('2026-04-12'), 'date')).toBe('2026-04-12'); }); + + it('treats Oracle DATE as datetime because the type stores time to seconds', () => { + const pickerType = getTemporalPickerType('DATE', 'oracle'); + + expect(pickerType).toBe('datetime'); + expect(resolveTemporalEditorSaveValue(undefined, dayjs('2026-06-11 19:42:13'), pickerType)) + .toBe('2026-06-11 19:42:13'); + }); + + it('keeps non Oracle DATE columns as date-only values', () => { + const pickerType = getTemporalPickerType('date', 'mysql'); + + expect(pickerType).toBe('date'); + expect(resolveTemporalEditorSaveValue(undefined, dayjs('2026-06-11 19:42:13'), pickerType)) + .toBe('2026-06-11'); + }); }); diff --git a/frontend/src/components/dataGridTemporal.ts b/frontend/src/components/dataGridTemporal.ts index a6a0195..717c99d 100644 --- a/frontend/src/components/dataGridTemporal.ts +++ b/frontend/src/components/dataGridTemporal.ts @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import { isOracleLikeDialect } from '../utils/sqlDialect'; export type TemporalPickerType = 'datetime' | 'date' | 'time' | 'year' | null; @@ -9,20 +10,16 @@ export const TEMPORAL_FORMATS: Record = { year: 'YYYY', }; -export const isTemporalColumnType = (columnType?: string): boolean => { - const raw = String(columnType || '').trim().toLowerCase(); - if (!raw) return false; - if (raw.includes('datetime') || raw.includes('timestamp')) return true; - const base = raw.split(/[ (]/)[0]; - return base === 'date' || base === 'time' || base === 'year'; +export const isTemporalColumnType = (columnType?: string, dbType?: string): boolean => { + return !!getTemporalPickerType(columnType, dbType); }; -export const getTemporalPickerType = (columnType?: string): TemporalPickerType => { +export const getTemporalPickerType = (columnType?: string, dbType?: string): TemporalPickerType => { const raw = String(columnType || '').trim().toLowerCase(); if (!raw) return null; if (raw.includes('datetime') || raw.includes('timestamp')) return 'datetime'; const base = raw.split(/[ (]/)[0]; - if (base === 'date') return 'date'; + if (base === 'date') return isOracleLikeDialect(String(dbType || '')) ? 'datetime' : 'date'; if (base === 'time') return 'time'; if (base === 'year') return 'year'; return null;