diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 8be3b5d..ade6086 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -12,6 +12,14 @@ import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities'; import { convertMongoShellToJsonCommand } from '../utils/mongodb'; import { getShortcutDisplay, isEditableElement, isShortcutMatch } from '../utils/shortcuts'; +const SQL_KEYWORDS = [ + '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', +]; + const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { const [query, setQuery] = useState(tab.query || 'SELECT * FROM '); @@ -62,6 +70,8 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { [connections] ); const addSqlLog = useStore(state => state.addSqlLog); + const addTab = useStore(state => state.addTab); + const savedQueries = useStore(state => state.savedQueries); const currentConnectionIdRef = useRef(currentConnectionId); const currentDbRef = useRef(currentDb); const connectionsRef = useRef(connections); @@ -76,6 +86,18 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { const shortcutOptions = useStore(state => state.shortcutOptions); const activeTabId = useStore(state => state.activeTabId); + const currentSavedQuery = useMemo(() => { + const savedId = String(tab.savedQueryId || '').trim(); + if (savedId) { + return savedQueries.find((item) => item.id === savedId) || null; + } + const tabId = String(tab.id || '').trim(); + if (!tabId) { + return null; + } + return savedQueries.find((item) => item.id === tabId) || null; + }, [savedQueries, tab.id, tab.savedQueryId]); + useEffect(() => { currentConnectionIdRef.current = currentConnectionId; }, [currentConnectionId]); @@ -511,6 +533,17 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { } const currentDatabase = currentDbRef.current || ''; + const wordPrefix = (word.word || '').toLowerCase(); + const startsWithPrefix = (candidate: string) => !wordPrefix || candidate.toLowerCase().startsWith(wordPrefix); + const expectsTableName = /\b(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM|TABLE|DESCRIBE|DESC|EXPLAIN)\s+[`"]?[\w.]*$/i.test(linePrefix.trim()); + const shouldBoostKeywords = !expectsTableName + && wordPrefix.length > 0 + && SQL_KEYWORDS.some((keyword) => keyword.toLowerCase().startsWith(wordPrefix)); + const sortGroups = shouldBoostKeywords + ? { keyword: '00', columnCurrent: '10', columnOther: '11', tableCurrent: '20', tableOther: '21', db: '30' } + : expectsTableName + ? { keyword: '20', columnCurrent: '10', columnOther: '11', tableCurrent: '00', tableOther: '01', db: '30' } + : { keyword: '30', columnCurrent: '00', columnOther: '01', tableCurrent: '10', tableOther: '11', db: '20' }; // 相关列提示:匹配 SQL 中引用的表(FROM/JOIN 等) // 权重最高,输入 WHERE 条件时优先显示 @@ -518,7 +551,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { .filter(c => { const fullIdent = `${c.dbName}.${c.tableName}`.toLowerCase(); const shortIdent = (c.tableName || '').toLowerCase(); - return foundTables.has(fullIdent) || foundTables.has(shortIdent); + return (foundTables.has(fullIdent) || foundTables.has(shortIdent)) && startsWithPrefix(c.name || ''); }) .map(c => { // 当前库的表字段优先级更高 @@ -529,12 +562,18 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { insertText: c.name, detail: `${c.type} (${c.dbName}.${c.tableName})`, range, - sortText: isCurrentDb ? '00' + c.name : '01' + c.name // FROM 表字段最优先 + sortText: isCurrentDb ? sortGroups.columnCurrent + c.name : sortGroups.columnOther + c.name, }; }); // 表提示:当前库显示表名,其他库显示 db.table 格式 - const tableSuggestions = tablesRef.current.map(t => { + const tableSuggestions = tablesRef.current + .filter(t => { + const isCurrentDb = (t.dbName || '').toLowerCase() === currentDatabase.toLowerCase(); + const label = isCurrentDb ? t.tableName : `${t.dbName}.${t.tableName}`; + return startsWithPrefix(label || ''); + }) + .map(t => { const isCurrentDb = (t.dbName || '').toLowerCase() === currentDatabase.toLowerCase(); const label = isCurrentDb ? t.tableName : `${t.dbName}.${t.tableName}`; const insertText = isCurrentDb ? t.tableName : `${t.dbName}.${t.tableName}`; @@ -544,27 +583,31 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { insertText, detail: `Table (${t.dbName})`, range, - sortText: isCurrentDb ? '10' + t.tableName : '11' + t.tableName // 表次优先 + sortText: isCurrentDb ? sortGroups.tableCurrent + t.tableName : sortGroups.tableOther + t.tableName, }; }); // 数据库提示 - const dbSuggestions = visibleDbsRef.current.map(db => ({ - label: db, - kind: monaco.languages.CompletionItemKind.Module, - insertText: db, - detail: 'Database', - range, - sortText: '20' + db // 数据库最后 - })); + const dbSuggestions = visibleDbsRef.current + .filter((db) => startsWithPrefix(db)) + .map(db => ({ + label: db, + kind: monaco.languages.CompletionItemKind.Module, + insertText: db, + detail: 'Database', + range, + sortText: sortGroups.db + db, + })); // 关键字提示 - const keywordSuggestions = ['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 => ({ + const keywordSuggestions = SQL_KEYWORDS + .filter((k) => startsWithPrefix(k)) + .map(k => ({ label: k, kind: monaco.languages.CompletionItemKind.Keyword, insertText: k, range, - sortText: '30' + k // 关键字权重最低 + sortText: sortGroups.keyword + k, })); const suggestions = [ @@ -1425,16 +1468,60 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { }; }, [activeTabId, tab.id, handleRun]); + const resolveDefaultQueryName = () => { + const rawTitle = String(tab.title || '').trim(); + if (!rawTitle || rawTitle.startsWith('新建查询')) { + return '未命名查询'; + } + return rawTitle; + }; + + const persistQuery = (payload: { id: string; name: string; createdAt?: number }) => { + const sql = getCurrentQuery(); + const saved = { + id: payload.id, + name: payload.name, + sql, + connectionId: currentConnectionId, + dbName: currentDb || tab.dbName || '', + createdAt: payload.createdAt ?? Date.now(), + }; + saveQuery(saved); + addTab({ + ...tab, + title: payload.name, + query: sql, + connectionId: currentConnectionId, + dbName: currentDb || tab.dbName || '', + savedQueryId: payload.id, + }); + return saved; + }; + + const handleQuickSave = () => { + const existed = currentSavedQuery || null; + const fallbackSavedId = String(tab.savedQueryId || '').trim(); + const saveId = existed?.id || fallbackSavedId || ''; + if (!saveId) { + saveForm.setFieldsValue({ name: resolveDefaultQueryName() }); + setIsSaveModalOpen(true); + return; + } + const saveName = existed?.name || resolveDefaultQueryName(); + persistQuery({ id: saveId, name: saveName, createdAt: existed?.createdAt }); + message.success('查询已保存!'); + }; + const handleSave = async () => { try { const values = await saveForm.validateFields(); - saveQuery({ - id: tab.id.startsWith('saved-') ? tab.id : `saved-${Date.now()}`, - name: values.name, - sql: getCurrentQuery(), - connectionId: currentConnectionId, - dbName: currentDb || tab.dbName || '', - createdAt: Date.now() + const existed = currentSavedQuery || null; + const fallbackSavedId = String(tab.savedQueryId || '').trim(); + const nextSavedId = existed?.id || fallbackSavedId || `saved-${Date.now()}`; + persistQuery({ + id: nextSavedId, + name: String(values.name || '').trim() || '未命名查询', + createdAt: existed?.createdAt, }); message.success('查询已保存!'); setIsSaveModalOpen(false); @@ -1555,10 +1642,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { )} - diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 99caff9..0ba9a08 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1479,7 +1479,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> type: 'query', connectionId: q.connectionId, dbName: q.dbName, - query: q.sql + query: q.sql, + savedQueryId: q.id, }); return; } else if (node.type === 'redis-db') { @@ -3259,13 +3260,15 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> label: '新建查询', icon: , onClick: () => { + const tableName = String(node.dataRef?.tableName || '').trim(); + const queryTemplate = tableName ? `SELECT * FROM ${tableName};` : 'SELECT * FROM '; addTab({ id: `query-${Date.now()}`, title: `新建查询`, type: 'query', connectionId: node.dataRef.id, dbName: node.dataRef.dbName, - query: '' + query: queryTemplate }); } }, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 96ac6da..662a02c 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -128,6 +128,7 @@ export interface TabData { viewName?: string; // View name for view definition tabs routineName?: string; // Routine name for function/procedure definition tabs routineType?: string; // 'FUNCTION' or 'PROCEDURE' + savedQueryId?: string; // Saved query identity for quick-save behavior } export interface DatabaseNode {