import React, { useEffect, useState, useContext, useMemo, useRef, useCallback } from 'react'; import { Table, Tabs, Button, message, Input, Checkbox, Modal, AutoComplete, Tooltip, Select, Empty, Space, Tag } from 'antd'; import { ReloadOutlined, SaveOutlined, PlusOutlined, DeleteOutlined, MenuOutlined, FileTextOutlined, EyeOutlined, EditOutlined, ExclamationCircleOutlined, CopyOutlined } 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, { loader } from '@monaco-editor/react'; import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, TriggerDefinition } from '../types'; import { useStore } from '../store'; import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App'; 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[]; } const COMMON_TYPES = [ { value: 'int' }, { value: 'varchar(255)' }, { value: 'text' }, { value: 'datetime' }, { value: 'tinyint(1)' }, { value: 'decimal(10,2)' }, { value: 'bigint' }, { value: 'json' }, ]; const COMMON_DEFAULTS = [ { value: 'CURRENT_TIMESTAMP' }, { value: 'NULL' }, { value: '0' }, { value: "''" }, ]; const MYSQL_INDEX_TYPE_OPTIONS = [ { label: '默认', value: 'DEFAULT' }, { label: 'BTREE', value: 'BTREE' }, { label: 'HASH', value: 'HASH' }, { label: 'FULLTEXT', value: 'FULLTEXT' }, { label: 'SPATIAL', value: 'SPATIAL' }, { label: 'RTREE', value: 'RTREE' }, ]; 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 (!width) { 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 [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 [selectedIndex, setSelectedIndex] = useState(null); 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 darkMode = theme === 'dark'; const resizeGuideColor = darkMode ? '#f6c453' : '#1890ff'; const readOnly = !!tab.readOnly; 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 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 主题 useEffect(() => { loader.init().then(monaco => { monaco.editor.defineTheme('transparent-dark', { base: 'vs-dark', inherit: true, rules: [], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#ffffff10', 'editorGutter.background': '#00000000', } }); monaco.editor.defineTheme('transparent-light', { base: 'vs', inherit: true, rules: [], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#00000010', 'editorGutter.background': '#00000000', } }); }); }, []); useEffect(() => { if (!containerRef.current) return; const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const h = Math.max(200, entry.contentRect.height - 40); setTableHeight(h); } }); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); }, [activeKey]); // Re-attach when tab switches // --- Resizable Columns State --- const [tableColumns, setTableColumns] = useState([]); const resizeDragRef = useRef<{ startX: number; startWidth: number; index: number; containerLeft: number } | 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 initialCols = [ ...(readOnly ? [] : [{ key: 'sort', width: 40, render: () => , }]), { 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) => (