From 70b586e45a27fa4f72731d113d41538d664a0619 Mon Sep 17 00:00:00 2001 From: "meilin.huang" <954537473@qq.com> Date: Mon, 13 Feb 2023 21:11:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20sqlexec=E7=BB=84=E4=BB=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=BC=98=E5=8C=96=E3=80=81=E6=96=B0=E5=A2=9E=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=9B=B8=E5=85=B3=E7=B3=BB=E7=BB=9F=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E9=85=8D=E7=BD=AE=E3=80=81=E7=9B=B8=E5=85=B3=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mayfly_go_web/package.json | 10 +- mayfly_go_web/src/common/utils/export.ts | 39 + mayfly_go_web/src/main.ts | 2 + .../src/store/modules/themeConfig.ts | 10 +- mayfly_go_web/src/theme/app.scss | 4 + .../src/views/ops/component/TagMenu.vue | 15 +- mayfly_go_web/src/views/ops/db/DbList.vue | 23 +- mayfly_go_web/src/views/ops/db/SqlExec.vue | 1717 ++--------------- .../views/ops/db/component/InstanceTree.vue | 241 ++- .../src/views/ops/db/component/tab/Query.vue | 688 +++++++ .../views/ops/db/component/tab/TableData.vue | 508 +++++ mayfly_go_web/src/views/ops/db/db.ts | 447 +++++ mayfly_go_web/src/views/ops/db/enums.ts | 3 +- .../src/views/ops/mongo/MongoDataOp.vue | 288 ++- .../src/views/ops/mongo/MongoInstanceTree.vue | 37 +- .../src/views/ops/mongo/MongoList.vue | 21 - .../src/views/ops/redis/DataOperation.vue | 111 +- .../src/views/ops/redis/RedisInstanceTree.vue | 42 +- .../src/views/ops/redis/RedisList.vue | 20 - .../src/views/system/config/ConfigList.vue | 10 +- mayfly_go_web/yarn.lock | 178 +- server/go.mod | 8 +- server/internal/db/api/db.go | 1 + server/internal/db/api/form/db.go | 2 +- server/internal/db/application/application.go | 4 +- server/internal/db/application/db.go | 15 +- server/internal/db/application/db_sql_exec.go | 79 +- .../internal/db/domain/entity/db_sql_exec.go | 1 + server/internal/sys/application/config_app.go | 15 + server/internal/sys/domain/entity/config.go | 16 + server/mayfly-go.sql | 2 + server/pkg/cache/str_cache.go | 10 + server/pkg/model/model.go | 2 +- server/pkg/utils/byte_utils.go | 27 + server/pkg/utils/json_utils.go | 10 + 35 files changed, 2486 insertions(+), 2120 deletions(-) create mode 100644 mayfly_go_web/src/common/utils/export.ts create mode 100644 mayfly_go_web/src/views/ops/db/component/tab/Query.vue create mode 100644 mayfly_go_web/src/views/ops/db/component/tab/TableData.vue create mode 100644 mayfly_go_web/src/views/ops/db/db.ts create mode 100644 server/pkg/utils/byte_utils.go diff --git a/mayfly_go_web/package.json b/mayfly_go_web/package.json index 2ba8baa3..594a135b 100644 --- a/mayfly_go_web/package.json +++ b/mayfly_go_web/package.json @@ -9,22 +9,22 @@ "dependencies": { "@element-plus/icons-vue": "^2.0.10", "asciinema-player": "^3.0.1", - "axios": "^1.2.0", + "axios": "^1.3.2", "countup.js": "^2.0.7", "cropperjs": "^1.5.11", "echarts": "^5.4.0", - "element-plus": "^2.2.29", + "element-plus": "^2.2.30", "jsencrypt": "^3.2.1", "lodash": "^4.17.21", "mitt": "^3.0.0", - "monaco-editor": "^0.34.1", - "monaco-sql-languages": "^0.9.5", + "monaco-editor": "^0.35.0", + "monaco-sql-languages": "^0.11.0", "monaco-themes": "^0.4.2", "nprogress": "^0.2.0", "screenfull": "^6.0.2", "sortablejs": "^1.13.0", "sql-formatter": "^9.2.0", - "vue": "^3.2.45", + "vue": "^3.2.47", "vue-clipboard3": "^1.0.1", "vue-router": "^4.1.6", "vuex": "^4.0.2", diff --git a/mayfly_go_web/src/common/utils/export.ts b/mayfly_go_web/src/common/utils/export.ts new file mode 100644 index 00000000..9cbc05f7 --- /dev/null +++ b/mayfly_go_web/src/common/utils/export.ts @@ -0,0 +1,39 @@ +export function exportCsv(filename: string, columns: string[], datas: []) { + // 二维数组 + const cvsData = [columns]; + for (let data of datas) { + // 数据值组成的一维数组 + let dataValueArr: any = []; + for (let column of columns) { + let val: any = data[column]; + if (typeof val == 'string' && val) { + // csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了 + if (val.indexOf(',') != -1) { + // 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误 + if (val.indexOf('"') != -1) { + val = val.replace(/\"/g, "\"\""); + } + // 再将逗号转义 + val = `"${val}"`; + } + dataValueArr.push(val); + } else { + dataValueArr.push(val); + } + + } + cvsData.push(dataValueArr); + } + const csvString = cvsData.map((e) => e.join(',')).join('\n'); + // 导出 + let link = document.createElement('a'); + let exportContent = '\uFEFF'; + let blob = new Blob([exportContent + csvString], { + type: 'text/plain;charset=utrf-8', + }); + link.id = 'download-csv'; + link.setAttribute('href', URL.createObjectURL(blob)); + link.setAttribute('download', `${filename}.csv`); + document.body.appendChild(link); + link.click(); +} \ No newline at end of file diff --git a/mayfly_go_web/src/main.ts b/mayfly_go_web/src/main.ts index d1e842fb..e0fd47ad 100644 --- a/mayfly_go_web/src/main.ts +++ b/mayfly_go_web/src/main.ts @@ -18,6 +18,8 @@ import SvgIcon from '@/components/svgIcon/index.vue'; import '@/assets/font/font.css' const app = createApp(App); +// 屏蔽警告信息 +app.config.warnHandler = () => null; /** * 导出全局注册 element plus svg 图标 diff --git a/mayfly_go_web/src/store/modules/themeConfig.ts b/mayfly_go_web/src/store/modules/themeConfig.ts index 0575779e..8708c613 100644 --- a/mayfly_go_web/src/store/modules/themeConfig.ts +++ b/mayfly_go_web/src/store/modules/themeConfig.ts @@ -107,13 +107,13 @@ const themeConfigModule: Module = { layout: 'classic', // ssh终端字体颜色 - terminalForeground: '#7e9192', + terminalForeground: '#50583E', // ssh终端背景色 - terminalBackground: '#002833', + terminalBackground: '#FFFFDD', // ssh终端cursor色 - terminalCursor: '#268F81', - terminalFontSize: 15, - terminalFontWeight: 'normal', + terminalCursor: '#979b7c', + terminalFontSize: 14, + terminalFontWeight: 'bold', // 编辑器主题 editorTheme: 'vs', diff --git a/mayfly_go_web/src/theme/app.scss b/mayfly_go_web/src/theme/app.scss index 4abb48e0..d4537490 100644 --- a/mayfly_go_web/src/theme/app.scss +++ b/mayfly_go_web/src/theme/app.scss @@ -293,4 +293,8 @@ body, .el-table-z-index-inherit .el-table .el-table__cell { z-index: inherit !important; +} + +.f12 { + font-size: 12px } \ No newline at end of file diff --git a/mayfly_go_web/src/views/ops/component/TagMenu.vue b/mayfly_go_web/src/views/ops/component/TagMenu.vue index fa71cb30..368a01ca 100644 --- a/mayfly_go_web/src/views/ops/component/TagMenu.vue +++ b/mayfly_go_web/src/views/ops/component/TagMenu.vue @@ -24,7 +24,7 @@ diff --git a/mayfly_go_web/src/views/ops/db/DbList.vue b/mayfly_go_web/src/views/ops/db/DbList.vue index 1a4e07b3..7d7fa1f2 100644 --- a/mayfly_go_web/src/views/ops/db/DbList.vue +++ b/mayfly_go_web/src/views/ops/db/DbList.vue @@ -45,10 +45,9 @@
- {{ db }} + {{ db }} 数据操作 + @click="showTableInfo(scope.row, db)" style="position: absolute; right: 4px">操作
@@ -181,6 +180,8 @@ size="small">DELETE INSERT + QUERY @@ -290,8 +291,6 @@ import config from '@/common/config'; import { getSession } from '@/common/utils/storage'; import { isTrue } from '@/common/assert'; import { Search as SearchIcon } from '@element-plus/icons-vue' -import router from '@/router'; -import { store } from '@/store'; import { tagApi } from '../tag/api.ts'; import { dateFormat } from '@/common/utils/date'; @@ -695,20 +694,6 @@ const dropTable = async (row: any) => { }); } catch (err) { } }; -const openSqlExec = (row: any, db: any) => { - // 判断db是否发生改变 - let oldDb = store.state.sqlExecInfo.dbOptInfo.db; - if (db && oldDb !== db) { - const { tagPath, id } = row; - let params = { - tagPath, - dbId: id, - db - } - store.dispatch('sqlExecInfo/setSqlExecInfo', params); - } - router.push({ name: 'SqlExec' }); -} // 点击查看时初始化数据 const selectDb = (row: any) => { diff --git a/mayfly_go_web/src/views/ops/db/SqlExec.vue b/mayfly_go_web/src/views/ops/db/SqlExec.vue index bf564773..79398fea 100644 --- a/mayfly_go_web/src/views/ops/db/SqlExec.vue +++ b/mayfly_go_web/src/views/ops/db/SqlExec.vue @@ -2,255 +2,33 @@
- 新建查询 + 新建查询 - + - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - -
- -
- - - {{ item }} - - - - 保存 - - 删除 - -
-
-
- -
-
-
-
-
- - - - - - 导出 - - - - 提交 - - - - 取消 - - - - - - - - - - - -
- -
-
- - - - - - - + - - - - - - - - - - - - - - gi - - - - - 提交 - - - - 取消 - - - - - - - - - - - - - - - - - - - -
{{ dt.sql }} -
+ +
- - - - - - - - - - - - - - - - - - - @@ -259,239 +37,185 @@ diff --git a/mayfly_go_web/src/views/ops/db/component/tab/TableData.vue b/mayfly_go_web/src/views/ops/db/component/tab/TableData.vue new file mode 100644 index 00000000..56ee36a5 --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/component/tab/TableData.vue @@ -0,0 +1,508 @@ + + + + + diff --git a/mayfly_go_web/src/views/ops/db/db.ts b/mayfly_go_web/src/views/ops/db/db.ts new file mode 100644 index 00000000..7e8ea5d1 --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/db.ts @@ -0,0 +1,447 @@ +/* eslint-disable no-unused-vars */ +import { dbApi } from './api'; +import SqlExecBox from './component/SqlExecBox'; + +const dbInstCache: Map = new Map(); + +export class DbInst { + /** + * 实例id + */ + id: number + + /** + * 数据库类型, mysql postgres + */ + type: string + + /** + * schema -> db + */ + dbs: Map = new Map() + + /** + * 默认查询分页数量 + */ + static DefaultLimit = 20; + + /** + * 获取指定数据库实例,若不存在则新建并缓存 + * @param dbName 数据库名 + * @returns db实例 + */ + getDb(dbName: string) { + if (!dbName) { + throw new Error('dbName不能为空') + } + let db = this.dbs.get(dbName) + if (db) { + return db; + } + console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`); + db = new Db(); + db.name = dbName; + this.dbs.set(dbName, db); + return db; + } + + /** + * 加载数据库表信息 + * @param dbName 数据库名 + * @returns 表信息 + */ + async loadTables(dbName: string) { + const db = this.getDb(dbName); + // 优先从 table map中获取 + let tables = db.tables; + if (tables) { + return tables; + } + console.log(`load tables -> dbName: ${dbName}`); + tables = await dbApi.tableMetadata.request({ id: this.id, db: dbName }); + db.tables = tables; + return tables; + } + + /** + * 获取表的所有列信息 + * @param table 表名 + */ + async loadColumns(dbName: string, table: string) { + const db = this.getDb(dbName); + // 优先从 table map中获取 + let columns = db.getColumns(table); + if (columns) { + return columns; + } + console.log(`load columns -> dbName: ${dbName}, table: ${table}`); + columns = await dbApi.columnMetadata.request({ + id: this.id, + db: dbName, + tableName: table, + }); + db.columnsMap.set(table, columns); + return columns; + } + + /** + * 获取指定表的指定信息 + * @param table 表名 + */ + async loadTableColumn(dbName: string, table: string, columnName?: string) { + // 确保该表的列信息都已加载 + await this.loadColumns(dbName, table); + return this.getDb(dbName).getColumn(table, columnName); + } + + /** + * 获取库信息提示 + */ + async loadDbHints(dbName: string) { + const db = this.getDb(dbName); + if (db.tableHints) { + return db.tableHints; + } + console.log(`load db-hits -> dbName: ${dbName}`); + const hits = await dbApi.hintTables.request({ id: this.id, db: db.name, }) + db.tableHints = hits; + return hits; + } + + /** + * 执行sql + * + * @param sql sql + * @param remark 执行备注 + */ + async runSql(dbName: string, sql: string, remark: string = '') { + return await dbApi.sqlExec.request({ + id: this.id, + db: dbName, + sql: sql.trim(), + remark, + }); + } + + // 获取指定表的默认查询sql + getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) { + const baseSql = `SELECT * FROM ${table} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''}`; + if (this.type == 'mysql') { + return `${baseSql} LIMIT ${(pageNum - 1) * limit}, ${limit};`; + } + if (this.type == 'postgres') { + return `${baseSql} OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`; + } + return baseSql; + } + + /** + * 生成指定数据的insert语句 + * @param dbName 数据库名 + * @param table 表名 + * @param datas 要生成的数据 + */ + genInsertSql(dbName: string, table: string, datas: any[]): string { + if (!datas) { + return ''; + } + const columns = this.getDb(dbName).getColumns(table); + const sqls = []; + for (let data of datas) { + let colNames = []; + let values = []; + for (let column of columns) { + const colName = column.columnName; + colNames.push(colName); + values.push(DbInst.wrapValueByType(data[colName])); + } + sqls.push(`INSERT INTO ${table} (${colNames.join(', ')}) VALUES(${values.join(', ')})`); + } + return sqls.join(';\n') + ';' + } + + /** + * 生成根据主键删除的sql语句 + * @param table 表名 + * @param datas 要删除的记录 + */ + genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) { + const primaryKey = this.getDb(db).getColumn(table); + const primaryKeyColumnName = primaryKey.columnName; + const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(','); + return `DELETE FROM ${table} WHERE ${primaryKeyColumnName} IN (${ids})`; + } + + /* + * 弹框提示是否执行sql + */ + promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => { + SqlExecBox({ + sql, dbId: this.id, db, + runSuccessCallback: successFunc, + cancelCallback: cancelFunc, + }); + }; + + /** + * 获取数据库实例id,若不存在,则新建一个并缓存 + * @param dbId 数据库实例id + * @param dbType 第一次获取时为必传项,即第一次创建时 + * @returns 数据库实例 + */ + static getInst(dbId: number, dbType?: string): DbInst { + let dbInst = dbInstCache.get(dbId); + if (dbInst) { + return dbInst; + } + if (!dbType) { + throw new Error('DbInst不存在, dbType为必传项') + } + console.log(`new dbInst -> dbId: ${dbId}, dbType: ${dbType}`); + dbInst = new DbInst(); + dbInst.id = dbId; + dbInst.type = dbType; + dbInstCache.set(dbInst.id, dbInst); + return dbInst; + } + + /** + * 清空所有实例缓存信息 + */ + static clearAll() { + dbInstCache.clear(); + } + + /** + * 获取count sql + * @param table 表名 + * @param condition 条件 + * @returns count sql + */ + static getDefaultCountSql = (table: string, condition?: string) => { + return `SELECT COUNT(*) count FROM ${table} ${condition ? 'WHERE ' + condition : ''}`; + }; + + /** + * 根据返回值包装值,若值为字符串类型则添加'' + * @param val 值 + * @returns 包装后的值 + */ + static wrapValueByType = (val: any) => { + if (val == null) { + return 'NULL'; + } + if (typeof val == 'number') { + return val; + } + return `'${val}'`; + }; + + /** + * 根据字段类型包装字段值,如为字符串等则添加‘’,数字类型则直接返回即可 + */ + static wrapColumnValue(columnType: string, value: any) { + if (columnType.match(/int|double|float|nubmer|decimal|byte|bit/gi)) { + return value; + } + return `'${value}'`; + }; + + /** + * + * @param str 字符串 + * @param tableData 表数据 + * @param flag 标志 + * @returns 列宽度 + */ + static flexColumnWidth = (str: any, tableData: any, flag = 'equal') => { + // str为该列的字段名(传字符串);tableData为该表格的数据源(传变量); + // flag为可选值,可不传该参数,传参时可选'max'或'equal',默认为'max' + // flag为'max'则设置列宽适配该列中最长的内容,flag为'equal'则设置列宽适配该列中第一行内容的长度。 + str = str + ''; + let columnContent = ''; + if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) { + return; + } + if (!str || !str.length || str.length === 0 || str === undefined) { + return; + } + if (flag === 'equal') { + // 获取该列中第一个不为空的数据(内容) + for (let i = 0; i < tableData.length; i++) { + // 转为字符串后比较 + if ((tableData[i][str] + '').length > 0) { + columnContent = tableData[i][str] + ''; + break; + } + } + } else { + // 获取该列中最长的数据(内容) + let index = 0; + for (let i = 0; i < tableData.length; i++) { + if (tableData[i][str] === null) { + return; + } + const now_temp = tableData[i][str] + ''; + const max_temp = tableData[index][str] + ''; + if (now_temp.length > max_temp.length) { + index = i; + } + } + columnContent = tableData[index][str] + ''; + } + const contentWidth: number = DbInst.getContentWidth(columnContent); + // 获取列名称的长度 加上排序图标长度 + const columnWidth: number = DbInst.getContentWidth(str) + 43; + const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth; + return flexWidth + 'px'; + }; + + /** + * 获取内容所需要占用的宽度 + */ + static getContentWidth = (content: any): number => { + // 以下分配的单位长度可根据实际需求进行调整 + let flexWidth = 0; + for (const char of content) { + if (flexWidth > 500) { + break; + } + if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) { + // 如果是小写字母、数字字符,分配8个单位宽度 + flexWidth += 8.5; + continue; + } + if (char >= 'A' && char <= 'Z') { + flexWidth += 9; + continue; + } + if (char >= '\u4e00' && char <= '\u9fa5') { + // 如果是中文字符,为字符分配16个单位宽度 + flexWidth += 16; + } else { + // 其他种类字符,为字符分配9个单位宽度 + flexWidth += 8; + } + } + if (flexWidth > 500) { + // 设置最大宽度 + flexWidth = 500; + } + return flexWidth; + }; +} + +/** + * 数据库实例信息 + */ +class Db { + name: string // 库名 + tables: [] // 数据库实例表信息 + columnsMap: Map = new Map // table -> columns + tableHints: any = null // 提示词 + + /** + * 获取指定表列信息(前提需要dbInst.loadColumns) + * @param table 表名 + */ + getColumns(table: string) { + return this.columnsMap.get(table); + } + + /** + * 获取指定表中的指定列名信息,若列名为空则默认返回主键 + * @param table 表名 + * @param columnName 列名 + */ + getColumn(table: string, columnName: string = '') { + const cols = this.getColumns(table); + if (!columnName) { + const col = cols.find((c: any) => c.columnKey == 'PRI'); + return col || cols[0]; + } + return cols.find((c: any) => c.columnName == columnName); + } +} + +export enum TabType { + /** + * 表数据 + */ + TableData, + + /** + * 查询框 + */ + Query, +} + +export class TabInfo { + /** + * tab唯一key。与label、name都一致 + */ + key: string + + /** + * 数据库实例id + */ + dbId: number + + /** + * 数据库类型 + */ + dbType: string + + /** + * 库名 + */ + db: string = '' + + /** + * tab 类型 + */ + type: TabType + + /** + * tab需要的其他信息 + */ + other: any + + getNowDbInst() { + if (!this.dbType) { + throw new Error('dbType不能为空') + } + return DbInst.getInst(this.dbId, this.dbType); + } + + getNowDb() { + return this.getNowDbInst().getDb(this.db); + } +} + +/** 修改表字段所需数据 */ +export type UpdateFieldsMeta = { + // 主键值 + primaryKey: string + // 主键名 + primaryKeyName: string + // 主键类型 + primaryKeyType: string + // 新值 + fields: FieldsMeta[] +} + +export type FieldsMeta = { + // 字段所在div + div: HTMLElement + // 字段名 + fieldName: string + // 字段所在的表格行数据 + row: any + // 字段类型 + fieldType: string + // 原值 + oldValue: string + // 新值 + newValue: string +} \ No newline at end of file diff --git a/mayfly_go_web/src/views/ops/db/enums.ts b/mayfly_go_web/src/views/ops/db/enums.ts index eee82bf5..914988aa 100644 --- a/mayfly_go_web/src/views/ops/db/enums.ts +++ b/mayfly_go_web/src/views/ops/db/enums.ts @@ -7,5 +7,6 @@ export default { // 数据库sql执行类型 DbSqlExecTypeEnum: new Enum().add('UPDATE', 'UPDATE', 1) .add('DELETE', 'DELETE', 2) - .add('INSERT', 'INSERT', 3), + .add('INSERT', 'INSERT', 3) + .add('QUERY', 'QUERY', 4), } \ No newline at end of file diff --git a/mayfly_go_web/src/views/ops/mongo/MongoDataOp.vue b/mayfly_go_web/src/views/ops/mongo/MongoDataOp.vue index a3347614..1756caad 100644 --- a/mayfly_go_web/src/views/ops/mongo/MongoDataOp.vue +++ b/mayfly_go_web/src/views/ops/mongo/MongoDataOp.vue @@ -1,69 +1,67 @@ diff --git a/mayfly_go_web/src/views/ops/mongo/MongoInstanceTree.vue b/mayfly_go_web/src/views/ops/mongo/MongoInstanceTree.vue index 0123482a..d84761c7 100644 --- a/mayfly_go_web/src/views/ops/mongo/MongoInstanceTree.vue +++ b/mayfly_go_web/src/views/ops/mongo/MongoInstanceTree.vue @@ -79,9 +79,8 @@