@@ -23,7 +23,7 @@ import {
arrayMove
} from '@dnd-kit/sortable' ;
import { CSS } from '@dnd-kit/utilities' ;
import { ImportData , ExportData , ExportQuery , ApplyChanges , DBGetColumns , DBGetIndexes , DBShowCreateTable } from '../../wailsjs/go/app/App' ;
import { ImportData , ExportTable , ExportData , ExportQuery , ApplyChanges , PreviewChanges , DBGetColumns , DBGetIndexes , DBShowCreateTable } from '../../wailsjs/go/app/App' ;
import ImportPreviewModal from './ImportPreviewModal' ;
import { useStore } from '../store' ;
import type { ColumnDefinition , IndexDefinition } from '../types' ;
@@ -34,8 +34,8 @@ import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues
import { getDataSourceCapabilities , resolveDataSourceType } from '../utils/dataSourceCapabilities' ;
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig' ;
import {
getDensityParams ,
resolveDataTableColumnWidth ,
resolveDataTableDefaultColumnWidth ,
resolveDataTableVerticalBorderColor ,
} from '../utils/dataGridDisplay' ;
import { resolvePaginationPageText , resolvePaginationSummaryText , resolvePaginationTotalForControl } from '../utils/dataGridPagination' ;
@@ -508,7 +508,7 @@ const sortableHeaderStaticStyles = `
align-items: center;
width: 100%;
height: 100%;
min-height: 44 px;
min-height: var(--gonavi-header-min-height, 40 px) ;
padding: 0 10px;
user-select: none;
cursor: inherit;
@@ -613,10 +613,20 @@ interface EditableCellProps {
handleSave : ( record : Item ) = > void ;
focusCell ? : ( record : Item , dataIndex : string , title : React.ReactNode ) = > void ;
columnType? : string ;
inputCellPadding? : React.CSSProperties ;
as ? : any ;
modifiedColumns? : Record < string , Set < string > > ;
rowKeyStr ? : ( k : React.Key ) = > string ;
deletedRowKeys? : Set < string > ;
darkMode? : boolean ;
[ key : string ] : any ;
}
// 模块级变量:绕过 React 渲染链条,在事件处理器中直接读取最新删除状态。
// EditableCell 内部通过 React.memo 包裹,且 Ant Design rc-table 有多层 memo 缓存,
// 仅靠 props 传递 deletedRowKeys 可能因缓存而不触发重渲染。
let globalDeletedRowKeys : Set < string > = new Set ( ) ;
const EditableCell : React.FC < EditableCellProps > = React . memo ( ( {
title ,
editable ,
@@ -626,7 +636,12 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
handleSave ,
focusCell ,
columnType ,
inputCellPadding ,
as : Component = 'td' ,
modifiedColumns ,
rowKeyStr ,
deletedRowKeys ,
darkMode ,
. . . restProps
} ) = > {
const [ editing , setEditing ] = useState ( false ) ;
@@ -711,6 +726,16 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
const pickerType = getTemporalPickerType ( columnType ) ;
const isDateTimeField = ! ! pickerType && ! ( /^0{4}-0{2}-0{2}/ . test ( String ( record ? . [ dataIndex ] || '' ) ) ) ;
const isRowDeleted = deletedRowKeys && rowKeyStr && record ? . [ GONAVI_ROW_KEY ] !== undefined
? deletedRowKeys . has ( rowKeyStr ( record [ GONAVI_ROW_KEY ] ) )
: false ;
const isModified = ! editing && modifiedColumns && rowKeyStr && record ? . [ GONAVI_ROW_KEY ] !== undefined
? modifiedColumns [ rowKeyStr ( record [ GONAVI_ROW_KEY ] ) ] ? . has ( dataIndex )
: false ;
const modifiedStyle : React.CSSProperties = isModified ? { backgroundColor : darkMode ? 'rgba(255, 214, 102, 0.16)' : '#FFF3B0' } : { } ;
if ( editable ) {
childNode = editing ? (
< Form.Item style = { { margin : 0 } } name = { getCellFieldName ( record , dataIndex ) } >
@@ -772,6 +797,7 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
) : (
< Input
ref = { inputRef }
style = { inputCellPadding }
onPressEnter = { ( ) = > { void save ( ) ; } }
onBlur = { ( ) = > { void save ( ) ; } }
onFocus = { ( e ) = > {
@@ -795,7 +821,7 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
) : (
< div
className = "editable-cell-value-wrap"
style = { { paddingRight : 24 , minHeight : 20 , position : 'relative' } }
style = { { paddingRight : 0 , minHeight : 20 , position : 'relative' , width : '100%' , minWidth : 0 , display : 'flex' , alignItems : 'center' , . . . modifiedStyle } }
onContextMenu = { handleContextMenu }
>
{ children }
@@ -804,7 +830,13 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
} else if ( cellContextMenuContext ) {
// 非编辑模式(只读查询结果)也绑定右键菜单,支持复制为 INSERT/JSON/CSV 等操作
childNode = (
< div onContextMenu = { handleContextMenu } style = { { minHeight : 20 } } >
< div onContextMenu = { handleContextMenu } style = { { minHeight : 20 , display : 'flex' , alignItems : 'center' , width : '100%' , minWidth : 0 , . . . modifiedStyle } } >
{ children }
< / div >
) ;
} else if ( isModified ) {
childNode = (
< div style = { modifiedStyle } >
{ children }
< / div >
) ;
@@ -812,6 +844,11 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
const handleDoubleClick = ( ) = > {
if ( ! editable ) return ;
if ( isRowDeleted ) return ;
// 模块级检查:绕过 React 渲染链条,确保即使组件因 memo 缓存未重渲染也能拿到最新状态
if ( record ? . [ GONAVI_ROW_KEY ] !== undefined
&& rowKeyStr
&& globalDeletedRowKeys . has ( rowKeyStr ( record [ GONAVI_ROW_KEY ] ) ) ) return ;
// 已在编辑态时再次双击不应退出编辑;双击应支持在 Input 内进行全选。
if ( editing ) return ;
const raw = record ? . [ dataIndex ] ;
@@ -1081,8 +1118,7 @@ export const buildDataGridCommitChangeSet = ({
} ;
// P2 性能优化:提取内联 style 对象为模块级常量,避免每次 render 创建新对象
const CELL_ELLIPSIS_STYLE : React.CSSProperties = { overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } ;
const VIRTUAL_CELL_WRAPPER_STYLE : React.CSSProperties = { margin : - 8 , padding : '8px 8px 8px 8px' } ;
const CELL_ELLIPSIS_STYLE : React.CSSProperties = { overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' , minWidth : 0 , width : '100%' } ;
const DataGrid : React.FC < DataGridProps > = ( {
data , columnNames , loading , tableName , exportScope = 'table' , dbName , connectionId , pkColumns = [ ] , editLocator , readOnly = false ,
@@ -1113,8 +1149,17 @@ const DataGrid: React.FC<DataGridProps> = ({
const resolvedAppearance = resolveAppearanceValues ( appearance ) ;
const opacity = normalizeOpacityForPlatform ( resolvedAppearance . opacity ) ;
const showDataTableVerticalBorders = appearance . showDataTableVerticalBorders === true ;
const dataTableColumnWidthMode = appearance . dataTableColumnWidthMode ;
const defaultColumnWidth = resolveDataTableDefaultColumnWidth ( dataTableColumnWidthMode ) ;
const dataTableDensity = appearance . dataTableDensity ;
const densityParams = useMemo ( ( ) = > getDensityParams ( dataTableDensity ) , [ dataTableDensity ] ) ;
const virtualCellWrapperStyle = useMemo < React.CSSProperties > ( ( ) = > ( {
margin : - 8 ,
padding : densityParams.cellPadding ,
display : 'flex' ,
alignItems : 'center' ,
minWidth : 0 ,
} ) , [ densityParams ] ) ;
const headerCellMinHeight = densityParams . headerMinHeight ;
const inputCellPadding : React.CSSProperties = { padding : densityParams.inputCellPadding } ;
const dataTableVerticalBorderColor = resolveDataTableVerticalBorderColor ( {
darkMode ,
visible : showDataTableVerticalBorders ,
@@ -1732,7 +1777,7 @@ const DataGrid: React.FC<DataGridProps> = ({
< span
style = { {
marginTop : 2 ,
fontSize : 11 ,
fontSize : densityParams.metaFontSize ,
color : columnMetaHintColor ,
overflow : 'hidden' ,
textOverflow : 'ellipsis' ,
@@ -1747,7 +1792,7 @@ const DataGrid: React.FC<DataGridProps> = ({
< span
style = { {
marginTop : 2 ,
fontSize : 11 ,
fontSize : densityParams.metaFontSize ,
color : columnMetaHintColor ,
overflow : 'hidden' ,
textOverflow : 'ellipsis' ,
@@ -1771,7 +1816,7 @@ const DataGrid: React.FC<DataGridProps> = ({
< span style = { { display : 'inline-flex' , maxWidth : '100%' } } > { titleNode } < / span >
< / Tooltip >
) ;
} , [ columnMetaHintColor , columnMetaTooltipColor , columnMetaMap , columnMetaMapByLowerName , showColumnComment , showColumnType ] ) ;
} , [ columnMetaHintColor , columnMetaTooltipColor , columnMetaMap , columnMetaMapByLowerName , showColumnComment , showColumnType , densityParams ]) ;
const closeCellEditor = useCallback ( ( ) = > {
setCellEditorOpen ( false ) ;
@@ -1860,8 +1905,8 @@ const DataGrid: React.FC<DataGridProps> = ({
}
. ${ gridId } .ant-table-tbody > tr > td,
. ${ gridId } .ant-table-tbody .ant-table-row > .ant-table-cell,
. ${ gridId } .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { background: transparent !important; border-bottom: 1px solid ${ darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' } !important; border-inline-end: 1px solid ${ dataTableVerticalBorderColor } !important; }
. ${ gridId } .ant-table-thead > tr > th { background: transparent !important; border-bottom: 1px solid ${ darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' } !important; border-inline-end: 1px solid ${ dataTableVerticalBorderColor } !important; }
. ${ gridId } .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { background: transparent !important; border-bottom: 1px solid ${ darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' } !important; border-inline-end: 1px solid ${ dataTableVerticalBorderColor } !important; font-size: ${ densityParams . dataFontSize } px !important; vertical-align: middle !important; }
. ${ gridId } .ant-table-thead > tr > th { background: transparent !important; border-bottom: 1px solid ${ darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' } !important; border-inline-end: 1px solid ${ dataTableVerticalBorderColor } !important; font-size: ${ densityParams . dataFontSize } px !important; }
. ${ gridId } .ant-table-tbody > tr > td:last-child,
. ${ gridId } .ant-table-tbody .ant-table-row > .ant-table-cell:last-child,
. ${ gridId } .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell:last-child,
@@ -1878,16 +1923,39 @@ const DataGrid: React.FC<DataGridProps> = ({
padding-right: 0 !important;
}
. ${ gridId } .ant-table-selection-column {
vertical-align: middle !important;
text-align: center !important;
padding-inline-start: 0 !important;
padding-inline-end: 0 !important;
}
. ${ gridId } .ant-table-selection-column .ant-checkbox-wrapper {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
margin-right: 0 !important;
}
/* 窄表场景下 rc-table 会按视口等比放大选择列宽度,不能再额外锁死 header 宽度;
这里只统一 header/body 的内边距与对齐方式,避免第一列把后续数据列整体顶偏。 */
. ${ gridId } .ant-table-tbody > tr > td.ant-table-selection-column,
. ${ gridId } .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column,
. ${ gridId } .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-selection-column {
. ${ gridId } .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column {
text-align: center !important;
vertical-align: middle !important;
padding-inline-start: 0 !important;
padding-inline-end: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
. ${ gridId } .ant-table-tbody > tr > td.ant-table-selection-column .ant-checkbox-wrapper,
. ${ gridId } .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column .ant-checkbox-wrapper {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
margin-right: 0 !important;
}
. ${ gridId } .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-selection-column {
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding-inline-start: 0 !important;
padding-inline-end: 0 !important;
padding-left: 0 !important;
@@ -1919,10 +1987,14 @@ const DataGrid: React.FC<DataGridProps> = ({
. ${ gridId } .row-added > .ant-table-cell { background-color: ${ rowAddedBg } !important; color: ${ darkMode ? '#e6fffb' : 'inherit' } ; }
. ${ gridId } .row-modified td,
. ${ gridId } .row-modified > .ant-table-cell { background-color: ${ rowModBg } !important; color: ${ darkMode ? '#e6f7ff' : 'inherit' } ; }
. ${ gridId } .row-deleted td,
. ${ gridId } .row-deleted > .ant-table-cell { background-color: ${ darkMode ? '#1f1f1f' : '#f0f0f0' } !important; color: ${ darkMode ? '#595959' : '#bfbfbf' } !important; text-decoration: line-through; }
. ${ gridId } .ant-table-tbody > tr.row-added:hover > td,
. ${ gridId } .ant-table-tbody .ant-table-row.row-added:hover > .ant-table-cell { background-color: ${ rowAddedHover } !important; }
. ${ gridId } .ant-table-tbody > tr.row-modified:hover > td,
. ${ gridId } .ant-table-tbody .ant-table-row.row-modified:hover > .ant-table-cell { background-color: ${ rowModHover } !important; }
. ${ gridId } .ant-table-tbody > tr.row-deleted:hover > td,
. ${ gridId } .ant-table-tbody .ant-table-row.row-deleted:hover > .ant-table-cell { background-color: ${ darkMode ? '#2a2a2a' : '#e8e8e8' } !important; }
. ${ gridId } .ant-table-tbody > tr > td[data-col-name],
. ${ gridId } .ant-table-tbody .ant-table-row > .ant-table-cell[data-col-name] { user-select: none; -webkit-user-select: none; cursor: crosshair; }
. ${ gridId } .ant-table-tbody > tr > td[data-cell-selected="true"],
@@ -2310,7 +2382,7 @@ const DataGrid: React.FC<DataGridProps> = ({
justify-content: center;
line-height: 1;
}
` , [ themeStyles , gridId , tableBodyBottomPadding , darkMode , opacity , dataTableVerticalBorderColor ] ) ;
` , [ themeStyles , gridId , tableBodyBottomPadding , darkMode , opacity , dataTableVerticalBorderColor , densityParams ]) ;
const recalculateTableMetrics = useCallback ( ( targetElement? : HTMLElement | null ) = > {
const target = targetElement || containerRef . current ;
@@ -2387,6 +2459,15 @@ const DataGrid: React.FC<DataGridProps> = ({
const [ addedRows , setAddedRows ] = useState < any [ ] > ( [ ] ) ;
const [ modifiedRows , setModifiedRows ] = useState < Record < string , any > > ( { } ) ;
const [ deletedRowKeys , setDeletedRowKeys ] = useState < Set < string > > ( new Set ( ) ) ;
// 同步到模块级变量,确保 EditableCell 事件处理器始终读取最新删除状态
globalDeletedRowKeys = deletedRowKeys ;
const [ modifiedColumns , setModifiedColumns ] = useState < Record < string , Set < string > > > ( { } ) ;
const [ previewModalOpen , setPreviewModalOpen ] = useState ( false ) ;
const [ previewSqlData , setPreviewSqlData ] = useState < {
deletes : string [ ] ;
updates : string [ ] ;
inserts : string [ ] ;
} > ( { deletes : [ ] , updates : [ ] , inserts : [ ] } ) ;
const normalizeFilterLogic = useCallback ( ( logic : unknown ) : 'AND' | 'OR' = > {
return String ( logic || '' ) . trim ( ) . toUpperCase ( ) === 'OR' ? 'OR' : 'AND' ;
@@ -2496,6 +2577,7 @@ const DataGrid: React.FC<DataGridProps> = ({
setAddedRows ( [ ] ) ;
setModifiedRows ( { } ) ;
setDeletedRowKeys ( new Set ( ) ) ;
setModifiedColumns ( { } ) ;
setSelectedRowKeys ( [ ] ) ;
setCopiedCellPatch ( null ) ;
setCopiedRowsForPaste ( [ ] ) ;
@@ -3087,16 +3169,18 @@ const DataGrid: React.FC<DataGridProps> = ({
} , [ addedRows , rowKeyStr ] ) ;
const displayData = useMemo ( ( ) = > {
return [ . . . data , . . . addedRows ] . filter ( item = > {
const k = item ? . [ GONAVI_ROW_KEY ] ;
return k === undefined ? true : ! deletedRowKeys . has ( rowKeyStr ( k ) ) ;
} ) ;
} , [ data , addedRows , deletedRowKeys ] ) ;
return [ . . . data , . . . addedRows ] ;
} , [ data , addedRows ] ) ;
useEffect ( ( ) = > { displayDataRef . current = displayData ; } , [ displayData ] ) ;
const hasChanges = addedRows . length > 0 || Object . keys ( modifiedRows ) . length > 0 || deletedRowKeys . size > 0 ;
const allSelectedAreDeleted = useMemo ( ( ) = > {
if ( selectedRowKeys . length === 0 ) return false ;
return selectedRowKeys . every ( key = > deletedRowKeys . has ( rowKeyStr ( key ) ) ) ;
} , [ selectedRowKeys , deletedRowKeys , rowKeyStr ] ) ;
const addedRowKeySet = useMemo ( ( ) = > {
const next = new Set < string > ( ) ;
addedRows . forEach ( ( row ) = > {
@@ -3113,7 +3197,8 @@ const DataGrid: React.FC<DataGridProps> = ({
if ( k === undefined || k === null ) return '' ;
const keyStr = rowKeyStr ( k ) ;
if ( addedRowKeySet . has ( keyStr ) ) return 'row-added' ;
if ( modifiedRowKeySet . has ( keyStr ) || deletedRowKeys. has ( keyStr ) ) return 'row-modifi ed' ;
if ( deletedRowKeys . has ( keyStr ) ) return 'row-delet ed' ;
if ( modifiedRowKeySet . has ( keyStr ) ) return 'row-modified' ;
return '' ;
} , [ addedRowKeySet , modifiedRowKeySet , deletedRowKeys , rowKeyStr ] ) ;
@@ -3163,7 +3248,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const currentWidth = resolveDataTableColumnWidth ( {
manualWidth : columnWidths [ key ] ,
widthMode : dataTableColumnWidthMode ,
density : dataTableDensity ,
} ) ;
const containerLeft = containerRef . current ? . getBoundingClientRect ( ) . left ? ? 0 ;
@@ -3195,7 +3280,7 @@ const DataGrid: React.FC<DataGridProps> = ({
document . body . style . userSelect = 'none' ;
} , [ columnWidths , dataTableColumnWidthMode ] ) ;
} , [ columnWidths , dataTableDensity ] ) ;
const measureTextWidth = useCallback ( ( text : string , font : string ) = > {
if ( typeof document === 'undefined' ) {
@@ -3224,6 +3309,29 @@ const DataGrid: React.FC<DataGridProps> = ({
return ( text : string ) = > measureTextWidth ( text , font ) ;
} , [ measureTextWidth ] ) ;
const autoFitDoneRef = useRef < string > ( '' ) ;
useEffect ( ( ) = > {
if ( displayColumnNames . length === 0 || displayData . length === 0 ) return ;
const sig = displayColumnNames . join ( ',' ) ;
if ( autoFitDoneRef . current === sig ) return ;
const font = ` ${ densityParams . dataFontSize } px -apple-system, sans-serif ` ;
const newWidths : Record < string , number > = { } ;
displayColumnNames . forEach ( ( key ) = > {
const autoWidth = calculateAutoFitColumnWidth ( {
headerTexts : [ key ] ,
valueTexts : displayData.slice ( 0 , 200 ) . map ( ( row ) = > row ? . [ key ] ) ,
measureHeaderText : ( t ) = > measureTextWidth ( t , ` 600 ${ font } ` ) ,
measureCellText : ( t ) = > measureTextWidth ( t , ` 400 ${ font } ` ) ,
minWidth : 40 ,
maxWidth : 600 ,
defaultWidth : densityParams.defaultColumnWidth ,
} ) ;
newWidths [ key ] = autoWidth ;
} ) ;
autoFitDoneRef . current = sig ;
setColumnWidths ( ( prev ) = > ( { . . . newWidths , . . . prev } ) ) ;
} , [ displayColumnNames , displayData , densityParams , measureTextWidth ] ) ;
const handleResizeAutoFit = useCallback ( ( key : string ) = > ( e : React.MouseEvent ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
@@ -3241,14 +3349,14 @@ const DataGrid: React.FC<DataGridProps> = ({
const defaultWidth = resolveDataTableColumnWidth ( {
manualWidth : columnWidths [ key ] ,
widthMode : dataTableColumnWidthMode ,
density : dataTableDensity ,
} ) ;
const containerWidth = containerRef . current ? . clientWidth ? ? 0 ;
const nextWidth = calculateAutoFitColumnWidth ( {
headerTexts ,
valueTexts : displayDataRef.current.map ( ( row ) = > row ? . [ key ] ) ,
measureHeaderText : buildAutoFitMeasurer ( headerEl , '600 13px sans-serif' ) ,
measureCellText : buildAutoFitMeasurer ( sampleCell ? ? null , '400 13px sans-serif' ) ,
valueTexts : displayDataRef.current.slice ( 0 , 200 ) . map ( ( row ) = > row ? . [ key ] ) ,
measureHeaderText : buildAutoFitMeasurer ( headerEl , ` 600 ${ densityParams . dataFontSize } px -apple-system, sans-serif` ) ,
measureCellText : buildAutoFitMeasurer ( sampleCell ? ? null , ` 400 ${ densityParams . dataFontSize } px -apple-system, sans-serif` ) ,
defaultWidth ,
minWidth : 80 ,
maxWidth : Math.max ( 720 , Math . floor ( containerWidth * 0.85 ) ) ,
@@ -3260,7 +3368,7 @@ const DataGrid: React.FC<DataGridProps> = ({
columnMetaMap ,
columnMetaMapByLowerName ,
columnWidths ,
dataTableColumnWidthMode ,
dataTableDensity ,
showColumnComment ,
showColumnType ,
] ) ;
@@ -3306,35 +3414,53 @@ const DataGrid: React.FC<DataGridProps> = ({
const handleCellSave = useCallback ( ( row : any ) = > {
const rowKey = row ? . [ GONAVI_ROW_KEY ] ;
if ( rowKey === undefined ) return ;
const keyStr = rowKeyStr ( rowKey ) ;
const isAdded = addedRows . some ( r = > r ? . [ GONAVI_ROW_KEY ] === rowKey ) ;
if ( isAdded ) {
setAddedRows ( prev = > prev . map ( r = > r ? . [ GONAVI_ROW_KEY ] === rowKey ? { . . . r , . . . row } : r ) ) ;
} else {
// 查找原始行数据,对比是否真正有值变更
const originalRow = data . find ( r = > r ? . [ GONAVI_ROW_KEY ] === rowKey ) ;
if ( originalRow ) {
const changedFields : Record < str ing , any > = { } ;
for ( const col of Object . keys ( row ) ) {
if ( col === GONAVI_ROW_KEY ) continue ;
if ( ! isCellValueEqualForDiff ( originalRow [ col ] , row [ col ] ) ) {
changedFields [ col ] = row [ col ] ;
}
}
if ( Object . keys ( changedFields ) . length === 0 ) {
// 没有实际变更,从 modifiedRows 中移除该行(如有)
setModifiedRows ( prev = > {
const keyStr = rowKeyStr ( rowKey ) ;
if ( ! ( keyStr in prev ) ) return prev ;
const next = { . . . prev } ;
delete next [ keyStr ] ;
return next ;
} ) ;
return ;
return ;
}
if ( deletedRowKeys . has ( keyStr ) ) return ;
// 查找原始行数据,对比是否真正有值变更
const originalRow = data . f ind ( r = > r ? . [ GONAVI_ROW_KEY ] === rowKey ) ;
i f ( originalRow ) {
const changedFields : Record < string , any > = { } ;
for ( const col of Object . keys ( row ) ) {
if ( col === GONAVI_ROW_KEY ) continue ;
if ( ! isCellValueEqualForDiff ( originalRow [ col ] , row [ col ] ) ) {
changedFields [ col ] = row [ col ] ;
}
}
setModifiedRows ( prev = > ( { . . . prev , [ rowKeyStr ( rowKey ) ] : row } ) ) ;
if ( Object . keys ( changedFields ) . length === 0 ) {
// 没有实际变更,从 modifiedRows 中移除该行
setModifiedRows ( prev = > {
if ( ! ( keyStr in prev ) ) return prev ;
const next = { . . . prev } ;
delete next [ keyStr ] ;
return next ;
} ) ;
// 同时清除该行的 modifiedColumns
setModifiedColumns ( prev = > {
if ( ! ( keyStr in prev ) ) return prev ;
const next = { . . . prev } ;
delete next [ keyStr ] ;
return next ;
} ) ;
return ;
}
// 更新 modifiedColumns: 记录所有变更的列
setModifiedColumns ( prev = > {
const newCols = new Set ( Object . keys ( changedFields ) ) ;
// 如果和之前一样,避免不必要的 state 更新
if ( prev [ keyStr ] && prev [ keyStr ] . size === newCols . size &&
[ . . . newCols ] . every ( c = > prev [ keyStr ] . has ( c ) ) ) {
return prev ;
}
return { . . . prev , [ keyStr ] : newCols } ;
} ) ;
setModifiedRows ( prev = > ( { . . . prev , [ keyStr ] : row } ) ) ;
}
} , [ addedRows , data ] ) ;
} , [ addedRows , data , rowKeyStr ]) ;
const handleDataPanelSave = useCallback ( ( ) = > {
if ( ! focusedCellInfo ) return ;
@@ -3391,12 +3517,19 @@ const DataGrid: React.FC<DataGridProps> = ({
const mergedDisplayData = useMemo ( ( ) = > {
return displayData . map ( row = > {
const k = row ? . [ GONAVI_ROW_KEY ] ;
if ( k !== undefined && modifiedRows [ rowKeyStr( k ) ] ) {
return { . . . row , . . . modifiedRows [ rowKeyStr ( k ) ] } ;
const keyStr = k !== undefined ? rowKeyStr ( k ) : undefined ;
let result = row ;
if ( keyStr !== undefined && modifiedRows [ keyStr ] ) {
result = { . . . row , . . . modifiedRows [ keyStr ] } ;
}
return row ;
if ( keyStr !== undefined && deletedRowKeys . has ( keyStr ) ) {
// 为已删除行创建新对象引用,确保 Ant Design 数据源检测到变化并触发行重渲染
// 仅当 result 尚未被 modifiedRows 分支重新分配时才创建新引用
result = result === row ? { . . . row } : result ;
}
return result ;
} ) ;
} , [ displayData , modifiedRows ] ) ;
} , [ displayData , modifiedRows , deletedRowKeys ]) ;
const pageFindMatches = useMemo ( ( ) = > collectDataGridFindMatches (
mergedDisplayData ,
@@ -3771,7 +3904,7 @@ const DataGrid: React.FC<DataGridProps> = ({
// 不使用 ellipsis, 避免 Ant Design 的 Tooltip 展开行为
width : resolveDataTableColumnWidth ( {
manualWidth : columnWidths [ key ] ,
widthMode : dataTableColumnWidthMode ,
density : dataTableDensity ,
} ) ,
sorter : onSort ? { multiple : displayColumnNames.indexOf ( key ) + 1 } : false ,
sortOrder : ( sortInfo . find ( s = > s . columnKey === key && s . enabled !== false ) ? . order || null ) as SortOrder | undefined ,
@@ -3815,7 +3948,7 @@ const DataGrid: React.FC<DataGridProps> = ({
} ,
} ) ,
} ) ) ;
} , [ displayColumnNames , columnWidths , sortInfo , handleResizeStart , handleResizeAutoFit , canModifyData , onSort , renderColumnTitle , dataTableColumnWidthMode , normalizedPageFindText ] ) ;
} , [ displayColumnNames , columnWidths , sortInfo , handleResizeStart , handleResizeAutoFit , canModifyData , onSort , renderColumnTitle , dataTableDensity , normalizedPageFindText ] ) ;
const mergedColumns = useMemo ( ( ) = > columns . map ( ( col ) : ColumnType < any > = > {
const dataIndex = String ( col . dataIndex ) ;
@@ -3844,9 +3977,15 @@ const DataGrid: React.FC<DataGridProps> = ({
cellProps . handleSave = handleCellSave ;
cellProps . focusCell = openCellEditor ;
cellProps . columnType = ( columnMetaMap [ dataIndex ] || columnMetaMapByLowerName [ dataIndex . toLowerCase ( ) ] ) ? . type ;
cellProps . inputCellPadding = inputCellPadding ;
} else if ( col . editable && ! enableInlineEditableCell ) {
// 可编辑但非 inline( 虚拟模式下) : 双击和右键通过 onCell 绑定
cellProps . onDoubleClick = ( ) = > handleVirtualCellActivate ( record , dataIndex , dataIndex ) ;
// 可编辑但非 inline( 虚拟模式下) : 已删除行不绑定双击处理
const rowDeleted = record ? . [ GONAVI_ROW_KEY ] !== undefined
? deletedRowKeys . has ( rowKeyStr ( record [ GONAVI_ROW_KEY ] ) )
: false ;
if ( ! rowDeleted ) {
cellProps . onDoubleClick = ( ) = > handleVirtualCellActivate ( record , dataIndex , dataIndex ) ;
}
cellProps . onContextMenu = ( e : React.MouseEvent ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
@@ -3860,22 +3999,33 @@ const DataGrid: React.FC<DataGridProps> = ({
showCellContextMenu ( e , record , dataIndex , dataIndex ) ;
} ;
}
cellProps . modifiedColumns = modifiedColumns ;
cellProps . rowKeyStr = rowKeyStr ;
cellProps . deletedRowKeys = deletedRowKeys ;
cellProps . darkMode = darkMode ;
return cellProps ;
} ,
render : ( text : any , record : Item , index : number ) = > {
const originalRenderContent = col . render ? ( col . render as any ) ( text , record , index ) : text ;
const rowDeletedForRender = record ? . [ GONAVI_ROW_KEY ] !== undefined
? deletedRowKeys . has ( rowKeyStr ( record [ GONAVI_ROW_KEY ] ) )
: false ;
if ( enableVirtual && enableInlineEditableCell ) {
return (
< EditableCell
title = { dataIndex }
editable = { ! ! col . editable }
editable = { ! ! col . editable && ! rowDeletedForRender }
dataIndex = { dataIndex }
record = { record }
handleSave = { handleCellSave }
focusCell = { openCellEditor }
columnType = { ( columnMetaMap [ dataIndex ] || columnMetaMapByLowerName [ dataIndex . toLowerCase ( ) ] ) ? . type }
as = "div"
style = { VIRTUAL_CELL_WRAPPER_STYLE }
style = { virtualCellWrapperStyle }
modifiedColumns = { modifiedColumns }
rowKeyStr = { rowKeyStr }
deletedRowKeys = { deletedRowKeys }
darkMode = { darkMode }
>
{ originalRenderContent }
< / EditableCell >
@@ -3884,7 +4034,7 @@ const DataGrid: React.FC<DataGridProps> = ({
if ( enableVirtual ) {
return (
< div
style = { VIRTUAL_CELL_WRAPPER_STYLE }
style = { virtualCellWrapperStyle }
onContextMenu = { ( e ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
@@ -3898,7 +4048,7 @@ const DataGrid: React.FC<DataGridProps> = ({
return originalRenderContent ;
}
} ;
} ) , [ columns , enableInlineEditableCell , enableVirtual , handleCellSave , openCellEditor , handleVirtualCellActivate , showCellContextMenu , columnMetaMap , columnMetaMapByLowerName ] ) ;
} ) , [ columns , enableInlineEditableCell , enableVirtual , handleCellSave , openCellEditor , handleVirtualCellActivate , showCellContextMenu , columnMetaMap , columnMetaMapByLowerName , virtualCellWrapperStyle , modifiedColumns , rowKeyStr , deletedRowKeys , darkMode ]) ;
const handleAddRow = ( ) = > {
const newKey = ` new- ${ Date . now ( ) } ` ;
@@ -3957,14 +4107,96 @@ const DataGrid: React.FC<DataGridProps> = ({
} , [ copiedRowsForPaste , displayOutputColumnNames ] ) ;
const handleDeleteSelected = ( ) = > {
const addedKeysToRemove : string [ ] = [ ] ;
const baseKeysToDelete : string [ ] = [ ] ;
for ( const key of selectedRowKeys ) {
const keyStr = rowKeyStr ( key ) ;
if ( addedRowKeySet . has ( keyStr ) ) {
addedKeysToRemove . push ( keyStr ) ;
} else if ( ! deletedRowKeys . has ( keyStr ) ) {
baseKeysToDelete . push ( keyStr ) ;
}
}
if ( addedKeysToRemove . length > 0 ) {
const removeSet = new Set ( addedKeysToRemove ) ;
setAddedRows ( prev = > prev . filter ( row = > {
const k = row ? . [ GONAVI_ROW_KEY ] ;
return k === undefined || k === null || ! removeSet . has ( rowKeyStr ( k ) ) ;
} ) ) ;
}
if ( baseKeysToDelete . length > 0 ) {
setDeletedRowKeys ( prev = > {
const newDeleted = new Set ( prev ) ;
baseKeysToDelete . forEach ( key = > newDeleted . add ( key ) ) ;
return newDeleted ;
} ) ;
}
setSelectedRowKeys ( [ ] ) ;
} ;
const handleUndoDeleteSelected = ( ) = > {
setDeletedRowKeys ( prev = > {
const newDeleted = new Set ( prev ) ;
selectedRowKeys . forEach ( key = > newDeleted . add ( rowKeyStr ( key ) ) ) ;
selectedRowKeys . forEach ( key = > newDeleted . delete ( rowKeyStr ( key ) ) ) ;
return newDeleted ;
} ) ;
setSelectedRowKeys ( [ ] ) ;
} ;
const handlePreviewChanges = useCallback ( async ( ) = > {
if ( ! connectionId || ! tableName ) return ;
const conn = connections . find ( c = > c . id === connectionId ) ;
if ( ! conn ) return ;
const changeSetResult = buildDataGridCommitChangeSet ( {
addedRows ,
modifiedRows ,
deletedRowKeys ,
data ,
editLocator : effectiveEditLocator ,
visibleColumnNames ,
rowKeyToString : rowKeyStr ,
normalizeCommitCellValue ,
shouldCommitColumn ,
} ) ;
if ( ! changeSetResult . ok ) {
void message . error ( changeSetResult . error || '无法构建变更集' ) ;
return ;
}
const { changes } = changeSetResult ;
const config = {
. . . conn . config ,
port : Number ( conn . config . port ) ,
password : conn.config.password || "" ,
database : conn.config.database || "" ,
useSSH : conn.config.useSSH || false ,
ssh : conn.config.ssh || { host : "" , port : 22 , user : "" , password : "" , keyPath : "" }
} ;
try {
const res = await PreviewChanges ( buildRpcConnectionConfig ( config ) as any , dbName || '' , tableName , {
inserts : changes.inserts ,
updates : changes.updates ,
deletes : changes.deletes ,
locatorStrategy : effectiveEditLocator?.strategy || '' ,
} as any ) ;
if ( res . success ) {
const d = res . data as { deletes : string [ ] ; updates : string [ ] ; inserts : string [ ] } ;
setPreviewSqlData ( {
deletes : d?.deletes || [ ] ,
updates : d?.updates || [ ] ,
inserts : d?.inserts || [ ] ,
} ) ;
setPreviewModalOpen ( true ) ;
} else {
void message . error ( res . message || '生成预览 SQL 失败' ) ;
}
} catch ( e : any ) {
void message . error ( '生成预览 SQL 失败:' + ( e ? . message || e ) ) ;
}
} , [ addedRows , modifiedRows , deletedRowKeys , data , effectiveEditLocator ,
visibleColumnNames , rowKeyStr , normalizeCommitCellValue , shouldCommitColumn ,
connectionId , tableName , connections ] ) ;
const handleCommit = async ( ) = > {
if ( ! connectionId || ! tableName ) return ;
const conn = connections . find ( c = > c . id === connectionId ) ;
@@ -4024,6 +4256,7 @@ const DataGrid: React.FC<DataGridProps> = ({
setAddedRows ( [ ] ) ;
setModifiedRows ( { } ) ;
setDeletedRowKeys ( new Set ( ) ) ;
setModifiedColumns ( { } ) ;
if ( onReload ) onReload ( ) ;
} else {
addSqlLog ( {
@@ -4795,11 +5028,16 @@ const DataGrid: React.FC<DataGridProps> = ({
selectedRowKeys ,
onChange : setSelectedRowKeys ,
columnWidth : selectionColumnWidth ,
renderCell : ( _checked : boolean , _record : any , _index : number , originNode : React.ReactNode ) = > (
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' , width : '100%' , height : '100%' } } >
{ originNode }
< / div >
) ,
} ) , [ selectedRowKeys , selectionColumnWidth ] ) ;
const rowPropsFactory = useCallback ( ( record : any ) = > ( { record } as any ) , [ ] ) ;
const totalWidth = columns . reduce ( ( sum : number , col : any ) = > sum + ( Number ( col . width ) || defaultColumnWidth ) , 0 ) + selectionColumnWidth ;
const totalWidth = columns . reduce ( ( sum : number , col : any ) = > sum + ( Number ( col . width ) || densityParams . defaultColumnWidth) , 0 ) + selectionColumnWidth ;
const useContextMenuRow = false ;
const tableScrollX = useMemo ( ( ) = > {
// rc-table 在 scroll.x 小于容器宽度时会把实际列宽按视口补齐。
@@ -5384,7 +5622,7 @@ const DataGrid: React.FC<DataGridProps> = ({
} , [ pagination , onPageChange ] ) ;
return (
< div className = { ` ${ gridId } ${ cellEditMode ? ' cell-edit-mode' : '' } data-grid-root ` } style = { { flex : '1 1 auto' , height : '100%' , overflow : 'hidden' , padding : 0 , display : 'flex' , flexDirection : 'column' , minHeight : 0 , minWidth : 0 , background : 'transparent' } } >
< div className = { ` ${ gridId } ${ cellEditMode ? ' cell-edit-mode' : '' } data-grid-root ` } style = { { '--gonavi-header-min-height' : ` ${ headerCellMinHeight } px ` , flex : '1 1 auto' , height : '100%' , overflow : 'hidden' , padding : 0 , display : 'flex' , flexDirection : 'column' , minHeight : 0 , minWidth : 0 , background : 'transparent' } as React . CSSProperties } >
{ /* Toolbar + Filter Panel */ }
< div style = { { margin : ` ${ panelOuterGap } px 0 ${ panelOuterGap } px 0 ` , border : ` 1px solid ${ panelFrameColor } ` , borderRadius : ` ${ panelRadius } px ` , background : bgFilter , overflow : 'hidden' , boxSizing : 'border-box' } } >
< div className = "data-grid-toolbar-scroll" data-grid-primary-actions = "true" style = { { padding : showFilter ? ` ${ panelPaddingY } px ${ panelPaddingX } px ${ toolbarBottomPadding } px ${ panelPaddingX } px ` : ` ${ panelPaddingY } px ${ panelPaddingX } px ` , border : 'none' , borderRadius : 0 , background : 'transparent' , display : 'flex' , gap : 8 , alignItems : 'center' , flexWrap : 'nowrap' , minWidth : 0 , overflowX : 'auto' , overflowY : 'hidden' , scrollbarGutter : 'stable' , WebkitOverflowScrolling : 'touch' , boxSizing : 'border-box' } } >
@@ -5426,7 +5664,11 @@ const DataGrid: React.FC<DataGridProps> = ({
>
{ copiedRowsForPaste . length > 0 ? ` 粘贴行 ( ${ copiedRowsForPaste . length } ) ` : '粘贴行' }
< / Button >
< Button icon = { < D eleteOutlined / > } danger disabled = { s elec tedRowKeys . length === 0 } onClick = { handleDeleteSelected } > 删 除 选 中 < / Button >
{ allS elec tedAreD eleted ? (
< Button icon = { < UndoOutlined / > } disabled = { selectedRowKeys . length === 0 } onClick = { handleUndoDeleteSelected } > 撤 销 删 除 < / Button >
) : (
< Button icon = { < DeleteOutlined / > } danger disabled = { selectedRowKeys . length === 0 } onClick = { handleDeleteSelected } > 删 除 选 中 < / Button >
) }
{ selectedRowKeys . length > 0 && < span style = { { fontSize : '12px' , color : '#888' } } > 已 选 { selectedRowKeys . length } < / span > }
< div style = { { width : 1 , background : toolbarDividerColor , height : 20 , margin : '0 8px' } } / >
< Button
@@ -5501,10 +5743,23 @@ const DataGrid: React.FC<DataGridProps> = ({
) }
< div style = { { width : 1 , background : toolbarDividerColor , height : 20 , margin : '0 8px' } } / >
< Button icon = { < SaveOutlined / > } type = "primary" disabled = { ! hasChanges } onClick = { handleCommit } > 提 交 事 务 ( { addedRows . length + Object . keys ( modifiedRows ) . length + deletedRowKeys . size } ) < / Button >
{ hasChanges && (
< Dropdown menu = { { items : [
{
key : 'preview-sql' ,
label : '生成预览 SQL' ,
icon : < ConsoleSqlOutlined / > ,
onClick : handlePreviewChanges ,
}
] } } >
< Button icon = { < ConsoleSqlOutlined / > } > 预 览 SQL < DownOutlined / > < / Button >
< / Dropdown >
) }
{ hasChanges && ( < Button icon = { < UndoOutlined / > } onClick = { ( ) = > {
setAddedRows ( [ ] ) ;
setModifiedRows ( { } ) ;
setDeletedRowKeys ( new Set ( ) ) ;
setModifiedColumns ( { } ) ;
} } > 回 滚 < / Button > ) }
< / >
) }
@@ -6676,6 +6931,96 @@ const DataGrid: React.FC<DataGridProps> = ({
} }
/ >
{ /* Preview SQL Modal */ }
< Modal
title = "变更预览"
open = { previewModalOpen }
onCancel = { ( ) = > setPreviewModalOpen ( false ) }
width = { 800 }
footer = { null }
>
< div style = { { marginBottom : 16 } } >
{ previewSqlData . deletes . length > 0 && (
< div style = { { marginBottom : 12 } } >
< div style = { { fontWeight : 'bold' , color : '#ff4d4f' , marginBottom : 8 } } >
DELETE ( { previewSqlData . deletes . length } )
< / div >
{ previewSqlData . deletes . map ( ( sql , i ) = > (
< div key = { ` del- ${ i } ` } style = { { position : 'relative' , marginBottom : 8 } } >
< pre style = { {
background : darkMode ? 'rgba(255, 77, 79, 0.10)' : '#fff2f0' ,
border : darkMode ? '1px solid rgba(255, 77, 79, 0.25)' : '1px solid #ffccc7' ,
padding : '8px 40px 8px 12px' , borderRadius : 4 ,
fontSize : 12 , whiteSpace : 'pre-wrap' , wordBreak : 'break-all' ,
margin : 0 ,
} } > { sql } < / pre >
< Button
size = "small" type = "text"
icon = { < CopyOutlined / > }
style = { { position : 'absolute' , top : 4 , right : 4 } }
onClick = { ( ) = > { navigator . clipboard . writeText ( sql ) . then ( ( ) = > message . success ( '已复制' ) ) ; } }
/ >
< / div >
) ) }
< / div >
) }
{ previewSqlData . updates . length > 0 && (
< div style = { { marginBottom : 12 } } >
< div style = { { fontWeight : 'bold' , color : '#fa8c16' , marginBottom : 8 } } >
UPDATE ( { previewSqlData . updates . length } )
< / div >
{ previewSqlData . updates . map ( ( sql , i ) = > (
< div key = { ` upd- ${ i } ` } style = { { position : 'relative' , marginBottom : 8 } } >
< pre style = { {
background : darkMode ? 'rgba(250, 140, 22, 0.10)' : '#fff7e6' ,
border : darkMode ? '1px solid rgba(250, 140, 22, 0.25)' : '1px solid #ffd591' ,
padding : '8px 40px 8px 12px' , borderRadius : 4 ,
fontSize : 12 , whiteSpace : 'pre-wrap' , wordBreak : 'break-all' ,
margin : 0 ,
} } > { sql } < / pre >
< Button
size = "small" type = "text"
icon = { < CopyOutlined / > }
style = { { position : 'absolute' , top : 4 , right : 4 } }
onClick = { ( ) = > { navigator . clipboard . writeText ( sql ) . then ( ( ) = > message . success ( '已复制' ) ) ; } }
/ >
< / div >
) ) }
< / div >
) }
{ previewSqlData . inserts . length > 0 && (
< div style = { { marginBottom : 12 } } >
< div style = { { fontWeight : 'bold' , color : '#52c41a' , marginBottom : 8 } } >
INSERT ( { previewSqlData . inserts . length } )
< / div >
{ previewSqlData . inserts . map ( ( sql , i ) = > (
< div key = { ` ins- ${ i } ` } style = { { position : 'relative' , marginBottom : 8 } } >
< pre style = { {
background : darkMode ? 'rgba(82, 196, 26, 0.10)' : '#f6ffed' ,
border : darkMode ? '1px solid rgba(82, 196, 26, 0.25)' : '1px solid #b7eb8f' ,
padding : '8px 40px 8px 12px' , borderRadius : 4 ,
fontSize : 12 , whiteSpace : 'pre-wrap' , wordBreak : 'break-all' ,
margin : 0 ,
} } > { sql } < / pre >
< Button
size = "small" type = "text"
icon = { < CopyOutlined / > }
style = { { position : 'absolute' , top : 4 , right : 4 } }
onClick = { ( ) = > { navigator . clipboard . writeText ( sql ) . then ( ( ) = > message . success ( '已复制' ) ) ; } }
/ >
< / div >
) ) }
< / div >
) }
{ previewSqlData . deletes . length === 0 && previewSqlData . updates . length === 0 && previewSqlData . inserts . length === 0 && (
< div style = { { color : darkMode ? '#888' : '#999' , textAlign : 'center' , padding : 24 } } > 无 变 更 < / div >
) }
< / div >
< div style = { { color : darkMode ? '#999' : '#888' , fontSize : 12 , borderTop : darkMode ? '1px solid #303030' : '1px solid #f0f0f0' , paddingTop : 8 } } >
共 { previewSqlData . deletes . length } 条 DELETE , { previewSqlData . updates . length } 条 UPDATE , { previewSqlData . inserts . length } 条 INSERT
< / div >
< / Modal >
{ /* Import Preview Modal */ }
< ImportPreviewModal
visible = { importPreviewVisible }