refactor: sqlexec组件重构优化、新增数据库相关系统参数配置、相关问题修复

This commit is contained in:
meilin.huang
2023-02-13 21:11:16 +08:00
parent 77aa724003
commit 70b586e45a
35 changed files with 2486 additions and 2120 deletions

View File

@@ -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",

View 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();
}

View File

@@ -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 图标

View File

@@ -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',

View File

@@ -293,4 +293,8 @@ body,
.el-table-z-index-inherit .el-table .el-table__cell {
z-index: inherit !important;
}
.f12 {
font-size: 12px
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>&nbsp;&nbsp;<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>
&nbsp;&nbsp;&nbsp;&nbsp;<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, () => { })">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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%">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>
<!-- 第四级 02sql -->
<el-sub-menu :index="inst.id + schema + '-sql'">
<el-sub-menu @click.stop="loadSqls(inst, schema)" :index="inst.id + schema + '-sql'">
<template #title>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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%">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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">

View 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>

View 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>

View 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
}

View File

@@ -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),
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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: [],

View File

@@ -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>
&nbsp;&nbsp;&nbsp;&nbsp;<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">

View File

@@ -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>

View File

@@ -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;

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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"`

View File

@@ -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())

View File

@@ -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 {

View File

@@ -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)

View File

@@ -19,4 +19,5 @@ const (
DbSqlExecTypeUpdate int8 = 1 // 更新类型
DbSqlExecTypeDelete int8 = 2 // 删除类型
DbSqlExecTypeInsert int8 = 3 // 插入类型
DbSqlExecTypeQuery int8 = 4 // 查询类型如select、show等
)

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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;
-- ----------------------------

View File

@@ -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)

View File

@@ -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中不为空的字段值条件的所有数据.

View 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)
}

View File

@@ -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)
}
}