refactor: sql取消执行逻辑调整、前端使用vueuse重构部分代码

This commit is contained in:
meilin.huang
2023-12-09 16:17:26 +08:00
parent 59a7ff9ac7
commit 6709135a0b
29 changed files with 630 additions and 677 deletions

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@vueuse/core": "^10.7.0",
"asciinema-player": "^3.6.2",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
@@ -21,7 +22,7 @@
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.44.0",
"monaco-editor": "^0.45.0",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
@@ -32,7 +33,7 @@
"splitpanes": "^3.1.5",
"sql-formatter": "^14.0.0",
"uuid": "^9.0.1",
"vue": "^3.3.10",
"vue": "^3.3.11",
"vue-router": "^4.2.5",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
@@ -54,7 +55,7 @@
"prettier": "^3.1.0",
"sass": "^1.69.0",
"typescript": "^5.3.2",
"vite": "^5.0.5",
"vite": "^5.0.7",
"vue-eslint-parser": "^9.3.2"
},
"browserslist": [

View File

@@ -28,6 +28,7 @@ import Setings from '@/layout/navBars/breadcrumb/setings.vue';
import mittBus from '@/common/utils/mitt';
import { getThemeConfig } from './common/utils/storage';
import { useWatermark } from '@/common/sysconfig';
import { useIntervalFn } from '@vueuse/core';
const setingsRef = ref();
const route = useRoute();
@@ -40,12 +41,6 @@ const openSetingsDrawer = () => {
setingsRef.value.openDrawer();
};
const prefers = matchMedia('(prefers-color-scheme: dark)');
const switchDarkFollowOS = () => {
// 跟随系统主题
themeConfigStores.switchDark(prefers.matches);
};
// 页面加载时
onMounted(() => {
nextTick(() => {
@@ -60,7 +55,6 @@ onMounted(() => {
themeConfigStores.setThemeConfig({ themeConfig: tc });
document.documentElement.style.cssText = getLocal('themeConfigStyle');
}
switchDarkFollowOS();
// 是否开启水印
useWatermark().then((res) => {
@@ -77,36 +71,35 @@ watch(
setTimeout(() => {
setWatermarkContent();
refreshWatermarkTime();
resume();
}, 500);
} else {
pause();
}
}
);
// 刷新水印时间
const { pause, resume } = useIntervalFn(() => {
if (!themeConfig.value.isWatermark) {
pause();
}
refreshWatermarkTime();
}, 60000);
const setWatermarkContent = () => {
themeConfigStores.setWatermarkUser();
themeConfigStores.setWatermarkNowTime();
};
let refreshWatermarkTimeInterval: any = null;
/**
* 刷新水印时间
*/
const refreshWatermarkTime = () => {
if (refreshWatermarkTimeInterval) {
clearInterval(refreshWatermarkTimeInterval);
}
refreshWatermarkTimeInterval = setInterval(() => {
if (themeConfig.value.isWatermark) {
themeConfigStores.setWatermarkNowTime();
} else {
clearInterval(refreshWatermarkTimeInterval);
}
}, 60000);
themeConfigStores.setWatermarkNowTime();
};
// 页面销毁时,关闭监听布局配置
onUnmounted(() => {
clearInterval(refreshWatermarkTimeInterval);
mittBus.off('openSetingsDrawer', () => {});
});

View File

@@ -49,32 +49,27 @@ class Api {
* 请求对应的该api
* @param {Object} param 请求该api的参数
*/
request(param: any = null, options: any = null, headers: any = null): Promise<any> {
request(param: any = null, options: any = {}): Promise<any> {
if (this.beforeHandler) {
this.beforeHandler(param);
}
return request.request(this.method, this.url, param, headers, options);
return request.request(this.method, this.url, param, options);
}
/**
* 请求对应的该api
* 允许取消的请求, 使用Api.cancelReq(key) 取消请求
* @param key 用于取消该key关联的请求
* @param {Object} param 请求该api的参数
*/
requestCanCancel(key: string, param: any = null, options: any = null, headers: any = null): Promise<any> {
allowCancelReq(key: string, param: any = null, options: RequestInit = {}): Promise<any> {
let controller = Api.abortControllers.get(key);
if (!controller) {
controller = new AbortController();
Api.abortControllers.set(key, controller);
}
if (options) {
options.signal = controller.signal;
} else {
options = {
signal: controller.signal,
};
}
options.signal = controller.signal;
return this.request(param, options, headers);
return this.request(param, options);
}
/** 静态方法 **/

View File

@@ -1,11 +1,20 @@
import router from '../router';
import Axios from 'axios';
import config from './config';
import { getClientId, getToken } from './utils/storage';
import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus';
import axios from 'axios';
export default {
request,
fetchReq,
get,
post,
put,
del,
getApiUrl,
};
export interface Result {
/**
* 响应码
@@ -30,6 +39,7 @@ enum ResultEnum {
}
const baseUrl: string = config.baseApiUrl;
// const baseUrl: string = 'http://localhost:18888/api';
// const baseWsUrl: string = config.baseWsUrl;
/**
@@ -42,13 +52,13 @@ function notifyErrorMsg(msg: string) {
}
// create an axios instance
const service = Axios.create({
const axiosInst = axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 60000, // request timeout
});
// request interceptor
service.interceptors.request.use(
axiosInst.interceptors.request.use(
(config: any) => {
// do something before request is sent
const token = getToken();
@@ -65,21 +75,8 @@ service.interceptors.request.use(
);
// response interceptor
service.interceptors.response.use(
(response) => {
// 获取请求返回结果
const res: Result = response.data;
if (res.code === ResultEnum.SUCCESS) {
return res.data;
}
// 如果提示没有权限则移除token使其重新登录
if (res.code === ResultEnum.NO_PERMISSION) {
router.push({
path: '/401',
});
}
return Promise.reject(res);
},
axiosInst.interceptors.response.use(
(response) => response,
(e: any) => {
const rejectPromise = Promise.reject(e);
@@ -125,35 +122,37 @@ service.interceptors.response.use(
* @param {Object} uri uri
* @param {Object} params 参数
*/
function request(method: string, url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
if (!url) throw new Error('请求url不能为空');
function request(method: string, url: string, params: any = null, options: any = {}): Promise<any> {
if (!url) {
throw new Error('请求url不能为空');
}
// 简单判断该url是否是restful风格
if (url.indexOf('{') != -1) {
url = templateResolve(url, params);
}
const query: any = {
const req: any = {
method,
url: url,
url,
...options,
};
if (headers) {
query.headers = headers;
}
// post和put使用json格式传参
if (method === 'post' || method === 'put') {
query.data = params;
req.data = params;
} else {
query.params = params;
req.params = params;
}
return service
.request(query)
.then((res) => res)
return axiosInst
.request(req)
.then((response) => {
// 获取请求返回结果
const result: Result = response.data;
return parseResult(result);
})
.catch((e) => {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (e.msg && e?.code != ResultEnum.NO_PERMISSION) {
notifyErrorMsg(e.msg);
}
return Promise.reject(e);
});
}
@@ -165,20 +164,20 @@ function request(method: string, url: string, params: any = null, headers: any =
* @param {Object} url uri
* @param {Object} params 参数
*/
function get(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('get', url, params, headers, options);
function get(url: string, params: any = null, options: any = {}): Promise<any> {
return request('get', url, params, options);
}
function post(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('post', url, params, headers, options);
function post(url: string, params: any = null, options: any = {}): Promise<any> {
return request('post', url, params, options);
}
function put(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('put', url, params, headers, options);
function put(url: string, params: any = null, options: any = {}): Promise<any> {
return request('put', url, params, options);
}
function del(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('delete', url, params, headers, options);
function del(url: string, params: any = null, options: any = {}): Promise<any> {
return request('delete', url, params, options);
}
function getApiUrl(url: string) {
@@ -191,11 +190,80 @@ export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;
}
export default {
request,
get,
post,
put,
del,
getApiUrl,
};
async function fetchReq(method: string, url: string, params: any = null, options: RequestInit = {}): Promise<any> {
options.method = method;
if (params) {
// post和put使用json格式传参
if (method === 'post' || method === 'put') {
options.body = JSON.stringify(params);
} else {
const searchParam = new URLSearchParams();
Object.keys(params).forEach((key) => {
const val = params[key];
if (val) {
searchParam.append(key, val);
}
});
url = `${url}?${searchParam.toString()}`;
}
}
// Part 1: Add headers and attach auth token
const headers = new Headers(options.headers || {});
const token = getToken();
if (token) {
headers.set('Authorization', token);
headers.set('ClientId', getClientId());
}
options.headers = headers;
try {
const res: Response = await fetch(`${baseUrl}${url}`, options);
if (!res.ok) {
throw new Error(`请求响应错误: 状态码=${res.status}`);
}
const jsonRes = await res.json();
// 获取请求返回结果
const result: Result = jsonRes;
return parseResult(result);
} catch (e: any) {
const rejectPromise = Promise.reject(e);
if (e?.name == 'AbortError') {
console.log('请求已取消');
return rejectPromise;
}
if (e.message) {
notifyErrorMsg(e.message);
return rejectPromise;
}
notifyErrorMsg('网络请求错误');
console.error(e);
return rejectPromise;
}
}
function parseResult(result: Result) {
if (result.code === ResultEnum.SUCCESS) {
return result.data;
}
// 如果提示没有权限则移除token使其重新登录
if (result.code === ResultEnum.NO_PERMISSION) {
router.push({
path: '/401',
});
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
notifyErrorMsg(result.msg);
}
return Promise.reject(result);
}

View File

@@ -2,8 +2,8 @@ import openApi from './openApi';
// 登录是否使用验证码配置key
const AccountLoginSecurity = 'AccountLoginSecurity';
const UseLoginCaptchaConfigKey = 'UseLoginCaptcha';
const UseWatermarkConfigKey = 'UseWatermark';
const MachineConfig = 'MachineConfig';
/**
* 获取系统配置值
@@ -43,15 +43,6 @@ export async function getAccountLoginSecurity(): Promise<any> {
return jsonValue;
}
/**
* 是否使用登录验证码
*
* @returns
*/
export async function useLoginCaptcha(): Promise<boolean> {
return await getBoolConfigValue(UseLoginCaptchaConfigKey, true);
}
/**
* 是否启用水印信息配置
*
@@ -75,13 +66,6 @@ export async function useWatermark(): Promise<any> {
}
}
function convertBool(value: string, defaultValue: boolean) {
if (!value) {
return defaultValue;
}
return value == '1' || value == 'true';
}
/**
* 获取LDAP登录配置
*
@@ -91,3 +75,32 @@ export async function getLdapEnabled(): Promise<any> {
const value = await openApi.getLdapEnabled();
return convertBool(value, false);
}
/**
* 是否启用水印信息配置
*
* @returns
*/
export async function getMachineConfig(): Promise<any> {
const value = await getConfigValue(MachineConfig);
const defaultValue = {
// 默认1gb
uploadMaxFileSize: '1GB',
};
if (!value) {
return defaultValue;
}
try {
const jsonValue = JSON.parse(value);
return jsonValue;
} catch (e) {
return defaultValue;
}
}
function convertBool(value: string, defaultValue: boolean) {
if (!value) {
return defaultValue;
}
return value == '1' || value == 'true';
}

View File

@@ -1,17 +0,0 @@
import { ref } from 'vue';
const vw = ref(document.documentElement.clientWidth);
const vh = ref(document.documentElement.clientHeight);
window.addEventListener('resize', () => {
vw.value = document.documentElement.clientWidth;
vh.value = document.documentElement.clientHeight;
});
/**
* 获取视图宽高
* @returns 视图宽高
*/
export function useViewport() {
return { vw, vh };
}

View File

@@ -15,6 +15,37 @@ export function formatByteSize(size: number, fixed = 2) {
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
}
/**
* 容量转为对应的字节大小,如 1KB转为 1024
* @param sizeString 1kb 1gb等
* @returns
*/
export function convertToBytes(sizeStr: string) {
sizeStr = sizeStr.trim();
const unit = sizeStr.slice(-2);
const valueStr = sizeStr.slice(0, -2);
const value = parseInt(valueStr, 10);
let bytes = 0;
switch (unit.toUpperCase()) {
case 'KB':
bytes = value * 1024;
break;
case 'MB':
bytes = value * 1024 * 1024;
break;
case 'GB':
bytes = value * 1024 * 1024 * 1024;
break;
default:
throw new Error('Invalid size unit');
}
return bytes;
}
/**
* 格式化json字符串
* @param txt json字符串

View File

@@ -34,8 +34,8 @@
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
import { ContextmenuItem } from './index';
import { useViewport } from '@/common/use';
import SvgIcon from '@/components/svgIcon/index.vue';
import { useWindowSize } from '@vueuse/core';
// 定义父组件传过来的值
const props = defineProps({
@@ -57,7 +57,7 @@ const props = defineProps({
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
const { vw, vh } = useViewport();
const { width: vw, height: vh } = useWindowSize();
// 定义变量内容
const state = reactive({

View File

@@ -1,212 +1,43 @@
<template>
<div class="dynamic-form">
<el-form
:model="form"
ref="dynamicForm"
:label-width="formInfo.labelWidth ? formInfo.labelWidth : '100px'"
:size="formInfo.size ? formInfo.size : 'small'"
>
<el-row v-for="fr in formInfo.formRows" :key="fr.key">
<el-col v-for="item in fr" :key="item.key" :span="item.span ? item.span : 24 / fr.length">
<el-form-item :prop="item.name" :label="item.label" :label-width="item.labelWidth" :required="item.required" :rules="item.rules">
<!-- input输入框 -->
<el-input
v-if="item.type === 'input'"
v-model.trim="form[item.name]"
:placeholder="item.placeholder"
:type="item.inputType"
clearable
<div class="dynamic-form">
<el-form v-bind="$attrs" ref="formRef" :model="formData" label-width="auto">
<el-form-item v-for="item in formItems as any" :key="item.name" :prop="item.model" :label="item.name" required>
<el-input v-if="!item.options" v-model="formData[item.model]" :placeholder="item.placeholder" autocomplete="off" clearable></el-input>
@change="item.change ? item.change(form) : ''"
></el-input>
<!-- 普通文本信息可用于不可修改字段等 -->
<span v-else-if="item.type === 'text'">{{ form[item.name] }}</span>
<!-- select选择框 -->
<!-- optionProps.label: 指定option中的label为options对象的某个属性值默认就是label字段 -->
<!-- optionProps.value: 指定option中的value为options对象的某个属性值默认就是value字段 -->
<el-select
v-else-if="item.type === 'select'"
v-model.trim="form[item.name]"
:placeholder="item.placeholder"
:filterable="item.filterable"
:remote="item.remote"
:remote-method="item.remoteMethod"
@focus="item.focus ? item.focus(form) : ''"
clearable
:disabled="item.updateDisabled && form.id != null"
style="width: 100%"
>
<el-option
v-for="i in item.options"
:key="i.key"
:label="i[item.optionProps ? item.optionProps.label || 'label' : 'label']"
:value="i[item.optionProps ? item.optionProps.value || 'value' : 'value']"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row type="flex" justify="center">
<slot name="btns" :submitDisabled="submitDisabled" :data="form" :submit="submit">
<el-button @click="reset" size="small"> </el-button>
<el-button type="primary" @click="submit" size="small"> </el-button>
</slot>
</el-row>
</el-form>
</div>
<el-select v-else v-model="formData[item.model]" :placeholder="item.placeholder" filterable autocomplete="off" clearable style="width: 100%">
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
import { watch, ref, toRefs, reactive, onMounted, defineComponent } from 'vue';
import { ElMessage } from 'element-plus';
<script lang="ts" setup>
import { useVModel } from '@vueuse/core';
import { ref } from 'vue';
export default defineComponent({
name: 'DynamicForm',
props: {
formInfo: { type: Object },
formData: { type: [Object, Boolean] },
},
setup(props: any, context) {
const dynamicForm: any = ref();
const state = reactive({
form: {},
submitDisabled: false,
});
watch(props.formData, (newValue, oldValue) => {
if (props.formData) {
state.form = { ...props.formData };
}
});
const submit = () => {
dynamicForm.value.validate((valid: boolean) => {
if (valid) {
// 提交的表单数据
const subform = { ...state.form };
const operation = state.form['id'] ? props.formInfo['updateApi'] : props.formInfo['createApi'];
if (operation) {
state.submitDisabled = true;
operation.request(state.form).then(
(res: any) => {
ElMessage.success('保存成功');
context.emit('submitSuccess', subform);
state.submitDisabled = false;
// this.cancel()
},
(e: any) => {
state.submitDisabled = false;
}
);
} else {
ElMessage.error('表单未设置对应的提交权限');
}
} else {
return false;
}
});
};
const reset = () => {
context.emit('reset');
resetFieldsAndData();
};
/**
* 重置表单以及表单数据
*/
const resetFieldsAndData = () => {
// 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
const df: any = dynamicForm;
df.resetFields();
// 重置表单数据
state.form = {};
};
return {
...toRefs(state),
dynamicForm,
submit,
reset,
resetFieldsAndData,
};
},
const props = defineProps({
formItems: { type: Array },
modelValue: { type: Object },
});
// @Component({
// name: 'DynamicForm'
// })
// export default class DynamicForm extends Vue {
// @Prop()
// formInfo: object
// @Prop()
// formData: [object,boolean]|undefined
// form = {}
// submitDisabled = false
const emit = defineEmits(['update:modelValue']);
// @Watch('formData', { deep: true })
// onRoleChange() {
// if (this.formData) {
// this.form = { ...this.formData }
// }
// }
const formRef: any = ref();
// submit() {
// const dynamicForm: any = this.$refs['dynamicForm']
// dynamicForm.validate((valid: boolean) => {
// if (valid) {
// // 提交的表单数据
// const subform = { ...this.form }
// const operation = this.form['id']
// ? this.formInfo['updateApi']
// : this.formInfo['createApi']
// if (operation) {
// this.submitDisabled = true
// operation.request(this.form).then(
// (res: any) => {
// ElMessage.success('保存成功')
// this.$emit('submitSuccess', subform)
// this.submitDisabled = false
// // this.cancel()
// },
// (e: any) => {
// this.submitDisabled = false
// }
// )
// } else {
// ElMessage.error('表单未设置对应的提交权限')
// }
// } else {
// return false
// }
// })
// }
const formData: any = useVModel(props, 'modelValue', emit);
// reset() {
// this.$emit('reset')
// this.resetFieldsAndData()
// }
const validate = async (func: any) => {
await formRef.value.validate(func);
};
// /**
// * 重置表单以及表单数据
// */
// resetFieldsAndData() {
// // 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
// const df: any = this.$refs['dynamicForm']
// df.resetFields()
// // 重置表单数据
// this.form = {}
// }
const resetFields = () => {
formRef.value.resetFields();
};
// mounted() {
// // 组件可能还没有初始化第一次初始化的时候无法watch对象
// this.form = { ...this.formData }
// }
// }
defineExpose({
validate,
resetFields,
});
</script>
<style lang="scss"></style>

View File

@@ -1,60 +1,55 @@
<template>
<div class="form-dialog">
<el-dialog :title="title" v-model="visible" :width="dialogWidth ? dialogWidth : '500px'">
<dynamic-form ref="df" :form-info="formInfo" :form-data="formData" @submitSuccess="submitSuccess">
<template #btns="props">
<slot name="btns">
<el-button :disabled="props.submitDisabled" type="primary" @click="props.submit" size="small"> </el-button>
<el-button :disabled="props.submitDisabled" @click="close()" size="small"> </el-button>
</slot>
</template>
</dynamic-form>
</el-dialog>
</div>
<div class="form-dialog">
<el-dialog @close="close" v-bind="$attrs" :title="title" v-model="dialogVisible" :width="width">
<dynamic-form ref="df" :form-items="formItems" v-model="formData" />
<template #footer>
<span>
<slot name="btns">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="confirm"> </el-button>
</slot>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { watch, ref, toRefs, reactive, onMounted, defineComponent } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import DynamicForm from './DynamicForm.vue';
export default defineComponent({
name: 'DynamicFormDialog',
components: {
DynamicForm,
},
props: {
visible: { type: Boolean },
dialogWidth: { type: String },
title: { type: String },
formInfo: { type: Object },
formData: { type: [Object, Boolean] },
},
import { useVModel } from '@vueuse/core';
setup(props: any, context) {
const df: any = ref();
const emit = defineEmits(['update:visible', 'update:modelValue', 'close', 'confirm']);
const close = () => {
// 更新父组件visible prop对应的值为false
context.emit('update:visible', false);
// 关闭窗口则将表单数据置为null
context.emit('update:formData', null);
context.emit('close');
// 取消动态表单的校验以及form数据
setTimeout(() => {
df.resetFieldsAndData();
}, 200);
};
const submitSuccess = (form: any) => {
context.emit('submitSuccess', form);
close();
};
return {
df,
close,
submitSuccess,
};
},
const props = defineProps({
title: { type: String },
visible: { type: Boolean },
width: { type: [String, Number], default: '500px' },
formItems: { type: Array },
modelValue: { type: Object },
});
const df: any = ref();
const formData: any = useVModel(props, 'modelValue', emit);
const dialogVisible: any = useVModel(props, 'visible', emit);
const close = () => {
emit('close');
// 取消动态表单的校验
setTimeout(() => {
formData.value = {};
df.value.resetFields();
}, 200);
};
const confirm = () => {
df.value.validate((valid: any) => {
if (!valid) {
return false;
}
emit('confirm', formData.value);
});
};
</script>

View File

@@ -0,0 +1,60 @@
<template>
<div class="dynamic-form-edit w100">
<el-table :data="formItems" stripe class="w100" empty-text="暂无表单项">
<el-table-column prop="name" label="model" min-width="100px">
<template #header>
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addItem()"> </el-button>
<span class="ml10">model</span>
</template>
<template #default="scope">
<el-input v-model="scope.row['model']" placeholder="字段model" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="name" label="label" min-width="100px">
<template #default="scope">
<el-input v-model="scope.row['name']" placeholder="字段title" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="placeholder" label="字段说明" min-width="140px">
<template #default="scope">
<el-input v-model="scope.row['placeholder']" placeholder="字段说明" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="options" label="可选值" min-width="140px">
<template #default="scope">
<el-input v-model="scope.row['options']" placeholder="可选值 ,分割" clearable> </el-input>
</template>
</el-table-column>
<el-table-column label="操作" wdith="20px">
<template #default="scope">
<el-button type="danger" @click="deleteItem(scope.$index)" icon="delete" plain></el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { useVModel } from '@vueuse/core';
const props = defineProps({
modelValue: { type: Array },
});
const emit = defineEmits(['update:modelValue']);
const formItems: any = useVModel(props, 'modelValue', emit);
const addItem = () => {
formItems.value.push({});
};
const deleteItem = (index: any) => {
formItems.value.splice(index, 1);
};
</script>
<style lang="scss"></style>

View File

@@ -1,2 +1,3 @@
export { default as DynamicForm } from './DynamicForm.vue';
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';
export { default as DynamicFormEdit } from './DynamicFormEdit.vue';

View File

@@ -2,8 +2,8 @@
<div class="layout-navbars-breadcrumb-user" :style="{ flex: layoutUserFlexNum }">
<div class="layout-navbars-breadcrumb-user-icon">
<el-switch
@change="switchDark(state.isDark)"
v-model="state.isDark"
@change="switchDark()"
v-model="isDark"
active-action-icon="Moon"
inactive-action-icon="Sunny"
style="--el-switch-off-color: #c4c9c4; --el-switch-on-color: #2c2c2c"
@@ -75,7 +75,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted } from 'vue';
import { ref, computed, reactive, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
@@ -83,17 +83,17 @@ import { resetRoute } from '@/router/index';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import { useThemeConfig } from '@/store/themeConfig';
import { clearSession, removeLocal } from '@/common/utils/storage';
import { clearSession } from '@/common/utils/storage';
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
import mittBus from '@/common/utils/mitt';
import openApi from '@/common/openApi';
import { saveThemeConfig, getThemeConfig } from '@/common/utils/storage';
import { useDark, usePreferredDark } from '@vueuse/core';
const router = useRouter();
const searchRef = ref();
const state = reactive({
isDark: false,
isScreenfull: false,
isShowUserNewsPopover: false,
disabledI18n: 'zh-cn',
@@ -165,8 +165,21 @@ const onHandleCommandClick = (path: string) => {
}
};
const switchDark = (isDark: boolean) => {
themeConfigStore.switchDark(isDark);
const isDark = useDark();
const preDark = usePreferredDark();
watch(preDark, (newValue) => {
isDark.value = newValue;
switchDark();
});
const switchDark = () => {
themeConfig.value.isDark = isDark.value;
if (isDark.value) {
themeConfig.value.editorTheme = 'vs-dark';
} else {
themeConfig.value.editorTheme = 'vs';
}
saveThemeConfig(themeConfig.value);
};
@@ -176,14 +189,14 @@ const onSearchClick = () => {
};
// 组件大小改变
const onComponentSizeChange = (size: string) => {
removeLocal('themeConfig');
themeConfig.value.globalComponentSize = size;
saveThemeConfig(themeConfig.value);
// proxy.$ELEMENT.size = size;
initComponentSize();
window.location.reload();
};
// const onComponentSizeChange = (size: string) => {
// removeLocal('themeConfig');
// themeConfig.value.globalComponentSize = size;
// saveThemeConfig(themeConfig.value);
// // proxy.$ELEMENT.size = size;
// initComponentSize();
// window.location.reload();
// };
// 初始化全局组件大小
const initComponentSize = () => {
@@ -208,7 +221,7 @@ onMounted(() => {
const themeConfig = getThemeConfig();
if (themeConfig) {
initComponentSize();
state.isDark = themeConfig.isDark;
isDark.value = themeConfig.isDark;
}
});
</script>

View File

@@ -146,18 +146,6 @@ export const useThemeConfig = defineStore('themeConfig', {
setThemeConfig(data: ThemeConfigState) {
this.themeConfig = data.themeConfig;
},
// 切换暗模式
switchDark(isDark: boolean) {
this.themeConfig.isDark = isDark;
const body = document.documentElement as HTMLElement;
if (isDark) {
body.setAttribute('class', 'dark');
this.themeConfig.editorTheme = 'vs-dark';
} else {
body.setAttribute('class', '');
this.themeConfig.editorTheme = 'vs';
}
},
// 设置水印配置信息
setWatermarkConfig(useWatermarkConfig: any) {
this.themeConfig.watermarkText = [];

View File

@@ -46,8 +46,8 @@ import { onMounted, reactive, ref, watch, toRefs, onUnmounted } from 'vue';
import { NodeType, TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue';
import { Contextmenu } from '@/components/contextmenu';
import { useViewport } from '@/common/use';
import { tagApi } from '../tag/api';
import { useWindowSize } from '@vueuse/core';
const props = defineProps({
resourceType: {
@@ -78,7 +78,7 @@ const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
const treeRef: any = ref(null);
const contextmenuRef = ref();
const { vh } = useViewport();
const { height: vh } = useWindowSize();
const state = reactive({
height: 600 as any,

View File

@@ -45,14 +45,20 @@
<el-button v-auth="perms.delDb" :disabled="selectionData.length < 1" @click="deleteDb()" type="danger" icon="delete">删除</el-button>
</template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
<template #type="{ data }">
<el-tooltip :content="data.type" placement="top">
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
</el-tooltip>
</template>
<template #host="{ data }">
{{ `${data.host}:${data.port}` }}
</template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
</template>
<template #action="{ data }">
<span v-if="actionBtns[perms.saveDb]">
<el-button type="primary" @click="editDb(data)" link>编辑</el-button>
@@ -173,6 +179,7 @@ import { DbType } from './dialect';
import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { useRoute } from 'vue-router';
import { getDbDialect } from './dialect/index';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
@@ -186,7 +193,7 @@ const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), Tabl
const columns = ref([
TableColumn.new('instanceName', '实例名'),
TableColumn.new('type', '类型'),
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
TableColumn.new('username', 'username'),
TableColumn.new('name', '名称'),

View File

@@ -20,6 +20,12 @@
>
</template>
<template #type="{ data }">
<el-tooltip :content="data.type" placement="top">
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
</el-tooltip>
</template>
<template #action="{ data }">
<el-button @click="showInfo(data)" link>详情</el-button>
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
@@ -66,6 +72,8 @@ import { dateFormat } from '@/common/utils/date';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect } from './dialect';
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
@@ -78,7 +86,7 @@ const queryConfig = [TableQuery.text('name', '名称')];
const columns = ref([
TableColumn.new('name', '名称'),
TableColumn.new('type', '类型'),
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
TableColumn.new('username', '用户名'),
TableColumn.new('params', '连接参数'),

View File

@@ -21,7 +21,6 @@ export const dbApi = {
param.sql = Base64.encode(param.sql);
}
}),
sqlExecCancel: Api.newPost('/dbs/{id}/exec-sql/cancel/{execId}'),
// 保存sql
saveSql: Api.newPost('/dbs/{id}/sql'),
// 获取保存的sql

View File

@@ -33,7 +33,7 @@
</el-upload>
</div>
<div style="float: right" class="fl">
<div class="fr">
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL</el-button>
</div>
</div>
@@ -128,7 +128,7 @@
</template>
<script lang="ts" setup>
import { h, nextTick, onMounted, reactive, toRefs, ref } from 'vue';
import { h, nextTick, onMounted, reactive, toRefs, ref, onBeforeUnmount } from 'vue';
import { getToken } from '@/common/utils/storage';
import { notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
@@ -150,8 +150,8 @@ import { ElNotification } from 'element-plus';
import syssocket from '@/common/syssocket';
import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect } from '../../dialect';
import { randomUuid } from '@/common/utils/string';
import { Splitpanes, Pane } from 'splitpanes';
import Api from '@/common/Api';
const emits = defineEmits(['saveSqlSuccess']);
@@ -252,6 +252,12 @@ onMounted(async () => {
await getNowDbInst().loadDbHints(props.dbName);
});
onBeforeUnmount(() => {
state.execResTabs.forEach((x: ExecResTab) => {
Api.removeAbortKey(x.loadingKey);
});
});
const onRemoveTab = (targetId: number) => {
let activeTab = state.activeTab;
const tabs = [...state.execResTabs];
@@ -347,7 +353,8 @@ const onRunSql = async (newTab = false) => {
execRes.errorMsg = '';
execRes.sql = '';
const loadingKey = randomUuid();
// 用于取消执行
const loadingKey = Api.genAbortKey(execRes.loadingKey);
execRes.loadingKey = loadingKey;
const colAndData: any = await getNowDbInst().runSql(props.dbName, sql, execRemark, loadingKey);
@@ -397,7 +404,7 @@ const onRunSql = async (newTab = false) => {
const getSql = () => {
let res = '' as string | undefined;
// 编辑器还没初始化
if (!monacoEditor?.getModel) {
if (!monacoEditor?.getModel()) {
return res;
}
// 选择选中的sql
@@ -405,6 +412,7 @@ const getSql = () => {
if (selection) {
res = monacoEditor.getModel()?.getValueInRange(selection);
}
// 整个编辑器的sql
if (!res) {
return monacoEditor.getModel()?.getValue();

View File

@@ -133,7 +133,8 @@ import { ContextmenuItem, Contextmenu } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue';
import { exportCsv, exportFile } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date';
import { dbApi } from '../../api';
import Api from '@/common/Api';
import { useIntervalFn } from '@vueuse/core';
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
@@ -285,7 +286,9 @@ const selectionRowsMap: Map<number, any> = new Map();
const cellUpdateMap: Map<number, UpdatedRow> = new Map();
// 数据加载时间计时器
let execTimeInterval: any = null;
const { pause, resume } = useIntervalFn(() => {
state.execTime += 0.1;
}, 100);
const state = reactive({
dbId: 0, // 当前选中操作的数据库实例
@@ -429,22 +432,17 @@ const setTableColumns = (columns: any) => {
};
const startLoading = () => {
if (execTimeInterval) {
endLoading();
}
execTimeInterval = setInterval(() => {
state.execTime += 0.1; // 每秒递增执行时间
}, 100);
state.execTime = 0;
resume();
};
const endLoading = () => {
state.execTime = 0;
clearInterval(execTimeInterval);
pause();
};
const cancelLoading = async () => {
if (props.loadingKey) {
await dbApi.sqlExecCancel.request({ id: state.dbId, execId: props.loadingKey });
Api.cancelReq(props.loadingKey);
endLoading();
}
};

View File

@@ -197,9 +197,17 @@ export class DbInst {
* @param remark 执行备注
*/
async runSql(dbName: string, sql: string, remark: string = '', key: string = '') {
if (key) {
return await dbApi.sqlExec.allowCancelReq(key, {
id: this.id,
db: dbName,
sql: sql.trim(),
remark,
});
}
return await dbApi.sqlExec.request({
id: this.id,
execId: key,
db: dbName,
sql: sql.trim(),
remark,

View File

@@ -24,39 +24,17 @@
</el-select>
</el-form-item>
<el-row style="margin-left: 30px; margin-bottom: 5px">
<el-button @click="onAddParam" type="success">新增占位符参数</el-button>
</el-row>
<el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`">
<el-row>
<el-col :span="5">
<el-input v-model.trim="param.model" placeholder="内容中用{{.model}}替换"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model.trim="param.name" placeholder="字段名"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.placeholder" placeholder="字段说明"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.options" placeholder="可选值 ,分割"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="2">
<el-button @click="onDeleteParam(index)" type="danger">删除</el-button>
</el-col>
</el-row>
<el-form-item class="w100">
<template #label>
<el-tooltip placement="top">
<template #content>
<span v-pre>1. 脚本内容中可使用{{.model}}作为占位符 </span>
<br />2. 执行脚本时可输入对应表单内容对占位符进行替换后执行
</template>
<span> 参数<SvgIcon name="question-filled" /> </span>
</el-tooltip>
</template>
<dynamic-form-edit v-model="params" />
</el-form-item>
<el-form-item required prop="script" class="100w">
@@ -82,6 +60,8 @@ import { ElMessage } from 'element-plus';
import { machineApi } from './api';
import { ScriptResultEnum } from './enums';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { DynamicFormEdit } from '@/components/dynamic-form';
import SvgIcon from '@/components/svgIcon/index.vue';
const props = defineProps({
visible: {
@@ -171,14 +151,6 @@ watch(props, (newValue: any) => {
}
});
const onAddParam = () => {
state.params.push({ name: '', model: '', placeholder: '' });
};
const onDeleteParam = (idx: number) => {
state.params.splice(idx, 1);
};
const btnOk = () => {
state.form.machineId = isCommon.value ? 9999999 : (machineId?.value as any);
scriptForm.value.validate((valid: any) => {

View File

@@ -45,35 +45,16 @@
</page-table>
</el-dialog>
<el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px">
<el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="auto">
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem as any" :key="item.name" :prop="item.model" :label="item.name" required>
<el-input
v-if="!item.options"
v-model="scriptParamsDialog.params[item.model]"
:placeholder="item.placeholder"
autocomplete="off"
clearable
></el-input>
<el-select
v-else
v-model="scriptParamsDialog.params[item.model]"
:placeholder="item.placeholder"
filterable
autocomplete="off"
clearable
style="width: 100%"
>
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="hasParamsRun()"> </el-button>
</span>
</template>
</el-dialog>
<dynamic-form-dialog
title="脚本参数"
width="400px"
v-model:visible="scriptParamsDialog.visible"
ref="paramsForm"
:form-items="scriptParamsDialog.paramsFormItem"
v-model="scriptParamsDialog.params"
@confirm="hasParamsRun"
>
</dynamic-form-dialog>
<el-dialog title="执行结果" v-model="resultDialog.visible" width="50%">
<div style="white-space: pre-line; padding: 10px; color: #000000">
@@ -115,6 +96,7 @@ import { ScriptResultEnum, ScriptTypeEnum } from './enums';
import ScriptEdit from './ScriptEdit.vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable';
import { DynamicFormDialog } from '@/components/dynamic-form';
const props = defineProps({
visible: { type: Boolean },
@@ -204,20 +186,9 @@ const runScript = async (script: any) => {
// 有参数的脚本执行函数
const hasParamsRun = async () => {
// 如果脚本参数弹窗显示,则校验参数表单数据通过后执行
if (state.scriptParamsDialog.visible) {
paramsForm.value.validate((valid: any) => {
if (valid) {
run(state.scriptParamsDialog.script);
state.scriptParamsDialog.params = {};
state.scriptParamsDialog.visible = false;
state.scriptParamsDialog.script = null;
paramsForm.value.resetFields();
} else {
return false;
}
});
}
await run(state.scriptParamsDialog.script);
state.scriptParamsDialog.visible = false;
state.scriptParamsDialog.script = null;
};
const run = async (script: any) => {

View File

@@ -49,7 +49,7 @@
:before-upload="beforeUpload"
:on-success="uploadSuccess"
action=""
:http-request="getUploadFile"
:http-request="uploadFile"
:headers="{ token }"
:show-file-list="false"
name="file"
@@ -68,7 +68,7 @@
ref="folderUploadRef"
webkitdirectory
directory
@change="getFolder"
@change="uploadFolder"
style="display: none"
/>
</div>
@@ -173,7 +173,7 @@
<el-table-column prop="size" label="大小" width="100" sortable>
<template #default="scope">
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatFileSize(scope.row.size) }} </span>
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span>
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && !scope.row.dirSize">
<el-button @click="getDirSize(scope.row)" type="primary" link :loading="scope.row.loadingDirSize">计算</el-button>
@@ -280,6 +280,8 @@ import { isTrue } from '@/common/assert';
import MachineFileContent from './MachineFileContent.vue';
import { notBlank } from '@/common/assert';
import { getToken } from '@/common/utils/storage';
import { formatByteSize, convertToBytes } from '@/common/utils/format';
import { getMachineConfig } from '@/common/sysconfig';
const props = defineProps({
machineId: { type: Number },
@@ -326,14 +328,15 @@ const state = reactive({
type: folderType,
data: null as any,
},
file: null as any,
machineConfig: { uploadMaxFileSize: '1GB' },
});
const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressShow, fileContent, createFileDialog } = toRefs(state);
onMounted(() => {
onMounted(async () => {
state.basePath = props.path;
setFiles(props.path);
state.machineConfig = await getMachineConfig();
});
const filterFiles = computed(() =>
@@ -616,16 +619,24 @@ function addFinderToList() {
folderUploadRef.value.click();
}
function getFolder(e: any) {
function uploadFolder(e: any) {
//e.target.files为文件夹里面的文件
// 把文件夹数据放到formData里面下面的files和paths字段根据接口来定
var form = new FormData();
form.append('basePath', state.nowPath);
let totalFileSize = 0;
for (let file of e.target.files) {
totalFileSize += file.size;
form.append('files', file);
form.append('paths', file.webkitRelativePath);
}
try {
if (!checkUploadFileSize(totalFileSize)) {
return;
}
// 上传操作
machineApi.uploadFile
.request(form, {
@@ -660,7 +671,7 @@ const onUploadProgress = (progressEvent: any) => {
state.progressNum = complete;
};
const getUploadFile = (content: any) => {
const uploadFile = (content: any) => {
const params = new FormData();
const path = state.nowPath;
params.append('file', content.file);
@@ -695,7 +706,16 @@ const uploadSuccess = (res: any) => {
};
const beforeUpload = (file: File) => {
state.file = file;
return checkUploadFileSize(file.size);
};
const checkUploadFileSize = (fileSize: number) => {
const bytes = convertToBytes(state.machineConfig.uploadMaxFileSize);
if (fileSize > bytes) {
ElMessage.error(`上传的文件超过系统配置的[${state.machineConfig.uploadMaxFileSize}]`);
return false;
}
return true;
};
const dontOperate = (data: any) => {
@@ -704,27 +724,6 @@ const dontOperate = (data: any) => {
return ls.indexOf(path) != -1;
};
/**
* 格式化文件大小
* @param {*} value
*/
const formatFileSize = (size: any) => {
const value = Number(size);
if (size && !isNaN(value)) {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
let index = 0;
let k = value;
if (value >= 1024) {
while (k > 1024) {
k = k / 1024;
index++;
}
}
return `${k.toFixed(2)}${units[index]}`;
}
return '-';
};
defineExpose({ showFileContent });
</script>
<style lang="scss">

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="750px" :destroy-on-close="true">
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="900px" :destroy-on-close="true">
<el-form ref="configForm" :model="form" label-width="auto">
<el-form-item prop="name" label="配置项" required>
<el-input v-model="form.name"></el-input>
@@ -22,43 +22,10 @@
</el-select>
</el-form-item>
<el-row style="margin-left: 30px; margin-bottom: 5px">
<el-button @click="onAddParam" size="small" type="success">新增配置项</el-button>
</el-row>
<el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`">
<el-row>
<el-col :span="5">
<el-input v-model="param.model" placeholder="model"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.name" placeholder="字段名"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.placeholder" placeholder="字段说明"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.options" placeholder="可选值 ,分割"></el-input>
</el-col>
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="2">
<el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button>
</el-col>
</el-row>
<el-form-item label="配置项" class="w100">
<dynamic-form-edit v-model="params" />
</el-form-item>
<!-- <el-form-item prop="value" label="配置值:" required>
<el-input v-model="form.value"></el-input>
</el-form-item> -->
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
@@ -76,6 +43,7 @@
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { configApi, accountApi } from '../api';
import { DynamicFormEdit } from '@/components/dynamic-form';
const props = defineProps({
visible: {
@@ -139,14 +107,6 @@ watch(props, (newValue: any) => {
}
});
const onAddParam = () => {
state.params.push({ name: '', model: '', placeholder: '' });
};
const onDeleteParam = (idx: number) => {
state.params.splice(idx, 1);
};
const cancel = () => {
// 更新父组件visible prop对应的值为false
emit('update:visible', false);

View File

@@ -25,41 +25,20 @@
</template>
</page-table>
<el-dialog :before-close="closeSetConfigDialog" title="配置项设置" v-model="paramsDialog.visible" width="600px">
<el-form v-if="paramsDialog.paramsFormItem.length > 0" ref="paramsFormRef" :model="paramsDialog.params" label-width="auto">
<el-form-item v-for="item in paramsDialog.paramsFormItem" :key="item.name" :prop="item.model" :label="item.name" required>
<el-input
v-if="!item.options && !item.type"
v-model="paramsDialog.params[item.model]"
:placeholder="item.placeholder"
autocomplete="off"
clearable
></el-input>
<el-checkbox
v-else-if="item.type == 'checkbox'"
v-model="paramsDialog.params[item.model]"
autocomplete="off"
:label="item.placeholder"
clearable
/>
<el-select
v-else
v-model="paramsDialog.params[item.model]"
:placeholder="item.placeholder"
filterable
autocomplete="off"
clearable
style="width: 100%"
>
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
</el-select>
</el-form-item>
</el-form>
<el-dialog @close="closeSetConfigDialog" title="配置项设置" v-model="paramsDialog.visible" width="600px">
<dynamic-form
ref="paramsFormRef"
v-if="paramsDialog.paramsFormItem.length > 0"
:form-items="paramsDialog.paramsFormItem"
v-model="paramsDialog.params"
/>
<el-form v-else ref="paramsFormRef" label-width="auto">
<el-form-item label="配置值" required>
<el-input v-model="paramsDialog.params" :placeholder="paramsDialog.config.remark" autocomplete="off" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeSetConfigDialog()"> </el-button>
@@ -80,6 +59,7 @@ import { ElMessage } from 'element-plus';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { DynamicForm } from '@/components/dynamic-form';
const perms = {
saveConfig: 'config:save',
@@ -163,7 +143,7 @@ const closeSetConfigDialog = () => {
const setConfig = async () => {
let paramsValue = state.paramsDialog.params;
if (state.paramsDialog.paramsFormItem.length > 0) {
await paramsFormRef.value.validate(async (valid: boolean) => {
await paramsFormRef.value.validate((valid: boolean) => {
if (!valid) {
paramsValue = null as any;
return false;

View File

@@ -345,6 +345,11 @@
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz"
integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
"@types/web-bluetooth@^0.0.20":
version "0.0.20"
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
"@typescript-eslint/eslint-plugin@^6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz#057338df21b6062c2f2fc5999fbea8af9973ac6d"
@@ -445,6 +450,16 @@
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-core@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.11.tgz#9fa26f8c81b9b34365f94ce1ed4d0e6e6f94a2ac"
integrity sha512-h97/TGWBilnLuRaj58sxNrsUU66fwdRKLOLQ9N/5iNDfp+DZhYH9Obhe0bXxhedl8fjAgpRANpiZfbgWyruQ0w==
dependencies:
"@babel/parser" "^7.23.5"
"@vue/shared" "3.3.11"
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-dom@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz#183811252be6aff4ac923f783124bb1590301907"
@@ -453,7 +468,31 @@
"@vue/compiler-core" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/compiler-sfc@3.3.10", "@vue/compiler-sfc@^3.3.10":
"@vue/compiler-dom@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.11.tgz#36a76ea3a296d41bad133a6912cb0a847d969e4f"
integrity sha512-zoAiUIqSKqAJ81WhfPXYmFGwDRuO+loqLxvXmfUdR5fOitPoUiIeFI9cTTyv9MU5O1+ZZglJVTusWzy+wfk5hw==
dependencies:
"@vue/compiler-core" "3.3.11"
"@vue/shared" "3.3.11"
"@vue/compiler-sfc@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.11.tgz#acfae240c875d067e0e2c9a4e2d910074408c73b"
integrity sha512-U4iqPlHO0KQeK1mrsxCN0vZzw43/lL8POxgpzcJweopmqtoYy9nljJzWDIQS3EfjiYhfdtdk9Gtgz7MRXnz3GA==
dependencies:
"@babel/parser" "^7.23.5"
"@vue/compiler-core" "3.3.11"
"@vue/compiler-dom" "3.3.11"
"@vue/compiler-ssr" "3.3.11"
"@vue/reactivity-transform" "3.3.11"
"@vue/shared" "3.3.11"
estree-walker "^2.0.2"
magic-string "^0.30.5"
postcss "^8.4.32"
source-map-js "^1.0.2"
"@vue/compiler-sfc@^3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz#8eb97d42f276089ec58fd0565ef3a813bceeaa87"
integrity sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==
@@ -477,6 +516,14 @@
"@vue/compiler-dom" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/compiler-ssr@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.11.tgz#598942a73b64f2bd3f95908b104a7fbb55fc41a2"
integrity sha512-Zd66ZwMvndxRTgVPdo+muV4Rv9n9DwQ4SSgWWKWkPFebHQfVYRrVjeygmmDmPewsHyznCNvJ2P2d6iOOhdv8Qg==
dependencies:
"@vue/compiler-dom" "3.3.11"
"@vue/shared" "3.3.11"
"@vue/devtools-api@^6.5.0":
version "6.5.0"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
@@ -493,43 +540,69 @@
estree-walker "^2.0.2"
magic-string "^0.30.5"
"@vue/reactivity@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.10.tgz#78fe3da319276d9e6d0f072037532928c472a287"
integrity sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==
"@vue/reactivity-transform@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.11.tgz#2bd486f4eff60c8724309925618891e722fcfadc"
integrity sha512-fPGjH0wqJo68A0wQ1k158utDq/cRyZNlFoxGwNScE28aUFOKFEnCBsvyD8jHn+0kd0UKVpuGuaZEQ6r9FJRqCg==
dependencies:
"@vue/shared" "3.3.10"
"@babel/parser" "^7.23.5"
"@vue/compiler-core" "3.3.11"
"@vue/shared" "3.3.11"
estree-walker "^2.0.2"
magic-string "^0.30.5"
"@vue/runtime-core@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.10.tgz#d7b78c5c0500b856cf9447ef81d4a1b1438fd5bb"
integrity sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==
"@vue/reactivity@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.11.tgz#91f8e6c9ac60a595a5278c836b197628fd947a0d"
integrity sha512-D5tcw091f0nuu+hXq5XANofD0OXnBmaRqMYl5B3fCR+mX+cXJIGNw/VNawBqkjLNWETrFW0i+xH9NvDbTPVh7g==
dependencies:
"@vue/reactivity" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/shared" "3.3.11"
"@vue/runtime-dom@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz#130dfffb8fee8051671aaf80c5104d2020544950"
integrity sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==
"@vue/runtime-core@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.11.tgz#63defba57bc54c1dac68a95b56c2633b1419193d"
integrity sha512-g9ztHGwEbS5RyWaOpXuyIVFTschclnwhqEbdy5AwGhYOgc7m/q3NFwr50MirZwTTzX55JY8pSkeib9BX04NIpw==
dependencies:
"@vue/runtime-core" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/reactivity" "3.3.11"
"@vue/shared" "3.3.11"
"@vue/runtime-dom@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.11.tgz#1146d8d280b0fec4d2e18c4a4c8f8121d0cecc09"
integrity sha512-OlhtV1PVpbgk+I2zl+Y5rQtDNcCDs12rsRg71XwaA2/Rbllw6mBLMi57VOn8G0AjOJ4Mdb4k56V37+g8ukShpQ==
dependencies:
"@vue/runtime-core" "3.3.11"
"@vue/shared" "3.3.11"
csstype "^3.1.2"
"@vue/server-renderer@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.10.tgz#f23d151f0e5021ebdc730052d9934c9178486742"
integrity sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==
"@vue/server-renderer@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.11.tgz#409aed8031a125791e2143552975ecd1958ad601"
integrity sha512-AIWk0VwwxCAm4wqtJyxBylRTXSy1wCLOKbWxHaHiu14wjsNYtiRCSgVuqEPVuDpErOlRdNnuRgipQfXRLjLN5A==
dependencies:
"@vue/compiler-ssr" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/compiler-ssr" "3.3.11"
"@vue/shared" "3.3.11"
"@vue/shared@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.10.tgz#1583a8d85a957d8b819078c465d2a11db7914b2f"
integrity sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==
"@vue/shared@3.3.11":
version "3.3.11"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.11.tgz#f6a038e15237edefcc90dbfe7edb806dd355c7bd"
integrity sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==
"@vueuse/core@^10.7.0":
version "10.7.0"
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-10.7.0.tgz#34f2f02f179dc0dcffc2be70d6b1233e011404b9"
integrity sha512-4EUDESCHtwu44ZWK3Gc/hZUVhVo/ysvdtwocB5vcauSV4B7NiGY5972WnsojB3vRNdxvAt7kzJWE2h9h7C9d5w==
dependencies:
"@types/web-bluetooth" "^0.0.20"
"@vueuse/metadata" "10.7.0"
"@vueuse/shared" "10.7.0"
vue-demi ">=0.14.6"
"@vueuse/core@^9.1.0":
version "9.2.0"
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.2.0.tgz"
@@ -540,11 +613,23 @@
"@vueuse/shared" "9.2.0"
vue-demi "*"
"@vueuse/metadata@10.7.0":
version "10.7.0"
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.7.0.tgz#7b05e6cfd376aa9bb339a81e16a89c12f3e88c03"
integrity sha512-GlaH7tKP2iBCZ3bHNZ6b0cl9g0CJK8lttkBNUX156gWvNYhTKEtbweWLm9rxCPIiwzYcr/5xML6T8ZUEt+DkvA==
"@vueuse/metadata@9.2.0":
version "9.2.0"
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.2.0.tgz"
integrity sha512-exN4KE6iquxDCdt72BgEhb3tlOpECtD61AUdXnUqBTIUCl70x1Ar/QXo3bYcvxmdMS2/peQyfeTzBjRTpvL5xw==
"@vueuse/shared@10.7.0":
version "10.7.0"
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.7.0.tgz#21e425cc5ede421e0cda38ac59a0beee6da86b1b"
integrity sha512-kc00uV6CiaTdc3i1CDC4a3lBxzaBE9AgYNtFN87B5OOscqeWElj/uza8qVDmk7/U8JbqoONLbtqiLJ5LGRuqlw==
dependencies:
vue-demi ">=0.14.6"
"@vueuse/shared@9.2.0":
version "9.2.0"
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.2.0.tgz"
@@ -1419,10 +1504,10 @@ mitt@^3.0.1:
resolved "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
monaco-editor@^0.44.0:
version "0.44.0"
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.44.0.tgz#3c0fe3655923bbf7dd647057302070b5095b6c59"
integrity sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==
monaco-editor@^0.45.0:
version "0.45.0"
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.45.0.tgz#6939123a6254aea9fea2d647697f846306dd4448"
integrity sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==
monaco-sql-languages@^0.11.0:
version "0.11.0"
@@ -1859,10 +1944,10 @@ uuid@^9.0.1:
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
vite@^5.0.5:
version "5.0.5"
resolved "https://registry.npmmirror.com/vite/-/vite-5.0.5.tgz#3eebe3698e3b32cea36350f58879258fec858a3c"
integrity sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==
vite@^5.0.7:
version "5.0.7"
resolved "https://registry.npmmirror.com/vite/-/vite-5.0.7.tgz#ad081d735f6769f76b556818500bdafb72c3fe93"
integrity sha512-B4T4rJCDPihrQo2B+h1MbeGL/k/GMAHzhQ8S0LjQ142s6/+l3hHTT095ORvsshj4QCkoWu3Xtmob5mazvakaOw==
dependencies:
esbuild "^0.19.3"
postcss "^8.4.32"
@@ -1880,6 +1965,11 @@ vue-demi@>=0.14.5:
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz#676d0463d1a1266d5ab5cba932e043d8f5f2fbd9"
integrity sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==
vue-demi@>=0.14.6:
version "0.14.6"
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92"
integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==
vue-eslint-parser@^9.3.1:
version "9.3.1"
resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz#429955e041ae5371df5f9e37ebc29ba046496182"
@@ -1913,16 +2003,16 @@ vue-router@^4.2.5:
dependencies:
"@vue/devtools-api" "^6.5.0"
vue@^3.3.10:
version "3.3.10"
resolved "https://registry.npmmirror.com/vue/-/vue-3.3.10.tgz#6e19c1982ee655a14babe1610288b90005f02ab1"
integrity sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==
vue@^3.3.11:
version "3.3.11"
resolved "https://registry.npmmirror.com/vue/-/vue-3.3.11.tgz#898d97025f73cdb5fc4e3ae3fd07a54615232140"
integrity sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w==
dependencies:
"@vue/compiler-dom" "3.3.10"
"@vue/compiler-sfc" "3.3.10"
"@vue/runtime-dom" "3.3.10"
"@vue/server-renderer" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/compiler-dom" "3.3.11"
"@vue/compiler-sfc" "3.3.11"
"@vue/runtime-dom" "3.3.11"
"@vue/server-renderer" "3.3.11"
"@vue/shared" "3.3.11"
which@^2.0.1:
version "2.0.2"

View File

@@ -25,7 +25,6 @@ import (
"mayfly-go/pkg/ws"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -84,9 +83,6 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
/** 数据库操作相关、执行sql等 ***/
// 取消执行sql函数map; key -> execId ; value -> cancelFunc
var cancelExecSqlMap = sync.Map{}
func (d *Db) ExecSql(rc *req.Ctx) {
g := rc.GinCtx
form := &form.DbSqlExecForm{}
@@ -112,14 +108,9 @@ func (d *Db) ExecSql(rc *req.Ctx) {
DbConn: dbConn,
}
ctx := rc.MetaCtx
// 如果存在执行id则保存取消函数用于后续可能的取消操作
if form.ExecId != "" {
cancelCtx, cancel := context.WithTimeout(rc.MetaCtx, 55*time.Second)
ctx = cancelCtx
cancelExecSqlMap.Store(form.ExecId, cancel)
defer cancelExecSqlMap.Delete(form.ExecId)
}
// 比前端超时时间稍微快一点,可以提示到前端
ctx, cancel := context.WithTimeout(rc.MetaCtx, 58*time.Second)
defer cancel()
sqls, err := sqlparser.SplitStatementToPieces(sql, sqlparser.WithDialect(dbConn.Info.Type.Dialect()))
biz.ErrIsNil(err, "SQL解析错误,请检查您的执行SQL")
@@ -150,14 +141,6 @@ func (d *Db) ExecSql(rc *req.Ctx) {
rc.ResData = colAndRes
}
func (d *Db) CancelExecSql(rc *req.Ctx) {
execId := ginx.PathParam(rc.GinCtx, "execId")
if cancelFunc, ok := cancelExecSqlMap.LoadAndDelete(execId); ok {
rc.ReqParam = execId
cancelFunc.(context.CancelFunc)()
}
}
// progressCategory sql文件执行进度消息类型
const progressCategory = "execSqlFileProgress"

View File

@@ -35,8 +35,6 @@ func InitDbRouter(router *gin.RouterGroup) {
req.NewPost(":dbId/exec-sql", d.ExecSql).Log(req.NewLog("db-执行Sql")),
req.NewPost(":dbId/exec-sql/cancel/:execId", d.CancelExecSql).Log(req.NewLog("db-取消执行Sql")),
req.NewPost(":dbId/exec-sql-file", d.ExecSqlFile).Log(req.NewLogSave("db-执行Sql文件")),
req.NewGet(":dbId/dump", d.DumpSql).Log(req.NewLogSave("db-导出sql文件")).NoRes(),