diff --git a/mayfly_go_web/package.json b/mayfly_go_web/package.json index 2ccd21ec..4b52c08d 100644 --- a/mayfly_go_web/package.json +++ b/mayfly_go_web/package.json @@ -10,7 +10,6 @@ "@element-plus/icons-vue": "^2.0.9", "asciinema-player": "^3.0.1", "axios": "^1.1.2", - "codemirror": "^5.65.5", "countup.js": "^2.0.7", "cropperjs": "^1.5.11", "echarts": "^5.3.3", diff --git a/mayfly_go_web/shim.d.ts b/mayfly_go_web/shim.d.ts index dd1cf107..50a3b987 100644 --- a/mayfly_go_web/shim.d.ts +++ b/mayfly_go_web/shim.d.ts @@ -4,7 +4,7 @@ declare module '*.vue' { const component: DefineComponent<{}, {}, any>; export default component; } -declare module 'codemirror'; declare module 'sql-formatter'; declare module 'jsoneditor'; -declare module 'asciinema-player'; \ No newline at end of file +declare module 'asciinema-player'; +declare module 'monaco-editor'; \ No newline at end of file diff --git a/mayfly_go_web/src/components/codemirror/codemirror.vue b/mayfly_go_web/src/components/codemirror/codemirror.vue deleted file mode 100644 index 38959646..00000000 --- a/mayfly_go_web/src/components/codemirror/codemirror.vue +++ /dev/null @@ -1,360 +0,0 @@ - - - - - \ No newline at end of file diff --git a/mayfly_go_web/src/components/codemirror/index.js b/mayfly_go_web/src/components/codemirror/index.js deleted file mode 100644 index e60ca184..00000000 --- a/mayfly_go_web/src/components/codemirror/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import _CodeMirror from 'codemirror' -import codemirror from './codemirror.vue' - -const CodeMirror = window.CodeMirror || _CodeMirror -const install = (Vue, config) => { - if (config) { - if (config.options) { - codemirror.props.globalOptions.default = () => config.options - } - if (config.events) { - codemirror.props.globalEvents.default = () => config.events - } - } - Vue.component(codemirror.name, codemirror) -} - -const VueCodemirror = { CodeMirror, codemirror, install } - -export default VueCodemirror -export { CodeMirror, codemirror, install } \ No newline at end of file diff --git a/mayfly_go_web/src/components/monaco/MonacoEditor.vue b/mayfly_go_web/src/components/monaco/MonacoEditor.vue new file mode 100644 index 00000000..18920622 --- /dev/null +++ b/mayfly_go_web/src/components/monaco/MonacoEditor.vue @@ -0,0 +1,209 @@ + + + + + \ No newline at end of file diff --git a/mayfly_go_web/src/views/ops/db/SqlExec.vue b/mayfly_go_web/src/views/ops/db/SqlExec.vue index 81b5ee9e..ca748fdf 100644 --- a/mayfly_go_web/src/views/ops/db/SqlExec.vue +++ b/mayfly_go_web/src/views/ops/db/SqlExec.vue @@ -87,10 +87,11 @@
- - + + {{ item }} @@ -111,10 +112,12 @@
-
+
- + + +
@@ -129,8 +132,8 @@ + size="small" :max-height="monacoOptions.tableMaxHeight" + empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改" stripe border class="mt5">
- + @@ -254,44 +257,44 @@ import { onMounted, toRefs, reactive, ref, watch } from 'vue'; import { dbApi } from './api'; -import {format as sqlFormatter} from 'sql-formatter'; -import {isTrue, notBlank, notEmpty} from '@/common/assert'; -import {ElMessage, ElMessageBox} from 'element-plus'; +import { format as sqlFormatter } from 'sql-formatter'; +import { isTrue, notBlank, notEmpty } from '@/common/assert'; +import { ElMessage, ElMessageBox } from 'element-plus'; import config from '@/common/config'; -import {getSession} from '@/common/utils/storage'; +import { getSession } from '@/common/utils/storage'; import SqlExecBox from './component/SqlExecBox'; -import {dateStrFormat} from '@/common/utils/date.ts'; -import {useStore} from '@/store/index.ts'; -import {tagApi} from '../tag/api.ts'; +import { dateStrFormat } from '@/common/utils/date.ts'; +import { useStore } from '@/store/index.ts'; +import { tagApi } from '../tag/api.ts'; import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker'; -import {language as sqlLanguage} from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js'; +import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js'; import * as monaco from 'monaco-editor'; -import {editor, languages, Position} from 'monaco-editor'; +import { editor, languages, Position } from 'monaco-editor'; // 主题仓库 https://github.com/brijeshb42/monaco-themes // 主题例子 https://editor.bitwiser.in/ -import Monokai from 'monaco-themes/themes/Monokai.json' -import Active4D from 'monaco-themes/themes/Active4D.json' -import ahe from 'monaco-themes/themes/All Hallows Eve.json' -import bop from 'monaco-themes/themes/Birds of Paradise.json' -import krTheme from 'monaco-themes/themes/krTheme.json' -import Dracula from 'monaco-themes/themes/Dracula.json' -import TextmateMac from 'monaco-themes/themes/Textmate (Mac Classic).json' +// import Monokai from 'monaco-themes/themes/Monokai.json' +// import Active4D from 'monaco-themes/themes/Active4D.json' +// import ahe from 'monaco-themes/themes/All Hallows Eve.json' +// import bop from 'monaco-themes/themes/Birds of Paradise.json' +// import krTheme from 'monaco-themes/themes/krTheme.json' +// import Dracula from 'monaco-themes/themes/Dracula.json' +import SolarizedLight from 'monaco-themes/themes/Solarized-light.json' +// import TextmateMac from 'monaco-themes/themes/Textmate (Mac Classic).json' import { Minus } from '@element-plus/icons-vue'; const store = useStore(); const monacoTextarea: any = ref(null); const token = getSession('token'); const tableMap = new Map(); - const defalutLimit = 20 -export type TableMeta = { +type TableMeta = { // 表名 - tableName:string, + tableName: string, // 表注释 - tableComment:string + tableComment: string } const state = reactive({ token: token, @@ -317,16 +320,16 @@ const state = reactive({ // 点击执行按钮执行结果信息 execRes: { data: [], - tableColumn: [] + tableColumn: [] }, loading: false, nowTableName: '', //当前表格数据操作的数据库表名,用于双击编辑表内容使用 - selectionDatas: [] + selectionDatas: [] }, params: { pageNum: 1, pageSize: 100, - tagPath: null + tagPath: null }, conditionDialog: { title: '', @@ -335,19 +338,19 @@ const state = reactive({ dataTab: null, visible: false, condition: '=', - value: null + value: null }, genSqlDialog: { visible: false, sql: '', }, monacoOptions: { - editor: {} as editor.IStandaloneCodeEditor, + editor: {} as editor.IStandaloneCodeEditor, height: '', - tableMaxHeight:250, - dbTables:{}, - theme:'', - defaultThemes:[ 'vs' ,'vs-dark', 'hc-black', 'hc-light', 'Monokai', 'Active4D', 'ahe', 'bop', 'krTheme', 'Dracula', 'TextmateMac'], + tableMaxHeight: 250, + dbTables: {}, + theme: 'SolarizedLight', + defaultThemes: ['SolarizedLight', 'vs', 'vs-dark'], } }); const { @@ -382,23 +385,32 @@ self.MonacoEnvironment = { const initMonacoEditor = () => { console.log('初始化编辑器') // options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language - - let defVal = `-- monaco editor`; + + // 初始化一些主题 + + monaco.editor.defineTheme('SolarizedLight', SolarizedLight); + // monaco.editor.defineTheme('Monokai', Monokai); + // monaco.editor.defineTheme('Active4D', Active4D); + // monaco.editor.defineTheme('ahe', ahe); + // monaco.editor.defineTheme('bop', bop); + // monaco.editor.defineTheme('krTheme', krTheme); + // monaco.editor.defineTheme('Dracula', Dracula); + // monaco.editor.defineTheme('TextmateMac', TextmateMac); monacoEditor = monaco.editor.create(monacoTextarea.value, { - value: defVal, language: 'sql', - theme: 'vs', + theme: state.monacoOptions.theme, automaticLayout: true, //自适应宽高布局 foldingStrategy: 'indentation',//代码可分小段折叠 roundedSelection: false, // 禁用选择文本背景的圆角 matchBrackets: 'near', - linkedEditing:true, + linkedEditing: true, cursorBlinking: 'smooth',// 光标闪烁样式 mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时,在编辑器中缩放字体 overviewRulerBorder: false, // 不要滚动条的边框 tabSize: 2, // tab 缩进长度 - // fontFamily:'consolas', // 字体 暂时不要设置,否则光标容易错位 + fontFamily: 'JetBrainsMono', // 字体 暂时不要设置,否则光标容易错位 + fontWeight: 'bold', // letterSpacing: 1, 字符间距 // quickSuggestions:false, // 禁用代码提示 minimap: { @@ -406,41 +418,32 @@ const initMonacoEditor = () => { }, }); - // 初始化一些主题 - monaco.editor.defineTheme('Monokai', Monokai); - monaco.editor.defineTheme('Active4D', Active4D); - monaco.editor.defineTheme('ahe', ahe); - monaco.editor.defineTheme('bop', bop); - monaco.editor.defineTheme('krTheme', krTheme); - monaco.editor.defineTheme('Dracula', Dracula); - monaco.editor.defineTheme('TextmateMac', TextmateMac); - // 动态设置主题 // monaco.editor.setTheme('hc-black'); // 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example monaco.languages.registerCompletionItemProvider('sql', { - triggerCharacters:['.'], + triggerCharacters: ['.'], provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise => { let word = model.getWordUntilPosition(position); - const {lineNumber, column} = position - const {startColumn, endColumn} = word + const { lineNumber, column } = position + const { startColumn, endColumn } = word // 当前行文本 let lineContent = model.getLineContent(lineNumber); // 注释行不需要代码提示 - if(lineContent.startsWith('--')){ - return {suggestions: []} + if (lineContent.startsWith('--')) { + return { suggestions: [] } } - + let range = { startLineNumber: lineNumber, endLineNumber: lineNumber, startColumn, endColumn, }; - + // 光标前文本 const textBeforePointer = model.getValueInRange({ startLineNumber: lineNumber, @@ -466,9 +469,9 @@ const initMonacoEditor = () => { const tokens = textBeforePointer.trim().split(/\s+/) const lastToken = tokens[tokens.length - 1].toLowerCase() - console.log("光标前文本:=>" + textBeforePointerMulti) + // console.log("光标前文本:=>" + textBeforePointerMulti) - console.log("最后输入的:=>" + lastToken) + // console.log("最后输入的:=>" + lastToken) if (lastToken.endsWith('.')) { // 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】 let str = lastToken.substring(0, lastToken.lastIndexOf('.')) @@ -476,52 +479,50 @@ const initMonacoEditor = () => { if (state.databaseList.indexOf(str) > -1) { let tables = await loadTableMetadata(str) let suggestions: languages.CompletionItem[] = [] - for(let item of tables){ - const {tableName, tableComment} = item + for (let item of tables) { + const { tableName, tableComment } = item suggestions.push({ - label: tableName + ( tableComment?' - ' + tableComment :'' ), + label: tableName + (tableComment ? ' - ' + tableComment : ''), kind: monaco.languages.CompletionItemKind.File, insertText: tableName, range -}); + }); } return { suggestions } } let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0]; // 表别名.表字段联想 - let tableInfo = getTableByAlias(sql,state.db, str) - if(tableInfo.tableName){ + let tableInfo = getTableByAlias(sql, state.db, str) + if (tableInfo.tableName) { let table = tableInfo.tableName let db = tableInfo.dbName // 取出表名并提示 let dbs = state.monacoOptions.dbTables[db] - let tables = dbs ? (dbs[table] || []) : []; - if((!tables || tables.length === 0) && db){ + let columns = dbs ? (dbs[table] || []) : []; + if ((!columns || columns.length === 0) && db) { state.monacoOptions.dbTables[db] = await loadHintTables(db) dbs = state.monacoOptions.dbTables[db] - tables = dbs ? (dbs[table] || []) : []; + columns = dbs ? (dbs[table] || []) : []; } - tables.sort() let suggestions: languages.CompletionItem[] = [] - tables.forEach((a:string)=>{ + columns.forEach((a: string, index: any) => { // 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间] - let fieldName = a.substring(0,a.indexOf(" ")) - let comment = a.replace(eval(`/${fieldName}\\s+/`), '') - let detail = fieldName + ( comment?' - ' + comment :'' ) + const nameAndComment = a.split(" ") + const fieldName = nameAndComment[0] suggestions.push({ - label: detail, + label: a, // [datetime][创建时间] kind: monaco.languages.CompletionItemKind.Property, - detail: detail, - insertText: fieldName, - range + detail: '', // 不显示detail, 否则选中时备注等会被遮挡 + insertText: fieldName + ' ', // create_time + range, + sortText: 100 + index + '' // 使用表字段声明顺序排序 }); }) return { suggestions } - // } - return { suggestions:[] } - } + return { suggestions: [] } + } // 库名联想 @@ -570,28 +571,27 @@ const initMonacoEditor = () => { kind: monaco.languages.CompletionItemKind.Folder, insertText: a, range - }); + }); }) // 表名联想 state.tableMetadata.forEach((tableMeta: TableMeta) => { - const {tableName, tableComment} = tableMeta + const { tableName, tableComment } = tableMeta suggestions.push({ label: tableName + ' - ' + tableComment, kind: monaco.languages.CompletionItemKind.File, detail: tableComment, - insertText: tableName, + insertText: tableName + ' ', range - }); + }); }) // 默认提示 return { suggestions: suggestions -}; + }; }, }); - }; /** @@ -600,8 +600,8 @@ const initMonacoEditor = () => { * @param db 默认数据库 * @param alias 别名 */ -const getTableByAlias = (sql: string, db: string, alias: string):{dbName: string, tableName: string} => { - +const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string, tableName: string } => { + // 表别名:表名 let result = {}; let defName = ''; @@ -617,10 +617,10 @@ where l.name='kevin' and exsits(select 1 from pharmacywestpas pw where p.outvisi unit all select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi) */ - + let match = sql.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi) - if(match && match.length>0){ - match.forEach(a=>{ + if (match && match.length > 0) { + match.forEach(a => { // 去掉前缀,取出 let t = a.substring(5, a.length) .replaceAll(/\s+as\s+/g, ' ') @@ -628,11 +628,11 @@ select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*( let withDb = t[0].split('.'); // 表名是 db名.表名 let tName = withDb.length > 1 ? withDb[1] : withDb[0] - let dbName = withDb.length > 1 ? withDb[0] :(db||'') - if(t.length == 2){ + let dbName = withDb.length > 1 ? withDb[0] : (db || '') + if (t.length == 2) { // 表别名:表名 - result[t[1]]= {tableName: tName, dbName} - }else{ + result[t[1]] = { tableName: tName, dbName } + } else { // 只有表名无别名 取第一个无别名的表为默认表 !defName && (defResult = { tableName: tName, dbName: db }) } @@ -652,18 +652,18 @@ onMounted(() => { }); /** - * 设置codemirror高度和数据表高度 + * 设置editor高度和数据表高度 */ const setHeight = () => { // 默认300px state.monacoOptions.height = window.innerHeight - 550 + 'px' - state.dataTabsTableHeight = window.innerHeight - 274 ; + state.dataTabsTableHeight = window.innerHeight - 274; }; /** * 拖拽改变sql编辑区和查询结果区高度 */ -const onDragSetHeight = (e: any) => { +const onDragSetHeight = () => { document.onmousemove = (e) => { e.preventDefault(); //得到鼠标拖动的宽高距离:取绝对值 @@ -948,16 +948,16 @@ const getColumnTip = (tableName: string, columnName: string) => { const getSql = () => { let res = '' as string | undefined; // 编辑器还没初始化 - if(!monacoEditor.getModel){ + if (!monacoEditor.getModel) { return res; } // 选择选中的sql let selection = monacoEditor.getSelection() - if (selection){ + if (selection) { res = monacoEditor.getModel()?.getValueInRange(selection) } // 整个编辑器的sql - if(!res){ + if (!res) { return monacoEditor.getModel()?.getValue() } return res @@ -982,22 +982,22 @@ const changeDb = async (db: string) => { return; } clearDb(); - + // 加载数据库下所有表 state.tableMetadata = await loadTableMetadata(db) - + // 加载数据库下所有表字段信息 state.monacoOptions.dbTables[db] = await loadHintTables(db) getSqlNames(); }; -const loadTableMetadata = async (db: string) =>{ - return await dbApi.tableMetadata.request({id: state.dbId, db}) +const loadTableMetadata = async (db: string) => { + return await dbApi.tableMetadata.request({ id: state.dbId, db }) } -const loadHintTables = async (db: string) =>{ - return await dbApi.hintTables.request({id: state.dbId, db,}) +const loadHintTables = async (db: string) => { + return await dbApi.hintTables.request({ id: state.dbId, db, }) } // 选择表事件 @@ -1195,6 +1195,7 @@ const onTableSortChange = async (sort: any) => { const changeSqlTemplate = () => { getUserSql(); }; + const changeEditorTheme = () => { monaco.editor.setTheme(state.monacoOptions.theme); }; @@ -1217,10 +1218,6 @@ const setSqlEditorValue = (value: string) => { monacoEditor.getModel()?.setValue(value); }; -const getCodermirrorValue = () => { - return codemirror.getValue(); -}; - /** * 获取用户保存的sql模板名称 */ @@ -1370,7 +1367,7 @@ const cellClick = (row: any, column: any, cell: any) => { return; } // 转为字符串比较,可能存在数字等 - let text = (row[property] || row[property]==0 ? row[property] : '') + ''; + let text = (row[property] || row[property] == 0 ? row[property] : '') + ''; let div = cell.children[0]; if (div) { let input = document.createElement('input'); @@ -1466,13 +1463,49 @@ const addRow = async () => { * 格式化sql */ const formatSql = () => { - let selectSql = getSql(); - if(selectSql){ - monacoEditor.getModel()?.setValue(sqlFormatter(selectSql)) + let selection = monacoEditor.getSelection() + let sql = monacoEditor.getModel()?.getValueInRange(selection) + // 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容 + if (sql) { + replaceSelection(sqlFormatter(sql), selection) + return; } - + monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue())); }; +/** + * 替换选中的内容 + */ +const replaceSelection = (str: string, selection: any) => { + if (!selection) { + monacoEditor.getModel().setValue(str); + return; + } + const { startLineNumber, endLineNumber, startColumn, endColumn } = selection + const model = monacoEditor.getModel(); + + const textBeforeSelection = model.getValueInRange({ + startLineNumber: 1, + startColumn: 0, + endLineNumber: startLineNumber, + endColumn: startColumn, + }) + + const textAfterSelection = model.getValueInRange({ + startLineNumber: endLineNumber, + startColumn: endColumn, + endLineNumber: model.getLineCount(), + endColumn: model.getLineMaxColumn(model.getLineCount()), + }) + + monacoEditor.setValue(textBeforeSelection + str + textAfterSelection) + monacoEditor.focus() + monacoEditor.setPosition({ + lineNumber: startLineNumber, + column: 0, + }) +} + const search = async () => { const res = await dbApi.dbs.request(state.params); state.dbs = res.list; @@ -1515,17 +1548,6 @@ watch(store.state.sqlExecInfo, async (newValue) => { font-size: 8pt; font-weight: 600; border: 1px solid #ccc; - - .CodeMirror { - flex-grow: 1; - z-index: 1; - - .CodeMirror-code { - line-height: 19px; - } - - font-family: 'JetBrainsMono'; - } } .editor-move-resize { diff --git a/mayfly_go_web/src/views/ops/db/component/SqlExecDialog.vue b/mayfly_go_web/src/views/ops/db/component/SqlExecDialog.vue index 4663c254..6775fb1b 100644 --- a/mayfly_go_web/src/views/ops/db/component/SqlExecDialog.vue +++ b/mayfly_go_web/src/views/ops/db/component/SqlExecDialog.vue @@ -1,8 +1,7 @@