import React, { useEffect, useState, useContext, useMemo, useRef, useCallback } from 'react'; import { Table, Tabs, Button, message, Input, Checkbox, Modal, AutoComplete, Tooltip, Select, Empty, Space, Tag, Radio } from 'antd'; import { ReloadOutlined, SaveOutlined, PlusOutlined, DeleteOutlined, MenuOutlined, FileTextOutlined, EyeOutlined, EditOutlined, ExclamationCircleOutlined, CopyOutlined, TableOutlined } from '@ant-design/icons'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import Editor from './MonacoEditor'; import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, TriggerDefinition } from '../types'; import { useStore } from '../store'; import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App'; import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils'; import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql'; import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges, type StarRocksCreateTableOptions, type StarRocksDistributionType, type StarRocksKeyModel, type StarRocksTableKind } from './tableDesignerSchemaSql'; import { normalizeSchemaStatementForExecution, parseTableCommentFromDDL, splitSchemaExecutionStatements } from './tableDesignerExecutionSql'; import TableDesignerSqlPreview from './TableDesignerSqlPreview'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { noAutoCapInputProps } from '../utils/inputAutoCap'; import { getColumnDefinitionExtra, normalizeColumnDefinition, } from '../utils/columnDefinition'; import { isMysqlFamilyDialect as isMysqlFamilySqlDialect, isOracleLikeDialect as isOracleLikeSqlDialect, isPgLikeDialect as isPgLikeSqlDialect, isSqlServerDialect as isSqlServerSqlDialect, quoteSqlIdentifierPart, quoteSqlIdentifierPath, resolveColumnTypeOptions, resolveSqlDialect, } from '../utils/sqlDialect'; import { splitQualifiedNameLast, stripIdentifierQuotes } from '../utils/qualifiedName'; interface EditableColumn extends ColumnDefinition { _key: string; isNew?: boolean; isAutoIncrement?: boolean; // Virtual field for UI } interface IndexDisplayRow { key: string; name: string; indexType: string; nonUnique: number; columnNames: string[]; } interface ForeignKeyDisplayRow { key: string; name: string; constraintName: string; refTableName: string; columnNames: string[]; refColumnNames: string[]; } type IndexKind = 'NORMAL' | 'UNIQUE' | 'PRIMARY' | 'FULLTEXT' | 'SPATIAL'; interface IndexFormState { name: string; columnNames: string[]; kind: IndexKind; indexType: string; } interface ForeignKeyFormState { constraintName: string; columnNames: string[]; refTableName: string; refColumnNames: string[]; } interface SchemaExecutionResult { ok: boolean; message?: string; failedStatementIndex?: number; statementCount: number; } // 通用兜底类型列表 const COMMON_TYPES = [ { value: 'int' }, { value: 'varchar(255)' }, { value: 'text' }, { value: 'datetime' }, { value: 'tinyint(1)' }, { value: 'decimal(10,2)' }, { value: 'bigint' }, { value: 'json' }, ]; // 按数据库方言分组的完整字段类型列表 const DB_TYPE_OPTIONS: Record = { mysql: [ // 数值 { value: 'tinyint' }, { value: 'tinyint(1)' }, { value: 'smallint' }, { value: 'mediumint' }, { value: 'int' }, { value: 'bigint' }, { value: 'float' }, { value: 'double' }, { value: 'decimal(10,2)' }, // 字符串 { value: 'char(50)' }, { value: 'varchar(255)' }, { value: 'tinytext' }, { value: 'text' }, { value: 'mediumtext' }, { value: 'longtext' }, // 二进制 { value: 'binary(255)' }, { value: 'varbinary(255)' }, { value: 'tinyblob' }, { value: 'blob' }, { value: 'mediumblob' }, { value: 'longblob' }, // 日期时间 { value: 'date' }, { value: 'time' }, { value: 'datetime' }, { value: 'timestamp' }, { value: 'year' }, // 其他 { value: 'json' }, { value: 'enum' }, { value: 'set' }, { value: 'bit(1)' }, ], postgres: [ // 数值 { value: 'smallint' }, { value: 'integer' }, { value: 'bigint' }, { value: 'real' }, { value: 'double precision' }, { value: 'numeric(10,2)' }, { value: 'serial' }, { value: 'bigserial' }, // 字符串 { value: 'char(50)' }, { value: 'varchar(255)' }, { value: 'text' }, // 布尔 { value: 'boolean' }, // 日期时间 { value: 'date' }, { value: 'time' }, { value: 'timestamp' }, { value: 'timestamptz' }, { value: 'interval' }, // 二进制 { value: 'bytea' }, // JSON { value: 'json' }, { value: 'jsonb' }, // 其他 { value: 'uuid' }, { value: 'inet' }, { value: 'cidr' }, { value: 'macaddr' }, { value: 'xml' }, { value: 'int4range' }, { value: 'tsquery' }, { value: 'tsvector' }, ], sqlserver: [ // 数值 { value: 'tinyint' }, { value: 'smallint' }, { value: 'int' }, { value: 'bigint' }, { value: 'float' }, { value: 'real' }, { value: 'decimal(10,2)' }, { value: 'numeric(10,2)' }, { value: 'money' }, { value: 'smallmoney' }, // 字符串 { value: 'char(50)' }, { value: 'varchar(255)' }, { value: 'varchar(max)' }, { value: 'nchar(50)' }, { value: 'nvarchar(255)' }, { value: 'nvarchar(max)' }, { value: 'text' }, { value: 'ntext' }, // 日期时间 { value: 'date' }, { value: 'time' }, { value: 'datetime' }, { value: 'datetime2' }, { value: 'datetimeoffset' }, { value: 'smalldatetime' }, // 二进制 { value: 'binary(255)' }, { value: 'varbinary(255)' }, { value: 'varbinary(max)' }, { value: 'image' }, // 其他 { value: 'bit' }, { value: 'uniqueidentifier' }, { value: 'xml' }, ], sqlite: [ { value: 'INTEGER' }, { value: 'REAL' }, { value: 'TEXT' }, { value: 'BLOB' }, { value: 'NUMERIC' }, ], oracle: [ { value: 'NUMBER(10)' }, { value: 'NUMBER(10,2)' }, { value: 'FLOAT' }, { value: 'BINARY_FLOAT' }, { value: 'BINARY_DOUBLE' }, { value: 'CHAR(50)' }, { value: 'VARCHAR2(255)' }, { value: 'NVARCHAR2(255)' }, { value: 'CLOB' }, { value: 'NCLOB' }, { value: 'BLOB' }, { value: 'DATE' }, { value: 'TIMESTAMP' }, { value: 'TIMESTAMP WITH TIME ZONE' }, { value: 'RAW(255)' }, { value: 'LONG RAW' }, { value: 'XMLTYPE' }, ], }; const COMMON_DEFAULTS = [ { value: 'CURRENT_TIMESTAMP' }, { value: 'NULL' }, { value: '0' }, { value: "''" }, ]; const PGLIKE_INDEX_TYPE_OPTIONS = [ { label: '默认', value: 'DEFAULT' }, { label: 'BTREE', value: 'BTREE' }, { label: 'HASH', value: 'HASH' }, { label: 'GIN', value: 'GIN' }, { label: 'GIST', value: 'GIST' }, { label: 'BRIN', value: 'BRIN' }, { label: 'SPGIST', value: 'SPGIST' }, ]; const SQLSERVER_INDEX_TYPE_OPTIONS = [ { label: '默认', value: 'DEFAULT' }, { label: 'CLUSTERED', value: 'CLUSTERED' }, { label: 'NONCLUSTERED', value: 'NONCLUSTERED' }, ]; const CHARSETS = [ { label: 'utf8mb4 (Recommended)', value: 'utf8mb4' }, { label: 'utf8', value: 'utf8' }, { label: 'latin1', value: 'latin1' }, { label: 'ascii', value: 'ascii' }, ]; const COLLATIONS = { 'utf8mb4': [ { label: 'utf8mb4_unicode_ci (Default)', value: 'utf8mb4_unicode_ci' }, { label: 'utf8mb4_general_ci', value: 'utf8mb4_general_ci' }, { label: 'utf8mb4_bin', value: 'utf8mb4_bin' }, { label: 'utf8mb4_0900_ai_ci', value: 'utf8mb4_0900_ai_ci' }, ], 'utf8': [ { label: 'utf8_unicode_ci', value: 'utf8_unicode_ci' }, { label: 'utf8_general_ci', value: 'utf8_general_ci' }, { label: 'utf8_bin', value: 'utf8_bin' }, ] }; // --- Resizable Header Component (Native, same interaction as DataGrid) --- const ResizableTitle = (props: any) => { const { onResizeStart, width, ...restProps } = props; const nextStyle = { ...(restProps.style || {}) } as React.CSSProperties; if (width) { nextStyle.width = width; } if (!onResizeStart) { return ; } return ( {restProps.children} { e.stopPropagation(); if (typeof onResizeStart === 'function') { onResizeStart(e); } }} onClick={(e) => e.stopPropagation()} style={{ position: 'absolute', right: 0, bottom: 0, top: 0, width: 10, cursor: 'col-resize', zIndex: 10, touchAction: 'none', }} /> ); }; // --- Sortable Row Component --- interface RowProps extends React.HTMLAttributes { 'data-row-key': string; } const SortableRow = ({ children, ...props }: RowProps) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: props['data-row-key'], }); const style: React.CSSProperties = { ...props.style, transform: CSS.Transform.toString(transform), transition, cursor: 'move', ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}), }; return ( {React.Children.map(children, child => { if ((child as React.ReactElement).key === 'sort') { return React.cloneElement(child as React.ReactElement, { children: ( ), }); } return child; })} ); }; const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { const isNewTable = !tab.tableName; const [columns, setColumns] = useState([]); const [originalColumns, setOriginalColumns] = useState([]); const [indexes, setIndexes] = useState([]); const [fks, setFks] = useState([]); const [triggers, setTriggers] = useState([]); const [ddl, setDdl] = useState(''); // New Table State const [newTableName, setNewTableName] = useState(''); const [charset, setCharset] = useState('utf8mb4'); const [collation, setCollation] = useState('utf8mb4_unicode_ci'); const [starRocksTableKind, setStarRocksTableKind] = useState('olap'); const [starRocksKeyModel, setStarRocksKeyModel] = useState('DUPLICATE'); const [starRocksKeyColumns, setStarRocksKeyColumns] = useState([]); const [starRocksPartitionClause, setStarRocksPartitionClause] = useState(''); const [starRocksDistributionType, setStarRocksDistributionType] = useState('HASH'); const [starRocksDistributionColumns, setStarRocksDistributionColumns] = useState([]); const [starRocksBucketMode, setStarRocksBucketMode] = useState<'AUTO' | 'NUMBER'>('AUTO'); const [starRocksBucketCount, setStarRocksBucketCount] = useState(''); const [starRocksProperties, setStarRocksProperties] = useState(''); const [starRocksRollups, setStarRocksRollups] = useState(''); const [starRocksExternalEngine, setStarRocksExternalEngine] = useState('hive'); const [starRocksExternalProperties, setStarRocksExternalProperties] = useState('"resource" = "hive0"\n"database" = "raw_db"\n"table" = "raw_table"'); const [loading, setLoading] = useState(false); const [previewSql, setPreviewSql] = useState(''); const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [activeKey, setActiveKey] = useState(tab.initialTab || "columns"); const [selectedColumnRowKeys, setSelectedColumnRowKeys] = useState([]); const [isCopyColumnsModalOpen, setIsCopyColumnsModalOpen] = useState(false); const [copyTableName, setCopyTableName] = useState(''); const [copyCharset, setCopyCharset] = useState('utf8mb4'); const [copyCollation, setCopyCollation] = useState('utf8mb4_unicode_ci'); const [copyExecuting, setCopyExecuting] = useState(false); const [tableComment, setTableComment] = useState(''); const [tableCommentDraft, setTableCommentDraft] = useState(''); const [isTableCommentModalOpen, setIsTableCommentModalOpen] = useState(false); const [tableCommentSaving, setTableCommentSaving] = useState(false); const [selectedIndexKeys, setSelectedIndexKeys] = useState([]); const [isIndexModalOpen, setIsIndexModalOpen] = useState(false); const [indexModalMode, setIndexModalMode] = useState<'create' | 'edit'>('create'); const [indexSaving, setIndexSaving] = useState(false); const [indexForm, setIndexForm] = useState({ name: '', columnNames: [], kind: 'NORMAL', indexType: 'DEFAULT', }); const [selectedForeignKey, setSelectedForeignKey] = useState(null); const [isForeignKeyModalOpen, setIsForeignKeyModalOpen] = useState(false); const [foreignKeyModalMode, setForeignKeyModalMode] = useState<'create' | 'edit'>('create'); const [foreignKeySaving, setForeignKeySaving] = useState(false); const [foreignKeyForm, setForeignKeyForm] = useState({ constraintName: '', columnNames: [], refTableName: '', refColumnNames: [], }); const [selectedTrigger, setSelectedTrigger] = useState(null); const [isTriggerModalOpen, setIsTriggerModalOpen] = useState(false); const [isTriggerEditModalOpen, setIsTriggerEditModalOpen] = useState(false); const [triggerEditMode, setTriggerEditMode] = useState<'create' | 'edit'>('create'); const [triggerEditSql, setTriggerEditSql] = useState(''); const [triggerExecuting, setTriggerExecuting] = useState(false); const [isCommentModalOpen, setIsCommentModalOpen] = useState(false); const [commentEditorColumnKey, setCommentEditorColumnKey] = useState(''); const [commentEditorColumnName, setCommentEditorColumnName] = useState(''); const [commentEditorValue, setCommentEditorValue] = useState(''); const connections = useStore(state => state.connections); const theme = useStore(state => state.theme); const appearance = useStore(state => state.appearance); const darkMode = theme === 'dark'; const isV2Ui = appearance.uiVersion === 'v2'; const resizeGuideColor = darkMode ? '#f6c453' : '#1890ff'; const readOnly = !!tab.readOnly; const designerTableTitle = tab.tableName || newTableName || '未命名表'; const designerDbTitle = tab.dbName || '默认库'; const designerColumnSummary = `${columns.length} 字段`; const panelRadius = 10; const panelFrameColor = darkMode ? 'rgba(0, 0, 0, 0.18)' : 'rgba(0, 0, 0, 0.12)'; const panelToolbarBorder = darkMode ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.10)'; const panelToolbarBg = darkMode ? 'rgba(20, 20, 20, 0.35)' : 'rgba(255, 255, 255, 0.72)'; const panelBodyBg = darkMode ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.82)'; const focusRowBg = darkMode ? 'rgba(246, 196, 83, 0.22)' : 'rgba(24, 144, 255, 0.12)'; const [tableHeight, setTableHeight] = useState(500); const containerRef = useRef(null); const shellRef = useRef(null); const pendingFocusColumnKeyRef = useRef(null); const focusHighlightTimerRef = useRef(null); const [focusColumnKey, setFocusColumnKey] = useState(''); const openCommentEditor = useCallback((record: EditableColumn) => { if (!record?._key) return; setCommentEditorColumnKey(record._key); setCommentEditorColumnName(record.name || ''); setCommentEditorValue(record.comment || ''); setIsCommentModalOpen(true); }, []); const closeCommentEditor = useCallback(() => { setIsCommentModalOpen(false); setCommentEditorColumnKey(''); setCommentEditorColumnName(''); setCommentEditorValue(''); }, []); // 透明 Monaco Editor 主题由 MonacoEditor 包装组件按需注册(含 stickyScroll 不透明背景) // 监听字段 Tab 容器高度,为所有 Tab 内表格计算 scroll.y // 当 Tab 切换时,字段 Tab 被 display:none 导致 height=0,跳过该次更新保持有效值 useEffect(() => { if (!containerRef.current) return; const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const h = entry.contentRect.height; // 跳过零高度观测(Tab 面板被隐藏时) if (h <= 0) return; setTableHeight(Math.max(200, h - 40)); } }); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); }, []); // 不依赖 activeKey,仅挂载一次,通过零高度守卫避免 Tab 切换异常 // --- Resizable Columns State --- const [tableColumns, setTableColumns] = useState([]); const [indexColumns, setIndexColumns] = useState([]); const resizeDragRef = useRef<{ startX: number; startWidth: number; index: number; containerLeft: number; setter: React.Dispatch> } | null>(null); const resizeRafRef = useRef(null); const latestResizeXRef = useRef(null); const ghostRef = useRef(null); const resizeListenerRef = useRef<{ move: ((e: MouseEvent) => void) | null; up: ((e: MouseEvent) => void) | null }>({ move: null, up: null, }); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); useEffect(() => { if (tab.initialTab) { setActiveKey(tab.initialTab); } }, [tab.initialTab]); useEffect(() => { setSelectedColumnRowKeys(prev => prev.filter(key => columns.some(c => c._key === key))); }, [columns]); useEffect(() => { return () => { if (focusHighlightTimerRef.current !== null) { window.clearTimeout(focusHighlightTimerRef.current); } }; }, []); const focusColumnRow = useCallback((targetKey: string): boolean => { if (activeKey !== 'columns') return false; const tableBody = containerRef.current?.querySelector('.ant-table-body') as HTMLElement | null; if (!tableBody) return false; const row = tableBody.querySelector(`tr[data-row-key="${targetKey}"]`) as HTMLTableRowElement | null; if (!row) return false; row.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); setFocusColumnKey(targetKey); if (focusHighlightTimerRef.current !== null) { window.clearTimeout(focusHighlightTimerRef.current); } focusHighlightTimerRef.current = window.setTimeout(() => { setFocusColumnKey(prev => (prev === targetKey ? '' : prev)); }, 1600); if (!readOnly) { const firstInput = row.querySelector('input') as HTMLInputElement | null; if (firstInput) { firstInput.focus(); firstInput.select(); } } return true; }, [activeKey, readOnly]); useEffect(() => { const pendingKey = pendingFocusColumnKeyRef.current; if (!pendingKey || activeKey !== 'columns') return; let cancelled = false; const tryFocus = () => { if (cancelled) return; if (focusColumnRow(pendingKey)) { pendingFocusColumnKeyRef.current = null; } }; const timerA = window.setTimeout(tryFocus, 0); const timerB = window.setTimeout(tryFocus, 96); return () => { cancelled = true; window.clearTimeout(timerA); window.clearTimeout(timerB); }; }, [activeKey, columns, focusColumnRow]); // Initial Columns Definition useEffect(() => { const columnTypeOptions = resolveColumnTypeOptions(getDbType()); const initialCols = [ { title: '名', dataIndex: 'name', key: 'name', width: 180, render: (text: string, record: EditableColumn) => readOnly ? text : ( handleColumnChange(record._key, 'name', e.target.value)} variant="borderless" /> ) }, { title: '类型', dataIndex: 'type', key: 'type', width: 150, render: (text: string, record: EditableColumn) => readOnly ? text : ( handleColumnChange(record._key, 'type', val)} style={{ width: '100%' }} variant="borderless" /> ) }, { title: '主键', dataIndex: 'key', key: 'key', width: 60, align: 'center', render: (text: string, record: EditableColumn) => ( handleColumnChange(record._key, 'key', e.target.checked ? 'PRI' : '')} /> ) }, { title: '自增', dataIndex: 'isAutoIncrement', key: 'isAutoIncrement', width: 60, align: 'center', render: (val: boolean, record: EditableColumn) => ( handleColumnChange(record._key, 'isAutoIncrement', e.target.checked)} /> ) }, { title: '不是 Null', dataIndex: 'nullable', key: 'nullable', width: 80, align: 'center', render: (text: string, record: EditableColumn) => ( handleColumnChange(record._key, 'nullable', e.target.checked ? 'NO' : 'YES')} /> ) }, { title: '默认', dataIndex: 'default', key: 'default', width: 180, // Increased default width render: (text: string, record: EditableColumn) => readOnly ? text : ( handleColumnChange(record._key, 'default', val)} style={{ width: '100%' }} variant="borderless" placeholder="NULL" /> ) }, { title: '注释', dataIndex: 'comment', key: 'comment', width: 200, render: (text: string, record: EditableColumn) => readOnly ? (
{text || ''}
) : (
handleColumnChange(record._key, 'comment', e.target.value)} onDoubleClick={() => openCommentEditor(record)} variant="borderless" />
) }, ...(readOnly ? [] : [{ title: '操作', key: 'action', width: 60, render: (_: any, record: EditableColumn) => ( } {!isNewTable && } {!isNewTable && !readOnly && supportsTableCommentOps() && ( )} {!readOnly && } {!readOnly && ( )} {!readOnly && ( )}
React.startTransition(() => setActiveKey(key))} style={{ flex: 1, minHeight: 0, padding: '8px 10px 10px 10px', borderBottomLeftRadius: panelRadius, borderBottomRightRadius: panelRadius, borderLeft: `1px solid ${panelFrameColor}`, borderRight: `1px solid ${panelFrameColor}`, borderBottom: `1px solid ${panelFrameColor}`, background: panelBodyBg }} items={[ { key: 'columns', label: '字段', children: columnsTabContent }, ...(isStarRocksNewTable ? [ { key: 'starrocks', label: 'StarRocks', children: starRocksAdvancedTabContent, }, ] : []), ...(!isNewTable ? [ { key: 'indexes', label: '索引', children: (
{!readOnly && (
{!supportsIndexSchemaOps() && ( 当前数据库暂不支持索引编辑,仅支持查看 )} {supportsIndexSchemaOps() && selectedIndexKeys.length > 0 && ( 已选择:{selectedIndexKeys.length} 个索引 )}
)}
索引数:{groupedIndexes.length},索引字段:{groupedIndexFieldCount}
({ onClick: () => { toggleIndexSelection(record.key); }, style: { cursor: 'pointer' } })} /> {selectedIndexCreateSql && selectedIndex && (
创建语句:{selectedIndex.name}
)} ) }, { key: 'foreignKeys', label: '外键', children: (
{!readOnly && (
{!supportsForeignKeySchemaOps() && ( 当前数据库暂不支持外键编辑,仅支持查看 )} {supportsForeignKeySchemaOps() && selectedForeignKey && ( 已选择:{selectedForeignKey.constraintName} )}
)}
vals?.length ? vals.join(', ') : '-', }, { title: '参考表', dataIndex: 'refTableName', key: 'refTableName', width: 220 }, { title: '参考字段', dataIndex: 'refColumnNames', key: 'refColumnNames', render: (vals: string[]) => vals?.length ? vals.join(', ') : '-', }, ]} rowKey="key" size="small" pagination={false} loading={loading} scroll={{ x: 980, y: tableHeight }} rowSelection={{ type: 'radio', selectedRowKeys: selectedForeignKey ? [selectedForeignKey.key] : [], onChange: (_, selectedRows) => setSelectedForeignKey((selectedRows[0] as ForeignKeyDisplayRow) || null), }} onRow={(record) => ({ onClick: () => { if (selectedForeignKey?.key === record.key) { setSelectedForeignKey(null); } else { setSelectedForeignKey(record); } }, style: { cursor: 'pointer' } })} /> ) }, { key: 'triggers', label: '触发器', children: (
{selectedTrigger ? `已选择: ${selectedTrigger.name}` : '请点击选择触发器'}
}} rowSelection={{ type: 'radio', selectedRowKeys: selectedTrigger ? [selectedTrigger.name] : [], onChange: (_, selectedRows) => setSelectedTrigger(selectedRows[0] || null), onSelect: (record, selected) => { // 点击单选按钮时,如果已选中则取消 if (selectedTrigger?.name === record.name) { setSelectedTrigger(null); } else { setSelectedTrigger(record); } }, }} onRow={(record) => ({ onClick: () => { // 点击已选中的行时取消选择 if (selectedTrigger?.name === record.name) { setSelectedTrigger(null); } else { setSelectedTrigger(record); } }, style: { cursor: 'pointer' } })} /> ) } ] : []), ...(!isNewTable ? [{ key: 'ddl', label: 'DDL', icon: , children: (
) }] : []) ]} /> { if (commentEditorColumnKey) { handleColumnChange(commentEditorColumnKey, 'comment', commentEditorValue); } closeCommentEditor(); }} okText="应用" cancelText="取消" width={640} destroyOnHidden > setCommentEditorValue(e.target.value)} autoSize={{ minRows: 8, maxRows: 18 }} placeholder="请输入字段注释" maxLength={2000} /> setIsCopyColumnsModalOpen(false)} onOk={handleExecuteCopySelectedColumns} okText="创建新表" cancelText="取消" confirmLoading={copyExecuting} width={560} >
已选择字段:{selectedColumns.length}
setCopyTableName(e.target.value)} maxLength={128} />
setIsTableCommentModalOpen(false)} onOk={handleSaveTableComment} okText="保存" cancelText="取消" confirmLoading={tableCommentSaving} width={640} > setTableCommentDraft(e.target.value)} autoSize={{ minRows: 5, maxRows: 12 }} placeholder="请输入表备注" maxLength={2048} />
当前备注:{tableComment || '(空)'}
setIsIndexModalOpen(false)} onOk={handleSubmitIndex} okText={indexModalMode === 'create' ? '创建' : '保存'} cancelText="取消" confirmLoading={indexSaving} width={620} > setIndexForm(prev => ({ ...prev, name: e.target.value }))} maxLength={128} disabled={indexForm.kind === 'PRIMARY'} /> { const fixedType = getFixedIndexType(val); if (fixedType) { // 固定类型(PRIMARY/FULLTEXT/SPATIAL)直接设置对应的索引方法 setIndexForm(prev => ({ ...prev, kind: val, name: val === 'PRIMARY' ? 'PRIMARY' : (prev.name === 'PRIMARY' ? '' : prev.name), indexType: fixedType, })); } else { const nextTypeOptions = getIndexTypeOptions(val); const currentType = indexForm.indexType || 'DEFAULT'; const isCurrentTypeValid = nextTypeOptions.some(opt => opt.value === currentType); setIndexForm(prev => ({ ...prev, kind: val, name: val === 'PRIMARY' ? 'PRIMARY' : (prev.name === 'PRIMARY' ? '' : prev.name), indexType: isCurrentTypeValid ? currentType : 'DEFAULT', })); } }} style={{ width: 220 }} /> setForeignKeyForm(prev => ({ ...prev, constraintName: e.target.value }))} maxLength={128} /> setForeignKeyForm(prev => ({ ...prev, refTableName: e.target.value }))} maxLength={256} />