mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
refactor: sqlexec组件重构优化、新增数据库相关系统参数配置、相关问题修复
This commit is contained in:
@@ -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",
|
||||
|
||||
39
mayfly_go_web/src/common/utils/export.ts
Normal file
39
mayfly_go_web/src/common/utils/export.ts
Normal file
@@ -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();
|
||||
}
|
||||
@@ -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 图标
|
||||
|
||||
@@ -107,13 +107,13 @@ const themeConfigModule: Module<ThemeConfigState, RootStateTypes> = {
|
||||
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',
|
||||
|
||||
@@ -293,4 +293,8 @@ body,
|
||||
|
||||
.el-table-z-index-inherit .el-table .el-table__cell {
|
||||
z-index: inherit !important;
|
||||
}
|
||||
|
||||
.f12 {
|
||||
font-size: 12px
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {reactive, ref, Ref, toRefs} from 'vue';
|
||||
import { reactive, ref, Ref, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
instanceMenuMaxHeight: {
|
||||
@@ -56,12 +56,19 @@ const clickTag = (tagPath: string) => {
|
||||
state.opend[tagPath] = !opend
|
||||
}
|
||||
|
||||
const open = (index: string) => {
|
||||
menuRef.value.open(index)
|
||||
const open = (index: string, isTag: boolean = false) => {
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
console.log(index)
|
||||
menuRef.value.open(index)
|
||||
if (isTag) {
|
||||
clickTag(index)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
open
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -45,10 +45,9 @@
|
||||
</el-input>
|
||||
<div class="el-tag--plain el-tag--success" v-for="db in filterDb.list" :key="db"
|
||||
style="border:1px var(--color-success-light-3) solid; margin-top: 3px;border-radius: 5px; padding: 2px;position: relative">
|
||||
<el-link type="success" plain size="small" :underline="false"
|
||||
@click="showTableInfo(scope.row, db)">{{ db }}</el-link>
|
||||
<el-link type="success" plain size="small" :underline="false">{{ db }}</el-link>
|
||||
<el-link type="primary" plain size="small" :underline="false"
|
||||
@click="openSqlExec(scope.row, db)" style="position: absolute; right: 4px">数据操作
|
||||
@click="showTableInfo(scope.row, db)" style="position: absolute; right: 4px">操作
|
||||
</el-link>
|
||||
</div>
|
||||
</el-popover>
|
||||
@@ -181,6 +180,8 @@
|
||||
size="small">DELETE</el-tag>
|
||||
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['INSERT'].value" color="#A8DEE0"
|
||||
size="small">INSERT</el-tag>
|
||||
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['QUERY'].value" color="#A8DEE0"
|
||||
size="small">QUERY</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sql" label="SQL" min-width="230" show-overflow-tooltip> </el-table-column>
|
||||
@@ -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) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,43 @@
|
||||
<template>
|
||||
<tag-menu :instanceMenuMaxHeight="instanceMenuMaxHeight" :tags="instances.tags" ref="menuRef">
|
||||
<tag-menu :instanceMenuMaxHeight="instanceMenuMaxHeight" :tags="tags" ref="menuRef">
|
||||
<template #submenu="props">
|
||||
<!-- 第二级:数据库实例 -->
|
||||
<el-sub-menu v-for="inst in instances.tree[props.tag.tagId]" :index="'instance-' + inst.id"
|
||||
<el-sub-menu v-for="inst in tree[props.tag.tagId]" :index="'instance-' + inst.id"
|
||||
:key="'instance-' + inst.id" @click.stop="changeInstance(inst, () => { })">
|
||||
<template #title>
|
||||
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
|
||||
<template #reference>
|
||||
<span> <el-icon>
|
||||
<span class="ml10">
|
||||
<el-icon>
|
||||
<MostlyCloudy color="#409eff" />
|
||||
</el-icon>{{ inst.name }}</span>
|
||||
</el-icon>{{ inst.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
|
||||
<el-form-item label="类型:">{{ inst.type }}</el-form-item>
|
||||
<el-form-item label="链接:">{{ inst.host }}:{{ inst.port }}</el-form-item>
|
||||
<el-form-item label="用户:">{{ inst.username }}</el-form-item>
|
||||
<el-form-item v-if="inst.remark" label="备注:">{{
|
||||
inst.remark
|
||||
}}</el-form-item>
|
||||
<el-form-item v-if="inst.remark" label="备注:">{{ inst.remark }}</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
<!-- 第三级:数据库 -->
|
||||
<el-sub-menu v-for="schema in instances.dbs[inst.id]" :index="inst.id + schema" :key="inst.id + schema"
|
||||
<el-sub-menu v-for="schema in dbs[inst.id]" :index="inst.id + schema" :key="inst.id + schema"
|
||||
:class="state.nowSchema === (inst.id + schema) && 'checked'"
|
||||
@click.stop="changeSchema(inst, schema)">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<Coin color="#67c23a" />
|
||||
</el-icon>
|
||||
<span class="checked-schema">{{ schema }}</span>
|
||||
<span class="checked-schema ml20">
|
||||
<el-icon>
|
||||
<Coin color="#67c23a" />
|
||||
</el-icon>{{ schema }}</span>
|
||||
</template>
|
||||
<!-- 第四级 01:表 -->
|
||||
<el-sub-menu :index="inst.id + schema + '-table'">
|
||||
<template #title>
|
||||
<div style="width: 100%" @click="loadTableNames(inst, schema, () => { })">
|
||||
<el-icon>
|
||||
<div class="ml30" style="width: 100%" @click="loadSchemaTables(inst, schema)">
|
||||
<el-icon>
|
||||
<Calendar color="#409eff" />
|
||||
</el-icon>
|
||||
<span>表</span>
|
||||
@@ -49,21 +49,22 @@
|
||||
<el-menu-item :index="inst.id + schema + '-tableSearch'"
|
||||
:key="inst.id + schema + '-tableSearch'">
|
||||
<template #title>
|
||||
|
||||
<el-input size="small" placeholder="过滤表" clearable
|
||||
@change="filterTableName(inst.id, schema)"
|
||||
@keyup="(e: any) => filterTableName(inst.id, schema, e)"
|
||||
v-model="state.filterParam[inst.id + schema]" />
|
||||
<span class="ml35">
|
||||
<el-input size="small" placeholder="表名、备注过滤表" clearable
|
||||
@change="filterTableName(inst.id, schema)"
|
||||
@keyup="(e: any) => filterTableName(inst.id, schema, e)"
|
||||
v-model="state.filterParam[inst.id + schema]" />
|
||||
</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<template v-for="tb in instances.tables[inst.id + schema]">
|
||||
<template v-for="tb in tables[inst.id + schema]">
|
||||
<el-menu-item :index="inst.id + schema + tb.tableName"
|
||||
:key="inst.id + schema + tb.tableName" v-if="tb.show"
|
||||
@click="loadTableData(inst, schema, tb.tableName)">
|
||||
@click="clickSchemaTable(inst, schema, tb.tableName)">
|
||||
<template #title>
|
||||
<div style="width: 100%">
|
||||
<el-icon>
|
||||
<div class="ml35" style="width: 100%">
|
||||
<el-icon>
|
||||
<Calendar color="#409eff" />
|
||||
</el-icon>
|
||||
<el-tooltip v-if="tb.tableComment" effect="customized"
|
||||
@@ -77,21 +78,23 @@
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<!-- 第四级 02:sql -->
|
||||
<el-sub-menu :index="inst.id + schema + '-sql'">
|
||||
<el-sub-menu @click.stop="loadSqls(inst, schema)" :index="inst.id + schema + '-sql'">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<List color="#f56c6c" />
|
||||
</el-icon>
|
||||
<span>sql</span>
|
||||
<span class="ml30">
|
||||
<el-icon>
|
||||
<List color="#f56c6c" />
|
||||
</el-icon>
|
||||
<span>sql</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-for="sql in instances.sqls[inst.id + schema]">
|
||||
<el-menu-item :index="inst.id + schema + sql.name" :key="inst.id + schema + sql.name"
|
||||
v-if="sql.show" @click="loadSql(inst, schema, sql.name)">
|
||||
<template v-for="sql in sqls[inst.id + schema]">
|
||||
<el-menu-item v-if="sql.show" :index="inst.id + schema + sql.name"
|
||||
:key="inst.id + schema + sql.name" @click="clickSqlName(inst, schema, sql.name)">
|
||||
<template #title>
|
||||
<div style="width: 100%">
|
||||
<el-icon>
|
||||
<Calendar color="#409eff" />
|
||||
<div class="ml35" style="width: 100%">
|
||||
<el-icon>
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span>{{ sql.name }}</span>
|
||||
</div>
|
||||
@@ -106,39 +109,57 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onBeforeMount, onMounted, reactive, ref, Ref, watch } from 'vue';
|
||||
import { store } from '@/store';
|
||||
import { onBeforeMount, reactive, toRefs } from 'vue';
|
||||
import TagMenu from '../../component/TagMenu.vue';
|
||||
import { dbApi } from '../api';
|
||||
import { DbInst } from '../db';
|
||||
|
||||
const props = defineProps({
|
||||
instanceMenuMaxHeight: {
|
||||
type: [Number, String],
|
||||
},
|
||||
instances: {
|
||||
type: Object, required: true
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['initLoadInstances', 'changeInstance', 'loadTableNames', 'loadTableData', 'changeSchema'])
|
||||
const emits = defineEmits(['changeInstance', 'clickSqlName', 'clickSchemaTable', 'changeSchema', 'loadSqlNames'])
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await initLoadInstances()
|
||||
await nextTick(()=>selectDb())
|
||||
await loadInstances();
|
||||
state.instanceMenuMaxHeight = window.innerHeight - 140 + 'px';
|
||||
})
|
||||
|
||||
const menuRef = ref(null) as Ref
|
||||
|
||||
const state = reactive({
|
||||
tags: {},
|
||||
tree: {},
|
||||
dbs: {},
|
||||
tables: {},
|
||||
sqls: {},
|
||||
nowSchema: '',
|
||||
filterParam: {},
|
||||
loading: {}
|
||||
loading: {},
|
||||
instanceMenuMaxHeight: '850px',
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始化加载实例数据
|
||||
*/
|
||||
const initLoadInstances = () => {
|
||||
emits('initLoadInstances')
|
||||
const {
|
||||
instanceMenuMaxHeight,
|
||||
tags,
|
||||
tree,
|
||||
dbs,
|
||||
sqls,
|
||||
tables,
|
||||
} = toRefs(state)
|
||||
|
||||
// 加载实例数据
|
||||
const loadInstances = async () => {
|
||||
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000, })
|
||||
if (!res.total) return
|
||||
// state.instances = { tags: {}, tree: {}, dbs: {}, tables: {}, sqls: {} }; // 初始化变量
|
||||
for (const db of res.list) {
|
||||
let arr = state.tree[db.tagId] || []
|
||||
const { tagId, tagPath } = db
|
||||
// tags
|
||||
state.tags[db.tagId] = { tagId, tagPath }
|
||||
|
||||
// tree
|
||||
arr.push(db)
|
||||
state.tree[db.tagId] = arr;
|
||||
|
||||
// dbs
|
||||
state.dbs[db.id] = db.database.split(' ')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,68 +179,98 @@ const changeSchema = (inst: any, schema: string) => {
|
||||
state.nowSchema = inst.id + schema
|
||||
emits('changeSchema', inst, schema)
|
||||
}
|
||||
/**
|
||||
* 加载schema下所有表
|
||||
|
||||
/** 加载schema下所有表
|
||||
*
|
||||
* @param inst 数据库实例
|
||||
* @param schema database名
|
||||
* @param fn 加载表集合后的回调函数,参数:res 表集合
|
||||
*/
|
||||
const loadTableNames = async (inst: any, schema: string, fn: Function) => {
|
||||
state.loading[inst.id + schema] = true
|
||||
await emits('loadTableNames', inst, schema, (res: any[]) => {
|
||||
state.loading[inst.id + schema] = false
|
||||
fn && fn(res)
|
||||
})
|
||||
const loadSchemaTables = async (inst: any, schema: string) => {
|
||||
const key = getSchemaKey(inst.id, schema);
|
||||
state.loading[key] = true
|
||||
try {
|
||||
let { id } = inst
|
||||
let tables = await DbInst.getInst(id, inst.type).loadTables(schema);
|
||||
tables && tables.forEach((a: any) => a.show = true)
|
||||
state.tables[key] = tables;
|
||||
changeSchema(inst, schema);
|
||||
} finally {
|
||||
state.loading[key] = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载选中表数据
|
||||
* @param inst 数据库实例
|
||||
* @param schema database名
|
||||
* @param tableName 表名
|
||||
*/
|
||||
const loadTableData = (inst: any, schema: string, tableName: string) => {
|
||||
emits('loadTableData', inst, schema, tableName)
|
||||
const clickSchemaTable = (inst: any, schema: string, tableName: string) => {
|
||||
emits('clickSchemaTable', inst, schema, tableName)
|
||||
}
|
||||
|
||||
const filterTableName = (instId: number, schema: string, event?: any) => {
|
||||
const key = getSchemaKey(instId, schema)
|
||||
if (event) {
|
||||
state.filterParam[instId + schema] = event.target.value
|
||||
state.filterParam[key] = event.target.value
|
||||
}
|
||||
let param = state.filterParam[instId + schema] as string
|
||||
let param = state.filterParam[key] as string
|
||||
param = param?.replace('/', '\/')
|
||||
const key = instId + schema;
|
||||
props.instances.tables[key].forEach((a: any) => {
|
||||
a.show = param ? eval('/' + param.split('').join('[_\w]*') + '[_\w]*/ig').test(a.tableName) : true
|
||||
state.tables[key].forEach((a: any) => {
|
||||
a.show = param ? eval('/' + param.split('').join('[_\w]*') + '[_\w]*/ig').test(a.tableName) || eval('/' + param.split('').join('[_\w]*') + '[_\w]*/ig').test(a.tableComment) : true
|
||||
})
|
||||
}
|
||||
|
||||
const selectDb = async (val?: any) => {
|
||||
let info = val || store.state.sqlExecInfo.dbOptInfo;
|
||||
if (info && info.dbId) {
|
||||
const { tagPath, dbId, db } = info
|
||||
menuRef.value.open(tagPath);
|
||||
menuRef.value.open('instance-' + dbId);
|
||||
await changeInstance({ id: dbId }, () => {
|
||||
// 加载数据库
|
||||
nextTick(async () => {
|
||||
menuRef.value.open(dbId + db)
|
||||
state.nowSchema = (dbId + db)
|
||||
// 加载集合列表
|
||||
await nextTick(async () => {
|
||||
await loadTableNames({ id: dbId }, db, (res: any[]) => {
|
||||
// 展开集合列表
|
||||
menuRef.value.open(dbId + db + '-table')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
/**
|
||||
* 加载用户保存的sql脚本
|
||||
*
|
||||
* @param inst
|
||||
* @param schema
|
||||
*/
|
||||
const loadSqls = async (inst: any, schema: string) => {
|
||||
const key = getSchemaKey(inst.id, schema)
|
||||
let sqls = state.sqls[key];
|
||||
if (!sqls) {
|
||||
const sqls = await dbApi.getSqlNames.request({ id: inst.id, db: schema, })
|
||||
sqls && sqls.forEach((a: any) => a.show = true)
|
||||
state.sqls[key] = sqls;
|
||||
} else {
|
||||
sqls.forEach((a: any) => a.show = true);
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => store.state.sqlExecInfo.dbOptInfo, async newValue => {
|
||||
await selectDb(newValue)
|
||||
})
|
||||
const reloadSqls = async (inst: any, schema: string) => {
|
||||
const sqls = await dbApi.getSqlNames.request({ id: inst.id, db: schema, })
|
||||
sqls && sqls.forEach((a: any) => a.show = true)
|
||||
state.sqls[getSchemaKey(inst.id, schema)] = sqls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击sql模板名称时间,加载用户保存的指定名称的sql内容,并回调子组件指定事件
|
||||
*/
|
||||
const clickSqlName = async (inst: any, schema: string, sqlName: string) => {
|
||||
emits('clickSqlName', inst, schema, sqlName)
|
||||
changeSchema(inst, schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实例以及库获取对应的唯一id
|
||||
*
|
||||
* @param inst 数据库实例
|
||||
* @param schema 数据库
|
||||
*/
|
||||
const getSchemaKey = (instId: any, schema: string) => {
|
||||
return instId + schema;
|
||||
}
|
||||
|
||||
const getSchemas = (dbId: any) => {
|
||||
return state.dbs[dbId] || []
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getSchemas,
|
||||
reloadSqls,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
688
mayfly_go_web/src/views/ops/db/component/tab/Query.vue
Normal file
688
mayfly_go_web/src/views/ops/db/component/tab/Query.vue
Normal file
@@ -0,0 +1,688 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<div class="fl">
|
||||
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay">
|
||||
</el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="format sql" placement="top">
|
||||
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick">
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck">
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-upload class="sql-file-exec" :before-upload="beforeUpload" :on-success="execSqlFileSuccess"
|
||||
:headers="{ Authorization: token }" :action="getUploadSqlFileUrl()" :show-file-list="false"
|
||||
name="file" multiple :limit="100">
|
||||
<el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top">
|
||||
<el-link type="success" :underline="false" icon="Document"></el-link>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div style="float: right" class="fl">
|
||||
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL
|
||||
</el-button>
|
||||
<el-button v-if="sqlName" @click="deleteSql()" type="danger" icon="delete" plain size="small">删除SQL
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt5 sqlEditor">
|
||||
<div :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt5">
|
||||
<el-row>
|
||||
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete"
|
||||
:underline="false"></el-link>
|
||||
|
||||
<span v-if="execRes.data.length > 0">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="success" :underline="false" @click="exportData"><span
|
||||
style="font-size: 12px">导出</span></el-link>
|
||||
</span>
|
||||
<span v-if="updatedFields.length > 0">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span
|
||||
style="font-size: 12px">提交</span></el-link>
|
||||
</span>
|
||||
<span v-if="updatedFields.length > 0">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="warning" :underline="false" @click="cancelUpdateFields"><span
|
||||
style="font-size: 12px">取消</span></el-link>
|
||||
</span>
|
||||
</el-row>
|
||||
<el-table @cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
|
||||
@selection-change="onDataSelectionChange" size="small" :data="execRes.data" :max-height="250"
|
||||
v-loading="loading" element-loading-text="查询中..."
|
||||
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改" stripe border class="mt5">
|
||||
<el-table-column v-if="execRes.tableColumn.length > 0 && table" type="selection" witih="35" />
|
||||
<el-table-column min-witih="100" :witih="DbInst.flexColumnWidth(item, execRes.data)" align="center"
|
||||
v-for="item in execRes.tableColumn" :key="item" :prop="item" :label="item" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, watch, onMounted, computed, reactive, toRefs } from 'vue';
|
||||
import { useStore } from '@/store/index.ts';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
import { isTrue, notBlank } from '@/common/assert';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import config from '@/common/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { editor } from 'monaco-editor';
|
||||
|
||||
// 主题仓库 https://github.com/brijeshb42/monaco-themes
|
||||
// 主题例子 https://editor.bitwiser.in/
|
||||
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
|
||||
|
||||
import { DbInst, UpdateFieldsMeta, FieldsMeta, TabInfo } from '../../db';
|
||||
import { exportCsv } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
import { dbApi } from '../../api';
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess'])
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: TabInfo,
|
||||
required: true
|
||||
},
|
||||
// sql脚本名,若有则去加载该sql内容
|
||||
sqlName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
editorHeight: {
|
||||
type: String,
|
||||
default: '600'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const store = useStore();
|
||||
const token = getSession('token');
|
||||
let monacoEditor = {} as editor.IStandaloneCodeEditor;
|
||||
|
||||
const state = reactive({
|
||||
token,
|
||||
ti: {} as TabInfo,
|
||||
dbs: [],
|
||||
dbId: null, // 当前选中操作的数据库实例
|
||||
table: '', // 当前单表操作sql的表信息
|
||||
sqlName: '',
|
||||
sql: '', // 当前编辑器的sql内容
|
||||
loading: false, // 是否在加载数据
|
||||
execRes: {
|
||||
data: [],
|
||||
tableColumn: []
|
||||
},
|
||||
selectionDatas: [] as any,
|
||||
editorHeight: '500',
|
||||
updatedFields: [] as UpdateFieldsMeta[],// 各个tab表被修改的字段信息
|
||||
});
|
||||
|
||||
const {
|
||||
editorHeight,
|
||||
ti,
|
||||
execRes,
|
||||
table,
|
||||
sqlName,
|
||||
loading,
|
||||
updatedFields,
|
||||
} = toRefs(state);
|
||||
|
||||
watch(() => props.editorHeight, (newValue: any) => {
|
||||
state.editorHeight = newValue;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in query mounted');
|
||||
state.ti = props.data;
|
||||
state.editorHeight = props.editorHeight;
|
||||
const other = state.ti.other;
|
||||
state.dbs = other && other.dbs;
|
||||
|
||||
if (other && other.sqlName) {
|
||||
state.sqlName = other.sqlName;
|
||||
const res = await dbApi.getSql.request({ id: state.ti.dbId, type: 1, name: state.sqlName, db: state.ti.db });
|
||||
state.sql = res.sql;
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => initMonacoEditor(), 50)
|
||||
})
|
||||
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
|
||||
})
|
||||
|
||||
// 获取布局配置信息
|
||||
const getThemeConfig: any = computed(() => {
|
||||
return store.state.themeConfig.themeConfig;
|
||||
});
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker() {
|
||||
return new EditorWorker();
|
||||
}
|
||||
};
|
||||
|
||||
const initMonacoEditor = () => {
|
||||
let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement
|
||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||
// 初始化一些主题
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
monacoEditor = monaco.editor.create(monacoTextarea, {
|
||||
language: 'sql',
|
||||
theme: getThemeConfig.value.editorTheme,
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
folding: false,
|
||||
roundedSelection: false, // 禁用选择文本背景的圆角
|
||||
matchBrackets: 'near',
|
||||
linkedEditing: true,
|
||||
cursorBlinking: 'smooth',// 光标闪烁样式
|
||||
mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时,在编辑器中缩放字体
|
||||
overviewRulerBorder: false, // 不要滚动条的边框
|
||||
tabSize: 2, // tab 缩进长度
|
||||
// fontFamily: 'JetBrainsMono', // 字体 暂时不要设置,否则光标容易错位
|
||||
fontWeight: 'bold',
|
||||
// letterSpacing: 1, 字符间距
|
||||
// quickSuggestions:false, // 禁用代码提示
|
||||
minimap: {
|
||||
enabled: false, // 不要小地图
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'run-sql-action' + state.ti.key,
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '执行SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0)
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await onRunSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'format-sql-action' + state.ti.key,
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '格式化SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0)
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 2,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await formatSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 动态设置主题
|
||||
// monaco.editor.setTheme('hc-black');
|
||||
|
||||
// 如果sql有值,则默认赋值
|
||||
if (state.sql) {
|
||||
monacoEditor.getModel()?.setValue(state.sql);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行sql
|
||||
*/
|
||||
const onRunSql = async () => {
|
||||
// 没有选中的文本,则为全部文本
|
||||
let sql = getSql() as string;
|
||||
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
let execRemark = '';
|
||||
let canRun = true;
|
||||
if (
|
||||
sql.startsWith('update') ||
|
||||
sql.startsWith('UPDATE') ||
|
||||
sql.startsWith('INSERT') ||
|
||||
sql.startsWith('insert') ||
|
||||
sql.startsWith('DELETE') ||
|
||||
sql.startsWith('delete')
|
||||
) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
||||
inputErrorMessage: '请输入执行该sql的备注信息',
|
||||
});
|
||||
execRemark = res.value;
|
||||
if (!execRemark) {
|
||||
canRun = false;
|
||||
}
|
||||
}
|
||||
if (!canRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.loading = true;
|
||||
|
||||
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集')
|
||||
}
|
||||
state.execRes.data = colAndData.res;
|
||||
state.execRes.tableColumn = colAndData.colNames;
|
||||
cancelUpdateFields()
|
||||
} catch (e: any) {
|
||||
state.execRes.data = [];
|
||||
state.execRes.tableColumn = [];
|
||||
state.table = '';
|
||||
return;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
// 即只有以该字符串开头的sql才可修改表数据内容
|
||||
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
|
||||
state.selectionDatas = [];
|
||||
const tableName = sql.split(/from/i)[1];
|
||||
if (tableName) {
|
||||
const tn = tableName.trim().split(' ')[0];
|
||||
state.table = tn;
|
||||
state.table = tn;
|
||||
} else {
|
||||
state.table = '';
|
||||
}
|
||||
} else {
|
||||
state.table = '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
||||
*/
|
||||
const getSql = () => {
|
||||
let res = '' as string | undefined;
|
||||
// 编辑器还没初始化
|
||||
if (!monacoEditor?.getModel) {
|
||||
return res;
|
||||
}
|
||||
// 选择选中的sql
|
||||
let selection = monacoEditor.getSelection()
|
||||
if (selection) {
|
||||
res = monacoEditor.getModel()?.getValueInRange(selection)
|
||||
}
|
||||
// 整个编辑器的sql
|
||||
if (!res) {
|
||||
return monacoEditor.getModel()?.getValue()
|
||||
}
|
||||
return res
|
||||
};
|
||||
|
||||
const saveSql = async () => {
|
||||
const sql = monacoEditor.getModel()?.getValue();
|
||||
notBlank(sql, 'sql内容不能为空');
|
||||
|
||||
let sqlName = state.sqlName;
|
||||
const newSql = !sqlName;
|
||||
if (newSql) {
|
||||
try {
|
||||
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern:
|
||||
/\w+/,
|
||||
inputErrorMessage: '请输入SQL脚本名',
|
||||
});
|
||||
sqlName = input.value;
|
||||
state.sqlName = sqlName;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await dbApi.saveSql.request({ id: state.ti.dbId, db: state.ti.db, sql: sql, type: 1, name: sqlName });
|
||||
ElMessage.success('保存成功');
|
||||
// 保存sql脚本成功事件
|
||||
emits('saveSqlSuccess', state.ti.dbId, state.ti.db);
|
||||
};
|
||||
|
||||
const deleteSql = async () => {
|
||||
const sqlName = state.sqlName;
|
||||
notBlank(sqlName, "该sql内容未保存");
|
||||
const { dbId, db } = state.ti;
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
|
||||
ElMessage.success('删除成功');
|
||||
emits('deleteSqlSuccess', dbId, db);
|
||||
} catch (err) { }
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化sql
|
||||
*/
|
||||
const formatSql = () => {
|
||||
let selection = monacoEditor.getSelection()
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
let sql = monacoEditor.getModel()?.getValueInRange(selection)
|
||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||
if (sql) {
|
||||
replaceSelection(sqlFormatter(sql), selection)
|
||||
return;
|
||||
}
|
||||
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue()));
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 替换选中的内容
|
||||
*/
|
||||
const replaceSelection = (str: string, selection: any) => {
|
||||
const model = monacoEditor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (!selection) {
|
||||
model.setValue(str);
|
||||
return;
|
||||
}
|
||||
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
|
||||
|
||||
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 exportData = () => {
|
||||
const dataList = state.execRes.data as any;
|
||||
isTrue(dataList.length > 0, '没有数据可导出');
|
||||
exportCsv(`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.execRes.tableColumn, dataList)
|
||||
};
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
|
||||
};
|
||||
|
||||
// 执行sql成功
|
||||
const execSqlFileSuccess = (res: any) => {
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取sql文件上传执行url
|
||||
const getUploadSqlFileUrl = () => {
|
||||
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`;
|
||||
};
|
||||
|
||||
|
||||
const onDataSelectionChange = (datas: []) => {
|
||||
state.selectionDatas = datas;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除数据事件
|
||||
*/
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = state.selectionDatas;
|
||||
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
|
||||
const { db } = state.ti;
|
||||
const dbInst = state.ti.getNowDbInst()
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
const primaryKeyColumnName = primaryKey.columnName;
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
|
||||
state.execRes.data = state.execRes.data.filter(
|
||||
(d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1)
|
||||
);
|
||||
state.selectionDatas = [];
|
||||
});
|
||||
};
|
||||
|
||||
// 监听单元格点击事件
|
||||
const cellClick = (row: any, column: any, cell: any) => {
|
||||
const property = column.property;
|
||||
const table = state.table;
|
||||
// 如果当前操作的表名不存在 或者 当前列的property不存在(如多选框),则不允许修改当前单元格内容
|
||||
if (!table || !property) {
|
||||
return;
|
||||
}
|
||||
let div: HTMLElement = cell.children[0];
|
||||
if (div && div.tagName === 'DIV') {
|
||||
// 转为字符串比较,可能存在数字等
|
||||
let text = (row[property] || row[property] == 0 ? row[property] : '') + '';
|
||||
let input = document.createElement('input');
|
||||
input.setAttribute('value', text);
|
||||
// 将表格witih也赋值于输入框,避免输入框长度超过表格长度
|
||||
input.setAttribute('style', 'height:23px;text-align:center;border:none;' + div.getAttribute('style'));
|
||||
cell.replaceChildren(input);
|
||||
input.focus();
|
||||
input.addEventListener('blur', async () => {
|
||||
row[property] = input.value;
|
||||
cell.replaceChildren(div);
|
||||
if (input.value !== text) {
|
||||
let currentUpdatedFields = state.updatedFields
|
||||
const dbInst = state.ti.getNowDbInst()
|
||||
const db = state.ti.getNowDb();
|
||||
// 主键
|
||||
const primaryKey = await dbInst.loadTableColumn(state.ti.db, table);
|
||||
const primaryKeyValue = row[primaryKey.columnName];
|
||||
// 更新字段列信息
|
||||
const updateColumn = db.getColumn(table, property);
|
||||
const newField = {
|
||||
div, row,
|
||||
fieldName: column.rawColumnKey,
|
||||
fieldType: updateColumn.columnType,
|
||||
oldValue: text,
|
||||
newValue: input.value
|
||||
} as FieldsMeta;
|
||||
|
||||
// 被修改的字段
|
||||
const primaryKeyFields = currentUpdatedFields.filter((meta) => meta.primaryKey === primaryKeyValue)
|
||||
let hasKey = false;
|
||||
if (primaryKeyFields.length <= 0) {
|
||||
primaryKeyFields[0] = {
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields: [newField]
|
||||
}
|
||||
} else {
|
||||
hasKey = true
|
||||
let hasField = primaryKeyFields[0].fields.some(a => {
|
||||
if (a.fieldName === newField.fieldName) {
|
||||
a.newValue = newField.newValue
|
||||
}
|
||||
return a.fieldName === newField.fieldName
|
||||
})
|
||||
if (!hasField) {
|
||||
primaryKeyFields[0].fields.push(newField)
|
||||
}
|
||||
}
|
||||
let fields = primaryKeyFields[0].fields
|
||||
|
||||
const fieldsParam = fields.filter((a) => {
|
||||
if (a.fieldName === column.rawColumnKey) {
|
||||
a.newValue = input.value
|
||||
}
|
||||
return a.fieldName === column.rawColumnKey
|
||||
})
|
||||
|
||||
const field = fieldsParam.length > 0 && fieldsParam[0] || {} as FieldsMeta
|
||||
if (field.oldValue === input.value) { // 新值=旧值
|
||||
// 删除数据
|
||||
div.classList.remove('update_field_active')
|
||||
let delIndex: number[] = [];
|
||||
currentUpdatedFields.forEach((a, i) => {
|
||||
if (a.primaryKey === primaryKeyValue) {
|
||||
a.fields = a.fields && a.fields.length > 0 ? a.fields.filter(f => f.fieldName !== column.rawColumnKey) : [];
|
||||
a.fields.length <= 0 && delIndex.push(i)
|
||||
}
|
||||
});
|
||||
delIndex.forEach(i => delete currentUpdatedFields[i])
|
||||
currentUpdatedFields = currentUpdatedFields.filter(a => a)
|
||||
} else {
|
||||
// 新增数据
|
||||
div.classList.add('update_field_active')
|
||||
if (hasKey) {
|
||||
currentUpdatedFields.forEach((value, index, array) => {
|
||||
if (value.primaryKey === primaryKeyValue) {
|
||||
array[index].fields = fields
|
||||
}
|
||||
})
|
||||
} else {
|
||||
currentUpdatedFields.push({
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields
|
||||
})
|
||||
}
|
||||
}
|
||||
state.updatedFields = currentUpdatedFields;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
let currentUpdatedFields = state.updatedFields;
|
||||
if (currentUpdatedFields.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const { db } = state.ti;
|
||||
const table = state.table;
|
||||
let res = '';
|
||||
let divs: HTMLElement[] = [];
|
||||
currentUpdatedFields.forEach(a => {
|
||||
let sql = `UPDATE ${table} SET `;
|
||||
let primaryKey = a.primaryKey;
|
||||
let primaryKeyType = a.primaryKeyType;
|
||||
let primaryKeyName = a.primaryKeyName;
|
||||
a.fields.forEach(f => {
|
||||
sql += ` ${f.fieldName} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`
|
||||
divs.push(f.div)
|
||||
})
|
||||
sql = sql.substring(0, sql.length - 1)
|
||||
sql += ` WHERE ${primaryKeyName} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
|
||||
res += sql;
|
||||
})
|
||||
|
||||
state.ti.getNowDbInst().promptExeSql(db, res, () => { }, () => {
|
||||
currentUpdatedFields = [];
|
||||
divs.forEach(a => {
|
||||
a.classList.remove('update_field_active');
|
||||
})
|
||||
state.updatedFields = [];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
state.updatedFields.forEach((a: any) => {
|
||||
a.fields.forEach((b: any) => {
|
||||
b.div.classList.remove('update_field_active')
|
||||
b.row[b.fieldName] = b.oldValue
|
||||
})
|
||||
})
|
||||
state.updatedFields = [];
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sql-file-exec {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sqlEditor {
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success)
|
||||
}
|
||||
</style>
|
||||
508
mayfly_go_web/src/views/ops/db/component/tab/TableData.vue
Normal file
508
mayfly_go_web/src/views/ops/db/component/tab/TableData.vue
Normal file
@@ -0,0 +1,508 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5">
|
||||
</el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-link @click="addRow()" type="primary" icon="plus" :underline="false"></el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-link @click="onDeleteData()" type="danger" icon="delete" :underline="false"></el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false">
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="生成insert sql" placement="top">
|
||||
<el-link @click="onGenerateInsertSql()" type="success" :underline="false">gi</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="导出当前页的csv文件" placement="top">
|
||||
<el-link type="success" :underline="false" @click="exportData"><span class="f12">导出</span></el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip v-if="state.updatedFields.length > 0" class="box-item" effect="dark" content="提交修改"
|
||||
placement="top">
|
||||
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="f12">提交</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider v-if="state.updatedFields.length > 0" direction="vertical" border-style="dashed" />
|
||||
<el-tooltip v-if="state.updatedFields.length > 0" class="box-item" effect="dark" content="取消修改"
|
||||
placement="top">
|
||||
<el-link @click="cancelUpdateFields" type="warning" :underline="false" class="f12">取消</el-link>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input v-model="condition" placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可" clearable size="small"
|
||||
style="width: 100%">
|
||||
<template #prepend>
|
||||
<el-popover trigger="click" :width="320" placement="right">
|
||||
<template #reference>
|
||||
<el-link type="success" :underline="false">选择列</el-link>
|
||||
</template>
|
||||
<el-table :data="columns" max-height="500" size="small" @row-click="
|
||||
(...event: any) => {
|
||||
onConditionRowClick(event);
|
||||
}
|
||||
" style="cursor: pointer">
|
||||
<el-table-column property="columnName" label="列名" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column property="columnComment" label="备注" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<el-button @click="onSelectByCondition()" icon="search" size="small"></el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-table @cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
|
||||
@sort-change="(sort: any) => onTableSortChange(sort)" @selection-change="onDataSelectionChange"
|
||||
:data="datas" size="small" :max-height="tableHeight" v-loading="loading" element-loading-text="查询中..."
|
||||
empty-text="暂无数据" stripe border class="mt5">
|
||||
<el-table-column v-if="datas.length > 0" type="selection" width="35" />
|
||||
<el-table-column min-width="100" :width="DbInst.flexColumnWidth(item, datas)" align="center"
|
||||
v-for="item in columnNames" :key="item" :prop="item" :label="item" show-overflow-tooltip
|
||||
:sortable="'custom'">
|
||||
<template #header>
|
||||
<el-tooltip raw-content placement="top" effect="customized">
|
||||
<template #content> {{ getColumnTip(item) }} </template>
|
||||
{{ item }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-row type="flex" class="mt5" justify="center">
|
||||
<el-pagination small :total="count" @current-change="pageChange()" layout="prev, pager, next, total, jumper"
|
||||
v-model:current-page="pageNum" :page-size="DbInst.DefaultLimit"></el-pagination>
|
||||
</el-row>
|
||||
<div style=" font-size: 12px; padding: 0 10px; color: #606266"><span>{{ state.sql }}</span>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
|
||||
<el-row>
|
||||
<el-col :span="5">
|
||||
<el-select v-model="conditionDialog.condition">
|
||||
<el-option label="=" value="="> </el-option>
|
||||
<el-option label="LIKE" value="LIKE"> </el-option>
|
||||
<el-option label=">" value=">"> </el-option>
|
||||
<el-option label=">=" value=">="> </el-option>
|
||||
<el-option label="<" value="<"> </el-option>
|
||||
<el-option label="<=" value="<="> </el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="19">
|
||||
<el-input v-model="conditionDialog.value" :placeholder="conditionDialog.placeholder" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancelCondition">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirmCondition">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, reactive, toRefs } from 'vue';
|
||||
import { isTrue, notEmpty } from '@/common/assert';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { DbInst, UpdateFieldsMeta, FieldsMeta, TabInfo } from '../../db';
|
||||
import { exportCsv } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
import { notBlank } from '../../../../../common/assert';
|
||||
|
||||
const emits = defineEmits(['genInsertSql', 'clickSqlName', 'clickSchemaTable', 'changeSchema', 'loadSqlNames'])
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: TabInfo,
|
||||
required: true
|
||||
},
|
||||
tableHeight: {
|
||||
type: String,
|
||||
default: '600'
|
||||
}
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
ti: {} as TabInfo,
|
||||
dbId: null, // 当前选中操作的数据库实例
|
||||
table: '', // 当前的表名
|
||||
datas: [],
|
||||
sql: '', // 当前数据tab执行的sql
|
||||
orderBy: '',
|
||||
condition: '', // 当前条件框的条件
|
||||
loading: false, // 是否在加载数据
|
||||
columnNames: [],
|
||||
columns: [],
|
||||
pageNum: 1,
|
||||
count: 0,
|
||||
selectionDatas: [] as any,
|
||||
conditionDialog: {
|
||||
title: '',
|
||||
placeholder: '',
|
||||
columnRow: null,
|
||||
dataTab: null,
|
||||
visible: false,
|
||||
condition: '=',
|
||||
value: null
|
||||
},
|
||||
tableHeight: '600',
|
||||
updatedFields: [] as UpdateFieldsMeta[],// 各个tab表被修改的字段信息
|
||||
});
|
||||
|
||||
const {
|
||||
datas,
|
||||
condition,
|
||||
loading,
|
||||
columns,
|
||||
columnNames,
|
||||
pageNum,
|
||||
count,
|
||||
conditionDialog,
|
||||
} = toRefs(state);
|
||||
|
||||
watch(() => props.tableHeight, (newValue: any) => {
|
||||
state.tableHeight = newValue;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in table data mounted');
|
||||
state.ti = props.data;
|
||||
state.tableHeight = props.tableHeight;
|
||||
state.table = state.ti.other.table;
|
||||
notBlank(state.table, "TableData组件other.table信息不能为空")
|
||||
|
||||
const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table);
|
||||
state.columns = columns;
|
||||
state.columnNames = columns.map((t: any) => t.columnName);
|
||||
await onRefresh();
|
||||
})
|
||||
|
||||
const onRefresh = async () => {
|
||||
// 查询条件置空
|
||||
state.condition = '';
|
||||
state.pageNum = 1;
|
||||
await selectData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据tab修改页数
|
||||
*/
|
||||
const pageChange = async () => {
|
||||
await selectData();
|
||||
};
|
||||
|
||||
/**
|
||||
* 单表数据信息查询数据
|
||||
*/
|
||||
const selectData = async () => {
|
||||
state.loading = true;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const { db } = state.ti;
|
||||
try {
|
||||
const countRes = await dbInst.runSql(db, DbInst.getDefaultCountSql(state.table));
|
||||
state.count = countRes.res[0].count;
|
||||
let sql = dbInst.getDefaultSelectSql(state.table, state.condition, state.orderBy, state.pageNum);
|
||||
state.sql = sql;
|
||||
if (state.count > 0) {
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
state.datas = colAndData.res;
|
||||
} else {
|
||||
state.datas = [];
|
||||
}
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
*/
|
||||
const exportData = () => {
|
||||
const dataList = state.datas as any;
|
||||
isTrue(dataList.length > 0, '没有数据可导出');
|
||||
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.columnNames, dataList)
|
||||
};
|
||||
|
||||
|
||||
const getColumnTip = (columnName: string) => {
|
||||
// 优先从 table map中获取
|
||||
let columns = getColumns();
|
||||
if (!columns) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const column = columns.find((c: any) => c.columnName == columnName);
|
||||
const comment = column.columnComment;
|
||||
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
|
||||
};
|
||||
|
||||
const getColumns = () => {
|
||||
return state.ti.getNowDb().getColumns(state.table);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 条件查询,点击列信息后显示输入对应的值
|
||||
*/
|
||||
const onConditionRowClick = (event: any) => {
|
||||
const row = event[0];
|
||||
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
|
||||
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
|
||||
state.conditionDialog.columnRow = row;
|
||||
state.conditionDialog.visible = true;
|
||||
};
|
||||
|
||||
// 确认条件
|
||||
const onConfirmCondition = () => {
|
||||
const conditionDialog = state.conditionDialog;
|
||||
let condition = state.condition;
|
||||
if (condition) {
|
||||
condition += ` AND `;
|
||||
}
|
||||
const row = conditionDialog.columnRow as any;
|
||||
condition += `${row.columnName} ${conditionDialog.condition} `;
|
||||
state.condition = condition + DbInst.wrapColumnValue(row.columnType, conditionDialog.value);
|
||||
onCancelCondition();
|
||||
};
|
||||
|
||||
const onCancelCondition = () => {
|
||||
state.conditionDialog.visible = false;
|
||||
state.conditionDialog.title = ``;
|
||||
state.conditionDialog.placeholder = ``;
|
||||
state.conditionDialog.value = null;
|
||||
state.conditionDialog.columnRow = null;
|
||||
state.conditionDialog.dataTab = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
const onSelectByCondition = async () => {
|
||||
notEmpty(state.condition, '条件不能为空');
|
||||
state.pageNum = 1;
|
||||
await selectData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 表排序字段变更
|
||||
*/
|
||||
const onTableSortChange = async (sort: any) => {
|
||||
if (!sort.prop) {
|
||||
return;
|
||||
}
|
||||
const sortType = sort.order == 'descending' ? 'DESC' : 'ASC';
|
||||
state.orderBy = `ORDER BY ${sort.prop} ${sortType}`;
|
||||
await onRefresh();
|
||||
};
|
||||
|
||||
const onDataSelectionChange = (datas: []) => {
|
||||
state.selectionDatas = datas;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除数据事件
|
||||
*/
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = state.selectionDatas;
|
||||
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
|
||||
const { db } = state.ti;
|
||||
const dbInst = state.ti.getNowDbInst()
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
|
||||
onRefresh();
|
||||
});
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async () => {
|
||||
emits('genInsertSql', state.ti.getNowDbInst().genInsertSql(state.ti.db, state.table, state.selectionDatas));
|
||||
};
|
||||
|
||||
|
||||
// 监听单元格点击事件
|
||||
const cellClick = (row: any, column: any, cell: any) => {
|
||||
const property = column.property;
|
||||
// 如果当前操作的表名不存在 或者 当前列的property不存在(如多选框),则不允许修改当前单元格内容
|
||||
if (!property) {
|
||||
return;
|
||||
}
|
||||
let div: HTMLElement = cell.children[0];
|
||||
if (div && div.tagName === 'DIV') {
|
||||
// 转为字符串比较,可能存在数字等
|
||||
let text = (row[property] || row[property] == 0 ? row[property] : '') + '';
|
||||
let input = document.createElement('input');
|
||||
input.setAttribute('value', text);
|
||||
// 将表格width也赋值于输入框,避免输入框长度超过表格长度
|
||||
input.setAttribute('style', 'height:23px;text-align:center;border:none;' + div.getAttribute('style'));
|
||||
cell.replaceChildren(input);
|
||||
input.focus();
|
||||
input.addEventListener('blur', async () => {
|
||||
row[property] = input.value;
|
||||
cell.replaceChildren(div);
|
||||
if (input.value !== text) {
|
||||
let currentUpdatedFields = state.updatedFields
|
||||
const db = state.ti.getNowDb();
|
||||
// 主键
|
||||
const primaryKey = db.getColumn(state.table);
|
||||
const primaryKeyValue = row[primaryKey.columnName];
|
||||
// 更新字段列信息
|
||||
const updateColumn = db.getColumn(state.table, property);
|
||||
const newField = {
|
||||
div, row,
|
||||
fieldName: column.rawColumnKey,
|
||||
fieldType: updateColumn.columnType,
|
||||
oldValue: text,
|
||||
newValue: input.value
|
||||
} as FieldsMeta;
|
||||
|
||||
// 被修改的字段
|
||||
const primaryKeyFields = currentUpdatedFields.filter((meta) => meta.primaryKey === primaryKeyValue)
|
||||
let hasKey = false;
|
||||
if (primaryKeyFields.length <= 0) {
|
||||
primaryKeyFields[0] = {
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields: [newField]
|
||||
}
|
||||
} else {
|
||||
hasKey = true
|
||||
let hasField = primaryKeyFields[0].fields.some(a => {
|
||||
if (a.fieldName === newField.fieldName) {
|
||||
a.newValue = newField.newValue
|
||||
}
|
||||
return a.fieldName === newField.fieldName
|
||||
})
|
||||
if (!hasField) {
|
||||
primaryKeyFields[0].fields.push(newField)
|
||||
}
|
||||
}
|
||||
let fields = primaryKeyFields[0].fields
|
||||
|
||||
const fieldsParam = fields.filter((a) => {
|
||||
if (a.fieldName === column.rawColumnKey) {
|
||||
a.newValue = input.value
|
||||
}
|
||||
return a.fieldName === column.rawColumnKey
|
||||
})
|
||||
|
||||
const field = fieldsParam.length > 0 && fieldsParam[0] || {} as FieldsMeta
|
||||
if (field.oldValue === input.value) { // 新值=旧值
|
||||
// 删除数据
|
||||
div.classList.remove('update_field_active')
|
||||
let delIndex: number[] = [];
|
||||
currentUpdatedFields.forEach((a, i) => {
|
||||
if (a.primaryKey === primaryKeyValue) {
|
||||
a.fields = a.fields && a.fields.length > 0 ? a.fields.filter(f => f.fieldName !== column.rawColumnKey) : [];
|
||||
a.fields.length <= 0 && delIndex.push(i)
|
||||
}
|
||||
});
|
||||
delIndex.forEach(i => delete currentUpdatedFields[i])
|
||||
currentUpdatedFields = currentUpdatedFields.filter(a => a)
|
||||
} else {
|
||||
// 新增数据
|
||||
div.classList.add('update_field_active')
|
||||
if (hasKey) {
|
||||
currentUpdatedFields.forEach((value, index, array) => {
|
||||
if (value.primaryKey === primaryKeyValue) {
|
||||
array[index].fields = fields
|
||||
}
|
||||
})
|
||||
} else {
|
||||
currentUpdatedFields.push({
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields
|
||||
})
|
||||
}
|
||||
}
|
||||
state.updatedFields = currentUpdatedFields;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
let currentUpdatedFields = state.updatedFields;
|
||||
if (currentUpdatedFields.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const { db } = state.ti;
|
||||
let res = '';
|
||||
let divs: HTMLElement[] = [];
|
||||
currentUpdatedFields.forEach(a => {
|
||||
let sql = `UPDATE ${state.table} SET `;
|
||||
let primaryKey = a.primaryKey;
|
||||
let primaryKeyType = a.primaryKeyType;
|
||||
let primaryKeyName = a.primaryKeyName;
|
||||
a.fields.forEach(f => {
|
||||
sql += ` ${f.fieldName} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`
|
||||
divs.push(f.div)
|
||||
})
|
||||
sql = sql.substring(0, sql.length - 1)
|
||||
sql += ` WHERE ${primaryKeyName} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
|
||||
res += sql;
|
||||
})
|
||||
|
||||
state.ti.getNowDbInst().promptExeSql(db, res, () => { }, () => {
|
||||
currentUpdatedFields = [];
|
||||
divs.forEach(a => {
|
||||
a.classList.remove('update_field_active');
|
||||
})
|
||||
state.updatedFields = [];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
state.updatedFields.forEach((a: any) => {
|
||||
a.fields.forEach((b: any) => {
|
||||
b.div.classList.remove('update_field_active')
|
||||
b.row[b.fieldName] = b.oldValue
|
||||
})
|
||||
})
|
||||
state.updatedFields = [];
|
||||
}
|
||||
|
||||
// 添加新数据行
|
||||
const addRow = async () => {
|
||||
const columns = state.ti.getNowDb().getColumns(state.table);
|
||||
// key: 字段名,value: 字段名提示
|
||||
let obj: any = {};
|
||||
columns.forEach((item: any) => {
|
||||
obj[`${item.columnName}`] = `'${item.columnComment || ''} ${item.columnName}[${item.columnType}]${item.nullable == 'YES' ? '' : '[not null]'}'`;
|
||||
});
|
||||
let columnNames = Object.keys(obj).join(',');
|
||||
let values = Object.values(obj).join(',');
|
||||
let sql = `INSERT INTO ${state.table} (${columnNames}) VALUES (${values});`;
|
||||
state.ti.getNowDbInst().promptExeSql(state.ti.db, sql, null, () => {
|
||||
onRefresh();
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success)
|
||||
}
|
||||
</style>
|
||||
447
mayfly_go_web/src/views/ops/db/db.ts
Normal file
447
mayfly_go_web/src/views/ops/db/db.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { dbApi } from './api';
|
||||
import SqlExecBox from './component/SqlExecBox';
|
||||
|
||||
const dbInstCache: Map<number, DbInst> = new Map();
|
||||
|
||||
export class DbInst {
|
||||
/**
|
||||
* 实例id
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* 数据库类型, mysql postgres
|
||||
*/
|
||||
type: string
|
||||
|
||||
/**
|
||||
* schema -> db
|
||||
*/
|
||||
dbs: Map<string, Db> = 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<string, any> = 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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -1,69 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<mongo-instance-tree
|
||||
@init-load-instances="loadInstances"
|
||||
@change-instance="changeInstance"
|
||||
@change-schema="changeDatabase"
|
||||
@load-table-names="loadTableNames"
|
||||
@load-table-data="changeCollection"
|
||||
:instances="state.instances"/>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-container id="data-exec" style="border: 1px solid #eee; margin-top: 1px">
|
||||
<el-tabs @tab-remove="removeDataTab" @tab-click="onDataTabClick" style="width: 100%; margin-left: 5px"
|
||||
v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
||||
|
||||
<el-row class="mt5 mb5">
|
||||
<el-col :span="2">
|
||||
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" class="">
|
||||
</el-link>
|
||||
<el-link @click="showInsertDocDialog" class="" type="primary" icon="plus" :underline="false">
|
||||
</el-link>
|
||||
</el-col>
|
||||
<el-col :span="22">
|
||||
<el-input ref="findParamInputRef" v-model="dt.findParamStr" placeholder="点击输入相应查询条件"
|
||||
@focus="showFindDialog(dt.key)">
|
||||
<template #prepend>查询参数</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="6" v-for="item in dt.datas" :key="item">
|
||||
<el-card :body-style="{ padding: '0px', position: 'relative' }">
|
||||
<el-input type="textarea" v-model="item.value" :rows="10" />
|
||||
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
|
||||
<div>
|
||||
<el-link @click="onJsonEditor(item)" :underline="false" type="success"
|
||||
icon="MagicStick"></el-link>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<mongo-instance-tree @init-load-instances="loadInstances" @change-instance="changeInstance"
|
||||
@load-table-names="loadTableNames" @load-table-data="changeCollection"
|
||||
:instances="state.instances" />
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-container id="data-exec" style="border: 1px solid #eee; margin-top: 1px">
|
||||
<el-tabs @tab-remove="removeDataTab" style="width: 100%; margin-left: 5px"
|
||||
v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label"
|
||||
:name="dt.key">
|
||||
<el-row class="mt5 mb5">
|
||||
<el-col :span="2">
|
||||
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false"
|
||||
class="">
|
||||
</el-link>
|
||||
<el-link @click="showInsertDocDialog" class="" type="primary" icon="plus"
|
||||
:underline="false">
|
||||
</el-link>
|
||||
</el-col>
|
||||
<el-col :span="22">
|
||||
<el-input ref="findParamInputRef" v-model="dt.findParamStr" placeholder="点击输入相应查询条件"
|
||||
@focus="showFindDialog(dt.key)">
|
||||
<template #prepend>查询参数</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="6" v-for="item in dt.datas" :key="item">
|
||||
<el-card :body-style="{ padding: '0px', position: 'relative' }">
|
||||
<el-input type="textarea" v-model="item.value" :rows="10" />
|
||||
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
|
||||
<div>
|
||||
<el-link @click="onJsonEditor(item)" :underline="false" type="success"
|
||||
icon="MagicStick"></el-link>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-link @click="onSaveDoc(item.value)" :underline="false" type="warning"
|
||||
icon="DocumentChecked"></el-link>
|
||||
<el-link @click="onSaveDoc(item.value)" :underline="false"
|
||||
type="warning" icon="DocumentChecked"></el-link>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?">
|
||||
<template #reference>
|
||||
<el-link :underline="false" type="danger" icon="DocumentDelete">
|
||||
</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
</el-col>
|
||||
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?">
|
||||
<template #reference>
|
||||
<el-link :underline="false" type="danger" icon="DocumentDelete">
|
||||
</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
|
||||
</el-row>
|
||||
|
||||
<el-dialog width="600px" title="find参数" v-model="findDialog.visible">
|
||||
<el-form label-width="70px">
|
||||
<el-form-item label="filter">
|
||||
@@ -101,7 +99,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog width="60%" title="json编辑器" v-model="jsonEditorDialog.visible" @close="onCloseJsonEditDialog"
|
||||
:close-on-click-modal="false">
|
||||
:close-on-click-modal="false">
|
||||
<monaco-editor v-model="jsonEditorDialog.doc" language="json" />
|
||||
</el-dialog>
|
||||
|
||||
@@ -110,26 +108,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {mongoApi} from './api';
|
||||
import {reactive, ref, toRefs} from 'vue';
|
||||
import {ElMessage} from 'element-plus';
|
||||
import { mongoApi } from './api';
|
||||
import { reactive, ref, toRefs } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {isTrue, notBlank} from '@/common/assert';
|
||||
import {useStore} from '@/store/index.ts';
|
||||
import { isTrue, notBlank } from '@/common/assert';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import MongoInstanceTree from '@/views/ops/mongo/MongoInstanceTree.vue';
|
||||
|
||||
const store = useStore();
|
||||
const findParamInputRef: any = ref(null);
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
mongoList: [] as any,
|
||||
query: {
|
||||
tagPath: null,
|
||||
},
|
||||
mongoId: null, // 当前选择操作的mongo
|
||||
database: '', // 当前选择操作的库
|
||||
collection: '', //当前选中的collection
|
||||
activeName: '', // 当前操作的tab
|
||||
dataTabs: {} as any, // 数据tabs
|
||||
findDialog: {
|
||||
@@ -150,7 +140,7 @@ const state = reactive({
|
||||
doc: '',
|
||||
item: {} as any,
|
||||
},
|
||||
instances:{tags:{}, tree:{}, dbs:{}, tables:{}}
|
||||
instances: { tags: {}, tree: {}, dbs: {}, tables: {} }
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -160,53 +150,50 @@ const {
|
||||
} = toRefs(state)
|
||||
|
||||
const changeInstance = async (inst: any, fn: Function) => {
|
||||
if (inst) {
|
||||
if (!state.instances.dbs[inst.id]) {
|
||||
const res = await mongoApi.databases.request({id: inst.id});
|
||||
state.instances.dbs[inst.id] = res.Databases;
|
||||
fn && fn(res.Databases)
|
||||
if (inst) {
|
||||
if (!state.instances.dbs[inst.id]) {
|
||||
const res = await mongoApi.databases.request({ id: inst.id });
|
||||
state.instances.dbs[inst.id] = res.Databases;
|
||||
fn && fn(res.Databases)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changeDatabase = async (inst: any, database: string) => {
|
||||
};
|
||||
|
||||
const loadTableNames = async (inst: any, database: string, fn:Function) => {
|
||||
let tbs = await mongoApi.collections.request({ id: inst.id, database });
|
||||
let tables = [];
|
||||
for(let tb of tbs){
|
||||
tables.push({tableName: tb, show: true})
|
||||
}
|
||||
state.instances.tables[inst.id+database] = tables
|
||||
fn(tables)
|
||||
const loadTableNames = async (inst: any, database: string, fn: Function) => {
|
||||
let tbs = await mongoApi.collections.request({ id: inst.id, database });
|
||||
let tables = [];
|
||||
for (let tb of tbs) {
|
||||
tables.push({ tableName: tb, show: true })
|
||||
}
|
||||
state.instances.tables[inst.id + database] = tables
|
||||
fn(tables)
|
||||
}
|
||||
|
||||
const changeCollection = (inst: any, schema: string, collection: string) => {
|
||||
state.collection = collection
|
||||
state.mongoId = inst.id
|
||||
state.database = schema
|
||||
let key = inst.id + schema +collection
|
||||
let dataTab = state.dataTabs[key];
|
||||
if (!dataTab) {
|
||||
// 默认查询参数
|
||||
const findParam = {
|
||||
filter: '{}',
|
||||
sort: '{"_id": -1}',
|
||||
skip: 0,
|
||||
limit: 12,
|
||||
};
|
||||
state.dataTabs[key] = {
|
||||
key: key,
|
||||
label: schema+'.'+collection,
|
||||
name: inst.id+schema+collection,
|
||||
datas: [],
|
||||
findParamStr: JSON.stringify(findParam),
|
||||
findParam,
|
||||
};
|
||||
}
|
||||
state.activeName = key;
|
||||
findCommand(key);
|
||||
const label = `${inst.id}:\`${schema}\`.${collection}`;
|
||||
let dataTab = state.dataTabs[label];
|
||||
if (!dataTab) {
|
||||
// 默认查询参数
|
||||
const findParam = {
|
||||
filter: '{}',
|
||||
sort: '{"_id": -1}',
|
||||
skip: 0,
|
||||
limit: 12,
|
||||
};
|
||||
state.dataTabs[label] = {
|
||||
key: label,
|
||||
label: label,
|
||||
name: label,
|
||||
mongoId: inst.id,
|
||||
database: schema,
|
||||
collection,
|
||||
datas: [],
|
||||
findParamStr: JSON.stringify(findParam),
|
||||
findParam,
|
||||
};
|
||||
}
|
||||
state.activeName = label;
|
||||
findCommand(label);
|
||||
};
|
||||
|
||||
const showFindDialog = (key: string) => {
|
||||
@@ -230,7 +217,7 @@ const confirmFindDialog = () => {
|
||||
};
|
||||
|
||||
const findCommand = async (key: string) => {
|
||||
const dataTab = state.dataTabs[key];
|
||||
const dataTab = getNowDataTab();
|
||||
const findParma = dataTab.findParam;
|
||||
let filter, sort;
|
||||
try {
|
||||
@@ -241,9 +228,9 @@ const findCommand = async (key: string) => {
|
||||
return;
|
||||
}
|
||||
const datas = await mongoApi.findCommand.request({
|
||||
id: state.mongoId,
|
||||
database: state.database,
|
||||
collection: state.collection,
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
collection: dataTab.collection,
|
||||
filter,
|
||||
sort,
|
||||
limit: findParma.limit || 12,
|
||||
@@ -287,10 +274,11 @@ const onInsertDoc = async () => {
|
||||
} catch (e) {
|
||||
ElMessage.error('文档内容错误,无法解析为json对象');
|
||||
}
|
||||
const dataTab = getNowDataTab();
|
||||
const res = await mongoApi.insertCommand.request({
|
||||
id: state.mongoId,
|
||||
database: state.database,
|
||||
collection: state.activeName,
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
collection: dataTab.collection,
|
||||
doc: docObj,
|
||||
});
|
||||
isTrue(res.InsertedID, '新增失败');
|
||||
@@ -314,10 +302,11 @@ const onSaveDoc = async (doc: string) => {
|
||||
const id = docObj._id;
|
||||
notBlank(id, '文档的_id属性不存在');
|
||||
delete docObj['_id'];
|
||||
const dataTab = getNowDataTab();
|
||||
const res = await mongoApi.updateByIdCommand.request({
|
||||
id: state.mongoId,
|
||||
database: state.database,
|
||||
collection: state.collection,
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
collection: dataTab.collection,
|
||||
docId: id,
|
||||
update: { $set: docObj },
|
||||
});
|
||||
@@ -329,10 +318,11 @@ const onDeleteDoc = async (doc: string) => {
|
||||
const docObj = parseDocJsonString(doc);
|
||||
const id = docObj._id;
|
||||
notBlank(id, '文档的_id属性不存在');
|
||||
const dataTab = getNowDataTab();
|
||||
const res = await mongoApi.deleteByIdCommand.request({
|
||||
id: state.mongoId,
|
||||
database: state.database,
|
||||
collection: state.collection,
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
collection: dataTab.collection,
|
||||
docId: id,
|
||||
});
|
||||
isTrue(res.DeletedCount == 1, '删除失败');
|
||||
@@ -352,15 +342,6 @@ const parseDocJsonString = (doc: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据tab点击
|
||||
*/
|
||||
const onDataTabClick = (tab: any) => {
|
||||
const name = tab.props.name;
|
||||
// 修改选择框绑定的表信息
|
||||
state.collection = name;
|
||||
};
|
||||
|
||||
const removeDataTab = (targetName: string) => {
|
||||
const tabNames = Object.keys(state.dataTabs);
|
||||
let activeName = state.activeName;
|
||||
@@ -373,29 +354,26 @@ const removeDataTab = (targetName: string) => {
|
||||
}
|
||||
});
|
||||
state.activeName = activeName;
|
||||
// 如果移除最后一个数据tab,则将选择框绑定的collection置空
|
||||
if (activeName == targetName) {
|
||||
state.collection = '';
|
||||
} else {
|
||||
state.collection = activeName;
|
||||
}
|
||||
|
||||
delete state.dataTabs[targetName];
|
||||
};
|
||||
|
||||
const loadInstances = async () => {
|
||||
const res = await mongoApi.mongoList.request({pageNum: 1, pageSize: 1000,});
|
||||
if(!res.total) return
|
||||
state.instances = {tags:{}, tree:{}, dbs:{}, tables:{}} ; // 初始化变量
|
||||
for (const db of res.list) {
|
||||
let arr = state.instances.tree[db.tagId] || []
|
||||
const {tagId, tagPath} = db
|
||||
// tags
|
||||
state.instances.tags[db.tagId]={tagId, tagPath}
|
||||
// 实例
|
||||
arr.push(db)
|
||||
state.instances.tree[db.tagId] = arr;
|
||||
}
|
||||
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000, });
|
||||
if (!res.total) return
|
||||
state.instances = { tags: {}, tree: {}, dbs: {}, tables: {} }; // 初始化变量
|
||||
for (const db of res.list) {
|
||||
let arr = state.instances.tree[db.tagId] || []
|
||||
const { tagId, tagPath } = db
|
||||
// tags
|
||||
state.instances.tags[db.tagId] = { tagId, tagPath }
|
||||
// 实例
|
||||
arr.push(db)
|
||||
state.instances.tree[db.tagId] = arr;
|
||||
}
|
||||
}
|
||||
|
||||
const getNowDataTab = () => {
|
||||
return state.dataTabs[state.activeName]
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -79,9 +79,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onBeforeMount, onMounted, reactive, ref, Ref, watch } from 'vue';
|
||||
import { onBeforeMount, reactive } from 'vue';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { store } from '@/store';
|
||||
import TagMenu from '../component/TagMenu.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -101,8 +100,6 @@ const setHeight = () => {
|
||||
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
|
||||
}
|
||||
|
||||
const menuRef = ref(null) as Ref
|
||||
|
||||
const state = reactive({
|
||||
instanceMenuMaxHeight: '800px',
|
||||
nowSchema: '',
|
||||
@@ -170,38 +167,6 @@ const filterTableName = (instId: number, schema: string, event?: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
const selectDb = async (val?: any) => {
|
||||
let info = val || store.state.mongoDbOptInfo.dbOptInfo;
|
||||
if (info && info.dbId) {
|
||||
const { tagPath, dbId, db } = info
|
||||
menuRef.value.open(tagPath);
|
||||
menuRef.value.open('mongo-instance-' + dbId);
|
||||
await changeInstance({ id: dbId }, () => {
|
||||
// 加载数据库
|
||||
nextTick(async () => {
|
||||
menuRef.value.open(dbId + db)
|
||||
// 加载集合列表
|
||||
await nextTick(async () => {
|
||||
await loadTableNames({ id: dbId }, db, (res: any[]) => {
|
||||
// 展开集合列表
|
||||
menuRef.value.open(dbId + db + '-table')
|
||||
// 加载第一张集合数据
|
||||
loadTableData({ id: dbId }, db, res[0].tableName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selectDb();
|
||||
})
|
||||
|
||||
watch(() => store.state.mongoDbOptInfo.dbOptInfo, async newValue => {
|
||||
await selectDb(newValue)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -65,9 +65,6 @@
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small"
|
||||
:underline="false">集合</el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="primary" @click="openDataOps(scope.row)" plain size="small" :underline="false">
|
||||
数据操作</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -195,8 +192,6 @@ import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { tagApi } from '../tag/api.ts';
|
||||
import MongoEdit from './MongoEdit.vue';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { store } from '@/store';
|
||||
import router from '@/router';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
|
||||
const state = reactive({
|
||||
@@ -406,22 +401,6 @@ const valChange = () => {
|
||||
search();
|
||||
};
|
||||
|
||||
const openDataOps = (row: any) => {
|
||||
state.dbOps.db = row.Name
|
||||
let data = {
|
||||
tagPath: state.currentData.tagPath,
|
||||
dbId: state.dbOps.dbId,
|
||||
db: state.dbOps.db,
|
||||
}
|
||||
state.databaseDialog.visible = false;
|
||||
// 判断db是否发生改变
|
||||
let oldDb = store.state.mongoDbOptInfo.dbOptInfo.db;
|
||||
if (oldDb !== row.Name) {
|
||||
store.dispatch('mongoDbOptInfo/setMongoDbOptInfo', data);
|
||||
}
|
||||
router.push({ name: 'MongoDataOp' });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,59 +6,62 @@
|
||||
@change-schema="loadInitSchema" :instances="state.instances" />
|
||||
</el-col>
|
||||
<el-col :span="20" style="border-left: 1px solid var(--el-card-border-color);">
|
||||
<el-col class="mt10">
|
||||
<el-form class="search-form" label-position="right" :inline="true" label-width="60px">
|
||||
<el-form-item label="key" label-width="40px">
|
||||
<el-input placeholder="match 支持*模糊key" style="width: 250px" v-model="scanParam.match"
|
||||
@clear="clear()" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="count" label-width="40px">
|
||||
<el-input placeholder="count" style="width: 70px" v-model.number="scanParam.count">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="searchKey()" type="success" icon="search" plain></el-button>
|
||||
<el-button @click="scan()" icon="bottom" plain>scan</el-button>
|
||||
<el-popover placement="right" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="primary" icon="plus" plain></el-button>
|
||||
</template>
|
||||
<el-tag @click="onAddData('string')" :color="getTypeColor('string')"
|
||||
style="cursor: pointer">string</el-tag>
|
||||
<el-tag @click="onAddData('hash')" :color="getTypeColor('hash')" class="ml5"
|
||||
style="cursor: pointer">hash</el-tag>
|
||||
<el-tag @click="onAddData('set')" :color="getTypeColor('set')" class="ml5"
|
||||
style="cursor: pointer">set</el-tag>
|
||||
<!-- <el-tag @click="onAddData('list')" :color="getTypeColor('list')" class="ml5" style="cursor: pointer">list</el-tag> -->
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<div style="float: right">
|
||||
<span>keys: {{ state.dbsize }}</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-table v-loading="state.loading" :data="state.keys" stripe :highlight-current-row="true"
|
||||
style="cursor: pointer">
|
||||
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
|
||||
<el-table-column prop="type" label="type" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :color="getTypeColor(scope.row.type)" size="small">{{ scope.row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ttl" label="ttl(过期时间)" width="140">
|
||||
<template #default="scope">
|
||||
{{ ttlConveter(scope.row.ttl) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button @click="getValue(scope.row)" type="success" icon="search" plain size="small">查看
|
||||
</el-button>
|
||||
<el-button @click="del(scope.row.key)" type="danger" icon="delete" plain size="small">删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt10 ml5">
|
||||
<el-col>
|
||||
<el-form class="search-form" label-position="right" :inline="true" label-width="60px">
|
||||
<el-form-item label="key" label-width="40px">
|
||||
<el-input placeholder="match 支持*模糊key" style="width: 250px" v-model="scanParam.match"
|
||||
@clear="clear()" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="count" label-width="40px">
|
||||
<el-input placeholder="count" style="width: 70px" v-model.number="scanParam.count">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="searchKey()" type="success" icon="search" plain></el-button>
|
||||
<el-button @click="scan()" icon="bottom" plain>scan</el-button>
|
||||
<el-popover placement="right" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="primary" icon="plus" plain></el-button>
|
||||
</template>
|
||||
<el-tag @click="onAddData('string')" :color="getTypeColor('string')"
|
||||
style="cursor: pointer">string</el-tag>
|
||||
<el-tag @click="onAddData('hash')" :color="getTypeColor('hash')" class="ml5"
|
||||
style="cursor: pointer">hash</el-tag>
|
||||
<el-tag @click="onAddData('set')" :color="getTypeColor('set')" class="ml5"
|
||||
style="cursor: pointer">set</el-tag>
|
||||
<!-- <el-tag @click="onAddData('list')" :color="getTypeColor('list')" class="ml5" style="cursor: pointer">list</el-tag> -->
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<div style="float: right">
|
||||
<span>keys: {{ state.dbsize }}</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-table v-loading="state.loading" :data="state.keys" stripe :highlight-current-row="true"
|
||||
style="cursor: pointer">
|
||||
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
|
||||
<el-table-column prop="type" label="type" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :color="getTypeColor(scope.row.type)" size="small">{{ scope.row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ttl" label="ttl(过期时间)" width="140">
|
||||
<template #default="scope">
|
||||
{{ ttlConveter(scope.row.ttl) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button @click="getValue(scope.row)" type="success" icon="search" plain
|
||||
size="small">查看
|
||||
</el-button>
|
||||
<el-button @click="del(scope.row.key)" type="danger" icon="delete" plain size="small">删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -92,10 +95,8 @@ import SetValue from './SetValue.vue';
|
||||
import ListValue from './ListValue.vue';
|
||||
import { isTrue, notBlank, notNull } from '@/common/assert';
|
||||
|
||||
import { useStore } from '@/store/index.ts';
|
||||
import RedisInstanceTree from '@/views/ops/redis/RedisInstanceTree.vue';
|
||||
|
||||
let store = useStore();
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
tags: [],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<tag-menu :instanceMenuMaxHeight="state.instanceMenuMaxHeight" :tags="instances.tags" ref="menuRef">
|
||||
<template #submenu="props">
|
||||
<el-sub-menu v-for="inst in instances.tree[props.tag.tagId]" :index="'redis-instance-' + inst.id"
|
||||
:key="'redis-instance-' + inst.id" @click.stop="changeInstance(inst)">
|
||||
<el-sub-menu v-for="inst in instances.tree[props.tag.tagId]" :index="'redis-' + inst.id"
|
||||
:key="'redis-' + inst.id" @click.stop="changeInstance(inst)">
|
||||
<template #title>
|
||||
<el-popover placement="right-start" title="redis实例信息" trigger="hover" :width="210">
|
||||
<template #reference>
|
||||
@@ -21,8 +21,7 @@
|
||||
</template>
|
||||
<!-- 第三级:数据库 -->
|
||||
<el-menu-item v-for="db in instances.dbs[inst.id]" :index="inst.id + db.name" :key="inst.id + db.name"
|
||||
:class="state.nowSchema === (inst.id + db.name) && 'checked'"
|
||||
@click="changeSchema(inst, db.name)">
|
||||
:class="state.nowSchema === (inst.id + db.name) && 'checked'" @click="changeSchema(inst, db.name)">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<Coin color="#67c23a" />
|
||||
@@ -38,8 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, onMounted, reactive, Ref, ref, watch } from 'vue';
|
||||
import { store } from '@/store';
|
||||
import { onBeforeMount, reactive, Ref, ref } from 'vue';
|
||||
import TagMenu from '../component/TagMenu.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -51,16 +49,14 @@ defineProps({
|
||||
const emits = defineEmits(['initLoadInstances', 'changeInstance', 'changeSchema'])
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await initLoadInstances()
|
||||
setHeight()
|
||||
await initLoadInstances();
|
||||
setHeight();
|
||||
})
|
||||
|
||||
const setHeight = () => {
|
||||
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
|
||||
}
|
||||
|
||||
const menuRef = ref(null) as Ref;
|
||||
|
||||
const state = reactive({
|
||||
instanceMenuMaxHeight: '800px',
|
||||
nowSchema: '',
|
||||
@@ -81,7 +77,7 @@ const initLoadInstances = () => {
|
||||
* @param fn 选中的实例后的回调函数
|
||||
*/
|
||||
const changeInstance = (inst: any, fn?: Function) => {
|
||||
emits('changeInstance', inst, fn)
|
||||
emits('changeInstance', inst, fn);
|
||||
}
|
||||
/**
|
||||
* 改变选中的数据库schema
|
||||
@@ -89,30 +85,10 @@ const changeInstance = (inst: any, fn?: Function) => {
|
||||
* @param schema 选中的数据库schema
|
||||
*/
|
||||
const changeSchema = (inst: any, schema: string) => {
|
||||
state.nowSchema = inst.id + schema
|
||||
emits('changeSchema', inst, schema)
|
||||
state.nowSchema = inst.id + schema;
|
||||
emits('changeSchema', inst, schema);
|
||||
}
|
||||
|
||||
const selectDb = async (val?: any) => {
|
||||
const info = val || store.state.redisDbOptInfo.dbOptInfo
|
||||
if (info && info.dbId) {
|
||||
const { tagPath, dbId } = info
|
||||
menuRef.value.open(tagPath);
|
||||
menuRef.value.open('redis-instance-' + dbId);
|
||||
await changeInstance({ id: dbId }, async (dbs: any[]) => {
|
||||
await changeSchema({ id: dbId }, dbs[0]?.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selectDb();
|
||||
})
|
||||
|
||||
watch(() => store.state.redisDbOptInfo.dbOptInfo, async newValue => {
|
||||
await selectDb(newValue)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
@click="showInfoDialog(scope.row)" :underline="false">单机信息</el-link>
|
||||
<el-link @click="onShowClusterInfo(scope.row)" v-if="scope.row.mode === 'cluster'"
|
||||
type="primary" :underline="false">集群信息</el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-link @click="openDataOpt(scope.row)" type="success" :underline="false">数据操作</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -159,8 +156,6 @@ import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { tagApi } from '../tag/api.ts';
|
||||
import RedisEdit from './RedisEdit.vue';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { store } from '@/store';
|
||||
import router from '@/router';
|
||||
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
@@ -296,21 +291,6 @@ const valChange = () => {
|
||||
state.currentData = null;
|
||||
search();
|
||||
};
|
||||
// 打开redis数据操作页
|
||||
const openDataOpt = (row: any) => {
|
||||
const { tagPath, id, db } = row;
|
||||
// 判断db是否发生改变
|
||||
let oldDbId = store.state.redisDbOptInfo.dbOptInfo.dbId;
|
||||
if (oldDbId !== id) {
|
||||
let params = {
|
||||
tagPath,
|
||||
dbId: id,
|
||||
db
|
||||
}
|
||||
store.dispatch('redisDbOptInfo/setRedisDbOptInfo', params);
|
||||
}
|
||||
router.push({ name: 'DataOperation' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="配置项"></el-table-column>
|
||||
<el-table-column prop="key" label="配置key"></el-table-column>
|
||||
<el-table-column prop="value" label="配置值" min-width="100px" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="name" label="配置项" min-width="100px" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="key" label="配置key" min-width="100px"></el-table-column>
|
||||
<el-table-column prop="value" label="配置值" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="100px" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="100px">
|
||||
<template #default="scope">
|
||||
{{ dateFormat(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="modifier" label="修改者" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="modifier" label="修改者" min-width="60px" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" min-width="50" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-link :disabled="scope.row.status == -1" type="warning"
|
||||
@@ -134,6 +134,8 @@ const showSetConfigDialog = (row: any) => {
|
||||
if (row.value) {
|
||||
state.paramsDialog.params = JSON.parse(row.value);
|
||||
}
|
||||
} else {
|
||||
state.paramsDialog.params = row.value;
|
||||
}
|
||||
} else {
|
||||
state.paramsDialog.params = row.value;
|
||||
|
||||
@@ -338,13 +338,13 @@
|
||||
estree-walker "^2.0.2"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-core@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz#d9311207d96f6ebd5f4660be129fb99f01ddb41b"
|
||||
integrity sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==
|
||||
"@vue/compiler-core@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.47.tgz#3e07c684d74897ac9aa5922c520741f3029267f8"
|
||||
integrity sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/shared" "3.2.47"
|
||||
estree-walker "^2.0.2"
|
||||
source-map "^0.6.1"
|
||||
|
||||
@@ -356,25 +356,25 @@
|
||||
"@vue/compiler-core" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
"@vue/compiler-dom@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz#c43cc15e50da62ecc16a42f2622d25dc5fd97dce"
|
||||
integrity sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==
|
||||
"@vue/compiler-dom@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305"
|
||||
integrity sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/compiler-core" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
|
||||
"@vue/compiler-sfc@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz#7f7989cc04ec9e7c55acd406827a2c4e96872c70"
|
||||
integrity sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q==
|
||||
"@vue/compiler-sfc@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz#1bdc36f6cdc1643f72e2c397eb1a398f5004ad3d"
|
||||
integrity sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/compiler-core" "3.2.45"
|
||||
"@vue/compiler-dom" "3.2.45"
|
||||
"@vue/compiler-ssr" "3.2.45"
|
||||
"@vue/reactivity-transform" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/compiler-core" "3.2.47"
|
||||
"@vue/compiler-dom" "3.2.47"
|
||||
"@vue/compiler-ssr" "3.2.47"
|
||||
"@vue/reactivity-transform" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
postcss "^8.1.10"
|
||||
@@ -404,13 +404,13 @@
|
||||
"@vue/compiler-dom" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
"@vue/compiler-ssr@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz#bd20604b6e64ea15344d5b6278c4141191c983b2"
|
||||
integrity sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==
|
||||
"@vue/compiler-ssr@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz#35872c01a273aac4d6070ab9d8da918ab13057ee"
|
||||
integrity sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/compiler-dom" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
|
||||
"@vue/devtools-api@^6.0.0-beta.11":
|
||||
version "6.0.0-beta.20.1"
|
||||
@@ -433,58 +433,58 @@
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
|
||||
"@vue/reactivity-transform@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz#07ac83b8138550c83dfb50db43cde1e0e5e8124d"
|
||||
integrity sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==
|
||||
"@vue/reactivity-transform@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e"
|
||||
integrity sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/compiler-core" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/compiler-core" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
|
||||
"@vue/reactivity@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.45.tgz#412a45b574de601be5a4a5d9a8cbd4dee4662ff0"
|
||||
integrity sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==
|
||||
"@vue/reactivity@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.47.tgz#1d6399074eadfc3ed35c727e2fd707d6881140b6"
|
||||
integrity sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==
|
||||
dependencies:
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/shared" "3.2.47"
|
||||
|
||||
"@vue/runtime-core@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.45.tgz#7ad7ef9b2519d41062a30c6fa001ec43ac549c7f"
|
||||
integrity sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==
|
||||
"@vue/runtime-core@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.47.tgz#406ebade3d5551c00fc6409bbc1eeb10f32e121d"
|
||||
integrity sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/reactivity" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
|
||||
"@vue/runtime-dom@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz#1a2ef6ee2ad876206fbbe2a884554bba2d0faf59"
|
||||
integrity sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==
|
||||
"@vue/runtime-dom@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz#93e760eeaeab84dedfb7c3eaf3ed58d776299382"
|
||||
integrity sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==
|
||||
dependencies:
|
||||
"@vue/runtime-core" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/runtime-core" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
csstype "^2.6.8"
|
||||
|
||||
"@vue/server-renderer@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.45.tgz#ca9306a0c12b0530a1a250e44f4a0abac6b81f3f"
|
||||
integrity sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==
|
||||
"@vue/server-renderer@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.47.tgz#8aa1d1871fc4eb5a7851aa7f741f8f700e6de3c0"
|
||||
integrity sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/compiler-ssr" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
|
||||
"@vue/shared@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.39.tgz"
|
||||
integrity sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==
|
||||
|
||||
"@vue/shared@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.45.tgz#a3fffa7489eafff38d984e23d0236e230c818bc2"
|
||||
integrity sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==
|
||||
"@vue/shared@3.2.47":
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c"
|
||||
integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==
|
||||
|
||||
"@vueuse/core@^9.1.0":
|
||||
version "9.2.0"
|
||||
@@ -586,10 +586,10 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.2.0.tgz#1cb65bd75162c70e9f8d118a905126c4a201d383"
|
||||
integrity sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==
|
||||
axios@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3"
|
||||
integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
@@ -756,10 +756,10 @@ dotenv@^10.0.0:
|
||||
resolved "https://registry.nlark.com/dotenv/download/dotenv-10.0.0.tgz"
|
||||
integrity sha1-PUInuPuV+BCWzdK2ZlP7LHCFuoE=
|
||||
|
||||
dt-sql-parser@^4.0.0-beta.2.2:
|
||||
version "4.0.0-beta.2.2"
|
||||
resolved "https://registry.npmmirror.com/dt-sql-parser/-/dt-sql-parser-4.0.0-beta.2.2.tgz"
|
||||
integrity sha512-LLAE659zgizdokkDniHFPk0PsLPV3cXFOQPW+QT+3W1/TQJ2h8yzKCBBufXmKAHMpAr+KjTRTa71VJRzWJx8Zg==
|
||||
dt-sql-parser@^4.0.0-beta.3.2:
|
||||
version "4.0.0-beta.3.2"
|
||||
resolved "https://registry.npmmirror.com/dt-sql-parser/-/dt-sql-parser-4.0.0-beta.3.2.tgz#1df3341b5e12eaa31d6fd9174d440ed17c1e2f15"
|
||||
integrity sha512-QrgsWzpqqUy6vAGkK4Ep4WdZfhy0s2SNR7QuwQ3RsdDNQ4rMzH1jQTdEBK6oU+16o1xpStwjnzk3vuxMOhiisw==
|
||||
dependencies:
|
||||
"@types/antlr4" "4.7.0"
|
||||
antlr4 "4.7.2"
|
||||
@@ -772,10 +772,10 @@ echarts@^5.4.0:
|
||||
tslib "2.3.0"
|
||||
zrender "5.4.0"
|
||||
|
||||
element-plus@^2.2.29:
|
||||
version "2.2.29"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.2.29.tgz#7dd72f9cafdc102ae3f9e4efe612e403ef713a74"
|
||||
integrity sha512-g4dcrURrKkR5uUX8n5RVnnqGnimoki9HfqS4yHHG6XwCHBkZGozdq4x+478BzeWUe31h++BO+7dakSx4VnM8RQ==
|
||||
element-plus@^2.2.30:
|
||||
version "2.2.30"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.2.30.tgz#b594efcbd6969f3f88130aa1edf50c98139d6e73"
|
||||
integrity sha512-HYSnmf2VMGa0gmw03evxevodPy3WimbAd4sfenOAhNs7Wl8IdT+YJjQyGAQjgEjRvhmujN4O/CZqhuEffRyOZg==
|
||||
dependencies:
|
||||
"@ctrl/tinycolor" "^3.4.1"
|
||||
"@element-plus/icons-vue" "^2.0.6"
|
||||
@@ -1327,17 +1327,17 @@ mitt@^3.0.0:
|
||||
resolved "https://registry.npmmirror.com/mitt/download/mitt-3.0.0.tgz"
|
||||
integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
|
||||
|
||||
monaco-editor@^0.34.1:
|
||||
version "0.34.1"
|
||||
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.34.1.tgz"
|
||||
integrity sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==
|
||||
monaco-editor@^0.35.0:
|
||||
version "0.35.0"
|
||||
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.35.0.tgz#49c4220c815262a900dacf0ae8a59bef66efab8b"
|
||||
integrity sha512-BJfkAZ0EJ7JgrgWzqjfBNP9hPSS8NlfECEDMEIIiozV2UaPq22yeuOjgbd3TwMh3anH0krWZirXZfn8KUSxiOA==
|
||||
|
||||
monaco-sql-languages@^0.9.5:
|
||||
version "0.9.5"
|
||||
resolved "https://registry.npmmirror.com/monaco-sql-languages/-/monaco-sql-languages-0.9.5.tgz"
|
||||
integrity sha512-IBIKQVIoW1Q90pJ/0Qi0sWMgbvho5ug17wx64hVid/XCr+L7ngJaTdaRnveOMPwg9qj+PQqOt1Ga0q0AwG85wA==
|
||||
monaco-sql-languages@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.npmmirror.com/monaco-sql-languages/-/monaco-sql-languages-0.11.0.tgz#9a5f9061cb2095b8ef8854c6eb8185924fcf87a9"
|
||||
integrity sha512-OoTQVLT6ldBXPEbQPw43hOy+u16dO+HkY/rCADXPM5NfRBQ1m9+04NdaQ94WcF2R6MPeansxW8AveIdjEhxqfw==
|
||||
dependencies:
|
||||
dt-sql-parser "^4.0.0-beta.2.2"
|
||||
dt-sql-parser "^4.0.0-beta.3.2"
|
||||
|
||||
monaco-themes@^0.4.2:
|
||||
version "0.4.2"
|
||||
@@ -1768,16 +1768,16 @@ vue-router@^4.1.6:
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.4.5"
|
||||
|
||||
vue@^3.2.45:
|
||||
version "3.2.45"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8"
|
||||
integrity sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==
|
||||
vue@^3.2.47:
|
||||
version "3.2.47"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0"
|
||||
integrity sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.2.45"
|
||||
"@vue/compiler-sfc" "3.2.45"
|
||||
"@vue/runtime-dom" "3.2.45"
|
||||
"@vue/server-renderer" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
"@vue/compiler-dom" "3.2.47"
|
||||
"@vue/compiler-sfc" "3.2.47"
|
||||
"@vue/runtime-dom" "3.2.47"
|
||||
"@vue/server-renderer" "3.2.47"
|
||||
"@vue/shared" "3.2.47"
|
||||
|
||||
vuex@^4.0.2:
|
||||
version "4.0.2"
|
||||
|
||||
@@ -15,7 +15,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
|
||||
go.mongodb.org/mongo-driver v1.11.1 // mongo
|
||||
golang.org/x/crypto v0.5.0 // ssh
|
||||
golang.org/x/crypto v0.6.0 // ssh
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.4.5
|
||||
@@ -50,10 +50,10 @@ require (
|
||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/net v0.6.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
@@ -415,6 +415,7 @@ func (d *Db) DeleteSql(rc *req.Ctx) {
|
||||
dbSql := &entity.DbSql{Type: 1, DbId: GetDbId(rc.GinCtx)}
|
||||
dbSql.CreatorId = rc.LoginAccount.Id
|
||||
dbSql.Name = rc.GinCtx.Query("name")
|
||||
dbSql.Db = rc.GinCtx.Query("db")
|
||||
|
||||
model.DeleteByCondition(dbSql)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type DbForm struct {
|
||||
}
|
||||
|
||||
type DbSqlSaveForm struct {
|
||||
Name string
|
||||
Name string `binding:"required"`
|
||||
Sql string `binding:"required"`
|
||||
Type int `binding:"required"`
|
||||
Db string `binding:"required"`
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package application
|
||||
|
||||
import "mayfly-go/internal/db/infrastructure/persistence"
|
||||
import (
|
||||
"mayfly-go/internal/db/infrastructure/persistence"
|
||||
)
|
||||
|
||||
var (
|
||||
dbApp Db = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo())
|
||||
|
||||
@@ -159,7 +159,7 @@ func (d *dbAppImpl) GetDatabases(ed *entity.Db) []string {
|
||||
biz.ErrIsNilAppendErr(err, "数据库连接失败: %s")
|
||||
defer dbConn.Close()
|
||||
|
||||
_, res, err := SelectDataByDb(dbConn, getDatabasesSql, true)
|
||||
_, res, err := SelectDataByDb(dbConn, getDatabasesSql)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库列表失败")
|
||||
for _, re := range res {
|
||||
databases = append(databases, re["dbname"].(string))
|
||||
@@ -248,7 +248,7 @@ type DbInstance struct {
|
||||
// 执行查询语句
|
||||
// 依次返回 列名数组,结果map,错误
|
||||
func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interface{}, error) {
|
||||
return SelectDataByDb(d.db, execSql, false)
|
||||
return SelectDataByDb(d.db, execSql)
|
||||
}
|
||||
|
||||
// 将查询结果映射至struct,可具体参考sqlx库
|
||||
@@ -259,7 +259,7 @@ func (d *DbInstance) SelectData2Struct(execSql string, dest interface{}) error {
|
||||
// 执行内部查询语句,不返回列名以及不限制行数
|
||||
// 依次返回 结果map,错误
|
||||
func (d *DbInstance) innerSelect(execSql string) ([]map[string]interface{}, error) {
|
||||
_, res, err := SelectDataByDb(d.db, execSql, true)
|
||||
_, res, err := SelectDataByDb(d.db, execSql)
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ func GetDbConn(d *entity.Db, db string) (*sql.DB, error) {
|
||||
return DB, nil
|
||||
}
|
||||
|
||||
func SelectDataByDb(db *sql.DB, selectSql string, isInner bool) ([]string, []map[string]interface{}, error) {
|
||||
func SelectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]interface{}, error) {
|
||||
rows, err := db.Query(selectSql)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -388,14 +388,7 @@ func SelectDataByDb(db *sql.DB, selectSql string, isInner bool) ([]string, []map
|
||||
colNames := make([]string, 0)
|
||||
// 是否第一次遍历,列名数组只需第一次遍历时加入
|
||||
isFirst := true
|
||||
rowNum := 0
|
||||
for rows.Next() {
|
||||
rowNum++
|
||||
// 非内部sql,则校验返回结果数量
|
||||
if !isInner {
|
||||
biz.IsTrue(rowNum <= Max_Rows, "结果集 > 2000, 请完善条件或分页信息")
|
||||
}
|
||||
|
||||
// 不Scan也会导致等待,该链接实际处于未工作的状态,然后也会导致连接数迅速达到最大
|
||||
err := rows.Scan(scans...)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,11 @@ import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/xwb1989/sqlparser"
|
||||
@@ -73,32 +77,42 @@ func createSqlExecRecord(execSqlReq *DbSqlExecReq) *entity.DbSqlExec {
|
||||
|
||||
func (d *dbSqlExecAppImpl) Exec(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
|
||||
sql := execSqlReq.Sql
|
||||
stmt, err := sqlparser.Parse(sql)
|
||||
if err != nil {
|
||||
// 就算解析失败也执行sql,让数据库来判断错误
|
||||
//global.Log.Error("sql解析失败: ", err)
|
||||
if strings.HasPrefix(strings.ToLower(execSqlReq.Sql), "select") ||
|
||||
strings.HasPrefix(strings.ToLower(execSqlReq.Sql), "show") {
|
||||
return doSelect(execSqlReq)
|
||||
}
|
||||
// 保存执行记录
|
||||
d.dbSqlExecRepo.Insert(createSqlExecRecord(execSqlReq))
|
||||
return doExec(execSqlReq.Sql, execSqlReq.DbInstance)
|
||||
}
|
||||
|
||||
dbSqlExecRecord := createSqlExecRecord(execSqlReq)
|
||||
var execRes *DbSqlExecRes
|
||||
isSelect := false
|
||||
|
||||
stmt, err := sqlparser.Parse(sql)
|
||||
if err != nil {
|
||||
// 就算解析失败也执行sql,让数据库来判断错误。如果是查询sql则简单判断是否有limit分页参数信息(兼容pgsql)
|
||||
// global.Log.Warnf("sqlparse解析sql[%s]失败: %s", sql, err.Error())
|
||||
lowerSql := strings.ToLower(execSqlReq.Sql)
|
||||
isSelect := strings.HasPrefix(lowerSql, "select")
|
||||
if isSelect {
|
||||
biz.IsTrue(strings.Contains(lowerSql, "limit"), "请完善分页信息")
|
||||
}
|
||||
var execErr error
|
||||
if isSelect || strings.HasPrefix(lowerSql, "show") {
|
||||
execRes, execErr = doRead(execSqlReq)
|
||||
} else {
|
||||
execRes, execErr = doExec(execSqlReq.Sql, execSqlReq.DbInstance)
|
||||
}
|
||||
if execErr != nil {
|
||||
return nil, execErr
|
||||
}
|
||||
d.saveSqlExecLog(isSelect, dbSqlExecRecord)
|
||||
return execRes, nil
|
||||
}
|
||||
|
||||
switch stmt := stmt.(type) {
|
||||
case *sqlparser.Select:
|
||||
isSelect = true
|
||||
execRes, err = doSelect(execSqlReq)
|
||||
execRes, err = doSelect(stmt, execSqlReq)
|
||||
case *sqlparser.Show:
|
||||
isSelect = true
|
||||
execRes, err = doSelect(execSqlReq)
|
||||
execRes, err = doRead(execSqlReq)
|
||||
case *sqlparser.OtherRead:
|
||||
isSelect = true
|
||||
execRes, err = doSelect(execSqlReq)
|
||||
execRes, err = doRead(execSqlReq)
|
||||
case *sqlparser.Update:
|
||||
execRes, err = doUpdate(stmt, execSqlReq, dbSqlExecRecord)
|
||||
case *sqlparser.Delete:
|
||||
@@ -111,12 +125,22 @@ func (d *dbSqlExecAppImpl) Exec(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.saveSqlExecLog(isSelect, dbSqlExecRecord)
|
||||
return execRes, nil
|
||||
}
|
||||
|
||||
if !isSelect {
|
||||
// 保存执行记录
|
||||
// 保存sql执行记录,如果是查询类则根据系统配置判断是否保存
|
||||
func (d *dbSqlExecAppImpl) saveSqlExecLog(isQuery bool, dbSqlExecRecord *entity.DbSqlExec) {
|
||||
if !isQuery {
|
||||
d.dbSqlExecRepo.Insert(dbSqlExecRecord)
|
||||
return
|
||||
}
|
||||
if sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbSaveQuerySQL).BoolValue(false) {
|
||||
dbSqlExecRecord.Table = "-"
|
||||
dbSqlExecRecord.OldValue = "-"
|
||||
dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery
|
||||
d.dbSqlExecRepo.Insert(dbSqlExecRecord)
|
||||
}
|
||||
return execRes, nil
|
||||
}
|
||||
|
||||
func (d *dbSqlExecAppImpl) DeleteBy(condition *entity.DbSqlExec) {
|
||||
@@ -127,7 +151,22 @@ func (d *dbSqlExecAppImpl) GetPageList(condition *entity.DbSqlExec, pageParam *m
|
||||
return d.dbSqlExecRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func doSelect(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
|
||||
func doSelect(selectStmt *sqlparser.Select, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
|
||||
selectExprsStr := sqlparser.String(selectStmt.SelectExprs)
|
||||
if selectExprsStr == "*" || strings.Contains(selectExprsStr, ".*") ||
|
||||
len(strings.Split(selectExprsStr, ",")) > 1 {
|
||||
limit := selectStmt.Limit
|
||||
biz.NotNil(limit, "请完善分页信息后执行")
|
||||
count, err := strconv.Atoi(sqlparser.String(limit.Rowcount))
|
||||
biz.ErrIsNil(err, "分页参数有误")
|
||||
maxCount := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbQueryMaxCount).IntValue(200)
|
||||
biz.IsTrue(count <= maxCount, fmt.Sprintf("查询结果集数需小于系统配置的%d条", maxCount))
|
||||
}
|
||||
|
||||
return doRead(execSqlReq)
|
||||
}
|
||||
|
||||
func doRead(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
|
||||
dbInstance := execSqlReq.DbInstance
|
||||
sql := execSqlReq.Sql
|
||||
colNames, res, err := dbInstance.SelectData(sql)
|
||||
|
||||
@@ -19,4 +19,5 @@ const (
|
||||
DbSqlExecTypeUpdate int8 = 1 // 更新类型
|
||||
DbSqlExecTypeDelete int8 = 2 // 删除类型
|
||||
DbSqlExecTypeInsert int8 = 3 // 插入类型
|
||||
DbSqlExecTypeQuery int8 = 4 // 查询类型,如select、show等
|
||||
)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/internal/sys/domain/repository"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils"
|
||||
)
|
||||
|
||||
const SysConfigKeyPrefix = "sys:config:"
|
||||
|
||||
type Config interface {
|
||||
GetPageList(condition *entity.Config, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
|
||||
|
||||
@@ -36,12 +41,22 @@ func (a *configAppImpl) Save(config *entity.Config) {
|
||||
} else {
|
||||
a.configRepo.Update(config)
|
||||
}
|
||||
cache.Del(SysConfigKeyPrefix + config.Key)
|
||||
}
|
||||
|
||||
func (a *configAppImpl) GetConfig(key string) *entity.Config {
|
||||
config := &entity.Config{Key: key}
|
||||
// 优先从缓存中获取
|
||||
cacheStr := cache.GetStr(SysConfigKeyPrefix + key)
|
||||
if cacheStr != "" {
|
||||
json.Unmarshal([]byte(cacheStr), &config)
|
||||
return config
|
||||
}
|
||||
|
||||
if err := a.configRepo.GetConfig(config, "Id", "Key", "Value"); err != nil {
|
||||
global.Log.Warnf("不存在key = [%s] 的系统配置", key)
|
||||
} else {
|
||||
cache.SetStr(SysConfigKeyPrefix+key, utils.ToJsonStr(config))
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package entity
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/pkg/model"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigKeyUseLoginCaptcha string = "UseLoginCaptcha" // 是否使用登录验证码
|
||||
ConfigKeyDbQueryMaxCount string = "DbQueryMaxCount" // 数据库查询的最大数量
|
||||
ConfigKeyDbSaveQuerySQL string = "DbSaveQuerySQL" // 数据库是否记录查询相关sql
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -41,3 +44,16 @@ func (c *Config) GetJsonMap() map[string]string {
|
||||
_ = json.Unmarshal([]byte(c.Value), &res)
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取配置的int值,如果配置值非int或不存在,则返回默认值
|
||||
func (c *Config) IntValue(defaultValue int) int {
|
||||
// 如果值不存在,则返回默认值
|
||||
if c.Id == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
if intV, err := strconv.Atoi(c.Value); err != nil {
|
||||
return defaultValue
|
||||
} else {
|
||||
return intV
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +352,8 @@ CREATE TABLE `t_sys_config` (
|
||||
BEGIN;
|
||||
INSERT INTO `t_sys_config` VALUES (1, '是否启用登录验证码', 'UseLoginCaptcha', NULL, '1', '1: 启用、0: 不启用', '2022-08-25 22:27:17', 1, 'admin', '2022-08-26 10:26:56', 1, 'admin');
|
||||
INSERT INTO `t_sys_config` VALUES (2, '是否启用水印', 'UseWartermark', NULL, '1', '1: 启用、0: 不启用', '2022-08-25 23:36:35', 1, 'admin', '2022-08-26 10:02:52', 1, 'admin');
|
||||
INSERT INTO `t_sys_config` VALUES (3, '数据库查询最大结果集', 'DbQueryMaxCount', '[]', '200', '允许sql查询的最大结果集数', '2023-02-11 14:29:03', 1, 'admin', '2023-02-11 14:40:56', 1, 'admin');
|
||||
INSERT INTO `t_sys_config` VALUES (4, '数据库是否记录查询SQL', 'DbSaveQuerySQL', '[]', '0', '1: 记录、0:不记录', '2023-02-11 16:07:14', 1, 'admin', '2023-02-11 16:44:17', 1, 'admin');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
|
||||
10
server/pkg/cache/str_cache.go
vendored
10
server/pkg/cache/str_cache.go
vendored
@@ -27,6 +27,16 @@ func SetStr(key, value string) {
|
||||
rediscli.Set(key, value, 0)
|
||||
}
|
||||
|
||||
// 删除指定key
|
||||
func Del(key string) {
|
||||
if rediscli.GetCli() == nil {
|
||||
checkStrCache()
|
||||
delete(strCache, key)
|
||||
return
|
||||
}
|
||||
rediscli.Del(key)
|
||||
}
|
||||
|
||||
func checkStrCache() {
|
||||
if strCache == nil {
|
||||
strCache = make(map[string]string)
|
||||
|
||||
@@ -145,7 +145,7 @@ func Insert(model interface{}) error {
|
||||
//
|
||||
// @param list为数组类型 如 var users *[]User,可指定为非model结构体,即只包含需要返回的字段结构体
|
||||
func ListBy(model interface{}, list interface{}, cols ...string) {
|
||||
global.Db.Model(model).Select(cols).Where(model).Find(list)
|
||||
global.Db.Model(model).Select(cols).Where(model).Order("id desc").Find(list)
|
||||
}
|
||||
|
||||
// 获取满足model中不为空的字段值条件的所有数据.
|
||||
|
||||
27
server/pkg/utils/byte_utils.go
Normal file
27
server/pkg/utils/byte_utils.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
func Bytes2Int8(bytes []byte) int8 {
|
||||
return int8(Byte2Uint16(bytes))
|
||||
}
|
||||
|
||||
func Bytes2Int(bytes []byte) int {
|
||||
return int(Byte2Uint64(bytes))
|
||||
}
|
||||
|
||||
func Bytes2Int64(bytes []byte) int64 {
|
||||
return int64(Byte2Uint64(bytes))
|
||||
}
|
||||
|
||||
func Byte2Uint64(bytes []byte) uint64 {
|
||||
return binary.LittleEndian.Uint64(bytes)
|
||||
}
|
||||
|
||||
func Byte2Uint32(bytes []byte) uint32 {
|
||||
return binary.LittleEndian.Uint32(bytes)
|
||||
}
|
||||
|
||||
func Byte2Uint16(bytes []byte) uint16 {
|
||||
return binary.LittleEndian.Uint16(bytes)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/pkg/global"
|
||||
)
|
||||
|
||||
func Json2Map(jsonStr string) map[string]interface{} {
|
||||
@@ -12,3 +13,12 @@ func Json2Map(jsonStr string) map[string]interface{} {
|
||||
_ = json.Unmarshal([]byte(jsonStr), &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func ToJsonStr(val any) string {
|
||||
if strBytes, err := json.Marshal(val); err != nil {
|
||||
global.Log.Error("toJsonStr error: ", err)
|
||||
return ""
|
||||
} else {
|
||||
return string(strBytes)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user