feat: sql查询支持多tab结果集

This commit is contained in:
meilin.huang
2023-11-24 12:12:47 +08:00
parent bb37ed3b95
commit 6b65605360
8 changed files with 263 additions and 111 deletions

View File

@@ -387,7 +387,7 @@ onBeforeUnmount(() => {
* 设置editor高度和数据表高度
*/
const setHeight = () => {
state.editorHeight = window.innerHeight - 500 + 'px';
state.editorHeight = window.innerHeight - 520 + 'px';
state.dataTabsTableHeight = window.innerHeight - 255;
state.tablesOpHeight = window.innerHeight - 220 + 'px';
};

View File

@@ -48,37 +48,77 @@
</el-icon>
</div>
<div class="mt5">
<el-row>
<span v-if="hasUpdatedFileds">
<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="hasUpdatedFileds">
<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>
<db-table-data
ref="dbTableRef"
:db-id="dbId"
:db="dbName"
:data="execRes.data"
:table="state.table"
:columns="execRes.tableColumn"
:loading="loading"
:height="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@selection-change="onDataSelectionChange"
@change-updated-field="changeUpdatedField"
@data-delete="onDeleteData"
></db-table-data>
<div class="mt5 sql-exec-res">
<el-tabs v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTab">
<el-tab-pane closable v-for="dt in state.execResTabs" :label="dt.label" :name="dt.label" :key="dt.label">
<template #label>
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
<template #reference>
<div>
<span>
<span v-if="dt.loading">
<SvgIcon class="mb2 is-loading" name="Loading" color="var(--el-color-primary)" />
</span>
<span v-else>
<SvgIcon class="mb2" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
<SvgIcon class="mb2" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
</span>
</span>
<span class="ml5">
{{ dt.label }}
</span>
</div>
</template>
<template #default>
<el-descriptions :column="1" size="small">
<el-descriptions-item label="耗时 :"> {{ dt.execTime }}ms </el-descriptions-item>
<el-descriptions-item label="结果集 :">
{{ dt.data?.length }}
</el-descriptions-item>
<el-descriptions-item label="SQL :">
{{ dt.sql }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</template>
<el-row>
<span v-if="dt.hasUpdatedFileds" class="mt5">
<span>
<el-link type="success" :underline="false" @click="submitUpdateFields(dt)"><span style="font-size: 12px">提交</span></el-link>
</span>
<span>
<el-divider direction="vertical" border-style="dashed" />
<el-link type="warning" :underline="false" @click="cancelUpdateFields(dt)"><span style="font-size: 12px">取消</span></el-link>
</span>
</span>
</el-row>
<db-table-data
v-if="!dt.errorMsg"
:ref="(el) => (dt.dbTableRef = el)"
:db-id="dbId"
:db="dbName"
:data="dt.data"
:table="dt.table"
:columns="dt.tableColumn"
:loading="dt.loading"
:height="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@change-updated-field="changeUpdatedField($event, dt)"
@data-delete="onDeleteData($event, dt)"
></db-table-data>
<el-result v-else icon="error" title="执行失败" :sub-title="dt.errorMsg"> </el-result>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script lang="ts" setup>
import { h, nextTick, watch, onMounted, reactive, toRefs, ref, Ref } from 'vue';
import { h, nextTick, watch, onMounted, reactive, toRefs, ref } from 'vue';
import { getToken } from '@/common/utils/storage';
import { notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
@@ -98,6 +138,7 @@ import { buildProgressProps } from '@/components/progress-notify/progress-notify
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
import { ElNotification } from 'element-plus';
import syssocket from '@/common/syssocket';
import SvgIcon from '@/components/svgIcon/index.vue';
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']);
@@ -120,29 +161,57 @@ const props = defineProps({
},
});
class ExecResTab {
label: string;
/**
* 当前结果集对应的sql
*/
sql: string;
loading: boolean;
dbTableRef: any;
tableColumn: any[] = [];
data: any[] = [];
execTime: number;
/**
* 当前单表操作sql关联的表信息
*/
table: string;
/**
* 是否有更新字段
*/
hasUpdatedFileds: boolean;
errorMsg: string;
constructor(label: string) {
this.label = label;
}
}
const token = getToken();
const monacoEditorRef: any = ref(null);
const dbTableRef = ref(null) as Ref;
let monacoEditor: editor.IStandaloneCodeEditor;
const state = reactive({
token,
table: '', // 当前单表操作sql的表信息
sql: '', // 当前编辑器的sql内容s
sqlName: '' as any, // sql模板名称
loading: false, // 是否在加载数据
execRes: {
data: [],
tableColumn: [],
},
selectionDatas: [] as any,
execResTabs: [] as ExecResTab[],
activeTab: '',
editorHeight: '500',
tableDataHeight: 255 as any,
hasUpdatedFileds: false,
});
const { tableDataHeight, execRes, loading, hasUpdatedFileds } = toRefs(state);
const { tableDataHeight } = toRefs(state);
watch(
() => props.editorHeight,
@@ -159,6 +228,11 @@ onMounted(async () => {
console.log('in query mounted');
state.editorHeight = props.editorHeight;
// 默认新建一个结果集tab
const label = '结果1';
state.execResTabs.push(new ExecResTab(label));
state.activeTab = label;
state.sqlName = props.sqlName;
if (props.sqlName) {
const res = await dbApi.getSql.request({ id: props.dbId, type: 1, db: props.dbName, name: props.sqlName });
@@ -201,6 +275,34 @@ const initMonacoEditor = () => {
},
});
// 注册快捷键ctrl + R 运行选中的sql
monacoEditor.addAction({
// An unique identifier of the contributed action.
// id: 'run-sql-action' + state.ti.key,
id: 'run-sql-action-on-newtab' + getKey(),
// 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.KeyR, 0),
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.6,
// 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(true);
} catch (e: any) {
e.message && ElMessage.error(e.message);
}
},
});
// 注册快捷键ctrl + shift + f 格式化sql
monacoEditor.addAction({
// An unique identifier of the contributed action.
@@ -229,6 +331,25 @@ const initMonacoEditor = () => {
});
};
const onRemoveTab = (targetName: string) => {
let activeTab = state.activeTab;
const tabs = [...state.execResTabs];
for (let i = 0; i < tabs.length; i++) {
const tabName = tabs[i].label;
if (tabName !== targetName) {
continue;
}
const nextTab = tabs[i + 1] || tabs[i - 1];
if (nextTab) {
activeTab = nextTab.label;
} else {
activeTab = '';
}
state.execResTabs.splice(i, 1);
state.activeTab = activeTab;
}
};
/**
* 拖拽改变sql编辑区和查询结果区高度
*/
@@ -254,7 +375,7 @@ const getKey = () => {
/**
* 执行sql
*/
const onRunSql = async () => {
const onRunSql = async (newTab = false) => {
// 没有选中的文本,则为全部文本
let sql = getSql() as string;
notBlank(sql && sql.trim(), '请选中需要执行的sql');
@@ -285,44 +406,67 @@ const onRunSql = async () => {
return;
}
let execRes: ExecResTab;
let i = 0;
let label;
// 新tab执行或者tabs为0则新建tab执行sql
if (newTab || state.execResTabs.length == 0) {
label = `结果${state.execResTabs.length + 1}`;
execRes = new ExecResTab(label);
state.execResTabs.push(execRes);
i = state.execResTabs.length - 1;
} else {
// 不是新建tab执行则在当前激活的tab上执行sql
i = state.execResTabs.findIndex((x) => x.label == state.activeTab);
execRes = state.execResTabs[i];
label = execRes.label;
}
state.activeTab = label;
const startTime = new Date().getTime();
try {
state.loading = true;
execRes.loading = true;
execRes.errorMsg = '';
execRes.sql = '';
const colAndData: any = await getNowDbInst().runSql(props.dbName, sql, execRemark);
if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集');
}
state.execRes.data = colAndData.res;
// 要实时响应,故需要用索引改变数据才生效
state.execResTabs[i].data = colAndData.res;
// 兼容表格字段配置
state.execRes.tableColumn = colAndData.colNames.map((x: any) => {
state.execResTabs[i].tableColumn = colAndData.colNames.map((x: any) => {
return {
columnName: x,
show: true,
};
});
cancelUpdateFields();
cancelUpdateFields(execRes);
} catch (e: any) {
state.execRes.data = [];
state.execRes.tableColumn = [];
state.table = '';
execRes.data = [];
execRes.tableColumn = [];
execRes.table = '';
execRes.errorMsg = e.msg;
return;
} finally {
state.loading = false;
state.execResTabs[i].loading = false;
execRes.sql = sql;
execRes.execTime = new Date().getTime() - startTime;
}
// 即只有以该字符串开头的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].split('\n')[0];
state.table = tn;
state.table = tn;
execRes.table = tn;
execRes.table = tn;
} else {
state.table = '';
execRes.table = '';
}
} else {
state.table = '';
execRes.table = '';
}
};
@@ -505,32 +649,28 @@ const getUploadSqlFileUrl = () => {
return `${config.baseApiUrl}/dbs/${props.dbId}/exec-sql-file?db=${props.dbName}&${joinClientParams()}`;
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
const changeUpdatedField = (updatedFields: any) => {
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
};
/**
* 数据删除事件
*/
const onDeleteData = async (deleteDatas: any) => {
const onDeleteData = async (deleteDatas: any, dt: ExecResTab) => {
const db = props.dbName;
const dbInst = getNowDbInst();
const primaryKey = await dbInst.loadTableColumn(db, state.table);
const primaryKey = await dbInst.loadTableColumn(db, dt.table);
const primaryKeyColumnName = primaryKey.columnName;
state.execRes.data = state.execRes.data.filter((d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1));
dt.data = dt.data.filter((d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1));
};
const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields();
const submitUpdateFields = (dt: ExecResTab) => {
dt?.dbTableRef?.submitUpdateFields();
};
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
const cancelUpdateFields = (dt: ExecResTab) => {
dt?.dbTableRef?.cancelUpdateFields();
};
</script>
@@ -550,4 +690,17 @@ const cancelUpdateFields = () => {
height: 3px;
text-align: center;
}
.sql-exec-res {
.el-tabs__header {
margin: 0 0 !important;
}
.el-tabs__item {
font-size: 12px;
height: 20px;
margin: 0px;
padding: 0 6px !important;
}
}
</style>

View File

@@ -360,6 +360,7 @@ onMounted(async () => {
state.dbType = props.dbType;
state.db = props.db;
state.table = props.table;
setTableData(props.data);
});
const setTableData = (datas: any) => {
@@ -386,7 +387,9 @@ const setTableColumns = (columns: any) => {
hidden: !x.show,
};
});
state.columns.unshift(rowNoColumn);
if (state.columns.length > 0) {
state.columns.unshift(rowNoColumn);
}
};
/**

View File

@@ -8,6 +8,11 @@
<el-form-item prop="code" label="角色code" required>
<el-input :disabled="form.id != null" v-model="form.code" placeholder="COMMON开头则为所有账号共有角色" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="status" label="状态" required>
<el-select v-model="form.status" placeholder="请选择状态" class="w100">
<el-option v-for="item in RoleStatusEnum" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="角色描述">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入角色描述"></el-input>
</el-form-item>
@@ -25,6 +30,7 @@
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { roleApi } from '../api';
import { RoleStatusEnum } from '../enums';
const props = defineProps({
visible: {

View File

@@ -20,12 +20,9 @@
>
</template>
<template #showmore="{ data }">
<el-link @click.prevent="showResources(data)" type="info">菜单&权限</el-link>
</template>
<template #action="{ data }">
<el-button v-if="actionBtns[perms.updateRole]" @click="editRole(data)" type="primary" link>编辑</el-button>
<el-button @click="showResources(data)" type="info" link>权限详情</el-button>
<el-button v-if="actionBtns[perms.saveRoleResource]" @click="editResource(data)" type="success" link>权限分配</el-button>
</template>
</page-table>
@@ -73,11 +70,10 @@ const columns = ref([
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('modifier', '更新账号'),
TableColumn.new('updateTime', '更新时间').isTime(),
TableColumn.new('showmore', '查看更多').isSlot().setMinWidth(150),
]);
const actionBtns = hasPerms([perms.updateRole, perms.saveRoleResource]);
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight().alignCenter();
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(260).fixedRight().alignCenter();
const state = reactive({
query: {
@@ -157,7 +153,9 @@ const deleteRole = async (data: any) => {
});
ElMessage.success('删除成功!');
search();
} catch (err) {}
} catch (err) {
//
}
};
const showResources = async (row: any) => {

View File

@@ -6,6 +6,25 @@
<span class="custom-tree-node">
<span v-if="data.type == ResourceTypeEnum.Menu.value">{{ node.label }}</span>
<span v-if="data.type == ResourceTypeEnum.Permission.value" style="color: #67c23a">{{ node.label }}</span>
<el-popover :show-after="500" placement="right-start" title="资源分配信息" trigger="hover" :width="200">
<template #reference>
<el-link style="margin-left: 25px" icon="InfoFilled" type="info" :underline="false" />
</template>
<template #default>
<el-descriptions :column="1" size="small">
<el-descriptions-item label="资源名称">
{{ data.name }}
</el-descriptions-item>
<el-descriptions-item label="分配账号">
{{ data.creator }}
</el-descriptions-item>
<el-descriptions-item label="分配时间">
{{ dateFormat(data.createTime) }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</span>
</template>
</el-tree>
@@ -14,9 +33,9 @@
</template>
<script lang="ts" setup>
import { getCurrentInstance, toRefs, reactive, watch } from 'vue';
import { ElMessageBox } from 'element-plus';
import { toRefs, reactive, watch } from 'vue';
import { ResourceTypeEnum } from '../enums';
import { dateFormat } from '@/common/utils/date';
const props = defineProps({
visible: {
@@ -33,8 +52,6 @@ const props = defineProps({
//定义事件
const emit = defineEmits(['update:visible', 'update:resources']);
const { proxy } = getCurrentInstance() as any;
const defaultProps = {
children: 'children',
label: 'name',
@@ -52,26 +69,6 @@ watch(
}
);
const info = (info: any) => {
ElMessageBox.alert(
'<strong style="margin-right: 18px">资源名称:</strong>' +
info.name +
' <br/><strong style="margin-right: 18px">分配账号:</strong>' +
info.creator +
' <br/><strong style="margin-right: 18px">分配时间:</strong>' +
proxy.$filters.dateFormat(info.createTime) +
'',
'分配信息',
{
type: 'info',
dangerouslyUseHTMLString: true,
closeOnClickModal: true,
showConfirmButton: false,
}
).catch(() => {});
return;
};
const closeDialog = () => {
emit('update:visible', false);
emit('update:resources', []);

View File

@@ -5,7 +5,7 @@ go 1.21
require (
github.com/buger/jsonparser v1.1.1
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.9.0
github.com/glebarez/sqlite v1.10.0
github.com/go-gormigrate/gormigrate/v2 v2.1.0
github.com/go-ldap/ldap/v3 v3.4.5
github.com/go-playground/locales v0.14.1

View File

@@ -30,7 +30,8 @@ func (d *DbConn) SelectData2Struct(execSql string, dest any) error {
// WalkTableRecord 遍历表记录
func (d *DbConn) WalkTableRecord(selectSql string, walk func(record map[string]any, columns []string)) error {
return walkTableRecord(d.db, selectSql, walk)
_, err := walkTableRecord(d.db, selectSql, walk)
return err
}
// 执行 update, insert, delete建表等sql
@@ -66,26 +67,20 @@ func (d *DbConn) Close() {
}
func selectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]any, error) {
// 列名用于前端表头名称按照数据库与查询字段顺序显示
var colNames []string
result := make([]map[string]any, 0, 16)
err := walkTableRecord(db, selectSql, func(record map[string]any, columns []string) {
columns, err := walkTableRecord(db, selectSql, func(record map[string]any, columns []string) {
result = append(result, record)
if colNames == nil {
colNames = make([]string, len(columns))
copy(colNames, columns)
}
})
if err != nil {
return nil, nil, err
}
return colNames, result, nil
return columns, result, nil
}
func walkTableRecord(db *sql.DB, selectSql string, walk func(record map[string]any, columns []string)) error {
func walkTableRecord(db *sql.DB, selectSql string, walk func(record map[string]any, columns []string)) ([]string, error) {
rows, err := db.Query(selectSql)
if err != nil {
return err
return nil, err
}
// rows对象一定要close掉如果出错不关掉则会很迅速的达到设置最大连接数
// 后面的链接过来直接报错或拒绝,实际上也没有起效果
@@ -97,7 +92,7 @@ func walkTableRecord(db *sql.DB, selectSql string, walk func(record map[string]a
colTypes, err := rows.ColumnTypes()
if err != nil {
return err
return nil, err
}
lenCols := len(colTypes)
// 列名用于前端表头名称按照数据库与查询字段顺序显示
@@ -115,7 +110,7 @@ func walkTableRecord(db *sql.DB, selectSql string, walk func(record map[string]a
for rows.Next() {
// 不Scan也会导致等待该链接实际处于未工作的状态然后也会导致连接数迅速达到最大
if err := rows.Scan(scans...); err != nil {
return err
return nil, err
}
// 每行数据
rowData := make(map[string]any, lenCols)
@@ -126,7 +121,7 @@ func walkTableRecord(db *sql.DB, selectSql string, walk func(record map[string]a
walk(rowData, colNames)
}
return nil
return colNames, nil
}
// 将查询的值转为对应列类型的实际值,不全部转为字符串