import React, { useState, useEffect, useRef } from 'react'; import Editor, { OnMount } from '@monaco-editor/react'; import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select } from 'antd'; import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined } from '@ant-design/icons'; import { format } from 'sql-formatter'; import { TabData, ColumnDefinition } from '../types'; import { useStore } from '../store'; import { MySQLQuery, DBGetTables, DBGetAllColumns, MySQLGetDatabases, DBGetColumns } from '../../wailsjs/go/app/App'; import DataGrid from './DataGrid'; const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { const [query, setQuery] = useState(tab.query || 'SELECT * FROM '); // DataGrid State const [results, setResults] = useState([]); const [columnNames, setColumnNames] = useState([]); const [pkColumns, setPkColumns] = useState([]); const [targetTableName, setTargetTableName] = useState(undefined); const [loading, setLoading] = useState(false); const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); const [saveForm] = Form.useForm(); // Database Selection const [currentConnectionId, setCurrentConnectionId] = useState(tab.connectionId); const [currentDb, setCurrentDb] = useState(tab.dbName || ''); const [dbList, setDbList] = useState([]); // Resizing state const [editorHeight, setEditorHeight] = useState(300); const editorRef = useRef(null); const monacoRef = useRef(null); const dragRef = useRef<{ startY: number, startHeight: number } | null>(null); const tablesRef = useRef([]); // Store tables for autocomplete const allColumnsRef = useRef<{tableName: string, name: string, type: string}[]>([]); // Store all columns const { connections, addSqlLog } = useStore(); const saveQuery = useStore(state => state.saveQuery); const darkMode = useStore(state => state.darkMode); const sqlFormatOptions = useStore(state => state.sqlFormatOptions); const setSqlFormatOptions = useStore(state => state.setSqlFormatOptions); // If opening a saved query, load its SQL useEffect(() => { if (tab.query) setQuery(tab.query); }, [tab.query]); // Fetch Database List useEffect(() => { const fetchDbs = async () => { const conn = connections.find(c => c.id === currentConnectionId); if (!conn) return; 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: "" } }; const res = await MySQLGetDatabases(config as any); if (res.success && Array.isArray(res.data)) { const dbs = res.data.map((row: any) => row.Database || row.database); setDbList(dbs); if (!currentDb) { if (conn.config.database) setCurrentDb(conn.config.database); else if (dbs.length > 0 && dbs[0] !== 'information_schema') setCurrentDb(dbs[0]); } } else { setDbList([]); } }; fetchDbs(); }, [currentConnectionId, connections, currentDb]); // Fetch Metadata for Autocomplete useEffect(() => { const fetchMetadata = async () => { const conn = connections.find(c => c.id === currentConnectionId); if (!conn || !currentDb) return; 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: "" } }; const resTables = await DBGetTables(config as any, currentDb); if (resTables.success && Array.isArray(resTables.data)) { const tableNames = resTables.data.map((row: any) => Object.values(row)[0] as string); tablesRef.current = tableNames; } else { tablesRef.current = []; } if (config.type === 'mysql' || !config.type) { const resCols = await DBGetAllColumns(config as any, currentDb); if (resCols.success && Array.isArray(resCols.data)) { allColumnsRef.current = resCols.data; } else { allColumnsRef.current = []; } } }; fetchMetadata(); }, [currentConnectionId, currentDb, connections]); // Handle Resizing const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); dragRef.current = { startY: e.clientY, startHeight: editorHeight }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; const handleMouseMove = (e: MouseEvent) => { if (!dragRef.current) return; const delta = e.clientY - dragRef.current.startY; const newHeight = Math.max(100, Math.min(window.innerHeight - 200, dragRef.current.startHeight + delta)); setEditorHeight(newHeight); }; const handleMouseUp = () => { dragRef.current = null; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; // Setup Autocomplete and Editor const handleEditorDidMount: OnMount = (editor, monaco) => { editorRef.current = editor; monacoRef.current = monaco; monaco.languages.registerCompletionItemProvider('sql', { provideCompletionItems: (model: any, position: any) => { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn, }; const tableRegex = /(?:FROM|JOIN|UPDATE|INTO)\s+[`"]?(\w+)[`"]?/gi; const foundTables = new Set(); let match; const fullText = model.getValue(); while ((match = tableRegex.exec(fullText)) !== null) { foundTables.add(match[1]); } const relevantColumns = allColumnsRef.current .filter(c => foundTables.has(c.tableName)) .map(c => ({ label: c.name, kind: monaco.languages.CompletionItemKind.Field, insertText: c.name, detail: `${c.type} (${c.tableName})`, range, sortText: '0' + c.name })); const suggestions = [ ...['SELECT', 'FROM', 'WHERE', 'LIMIT', 'INSERT', 'UPDATE', 'DELETE', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'ON', 'GROUP BY', 'ORDER BY', 'AS', 'AND', 'OR', 'NOT', 'NULL', 'IS', 'IN', 'VALUES', 'SET', 'CREATE', 'TABLE', 'DROP', 'ALTER', 'Add', 'MODIFY', 'CHANGE', 'COLUMN', 'KEY', 'PRIMARY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'AUTO_INCREMENT', 'COMMENT', 'SHOW', 'DESCRIBE', 'EXPLAIN'].map(k => ({ label: k, kind: monaco.languages.CompletionItemKind.Keyword, insertText: k, range })), ...tablesRef.current.map(t => ({ label: t, kind: monaco.languages.CompletionItemKind.Class, insertText: t, detail: 'Table', range })), ...relevantColumns ]; return { suggestions }; } }); }; const handleFormat = () => { try { const formatted = format(query, { language: 'mysql', keywordCase: sqlFormatOptions.keywordCase }); setQuery(formatted); } catch (e) { message.error("格式化失败: SQL 语法可能有误"); } }; const formatSettingsMenu: MenuProps['items'] = [ { key: 'upper', label: '关键字大写', icon: sqlFormatOptions.keywordCase === 'upper' ? '✓' : undefined, onClick: () => setSqlFormatOptions({ keywordCase: 'upper' }) }, { key: 'lower', label: '关键字小写', icon: sqlFormatOptions.keywordCase === 'lower' ? '✓' : undefined, onClick: () => setSqlFormatOptions({ keywordCase: 'lower' }) }, ]; const handleRun = async () => { if (!query.trim()) return; if (!currentDb) { message.error("请先选择数据库"); return; } setLoading(true); const conn = connections.find(c => c.id === currentConnectionId); if (!conn) { message.error("Connection not found"); setLoading(false); return; } 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: "" } }; // Detect Simple Table Query let simpleTableName: string | undefined = undefined; let primaryKeys: string[] = []; // Naive regex to detect SELECT * FROM table const tableMatch = query.match(/^\s*SELECT\s+\*\s+FROM\s+[`"]?(\w+)[`"]?\s*(?:WHERE.*)?(?:ORDER BY.*)?(?:LIMIT.*)?$/i); if (tableMatch) { simpleTableName = tableMatch[1]; // Fetch PKs for editing const resCols = await DBGetColumns(config as any, currentDb, simpleTableName); if (resCols.success) { primaryKeys = (resCols.data as ColumnDefinition[]).filter(c => c.key === 'PRI').map(c => c.name); } } setTargetTableName(simpleTableName); setPkColumns(primaryKeys); const startTime = Date.now(); try { const res = await MySQLQuery(config as any, currentDb, query); const duration = Date.now() - startTime; addSqlLog({ id: `log-${Date.now()}-query`, timestamp: Date.now(), sql: query, status: res.success ? 'success' : 'error', duration, message: res.success ? '' : res.message, affectedRows: (res.success && !Array.isArray(res.data)) ? (res.data as any).affectedRows : (Array.isArray(res.data) ? res.data.length : undefined), dbName: currentDb }); if (res.success) { if (Array.isArray(res.data)) { if (res.data.length > 0) { const cols = Object.keys(res.data[0]); setColumnNames(cols); setResults(res.data.map((row: any, i: number) => ({ ...row, key: i }))); } else { message.info('查询执行成功,但没有返回结果。'); setResults([]); setColumnNames([]); } } else { const affected = (res.data as any).affectedRows; message.success(`受影响行数: ${affected}`); setResults([]); setColumnNames([]); } } else { message.error(res.message); } } catch (e: any) { message.error("Error executing query: " + e.message); addSqlLog({ id: `log-${Date.now()}-error`, timestamp: Date.now(), sql: query, status: 'error', duration: Date.now() - startTime, message: e.message, dbName: currentDb }); } setLoading(false); }; const handleSave = async () => { try { const values = await saveForm.validateFields(); saveQuery({ id: tab.id.startsWith('saved-') ? tab.id : `saved-${Date.now()}`, name: values.name, sql: query, connectionId: currentConnectionId, dbName: currentDb || tab.dbName || '', createdAt: Date.now() }); message.success('查询已保存!'); setIsSaveModalOpen(false); } catch (e) { } }; return (
({ label: db, value: db }))} showSearch />
setQuery(val || '')} onMount={handleEditorDidMount} options={{ minimap: { enabled: false }, automaticLayout: true, scrollBeyondLastLine: false, fontSize: 14 }} />
setIsSaveModalOpen(false)} okText="确认" cancelText="取消" >
); }; export default QueryEditor;