11 Commits

Author SHA1 Message Date
meilin.huang
2118acf244 release: v1.9.0 2024-10-23 17:30:05 +08:00
meilin.huang
44a1bd626e feat: 数据库迁移至文件支持文件保存天数等 2024-10-22 20:39:44 +08:00
meilin.huang
ea3c70a8a8 feat: 新增统一文件模块,统一文件操作 2024-10-21 22:27:42 +08:00
zongyangleo
6343173cf8 !124 一些更新和bug
* fix: 代码合并
* feat:支持数据库版本兼容,目前兼容了oracle11g部分特性
* fix: 修改数据同步bug,数据sql里指定修改字段别,导致未正确记录修改字段值
* feat: 数据库迁移支持定时迁移和迁移到sql文件
2024-10-20 03:52:23 +00:00
meilin.huang
6837a9c867 fix: sql切割转义等问题处理 2024-10-18 17:15:58 +08:00
meilin.huang
a726927a28 feat: sql脚本执行调整 2024-10-18 12:32:53 +08:00
meilin.huang
e135e4ce64 feat: sql解析器替换、工单统一由‘我的流程’发起、流程定义支持自定义条件触发审批、资源隐藏编号、model支持物理删除等 2024-10-16 17:24:50 +08:00
zongyangleo
43edef412c !123 一些bug修复
* fix: 数据同步、数据迁移体验优化
* fix: des加密传输sql
* fix: 修复达梦字段注释显示问题
* fix: mysql timestamp 字段类型导出ddl错误修复
2024-08-22 00:43:39 +00:00
meilin.huang
2deb3109c2 feat: dbms表数据新增表单视图 2024-07-19 17:06:11 +08:00
meilin.huang
a80221a950 fix: 数据库实例删除等问题修复 2024-07-05 13:14:31 +08:00
meilin.huang
10630847df fix: 工单流程信息展示问题修复 2024-06-24 17:17:57 +08:00
298 changed files with 400635 additions and 2325 deletions

View File

@@ -10,7 +10,7 @@ RUN yarn config set registry 'https://registry.npmmirror.com' && \
yarn build
# 构建后端资源
FROM golang:1.22 as be-builder
FROM golang:1.23 as be-builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly

View File

@@ -5,4 +5,6 @@ VITE_PORT = 8889
VITE_OPEN = false
# public path 配置线上环境路径(打包)
VITE_PUBLIC_PATH = ''
VITE_PUBLIC_PATH = ''
VITE_EDITOR=idea

View File

@@ -1,68 +1,72 @@
{
"name": "mayfly",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"build-preview": "npm run build && npm run preview",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^10.10.0",
"asciinema-player": "^3.7.1",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"cropperjs": "^1.6.1",
"dayjs": "^1.11.11",
"echarts": "^5.5.0",
"element-plus": "^2.7.4",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.49.0",
"monaco-sql-languages": "^0.12.0",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"qrcode.vue": "^3.4.1",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.2",
"splitpanes": "^3.1.5",
"sql-formatter": "^15.0.2",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.4.27",
"vue-router": "^4.3.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
"xterm-addon-web-links": "^0.9.0"
},
"devDependencies": {
"@types/lodash": "^4.14.178",
"@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/compiler-sfc": "^3.4.27",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^9.25.0",
"prettier": "^3.2.5",
"sass": "^1.77.1",
"typescript": "^5.4.5",
"vite": "^5.2.12",
"vue-eslint-parser": "^9.4.2"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
"name": "mayfly",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"build-preview": "npm run build && npm run preview",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^11.1.0",
"asciinema-player": "^3.8.1",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"element-plus": "^2.8.6",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.0",
"monaco-sql-languages": "^0.12.2",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.2.4",
"qrcode.vue": "^3.5.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.3",
"splitpanes": "^3.1.5",
"sql-formatter": "^15.4.5",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.12",
"vue-router": "^4.4.5",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
"xterm-addon-web-links": "^0.9.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.14.178",
"@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/compiler-sfc": "^3.5.12",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^9.28.0",
"prettier": "^3.2.5",
"sass": "^1.80.3",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"vue-eslint-parser": "^9.4.3"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

@@ -69,7 +69,7 @@ class Api {
*/
async xhrReq(param: any = null, options: any = {}): Promise<any> {
if (this.beforeHandler) {
this.beforeHandler(param);
await this.beforeHandler(param);
}
return request.xhrReq(this.method, this.url, param, options);
}

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.8.7',
version: 'v1.9.0',
};
export default config;

View File

@@ -0,0 +1,38 @@
import CryptoJS from 'crypto-js';
import { getToken } from '@/common/utils/storage';
/**
* AES 加密数据
* @param word
* @param key
*/
export function AesEncrypt(word: string, key?: string) {
if (!key) {
key = getToken().substring(0, 24);
}
const sKey = CryptoJS.enc.Utf8.parse(key);
const encrypted = CryptoJS.AES.encrypt(word, sKey, {
iv: sKey,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}
export function AesDecrypt(word: string, key?: string): string {
if (!key) {
key = getToken().substring(0, 24);
}
const sKey = CryptoJS.enc.Utf8.parse(key);
// key 和 iv 使用同一个值
const decrypted = CryptoJS.AES.decrypt(word, sKey, {
iv: sKey,
mode: CryptoJS.mode.CBC, // CBC算法
padding: CryptoJS.pad.Pkcs7, //使用pkcs7 进行padding 后端需要注意
});
return decrypted.toString(CryptoJS.enc.Base64);
}

View File

@@ -14,4 +14,5 @@ export default {
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
getFileDetail: (keys: string[]) => request.get(`/sys/files/detail/${keys.join(',')}`),
};

View File

@@ -209,6 +209,36 @@ export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;
}
/**
* 获取文件url地址
* @param key 文件key
* @returns 文件url
*/
export function getFileUrl(key: string) {
return `${baseUrl}/sys/files/${key}`;
}
/**
* 获取系统文件上传url
* @param key 文件key
* @returns 文件上传url
*/
export function getUploadFileUrl(key: string = '') {
return `${baseUrl}/sys/files/upload?token=${getToken()}&fileKey=${key}`;
}
/**
* 下载文件
* @param key 文件key
*/
export function downloadFile(key: string) {
const a = document.createElement('a');
a.setAttribute('href', `${getFileUrl(key)}`);
a.setAttribute('target', '_blank');
a.click();
a.remove();
}
function parseResult(result: Result) {
if (result.code === ResultEnum.SUCCESS) {
return result.data;

View File

@@ -1,9 +1,9 @@
import Config from './config';
import { ElNotification } from 'element-plus';
import {ElNotification} from 'element-plus';
import SocketBuilder from './SocketBuilder';
import { getToken } from '@/common/utils/storage';
import {getToken} from '@/common/utils/storage';
import { joinClientParams } from './request';
import {joinClientParams} from './request';
class SysSocket {
/**
@@ -19,10 +19,11 @@ class SysSocket {
/**
* 消息类型
*/
messageTypes = {
messageTypes: any = {
0: 'error',
1: 'success',
2: 'info',
22: 'info',
};
/**
@@ -57,12 +58,20 @@ class SysSocket {
}
const type = this.getMsgType(message.type);
let msg = message.msg
let duration = 0
if (message.type == 22) {
let obj = JSON.parse(msg);
msg = `文件:${obj['title']} 执行成功: ${obj['executedStatements']}`
duration = 2000
}
ElNotification({
duration: 0,
duration: duration,
title: message.title,
message: message.msg,
message: msg,
type: type,
});
console.log(message)
})
.open((event: any) => console.log(event))
.close(() => {

View File

@@ -9,7 +9,19 @@ export function getValueByPath(obj: any, path: string) {
const keys = path.split('.');
let result = obj;
for (let key of keys) {
if (!result || typeof result !== 'object') {
if (!result) {
return undefined;
}
// 如果是字符串则尝试使用json解析
if (typeof result == 'string') {
try {
result = JSON.parse(result);
} catch (e) {
console.error(e);
return undefined;
}
}
if (typeof result !== 'object') {
return undefined;
}
@@ -23,7 +35,18 @@ export function getValueByPath(obj: any, path: string) {
}
const index = parseInt(matchIndex[1]);
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
let arrValue = result[arrayKey];
if (typeof arrValue == 'string') {
try {
arrValue = JSON.parse(arrValue);
} catch (e) {
result = undefined;
break;
}
}
result = Array.isArray(arrValue) ? arrValue[index] : undefined;
} else {
result = result[key];
}

View File

@@ -4,6 +4,7 @@ export interface ViteEnv {
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_PUBLIC_PATH: string;
VITE_EDITOR: string;
}
export function loadEnv(): ViteEnv {

View File

@@ -83,7 +83,7 @@ const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint);
// 判断是否显示 展开/合并 按钮
const showCollapse = computed(() => {
let show = false;
props.items.reduce((prev, current) => {
props.items.reduce((prev, current: any) => {
prev += (current![breakPoint.value]?.span ?? current?.span ?? 1) + (current![breakPoint.value]?.offset ?? current?.offset ?? 0);
if (typeof props.searchCol !== 'number') {
if (prev >= props.searchCol[breakPoint.value]) show = true;

View File

@@ -14,11 +14,11 @@ export function hasPerm(code: string) {
/**
* 判断用户是否拥有权限对象里对应的code
* @param perms { save: "xxx:save"}
* @returns {"xxx:save": true} key->permission code
* @param permCodes
*/
export function hasPerms(permCodes: any[]) {
const res = {};
const res = {} as { [key: string]: boolean };
for (let permCode of permCodes) {
if (hasPerm(permCode)) {
res[permCode] = true;

View File

@@ -35,38 +35,42 @@
<p class="title">时间表达式</p>
<table>
<thead>
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
<th>crontab完整表达式</th>
<tr>
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
<th>crontab完整表达式</th>
</tr>
</thead>
<tbody>
<td>
<span>{{ crontabValueObj.second }}</span>
</td>
<td>
<span>{{ crontabValueObj.min }}</span>
</td>
<td>
<span>{{ crontabValueObj.hour }}</span>
</td>
<td>
<span>{{ crontabValueObj.day }}</span>
</td>
<td>
<span>{{ crontabValueObj.mouth }}</span>
</td>
<td>
<span>{{ crontabValueObj.week }}</span>
</td>
<td>
<span>{{ crontabValueObj.year }}</span>
</td>
<td>
<span>{{ contabValueString }}</span>
</td>
<tr>
<td>
<span>{{ crontabValueObj.second }}</span>
</td>
<td>
<span>{{ crontabValueObj.min }}</span>
</td>
<td>
<span>{{ crontabValueObj.hour }}</span>
</td>
<td>
<span>{{ crontabValueObj.day }}</span>
</td>
<td>
<span>{{ crontabValueObj.mouth }}</span>
</td>
<td>
<span>{{ crontabValueObj.week }}</span>
</td>
<td>
<span>{{ crontabValueObj.year }}</span>
</td>
<td>
<span>{{ crontabValueString }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<CrontabResult :ex="contabValueString"></CrontabResult>
<CrontabResult :ex="crontabValueString"></CrontabResult>
<div class="pop_btn">
<el-button size="small" @click="hidePopup">取消</el-button>
@@ -202,7 +206,7 @@ function hidePopup() {
// 填充表达式
const submitFill = () => {
emit('fill', contabValueString.value);
emit('fill', crontabValueString.value);
hidePopup();
};
@@ -220,7 +224,7 @@ const clearCron = () => {
changeTab(state.activeName);
};
const contabValueString = computed(() => {
const crontabValueString = computed(() => {
let obj = state.crontabValueObj;
let str = obj.second + ' ' + obj.min + ' ' + obj.hour + ' ' + obj.day + ' ' + obj.mouth + ' ' + obj.week + (obj.year == '' ? '' : ' ' + obj.year);
return str;

View File

@@ -0,0 +1,17 @@
<template>
<el-select v-bind="$attrs" v-model="modelValue">
<el-option v-for="item in props.enums" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</template>
<script lang="ts" setup>
const props = defineProps({
enums: {
type: Object, // 需要为EnumValue类型
required: true,
},
});
const modelValue: any = defineModel('modelValue');
</script>
<style scoped lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-tag v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
<el-tag :disable-transitions="true" v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
</template>
<script lang="ts" setup>

View File

@@ -0,0 +1,60 @@
<template>
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
</el-tooltip>
{{ fileDetail?.filename }}
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import openApi from '@/common/openApi';
import { getFileUrl } from '@/common/request';
import { formatByteSize } from '@/common/utils/format';
const props = defineProps({
fileKey: {
type: String,
required: true,
},
files: {
type: [Array],
},
canDownload: {
type: Boolean,
default: true,
},
});
onMounted(async () => {
setFileInfo();
});
watch(
() => props.fileKey,
async (val) => {
if (val) {
setFileInfo();
}
}
);
const fileDetail: any = ref({});
const setFileInfo = async () => {
if (!props.fileKey) {
return;
}
if (props.files && props.files.length > 0) {
const file: any = props.files.find((file: any) => {
return file.fileKey === props.fileKey;
});
fileDetail.value = file;
return;
}
const files = await openApi.getFileDetail([props.fileKey]);
fileDetail.value = files?.[0];
};
</script>
<style lang="scss"></style>

View File

@@ -252,7 +252,9 @@ const changeLanguage = (value: any) => {
};
const setEditorValue = (value: any) => {
monacoEditorIns.getModel()?.setValue(value);
if (value) {
monacoEditorIns.getModel()?.setValue(value);
}
};
/**

View File

@@ -156,8 +156,8 @@
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
<el-pagination
:small="props.size == 'small'"
@current-change="handlePageNumChange"
@size-change="handlePageSizeChange"
@current-change="pageNumChange"
@size-change="pageSizeChange"
style="text-align: right"
layout="prev, pager, next, total, sizes"
:total="total"
@@ -185,7 +185,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { usePageTable } from '@/hooks/usePageTable';
import { ElTable } from 'element-plus';
const emit = defineEmits(['update:selectionData', 'pageChange']);
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
export interface PageTableProps {
size?: string;
@@ -257,6 +257,15 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
nowSearchItem.value = searchItem;
};
const pageSizeChange = (val: number) => {
emit('pageSizeChange', val);
handlePageSizeChange(val);
};
const pageNumChange = (val: number) => {
emit('pageNumChange', val);
handlePageNumChange(val);
};
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
props.pageable,
props.pageApi,
@@ -353,6 +362,7 @@ defineExpose({
tableRef: tableRef,
search: getTableData,
getData,
total,
});
</script>
<style scoped lang="scss">

View File

@@ -109,7 +109,7 @@ const state = reactive({
mouse: null as any,
touchpad: null as any,
errorMessage: '',
arguments: {},
arguments: {} as any,
status: TerminalStatus.NoConnected,
size: {
height: 710,

View File

@@ -42,7 +42,7 @@ const useCustomFetch = createFetch({
export function useApiFetch<T>(api: Api, params: any = null, reqOptions: RequestInit = {}) {
const uaf = useCustomFetch<T>(api.url, {
beforeFetch({ url, options }) {
async beforeFetch({ url, options }) {
options.method = api.method;
if (!params) {
return;
@@ -57,7 +57,7 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
}
if (api.beforeHandler) {
paramsValue = api.beforeHandler(paramsValue);
paramsValue = await api.beforeHandler(paramsValue);
}
if (paramsValue) {

View File

@@ -1 +1 @@
@import 'common/transition.scss';
@use 'common/transition.scss';

View File

@@ -1,4 +1,4 @@
@import 'mixins/index.scss';
@use 'mixins/index' as mixins;
/* Button 按钮
------------------------------- */
@@ -97,7 +97,7 @@
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
@include generalIcon;
@include mixins.generalIcon;
}
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色

View File

@@ -1,8 +1,8 @@
@import './app.scss';
@import './base.scss';
@import './other.scss';
@import './element.scss';
@import './media/media.scss';
@import './waves.scss';
@import './dark.scss';
@import './iconSelector.scss';
@use './app.scss';
@use './base.scss';
@use './other.scss';
@use './element.scss';
@use './media/media.scss';
@use './waves.scss';
@use './dark.scss';
@use './iconSelector.scss';

View File

@@ -1,94 +1,109 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
.big-data-down-left {
width: 100% !important;
flex-direction: unset !important;
flex-wrap: wrap;
.flex-warp-item {
min-height: 196.24px;
padding: 0 7.5px 15px 15px !important;
.flex-warp-item-box {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
}
}
.big-data-down-center {
width: 100% !important;
.big-data-down-center-one,
.big-data-down-center-two {
min-height: 196.24px;
padding-left: 15px !important;
.big-data-down-center-one-content {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
.flex-warp-item-box {
@extend .big-data-down-center-one-content;
}
}
}
.big-data-down-right {
.flex-warp-item {
.flex-warp-item-box {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
&:nth-of-type(2) {
padding-left: 15px !important;
}
&:last-of-type {
.flex-warp-item-box {
border: none !important;
}
}
}
}
@media screen and (max-width: index.$sm) {
.big-data-down-left {
width: 100% !important;
flex-direction: unset !important;
flex-wrap: wrap;
.flex-warp-item {
min-height: 196.24px;
padding: 0 7.5px 15px 15px !important;
.flex-warp-item-box {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
}
}
.big-data-down-center {
width: 100% !important;
.big-data-down-center-one,
.big-data-down-center-two {
min-height: 196.24px;
padding-left: 15px !important;
.big-data-down-center-one-content {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
.flex-warp-item-box {
@extend .big-data-down-center-one-content;
}
}
}
.big-data-down-right {
.flex-warp-item {
.flex-warp-item-box {
border: none !important;
border-bottom: 1px solid #ebeef5 !important;
}
&:nth-of-type(2) {
padding-left: 15px !important;
}
&:last-of-type {
.flex-warp-item-box {
border: none !important;
}
}
}
}
}
/* 页面宽度大于768px小于1200px
------------------------------- */
@media screen and (min-width: $sm) and (max-width: $lg) {
.chart-warp-bottom {
.big-data-down-left {
width: 50% !important;
}
.big-data-down-center {
width: 50% !important;
}
.big-data-down-right {
.flex-warp-item {
width: 50% !important;
&:nth-of-type(2) {
padding-left: 7.5px !important;
}
}
}
}
@media screen and (min-width: index.$sm) and (max-width: index.$lg) {
.chart-warp-bottom {
.big-data-down-left {
width: 50% !important;
}
.big-data-down-center {
width: 50% !important;
}
.big-data-down-right {
.flex-warp-item {
width: 50% !important;
&:nth-of-type(2) {
padding-left: 7.5px !important;
}
}
}
}
}
/* 页面宽度小于1200px
------------------------------- */
@media screen and (max-width: $lg) {
.chart-warp-top {
.up-left {
display: none;
}
}
.chart-warp-bottom {
overflow-y: auto !important;
flex-wrap: wrap;
.big-data-down-right {
width: 100% !important;
flex-direction: unset !important;
flex-wrap: wrap;
.flex-warp-item {
min-height: 196.24px;
padding: 0 7.5px 15px 15px !important;
}
}
}
}
@media screen and (max-width: index.$lg) {
.chart-warp-top {
.up-left {
display: none;
}
}
.chart-warp-bottom {
overflow-y: auto !important;
flex-wrap: wrap;
.big-data-down-right {
width: 100% !important;
flex-direction: unset !important;
flex-wrap: wrap;
.flex-warp-item {
min-height: 196.24px;
padding: 0 7.5px 15px 15px !important;
}
}
}
}

View File

@@ -1,10 +1,10 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
.el-cascader__dropdown.el-popper {
overflow: auto;
max-width: 100%;
}
}
@media screen and (max-width: index.$xs) {
.el-cascader__dropdown.el-popper {
overflow: auto;
max-width: 100%;
}
}

View File

@@ -1,12 +1,13 @@
@import './index.scss';
@use './index.scss';
/* 页面宽度小于800px
------------------------------- */
@media screen and (max-width: 800px) {
.el-dialog {
width: 90% !important;
}
.el-dialog.is-fullscreen {
width: 100% !important;
}
}
.el-dialog {
width: 90% !important;
}
.el-dialog.is-fullscreen {
width: 100% !important;
}
}

View File

@@ -1,35 +1,38 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
.error {
.error-flex {
flex-direction: column-reverse !important;
height: auto !important;
width: 100% !important;
}
.right,
.left {
flex: unset !important;
display: flex !important;
}
.left-item {
margin: auto !important;
}
.right img {
max-width: 450px !important;
@extend .left-item;
}
}
@media screen and (max-width: index.$sm) {
.error {
.error-flex {
flex-direction: column-reverse !important;
height: auto !important;
width: 100% !important;
}
.right,
.left {
flex: unset !important;
display: flex !important;
}
.left-item {
margin: auto !important;
}
.right img {
max-width: 450px !important;
@extend .left-item;
}
}
}
/* 页面宽度大于768px小于992px
------------------------------- */
@media screen and (min-width: $sm) and (max-width: $md) {
.error {
.error-flex {
padding-left: 30px !important;
}
}
}
@media screen and (min-width: index.$sm) and (max-width: index.$md) {
.error {
.error-flex {
padding-left: 30px !important;
}
}
}

View File

@@ -1,13 +1,14 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
.el-form-item__label {
width: 100% !important;
text-align: left !important;
}
.el-form-item__content {
margin-left: 0 !important;
}
}
@media screen and (max-width: index.$xs) {
.el-form-item__label {
width: 100% !important;
text-align: left !important;
}
.el-form-item__content {
margin-left: 0 !important;
}
}

View File

@@ -1,10 +1,11 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
.home-warning-media,
.home-dynamic-media {
margin-top: 15px;
}
}
@media screen and (max-width: index.$sm) {
.home-warning-media,
.home-dynamic-media {
margin-top: 15px;
}
}

View File

@@ -1,55 +1,61 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
// MessageBox 弹框
.el-message-box {
width: 80% !important;
}
@media screen and (max-width: index.$xs) {
// MessageBox 弹框
.el-message-box {
width: 80% !important;
}
}
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
// Breadcrumb 面包屑
.layout-navbars-breadcrumb-hide {
display: none;
}
// 外链视图
.layout-view-link {
a {
max-width: 80%;
text-align: center;
}
}
// 菜单搜索
.layout-search-dialog {
.el-autocomplete {
width: 80% !important;
}
}
@media screen and (max-width: index.$sm) {
// Breadcrumb 面包屑
.layout-navbars-breadcrumb-hide {
display: none;
}
// 外链视图
.layout-view-link {
a {
max-width: 80%;
text-align: center;
}
}
// 菜单搜索
.layout-search-dialog {
.el-autocomplete {
width: 80% !important;
}
}
}
/* 页面宽度小于1000px
------------------------------- */
@media screen and (max-width: 1000px) {
// 布局配置
.layout-drawer-content-flex {
position: relative;
&::after {
content: '手机版不支持切换布局';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
text-align: center;
height: 140px;
line-height: 140px;
background: rgba(255, 255, 255, 0.9);
color: #666666;
}
}
}
// 布局配置
.layout-drawer-content-flex {
position: relative;
&::after {
content: '手机版不支持切换布局';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
text-align: center;
height: 140px;
line-height: 140px;
background: rgba(255, 255, 255, 0.9);
color: #666666;
}
}
}

View File

@@ -1,21 +1,23 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
.login-container {
.login-content {
width: 90% !important;
padding: 20px 0 !important;
}
.login-content-form-btn {
width: 100% !important;
padding: 12px 0 !important;
}
.login-copyright {
.login-copyright-msg {
white-space: unset !important;
}
}
}
}
@media screen and (max-width: index.$xs) {
.login-container {
.login-content {
width: 90% !important;
padding: 20px 0 !important;
}
.login-content-form-btn {
width: 100% !important;
padding: 12px 0 !important;
}
.login-copyright {
.login-copyright-msg {
white-space: unset !important;
}
}
}
}

View File

@@ -1,12 +1,12 @@
@import './login.scss';
@import './error.scss';
@import './layout.scss';
@import './personal.scss';
@import './tagsView.scss';
@import './home.scss';
@import './chart.scss';
@import './form.scss';
@import './scrollbar.scss';
@import './pagination.scss';
@import './dialog.scss';
@import './cityLinkage.scss';
@use './login.scss';
@use './error.scss';
@use './layout.scss';
@use './personal.scss';
@use './tagsView.scss';
@use './home.scss';
@use './chart.scss';
@use './form.scss';
@use './scrollbar.scss';
@use './pagination.scss';
@use './dialog.scss';
@use './cityLinkage.scss';

View File

@@ -1,15 +1,16 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于576px
------------------------------- */
@media screen and (max-width: $xs) {
.el-pager,
.el-pagination__jump {
display: none !important;
}
@media screen and (max-width: index.$xs) {
.el-pager,
.el-pagination__jump {
display: none !important;
}
}
// 默认居中对齐
.el-pagination {
text-align: center !important;
}
text-align: center !important;
}

View File

@@ -1,16 +1,18 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
.personal-info {
padding-left: 0 !important;
margin-top: 15px;
}
.personal-recommend-col {
margin-bottom: 15px;
&:last-of-type {
margin-bottom: 0;
}
}
}
@media screen and (max-width: index.$sm) {
.personal-info {
padding-left: 0 !important;
margin-top: 15px;
}
.personal-recommend-col {
margin-bottom: 15px;
&:last-of-type {
margin-bottom: 0;
}
}
}

View File

@@ -1,56 +1,66 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
// 滚动条的宽度
::-webkit-scrollbar {
width: 3px !important;
height: 3px !important;
}
::-webkit-scrollbar-track-piece {
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
background-clip: padding-box;
min-height: 28px;
border-radius: 5px;
transition: 0.3s background-color;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
// element plus scrollbar
.el-scrollbar__bar.is-vertical {
width: 2px !important;
}
.el-scrollbar__bar.is-horizontal {
height: 2px !important;
}
@media screen and (max-width: index.$sm) {
// 滚动条的宽度
::-webkit-scrollbar {
width: 3px !important;
height: 3px !important;
}
::-webkit-scrollbar-track-piece {
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
background-clip: padding-box;
min-height: 28px;
border-radius: 5px;
transition: 0.3s background-color;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
// element plus scrollbar
.el-scrollbar__bar.is-vertical {
width: 2px !important;
}
.el-scrollbar__bar.is-horizontal {
height: 2px !important;
}
}
/* 页面宽度大于768px
------------------------------- */
@media screen and (min-width: 769px) {
// 滚动条的宽度
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track-piece {
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
background-clip: padding-box;
min-height: 28px;
border-radius: 5px;
transition: 0.3s background-color;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
// 滚动条的宽度
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track-piece {
background-color: var(--bg-main-color);
}
// 滚动条的设置
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
background-clip: padding-box;
min-height: 28px;
border-radius: 5px;
transition: 0.3s background-color;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
}

View File

@@ -1,11 +1,11 @@
@import './index.scss';
@use './index.scss' as index;
/* 页面宽度小于768px
------------------------------- */
@media screen and (max-width: $sm) {
.tags-view-form {
.tags-view-form-col {
margin-bottom: 20px;
}
}
}
@media screen and (max-width: index.$sm) {
.tags-view-form {
.tags-view-form-col {
margin-bottom: 20px;
}
}
}

View File

@@ -1,32 +1,32 @@
/* 第三方图标字体间距/大小设置
------------------------------- */
@mixin generalIcon {
font-size: 14px !important;
display: inline-block;
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
justify-content: center;
font-size: 14px !important;
display: inline-block;
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
justify-content: center;
}
/* 文本不换行
------------------------------- */
@mixin text-no-wrap() {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
/* 多行文本溢出
------------------------------- */
@mixin text-ellipsis($line: 2) {
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
}
/* 滚动条(页面未使用) div 中使用:
@@ -35,22 +35,26 @@
// @include scrollBar;
// }
@mixin scrollBar {
// 滚动条凹槽的颜色,还可以设置边框属性
&::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
}
// 滚动条的宽度
&::-webkit-scrollbar {
width: 9px;
height: 9px;
}
// 滚动条的设置
&::-webkit-scrollbar-thumb {
background-color: #dddddd;
background-clip: padding-box;
min-height: 28px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}
// 滚动条凹槽的颜色,还可以设置边框属性
&::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
}
// 滚动条的宽度
&::-webkit-scrollbar {
width: 9px;
height: 9px;
}
// 滚动条的设置
&::-webkit-scrollbar-thumb {
background-color: #dddddd;
background-clip: padding-box;
min-height: 28px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}
}

View File

@@ -1,28 +1,31 @@
/* wangeditor富文本编辑器
------------------------------- */
.w-e-toolbar {
border: 1px solid #ebeef5 !important;
border-bottom: 1px solid #ebeef5 !important;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
z-index: 2 !important;
border: 1px solid #ebeef5 !important;
border-bottom: 1px solid #ebeef5 !important;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
z-index: 2 !important;
}
.w-e-text-container {
border: 1px solid #ebeef5 !important;
border-top: none !important;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
z-index: 1 !important;
border: 1px solid #ebeef5 !important;
border-top: none !important;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
z-index: 1 !important;
}
/* web端自定义截屏
------------------------------- */
#screenShotContainer {
z-index: 9998 !important;
z-index: 9998 !important;
}
#toolPanel {
height: 42px !important;
height: 42px !important;
}
#optionPanel {
height: 37px !important;
}
height: 37px !important;
}

View File

@@ -0,0 +1,149 @@
<template>
<div>
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
<el-form-item prop="bizType" label="业务类型">
<EnumSelect v-model="form.bizType" :enums="FlowBizType" placeholder="请选择业务类型" />
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" type="textarea" placeholder="备注" auto-complete="off" clearable></el-input>
</el-form-item>
<el-divider content-position="left">业务信息</el-divider>
<component
ref="bizFormRef"
v-if="form.bizType"
:is="bizComponents[form.bizType]"
v-model:bizForm="form.bizForm"
@changeResourceCode="changeResourceCode"
>
</component>
</el-form>
<span v-if="flowProcdef || !state.form.procdefId">
<el-divider content-position="left">审批节点</el-divider>
<ProcdefTasks v-if="flowProcdef" :procdef="flowProcdef" />
<el-result v-if="!state.form.procdefId" icon="error" title="不存在审批节点" sub-title="该资源无需审批操作"> </el-result>
</span>
<template #footer>
<div>
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!state.form.procdefId"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef } from 'vue';
import { procdefApi, procinstApi } from './api';
import { ElMessage } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { FlowBizType } from './enums';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import ProcdefTasks from './components/ProcdefTasks.vue';
import RedisRunCmdFlowBizForm from './flowbiz/redis/RedisRunCmdFlowBizForm.vue';
const DbSqlExecFlowBizForm = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecFlowBizForm.vue'));
const props = defineProps({
title: {
type: String,
},
});
const visible = defineModel<boolean>('visible', { default: false });
//定义事件
const emit = defineEmits(['cancel', 'val-change']);
const formRef: any = useTemplateRef('formRef');
const bizFormRef: any = useTemplateRef('bizFormRef');
// 业务组件
const bizComponents: any = shallowReactive({
db_sql_exec_flow: DbSqlExecFlowBizForm,
redis_run_cmd_flow: RedisRunCmdFlowBizForm,
});
const rules = {
bizType: [
{
required: true,
message: '请选择流程业务类型',
trigger: ['change', 'blur'],
},
],
remark: [
{
required: true,
message: '请输入申请备注',
trigger: ['change', 'blur'],
},
],
};
const defaultForm = {
bizType: FlowBizType.DbSqlExec.value,
procdefId: -1,
status: null,
remark: '',
bizForm: {},
};
const state = reactive({
tasks: [] as any,
form: { ...defaultForm },
flowProcdef: null as any,
sortable: '' as any,
});
const { form, flowProcdef } = toRefs(state);
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(form);
const changeResourceCode = async (resourceType: any, code: string) => {
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
if (!state.flowProcdef) {
state.form.procdefId = 0;
} else {
state.form.procdefId = state.flowProcdef.id;
}
};
const btnOk = async () => {
try {
await formRef.value.validate();
await bizFormRef.value.validateBizForm();
} catch (e: any) {
ElMessage.error('请正确填写信息');
return false;
}
await procinstStart();
ElMessage.success('流程发起成功');
emit('val-change', state.form);
//重置表单域
cancel();
};
const cancel = () => {
visible.value = false;
emit('cancel');
state.flowProcdef = null;
formRef.value.resetFields();
bizFormRef.value.resetBizForm();
state.form = { ...defaultForm };
};
</script>
<style lang="scss"></style>

View File

@@ -17,6 +17,25 @@
<el-option v-for="item in ProcdefStatus" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="condition" label="触发条件">
<template #label>
触发条件
<el-tooltip content="go template语法。若输出结果为1则表示触发该审批流程" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
<el-input
v-model="form.condition"
:rows="10"
type="textarea"
placeholder="触发条件, 返回值=1, 则表示触发该审批流程"
auto-complete="off"
clearable
></el-input>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
</el-form-item>
@@ -118,6 +137,7 @@ const state = reactive({
name: null,
defKey: null,
status: null,
condition: '',
remark: null,
// 流程的审批节点任务
tasks: '',
@@ -141,6 +161,23 @@ watch(props, (newValue: any) => {
state.tasks = tasks;
} else {
state.form = { status: ProcdefStatus.Enable.value } as any;
state.form.condition = `{{/* DBMS-执行sql规则; param参数描述如下 */}}
{{/* stmtType: select / read / insert / update / delete / ddl ; */}}
{{ if eq .bizType "db_sql_exec_flow"}}
{{/* 不是select和read语句时开启流程审批 */}}
{{ if and (ne .param.stmtType "select") (ne .param.stmtType "read") }}
1
{{ end }}
{{ end }}
{{/* Redis-执行命令规则; param参数描述如下 */}}
{{/* cmdType: read(读命令) / write(写命令); */}}
{{/* cmd: get/set/hset...等 */}}
{{ if eq .bizType "redis_run_cmd_flow"}}
{{ if eq .param.cmdType "write" }}
1
{{ end }}
{{ end }}`;
state.tasks = [];
}
});

View File

@@ -66,7 +66,7 @@ const columns = [
];
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.save, perms.del]);
const actionBtns: any = hasPerms([perms.save, perms.del]);
const actionColumn = TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
const pageTableRef: Ref<any> = ref(null);

View File

@@ -1,28 +1,20 @@
<template>
<div>
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="40%" :close-on-click-modal="!props.instTaskId">
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="50%" :close-on-click-modal="!props.instTaskId">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<div>
<el-divider content-position="left">流程信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions :column="3" border>
<el-descriptions-item label="流程名">{{ procinst.procdefName }}</el-descriptions-item>
<el-descriptions-item label="业务">
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
</el-descriptions-item>
<el-descriptions-item label="发起人">
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
<!-- {{ procinst.creator }} -->
</el-descriptions-item>
<el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
<div v-if="procinst.duration">
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
</div>
<el-descriptions-item label="流程状态">
<enum-tag :enums="ProcinstStatus" :value="procinst.status"></enum-tag>
@@ -31,6 +23,13 @@
<enum-tag :enums="ProcinstBizStatus" :value="procinst.bizStatus"></enum-tag>
</el-descriptions-item>
<el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
<div v-if="procinst.duration">
<el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
</div>
<el-descriptions-item label="备注">
{{ procinst.remark }}
</el-descriptions-item>
@@ -44,14 +43,7 @@
<div>
<el-divider content-position="left">业务信息</el-divider>
<component
v-if="procinst.bizType"
ref="keyValueRef"
:is="bizComponents[procinst.bizType]"
:biz-key="procinst.bizKey"
:biz-form="procinst.bizForm"
>
</component>
<component v-if="procinst.bizType" ref="keyValueRef" :is="bizComponents[procinst.bizType]" :procinst="procinst"> </component>
</div>
<div v-if="props.instTaskId">
@@ -92,8 +84,8 @@ import EnumTag from '@/components/enumtag/EnumTag.vue';
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
import { formatDate } from '@/common/utils/format';
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue'));
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecBiz.vue'));
const RedisRunCmdBiz = defineAsyncComponent(() => import('./flowbiz/redis/RedisRunCmdBiz.vue'));
const props = defineProps({
procinstId: {
@@ -114,9 +106,9 @@ const visible = defineModel<boolean>('visible', { default: false });
const emit = defineEmits(['cancel', 'val-change']);
// 业务组件
const bizComponents = shallowReactive({
const bizComponents: any = shallowReactive({
db_sql_exec_flow: DbSqlExecBiz,
redis_run_write_cmd_flow: RedisRunWriteCmdBiz,
redis_run_cmd_flow: RedisRunCmdBiz,
});
const state = reactive({

View File

@@ -9,7 +9,7 @@
:columns="columns"
>
<template #tableHeader>
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
<el-button type="primary" icon="plus" @click="startProcInst()">发起流程</el-button>
</template>
<template #action="{ data }">
@@ -36,6 +36,8 @@
@val-change="valChange()"
@cancel="procinstDetail.procinstId = 0"
/>
<ProcInstEdit v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
</div>
</template>
@@ -49,6 +51,7 @@ import ProcinstDetail from './ProcinstDetail.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
import { ElMessage } from 'element-plus';
import { formatTime } from '@/common/utils/format';
import ProcInstEdit from './ProcInstEdit.vue';
const searchItems = [
SearchItem.select('status', '流程状态').withEnum(ProcinstStatus),
@@ -73,7 +76,7 @@ const columns = [
}
return formatTime(duration);
}),
TableColumn.new('bizHandleRes', '业务处理结果'),
// TableColumn.new('bizHandleRes', '业务处理结果'),
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
];
@@ -98,9 +101,13 @@ const state = reactive({
procinstId: 0,
instTaskId: 0,
},
procinstEdit: {
title: '发起流程',
visible: false,
},
});
const { selectionData, query, procinstDetail } = toRefs(state);
const { selectionData, query, procinstDetail, procinstEdit } = toRefs(state);
const search = async () => {
pageTableRef.value.search();
@@ -118,6 +125,10 @@ const showProcinst = (data: any) => {
state.procinstDetail.visible = true;
};
const startProcInst = () => {
state.procinstEdit.visible = true;
};
const valChange = () => {
state.procinstDetail.visible = false;
search();

View File

@@ -9,6 +9,7 @@ export const procdefApi = {
export const procinstApi = {
list: Api.newGet('/flow/procinsts'),
start: Api.newPost('/flow/procinsts/start'),
detail: Api.newGet('/flow/procinsts/{id}'),
cancel: Api.newPost('/flow/procinsts/{id}/cancel'),
tasks: Api.newGet('/flow/procinsts/tasks'),

View File

@@ -29,6 +29,6 @@ export const ProcinstTaskStatus = {
};
export const FlowBizType = {
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL'),
RedisRunWriteCmd: EnumValue.of('redis_run_write_cmd_flow', 'Redis-执行write命令'),
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL').setTagType('warning'),
RedisRunWriteCmd: EnumValue.of('redis_run_cmd_flow', 'Redis-执行命令').setTagType('danger'),
};

View File

@@ -1,79 +0,0 @@
<template>
<div>
<el-descriptions :column="3" border>
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
</el-descriptions-item>
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
<el-descriptions-item label="表">
{{ sqlExec.table }}
</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag size="small">{{ EnumValue.getLabelByValue(DbSqlExecTypeEnum, sqlExec.type) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="执行SQL">
<monaco-editor height="300px" language="sql" v-model="sqlExec.sql" :options="{ readOnly: true }" />
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, onMounted } from 'vue';
import EnumValue from '@/common/Enum';
import { dbApi } from '@/views/ops/db/api';
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { getDbDialect } from '@/views/ops/db/dialect';
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
const props = defineProps({
// 业务key
bizKey: {
type: [String],
default: '',
},
});
const state = reactive({
sqlExec: {
sql: '',
} as any,
db: {} as any,
});
const { sqlExec, db } = toRefs(state);
onMounted(() => {
getDbSqlExec(props.bizKey);
});
watch(
() => props.bizKey,
(newValue: any) => {
getDbSqlExec(newValue);
}
);
const getDbSqlExec = async (bizKey: string) => {
if (!bizKey) {
return;
}
const res = await dbApi.getSqlExecs.request({ flowBizKey: bizKey });
if (!res.list) {
return;
}
state.sqlExec = res.list?.[0];
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
state.db = dbRes.list?.[0];
};
</script>
<style lang="scss"></style>

View File

@@ -1,80 +0,0 @@
<template>
<div>
<el-descriptions :column="3" border>
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
<el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
<el-descriptions-item :span="1" label="mode">
{{ redis.mode }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="执行Cmd">
<el-input type="textarea" disabled v-model="cmd" rows="5" />
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, onMounted } from 'vue';
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
import { redisApi } from '@/views/ops/redis/api';
const props = defineProps({
// 业务表单
bizForm: {
type: [String],
default: '',
},
});
const state = reactive({
cmd: '',
db: 0,
redis: {} as any,
});
const { cmd, redis } = toRefs(state);
onMounted(() => {
parseRunCmdForm(props.bizForm);
});
watch(
() => props.bizForm,
(newValue: any) => {
parseRunCmdForm(newValue);
}
);
const parseRunCmdForm = async (bizForm: string) => {
if (!bizForm) {
return;
}
const form = JSON.parse(bizForm);
const cmds = form.cmd.map((item: any, index: number) => {
if (index === 0) {
return item; // 第一个元素直接返回原值
}
if (typeof item === 'string') {
return `'${item}'`; // 字符串加单引号
}
return item; // 其他类型直接返回
});
state.cmd = cmds.join(' ');
state.db = form.db;
const res = await redisApi.redisList.request({ id: form.id });
if (!res.list) {
return;
}
state.redis = res.list?.[0];
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,103 @@
<template>
<div>
<el-descriptions :column="3" border>
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="db.codePaths" /></el-descriptions-item>
<el-descriptions-item :span="1" label="名称">{{ db?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="主机">
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />
{{ `${db?.host}:${db?.port}` }}
</el-descriptions-item>
<el-descriptions-item :span="1" label="数据库">{{ dbName }}</el-descriptions-item>
<el-descriptions-item label="执行SQL">
<monaco-editor height="300px" language="sql" v-model="sql" :options="{ readOnly: true }" />
</el-descriptions-item>
</el-descriptions>
<div v-if="runRes && runRes.length > 0">
<el-divider content-position="left">处理结果</el-divider>
<el-table :data="runRes" :max-height="400">
<el-table-column prop="sql" label="SQL" show-overflow-tooltip />
<el-table-column prop="res" label="执行结果" :min-width="30" show-overflow-tooltip>
<template #default="scope">
<el-popover placement="top" :width="400" trigger="hover">
<template #reference>
<el-link icon="view" :type="scope.row.errorMsg ? 'danger' : 'success'" :underline="false"> </el-link>
</template>
<el-text v-if="scope.row.errorMsg">{{ scope.row.errorMsg }}</el-text>
<el-table v-else :data="scope.row.res" size="small">
<el-table-column v-for="col in scope.row.columns" :key="col.name" :label="col.name" :prop="col.name" />
</el-table>
</el-popover>
</template>
</el-table-column>
<!-- <el-table-column prop="errorMsg" label="错误信息" :min-width="60" show-overflow-tooltip /> -->
</el-table>
</div>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, onMounted } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { getDbDialect } from '@/views/ops/db/dialect';
import { tagApi } from '@/views/ops/tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
const props = defineProps({
procinst: {
type: [Object],
default: () => {},
},
});
const state = reactive({
// sqlExec: {
// sql: '',
// } as any,
db: {} as any,
dbName: '',
sql: '',
runRes: [],
});
const { db, dbName, sql, runRes } = toRefs(state);
onMounted(() => {
parseBizForm(props.procinst.bizForm);
});
watch(
() => props.procinst.bizForm,
(newValue: any) => {
parseBizForm(newValue);
}
);
const parseBizForm = async (bizFormStr: string) => {
if (props.procinst.bizHandleRes) {
state.runRes = JSON.parse(props.procinst.bizHandleRes);
} else {
state.runRes = [];
}
const bizForm = JSON.parse(bizFormStr);
state.sql = bizForm.sql;
state.dbName = bizForm.dbName;
const dbRes = await dbApi.dbs.request({ id: bizForm.dbId });
state.db = dbRes.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
state.db.codePaths = res.map((item: any) => item.codePath);
});
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,83 @@
<template>
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
<el-form-item prop="dbId" label="数据库" required>
<db-select-tree
placeholder="请选择数据库"
v-model:db-id="bizForm.dbId"
v-model:db-name="bizForm.dbName"
v-model:db-type="dbType"
@select-db="changeResourceCode"
/>
</el-form-item>
<el-form-item prop="sql" label="SQL" required>
<div class="w100">
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const rules = {
dbId: [
{
required: true,
message: '请选择数据库',
trigger: ['change', 'blur'],
},
],
sql: [
{
required: true,
message: '请输入执行SQL',
trigger: ['change', 'blur'],
},
],
};
const emit = defineEmits(['changeResourceCode']);
const formRef: any = ref(null);
const bizForm = defineModel<any>('bizForm', {
default: {
dbId: 0,
dbName: '',
sql: '',
},
});
const dbType = ref('');
watch(
() => bizForm.value.dbId,
() => {
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], dbType.value);
}
);
const changeResourceCode = async (db: any) => {
emit('changeResourceCode', TagResourceTypeEnum.Db.value, db.code);
};
const validateBizForm = async () => {
return formRef.value.validate();
};
const resetBizForm = () => {
//重置表单域
formRef.value.resetFields();
bizForm.value.dbId = 0;
bizForm.value.dbName = '';
};
defineExpose({ validateBizForm, resetBizForm });
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,89 @@
<template>
<div>
<el-descriptions :column="3" border>
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="redis.codePaths" /></el-descriptions-item>
<el-descriptions-item :span="2" label="编号">{{ redis?.code }}</el-descriptions-item>
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
<el-descriptions-item :span="1" label="mode">
{{ redis.mode }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="执行Cmd">
<el-input type="textarea" disabled v-model="cmd" rows="5" />
</el-descriptions-item>
</el-descriptions>
<div v-if="runRes && runRes.length > 0">
<el-divider content-position="left">处理结果</el-divider>
<el-table :data="runRes" :max-height="400">
<el-table-column prop="cmd" label="命令" show-overflow-tooltip />
<el-table-column prop="res" label="执行结果" :min-width="50" show-overflow-tooltip> </el-table-column>
</el-table>
</div>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, onMounted } from 'vue';
import { redisApi } from '@/views/ops/redis/api';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
import { tagApi } from '@/views/ops/tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
procinst: {
type: [Object],
default: () => {},
},
});
const state = reactive({
cmd: '',
runRes: [],
db: 0,
redis: {} as any,
});
const { cmd, redis, runRes } = toRefs(state);
onMounted(() => {
parseRunCmdForm(props.procinst.bizForm);
});
watch(
() => props.procinst.bizForm,
(newValue: any) => {
parseRunCmdForm(newValue);
}
);
const parseRunCmdForm = async (bizFormStr: string) => {
if (props.procinst.bizHandleRes) {
state.runRes = JSON.parse(props.procinst.bizHandleRes);
} else {
state.runRes = [];
}
if (!bizFormStr) {
return;
}
const bizForm = JSON.parse(bizFormStr);
state.cmd = bizForm.cmd;
state.db = bizForm.db;
const res = await redisApi.redisList.request({ id: bizForm.id });
if (!res.list) {
return;
}
state.redis = res.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Redis.value, codes: state.redis.code }).then((res) => {
state.redis.codePaths = res.map((item: any) => item.codePath);
});
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,148 @@
<template>
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
<el-form-item prop="id" label="库" required>
<TagTreeResourceSelect
v-bind="$attrs"
v-model="selectRedis"
@change="changeRedis"
:resource-type="TagResourceTypeEnum.Redis.value"
:tag-path-node-type="NodeTypeTagPath"
placeholder="请选择Redis实例与库"
>
</TagTreeResourceSelect>
</el-form-item>
<el-form-item prop="cmd" label="CMD" required>
<el-input type="textarea" v-model="bizForm.cmd" placeholder="如: SET 'key' 'value'; 多条命令;分割" :rows="5" />
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
import { redisApi } from '@/views/ops/redis/api';
import { sleep } from '@/common/utils/loading';
const rules = {
id: [
{
required: true,
message: '请选择Redis实例',
trigger: ['change', 'blur'],
},
],
cmd: [
{
required: true,
message: '请输入执行CMD',
trigger: ['change', 'blur'],
},
],
};
// tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const res = await redisApi.redisList.request({ tagPath: parentNode.key });
if (!res.total) {
return [];
}
const redisInfos = res.list;
await sleep(100);
return redisInfos.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
});
});
// redis实例节点类型
const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const redisInfo = parentNode.params;
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
id: redisInfo.id,
db: x,
name: `db${x}`,
keys: 0,
tagPath: redisInfo.tagPath,
redisName: redisInfo.name,
code: redisInfo.code,
});
});
if (redisInfo.mode == 'cluster') {
return dbs;
}
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: 'Keyspace' });
for (let db in res.Keyspace) {
for (let d of dbs) {
if (db == d.params.name) {
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0;
}
}
}
// 替换label
dbs.forEach((e: any) => {
e.label = `${e.params.name}`;
});
return dbs;
});
const emit = defineEmits(['changeResourceCode']);
const formRef: any = ref(null);
const bizForm = defineModel<any>('bizForm', {
default: {
id: 0,
db: 0,
cmd: '',
},
});
const redisName = ref('');
const tagPath = ref('');
const selectRedis = computed({
get: () => {
return redisName.value ? `${tagPath.value} > ${redisName.value} > db${bizForm.value.db}` : '';
},
set: () => {
//
},
});
const changeRedis = (nodeData: TagTreeNode) => {
const params = nodeData.params;
tagPath.value = params.tagPath;
redisName.value = params.redisName;
bizForm.value.id = params.id;
bizForm.value.db = parseInt(params.db);
changeResourceCode(params.code);
};
const changeResourceCode = async (redisCode: any) => {
emit('changeResourceCode', TagResourceTypeEnum.Redis.value, redisCode);
};
const validateBizForm = async () => {
return formRef.value.validate();
};
const resetBizForm = () => {
//重置表单域
formRef.value.resetFields();
bizForm.value.id = 0;
bizForm.value.db = 0;
bizForm.value.cmd = '';
};
defineExpose({ validateBizForm, resetBizForm });
</script>
<style lang="scss"></style>

View File

@@ -6,7 +6,15 @@
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
<el-upload
class="h100 personal-user-left-upload"
:action="getUploadFileUrl(`avatar_${userInfo.username}`)"
:limit="1"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
:on-success="handleAvatarSuccess"
accept=".png,.jpg,.jpeg"
>
<img :src="userInfo.photo" />
</el-upload>
</div>
@@ -89,7 +97,7 @@
</el-table-column>
<el-table-column prop="codePath" min-width="400" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.machine.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -123,7 +131,7 @@
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.db.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -164,7 +172,7 @@
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.redis.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -203,7 +211,7 @@
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.mongo.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -249,20 +257,23 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted, computed } from 'vue';
import { computed, onMounted, reactive, toRefs } from 'vue';
// import * as echarts from 'echarts';
import { formatAxis } from '@/common/utils/format';
import { formatAxis, formatDate } from '@/common/utils/format';
import { indexApi } from './api';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import { personApi } from '../personal/api';
import { formatDate } from '@/common/utils/format';
import SvgIcon from '@/components/svgIcon/index.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { resourceOpLogApi } from '../ops/tag/api';
import TagCodePath from '../ops/component/TagCodePath.vue';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { getAllTagInfoByCodePaths } from '../ops/component/tag';
import { ElMessage } from 'element-plus';
import { getFileUrl, getUploadFileUrl } from '@/common/request';
import { saveUser } from '@/common/utils/storage';
const router = useRouter();
const { userInfo } = storeToRefs(useUserInfo());
@@ -288,18 +299,22 @@ const state = reactive({
machine: {
num: 0,
opLogs: [],
tagInfos: {},
},
db: {
num: 0,
opLogs: [],
tagInfos: {},
},
redis: {
num: 0,
opLogs: [],
tagInfos: {},
},
mongo: {
num: 0,
opLogs: [],
tagInfos: {},
},
});
@@ -354,25 +369,56 @@ const getMsgs = async () => {
return await personApi.getMsgs.request(state.msgDialog.query);
};
const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.size >= 512 * 1024) {
ElMessage.error('头像不能超过512KB!');
return false;
}
return true;
};
const handleAvatarSuccess = (response: any, uploadFile: any) => {
userInfo.value.photo = URL.createObjectURL(uploadFile.raw);
const newUser = { ...userInfo.value };
newUser.photo = getFileUrl(`avatar_${userInfo.value.username}`);
// 存储用户信息到浏览器缓存
saveUser(newUser);
};
// 初始化数字滚动
const initData = async () => {
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
.then((res: any) => {
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.machine.tagInfos = tagInfos;
state.machine.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.db.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.db.tagInfos = tagInfos;
state.db.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.redis.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.redis.tagInfos = tagInfos;
state.redis.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.mongo.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.mongo.tagInfos = tagInfos;
state.mongo.opLogs = res.list;
});
indexApi.machineDashbord.request().then((res: any) => {
state.machine.num = res.machineNum;
@@ -425,7 +471,7 @@ const toPage = (item: any, codePath = '') => {
</script>
<style scoped lang="scss">
@import '@/theme/mixins/index.scss';
@use '@/theme/mixins/index.scss' as mixins;
.personal {
.personal-user {
@@ -463,7 +509,7 @@ const toPage = (item: any, codePath = '') => {
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
}
.personal-item {
@@ -473,11 +519,11 @@ const toPage = (item: any, codePath = '') => {
.personal-item-label {
color: gray;
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
}
}
}
@@ -508,7 +554,7 @@ const toPage = (item: any, codePath = '') => {
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
@include mixins.text-ellipsis(1);
color: grey;
text-decoration: none;
}

View File

@@ -144,6 +144,7 @@ import { personApi } from '@/views/personal/api';
import { AccountUsernamePattern } from '@/common/pattern';
import { getToken } from '@/common/utils/storage';
import { useThemeConfig } from '@/store/themeConfig';
import { getFileUrl } from '@/common/request';
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -347,26 +348,34 @@ const updateUserInfo = async () => {
}
};
const loginResDeal = (loginRes: any) => {
const loginResDeal = async (loginRes: any) => {
state.loginRes = loginRes;
// 用户信息
const userInfos = {
name: loginRes.name,
username: loginRes.username,
// 头像
photo: letterAvatar(loginRes.username),
time: new Date().getTime(),
lastLoginTime: loginRes.lastLoginTime,
lastLoginIp: loginRes.lastLoginIp,
photo: '',
};
const avatarFileKey = `avatar_${loginRes.username}`;
const avatarFileDetail = await openApi.getFileDetail([avatarFileKey]);
// 说明存在头像文件
if (avatarFileDetail.length > 0) {
userInfos.photo = getFileUrl(avatarFileKey);
} else {
userInfos.photo = letterAvatar(loginRes.username);
}
// 存储用户信息到浏览器缓存
saveUser(userInfos);
// 1、请注意执行顺序(存储用户信息到vuex)
useUserInfo().setUserInfo(userInfos);
const token = loginRes.token;
// 如果不需要 otp校验则该token即为accessToken否则为otp校验token
// 如果不需要otp校验则该token即为accessToken否则为otp校验token
if (loginRes.otp == -1) {
signInSuccess(token, loginRes.refresh_token);
return;

View File

@@ -51,7 +51,7 @@
</el-form-item>
</template>
<el-form-item prop="name" label="名称" required>
<el-form-item v-if="form.type == AuthCertTypeEnum.Public.value" prop="name" label="名称" required>
<el-input :disabled="form.id" v-model="form.name" placeholder="请输入凭证名 (全局唯一)"></el-input>
</el-form-item>

View File

@@ -22,7 +22,7 @@
</template>
</el-table-column>
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100px"> </el-table-column>
<!-- <el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100px"> </el-table-column> -->
<el-table-column prop="username" label="用户名" min-width="120px" show-overflow-tooltip> </el-table-column>
<el-table-column prop="ciphertextType" label="密文类型" width="100px">
<template #default="scope">
@@ -117,17 +117,17 @@ const cancelEdit = () => {
const btnOk = async (authCert: any) => {
const isEdit = authCert.id;
if (!isEdit) {
const res = await resourceAuthCertApi.listByQuery.request({
name: authCert.name,
pageNum: 1,
pageSize: 100,
});
if (res.total) {
ElMessage.error('该授权凭证名称已存在');
return;
}
}
// if (!isEdit) {
// const res = await resourceAuthCertApi.listByQuery.request({
// name: authCert.name,
// pageNum: 1,
// pageSize: 100,
// });
// if (res.total) {
// ElMessage.error('该授权凭证名称已存在');
// return;
// }
// }
if (isEdit || state.idx >= 0) {
authCerts.value[state.idx] = authCert;
@@ -135,8 +135,8 @@ const btnOk = async (authCert: any) => {
return;
}
if (authCerts.value?.filter((x: any) => x.username == authCert.username || x.name == authCert.name).length > 0) {
ElMessage.error('该名称或用户名已存在于该账号列表中');
if (authCerts.value?.filter((x: any) => x.username == authCert.username).length > 0) {
ElMessage.error('该用户名已存在于该账号列表中');
return;
}

View File

@@ -1,13 +1,13 @@
<template>
<div v-if="paths">
<el-row v-for="(path, idx) in paths?.slice(0, 1)" :key="idx">
<span v-for="item in parseTagPath(path)" :key="item.code">
<div v-if="codePaths">
<el-row v-for="(path, idx) in codePaths?.slice(0, 1)" :key="idx">
<span v-for="item in path" :key="item.code">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
class="mr2"
/>
<span> {{ item.code }}</span>
<span> {{ item.name ? item.name : item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
@@ -24,7 +24,7 @@
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
class="mr2"
/>
<span> {{ item.code }}</span>
<span> {{ item.name ? item.name : item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
</el-row>
@@ -36,14 +36,21 @@
<script lang="ts" setup>
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import { computed } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { getAllTagInfoByCodePaths } from './tag';
const props = defineProps({
path: {
type: [String, Array<string>],
},
tagInfos: {
type: Object, // key: code , value: code info
},
});
const codePaths: any = ref([]);
let allTagInfos: any = {};
const paths = computed(() => {
if (Array.isArray(props.path)) {
return props.path;
@@ -52,6 +59,32 @@ const paths = computed(() => {
return [props.path];
});
onMounted(() => {
setCodePaths();
});
watch(
() => props.path,
() => {
setCodePaths();
}
);
const setCodePaths = async () => {
if (!paths.value) {
return;
}
if (!props.tagInfos || Object.keys(props.tagInfos).length == 0) {
const tagInfos = await getAllTagInfoByCodePaths(paths.value as any);
allTagInfos = tagInfos;
} else {
allTagInfos = props.tagInfos;
}
codePaths.value = paths.value.map((p) => parseTagPath(p));
};
const parseTagPath = (tagPath: string = '') => {
if (!tagPath) {
return [];
@@ -61,27 +94,52 @@ const parseTagPath = (tagPath: string = '') => {
for (let code of codes) {
const typeAndCode = code.split('|');
let tagInfo;
if (typeAndCode.length == 1) {
const tagCode = typeAndCode[0];
if (!tagCode) {
continue;
}
res.push({
tagInfo = {
type: TagResourceTypeEnum.Tag.value,
code: typeAndCode[0],
});
};
res.push(tagInfo);
continue;
} else {
tagInfo = {
type: typeAndCode[0],
code: typeAndCode[1],
name: '',
};
}
res.push({
type: typeAndCode[0],
code: typeAndCode[1],
});
const ti = getTagInfo(tagInfo.type, tagInfo.code);
if (ti) {
tagInfo.name = ti.name;
}
res.push(tagInfo);
}
res[res.length - 1].isEnd = true;
return res;
};
const getTagInfo = (type: any, code: string) => {
if (type == TagResourceTypeEnum.Tag.value) {
return {};
}
if (allTagInfos && Object.keys(allTagInfos).length > 0) {
const key = `${type}|${code}`;
if (allTagInfos[key]) {
return allTagInfos[key];
}
}
return {};
};
</script>
<style lang="scss"></style>

View File

@@ -48,7 +48,7 @@
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs, nextTick } from 'vue';
import { nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { NodeType, TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue';
import { Contextmenu } from '@/components/contextmenu';
@@ -147,10 +147,10 @@ const loadNode = async (node: any, resolve: (data: any) => void, reject: () => v
return resolve(nodes);
};
const treeNodeClick = (data: any) => {
const treeNodeClick = async (data: any) => {
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
emit('nodeClick', data);
data.type.nodeClickFunc(data);
await data.type.nodeClickFunc(data);
}
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();

View File

@@ -31,9 +31,9 @@
/>
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
{{ data.code }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
</span>

View File

@@ -1,6 +1,7 @@
import { OptionsApi, SearchItem } from '@/components/SearchForm';
import { ContextmenuItem } from '@/components/contextmenu';
import { tagApi } from '../tag/api';
import {OptionsApi, SearchItem} from '@/components/SearchForm';
import {ContextmenuItem} from '@/components/contextmenu';
import {TagResourceTypeEnum} from '@/common/commonEnum';
import {tagApi} from '../tag/api';
export class TagTreeNode {
/**
@@ -161,7 +162,7 @@ export class NodeType {
*/
export function getTagPathSearchItem(resourceType: number) {
return SearchItem.select('tagPath', '标签').withOptionsApi(
OptionsApi.new(tagApi.getResourceTagPaths, { resourceType }).withConvertFn((res: any) => {
OptionsApi.new(tagApi.getResourceTagPaths, {resourceType}).withConvertFn((res: any) => {
return res.map((x: any) => {
return {
label: x,
@@ -178,7 +179,8 @@ export function getTagPathSearchItem(resourceType: number) {
* @returns {1: ['xxx'], 11: ['yyy']}
*/
export function getTagTypeCodeByPath(codePath: string) {
const result = {};
const result: any = {};
if (!codePath) return result
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
for (let part of parts) {
@@ -200,6 +202,40 @@ export function getTagTypeCodeByPath(codePath: string) {
return result;
}
/**
* 完善标签路径信息
* @param codePaths 标签路径
* @returns
*/
export async function getAllTagInfoByCodePaths(codePaths: string[]) {
if (!codePaths) return
const allTypeAndCode: any = {};
for (let codePath of codePaths) {
const typeAndCode = getTagTypeCodeByPath(codePath);
for (let type in typeAndCode) {
allTypeAndCode[type] = [...new Set(typeAndCode[type].concat(allTypeAndCode[type] || []))];
}
}
for (let type in allTypeAndCode) {
if (type == TagResourceTypeEnum.Tag.value) {
continue;
}
const tagInfo = await tagApi.listByQuery.request({type: type, codes: allTypeAndCode[type]});
allTypeAndCode[type] = tagInfo;
}
const code2CodeInfo: any = {};
for (let type in allTypeAndCode) {
for (let code of allTypeAndCode[type]) {
code2CodeInfo[`${type}|${code.code}`] = code;
}
}
return code2CodeInfo;
}
export function expandCodePath(codePath: string) {
const parts = codePath.split('/');
const result = [];

View File

@@ -10,14 +10,6 @@
width="38%"
>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-form-item prop="code" label="编号" required>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
@@ -88,7 +80,6 @@ import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import type { CheckboxValueType } from 'element-plus';
import { DbType } from '@/views/ops/db/dialect';
import { ResourceCodePattern } from '@/common/pattern';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
@@ -130,18 +121,6 @@ const rules = {
trigger: ['change', 'blur'],
},
],
code: [
{
required: true,
message: '请输入编码',
trigger: ['change', 'blur'],
},
{
pattern: ResourceCodePattern.pattern,
message: ResourceCodePattern.message,
trigger: ['blur'],
},
],
name: [
{
required: true,

View File

@@ -81,7 +81,7 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
<el-dropdown-item
<!-- <el-dropdown-item
:command="{ type: 'backupDb', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
@@ -98,7 +98,7 @@
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复任务
</el-dropdown-item>
</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -254,7 +254,7 @@ const perms = {
restoreDb: 'db:restore',
};
const actionBtns = hasPerms(Object.values(perms));
const actionBtns: any = hasPerms(Object.values(perms));
const pageTableRef: Ref<any> = ref(null);
const state = reactive({

View File

@@ -1,81 +1,137 @@
<template>
<div class="db-transfer-edit">
<el-dialog
:title="title"
v-model="dialogVisible"
:before-close="cancel"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="850px"
>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基本信息" :name="basicTab">
<el-form-item prop="srcDbId" label="源数据库" required>
<db-select-tree
placeholder="请选择源数据库"
v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName"
v-model:db-name="form.srcDbName"
v-model:tag-path="form.srcTagPath"
v-model:db-type="form.srcDbType"
@select-db="onSelectSrcDb"
/>
</el-form-item>
<el-divider content-position="left">基本信息</el-divider>
<el-form-item prop="targetDbId" label="目标数据库" required>
<db-select-tree
placeholder="请选择目标数据库"
v-model:db-id="form.targetDbId"
v-model:inst-name="form.targetInstName"
v-model:db-name="form.targetDbName"
v-model:tag-path="form.targetTagPath"
v-model:db-type="form.targetDbType"
@select-db="onSelectTargetDb"
/>
</el-form-item>
<el-form-item prop="taskName" label="任务名" required>
<el-input v-model.trim="form.taskName" placeholder="请输入任务名" auto-complete="off" />
</el-form-item>
<el-form-item prop="strategy" label="迁移策略" required>
<el-select v-model="form.strategy" filterable placeholder="迁移策略">
<el-option label="全量" :value="1" />
<el-option label="增量(暂不可用)" disabled :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-row class="w100">
<el-col :span="12">
<el-form-item prop="status" label="启用状态">
<el-switch v-model="form.status" inline-prompt active-text="启用" inactive-text="禁用" :active-value="1" :inactive-value="-1" />
</el-form-item>
</el-col>
<el-form-item prop="nameCase" label="转换表、字段名" required>
<el-select v-model="form.nameCase">
<el-option label="无" :value="1" />
<el-option label="大写" :value="2" />
<el-option label="小写" :value="3" />
</el-select>
</el-form-item>
<el-form-item prop="deleteTable" label="创建前删除表" required>
<el-select v-model="form.deleteTable">
<el-option label="是" :value="1" />
<el-option label="否" :value="2" />
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="数据库对象" :name="tableTab" :disabled="!baseFieldCompleted">
<el-form-item>
<el-input v-model="state.filterSrcTableText" style="width: 240px" placeholder="过滤表" />
</el-form-item>
<el-form-item>
<el-tree
ref="srcTreeRef"
style="width: 760px; max-height: 400px; overflow-y: auto"
default-expand-all
:expand-on-click-node="false"
:data="state.srcTableTree"
node-key="id"
show-checkbox
@check-change="handleSrcTableCheckChange"
:filter-node-method="filterSrcTableTreeNode"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-col :span="12">
<el-form-item prop="cronAble" label="定时迁移" required>
<el-radio-group v-model="form.cronAble">
<el-radio label="" :value="1" />
<el-radio label="" :value="-1" />
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
<CrontabInput v-model="form.cron" />
</el-form-item>
<el-form-item prop="srcDbId" label="源数据库" class="w100" required>
<db-select-tree
placeholder="请选择源数据库"
v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName"
v-model:db-name="form.srcDbName"
v-model:tag-path="form.srcTagPath"
v-model:db-type="form.srcDbType"
@select-db="onSelectSrcDb"
/>
</el-form-item>
<el-form-item prop="mode" label="迁移方式" required>
<el-radio-group v-model="form.mode">
<el-radio label="迁移到数据库" :value="1" />
<el-radio label="迁移到文件(自动命名)" :value="2" />
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.mode === 2">
<el-row class="w100">
<el-col :span="12">
<el-form-item prop="targetFileDbType" label="文件数据库类型" :required="form.mode === 2">
<el-select v-model="form.targetFileDbType" placeholder="数据库类型" clearable filterable>
<el-option
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
:key="key"
:value="dbTypeAndDialect[0]"
:label="dbTypeAndDialect[1].getInfo().name"
>
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
{{ dbTypeAndDialect[1].getInfo().name }}
</el-option>
<template #prefix>
<SvgIcon :name="getDbDialect(form.targetFileDbType!).getInfo().icon" :size="20" />
</template>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="文件保留天数">
<el-input-number v-model="form.fileSaveDays" :min="-1" :max="1000">
<template #suffix>
<span></span>
</template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="strategy" label="迁移策略" required>
<el-radio-group v-model="form.strategy">
<el-radio label="全量" :value="1" />
<el-radio label="增量(暂不可用)" :value="2" disabled />
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.mode == 1" prop="targetDbId" label="目标数据库" class="w100" :required="form.mode === 1">
<db-select-tree
placeholder="请选择目标数据库"
v-model:db-id="form.targetDbId"
v-model:inst-name="form.targetInstName"
v-model:db-name="form.targetDbName"
v-model:tag-path="form.targetTagPath"
v-model:db-type="form.targetDbType"
@select-db="onSelectTargetDb"
/>
</el-form-item>
<el-form-item prop="nameCase" label="转换表、字段名" required>
<el-radio-group v-model="form.nameCase">
<el-radio label="无" :value="1" />
<el-radio label="大写" :value="2" />
<el-radio label="小写" :value="3" />
</el-radio-group>
</el-form-item>
<el-divider content-position="left">数据库对象</el-divider>
<el-form-item>
<el-input v-model="state.filterSrcTableText" placeholder="过滤表" size="small" />
</el-form-item>
<el-form-item class="w100">
<el-tree
ref="srcTreeRef"
class="w100"
style="max-height: 200px; overflow-y: auto"
default-expand-all
:expand-on-click-node="false"
:data="state.srcTableTree"
node-key="id"
show-checkbox
@check-change="handleSrcTableCheckChange"
:filter-node-method="filterSrcTableTreeNode"
/>
</el-form-item>
</el-form>
<template #footer>
@@ -84,15 +140,20 @@
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, reactive, ref, toRefs, watch } from 'vue';
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
import { getDbDialect, getDbDialectMap } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import _ from 'lodash';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const props = defineProps({
data: {
@@ -108,15 +169,56 @@ const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const dialogVisible = defineModel<boolean>('visible', { default: false });
const rules = {};
const rules = {
taskName: [
{
required: true,
message: '请输入任务名',
trigger: ['change', 'blur'],
},
],
srcDbId: [
{
required: true,
message: '请选择源库',
trigger: ['change', 'blur'],
},
],
targetDbId: [
{
required: true,
message: '请选择目标库',
trigger: ['change', 'blur'],
},
],
targetFileDbType: [
{
required: true,
message: '请选择目标文件语言类型',
trigger: ['change', 'blur'],
},
],
cron: [
{
required: true,
message: '请选择cron表达式',
trigger: ['change', 'blur'],
},
],
};
const dbForm: any = ref(null);
const basicTab = 'basic';
const tableTab = 'table';
type FormData = {
id?: number;
taskName: string;
status: number;
cronAble: 1 | -1;
cron: string;
mode: 1 | 2;
targetFileDbType?: string;
fileSaveDays?: number;
dbType: 1 | 2;
srcDbId?: number;
srcDbName?: string;
srcDbType?: string;
@@ -136,6 +238,9 @@ type FormData = {
};
const basicFormData = {
mode: 1,
status: 1,
cronAble: -1,
strategy: 1,
nameCase: 1,
deleteTable: 1,
@@ -149,7 +254,6 @@ const srcTableListDisabled = ref(false);
const defaultKeys = ['tab-check', 'all', 'table-list'];
const state = reactive({
tabActiveName: 'basic',
form: basicFormData,
submitForm: {} as any,
srcTableFields: [] as string[],
@@ -172,20 +276,14 @@ const state = reactive({
],
});
const { tabActiveName, form, submitForm } = toRefs(state);
const { form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
// 基础字段信息是否填写完整
const baseFieldCompleted = computed(() => {
return state.form.srcDbId && state.form.targetDbId && state.form.targetDbName;
});
watch(dialogVisible, async (newValue: boolean) => {
if (!newValue) {
return;
}
state.tabActiveName = 'basic';
const propsData = props.data as any;
if (!propsData?.id) {
let d = {} as FormData;
@@ -196,8 +294,7 @@ watch(dialogVisible, async (newValue: boolean) => {
});
return;
}
state.form = props.data as FormData;
state.form = _.cloneDeep(props.data) as FormData;
let { srcDbId, targetDbId } = state.form;
// 初始化src数据源
@@ -224,6 +321,10 @@ watch(dialogVisible, async (newValue: boolean) => {
// 初始化勾选迁移表
srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
// 初始化默认值
state.form.cronAble = state.form.cronAble || 0;
state.form.mode = state.form.mode || 1;
});
watch(
@@ -323,10 +424,4 @@ const cancel = () => {
emit('cancel');
};
</script>
<style lang="scss">
.db-transfer-edit {
.el-select {
width: 100%;
}
}
</style>
<style lang="scss"></style>

View File

@@ -0,0 +1,247 @@
<template>
<div class="db-transfer-file">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="1000px">
<page-table
ref="pageTableRef"
:data="state.tableData"
v-model:query-form="state.query"
:show-selection="true"
v-model:selection-data="state.selectionData"
:columns="columns"
@page-num-change="
(args) => {
state.query.pageNum = args.pageNum;
search();
}
"
@page-size-change="
(args) => {
state.query.pageSize = args.pageNum;
search();
}
"
>
<template #tableHeader>
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
</template>
<template #fileKey="{ data }">
<FileInfo :fileKey="data.fileKey" :canDownload="actionBtns[perms.down] && data.status === 2" />
</template>
<template #fileDbType="{ data }">
<span>
<SvgIcon :name="getDbDialect(data.fileDbType).getInfo().icon" :size="18" />
{{ data.fileDbType }}
</span>
</template>
<template #action="{ data }">
<el-button v-if="actionBtns[perms.run] && data.status === DbTransferFileStatusEnum.Success.value" @click="openRun(data)" type="primary" link
>执行</el-button
>
<el-button v-if="data.logId" @click="openLog(data)" type="success" link>日志</el-button>
</template>
</page-table>
<TerminalLog v-model:log-id="state.logsDialog.logId" v-model:visible="state.logsDialog.visible" :title="state.logsDialog.title" />
</el-dialog>
<el-dialog :title="state.runDialog.title" v-model="state.runDialog.visible" :destroy-on-close="true" width="600px">
<el-form :model="state.runDialog.runForm" ref="runFormRef" label-width="auto" :rules="state.runDialog.formRules">
<el-form-item label="文件数据库类型" prop="dbType">
<SvgIcon :name="getDbDialect(state.runDialog.runForm.dbType).getInfo().icon" :size="18" /> {{ state.runDialog.runForm.dbType }}
</el-form-item>
<el-form-item label="选择目标数据库" prop="targetDbId" required>
<db-select-tree
placeholder="请选择目标数据库"
v-model:db-id="state.runDialog.runForm.targetDbId"
v-model:inst-name="state.runDialog.runForm.targetInstName"
v-model:db-name="state.runDialog.runForm.targetDbName"
v-model:tag-path="state.runDialog.runForm.targetTagPath"
v-model:db-type="state.runDialog.runForm.targetDbType"
@select-db="state.runDialog.onSelectRunTargetDb"
/>
</el-form-item>
</el-form>
<template #footer>
<div>
<el-button @click="state.runDialog.cancel()">取 消</el-button>
<el-button type="primary" :loading="state.runDialog.loading" @click="state.runDialog.btnOk">确 定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, Ref, ref, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import { getDbDialect } from '@/views/ops/db/dialect';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { ElMessage, ElMessageBox } from 'element-plus';
import { hasPerms } from '@/components/auth/auth';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getClientId } from '@/common/utils/storage';
import FileInfo from '@/components/file/FileInfo.vue';
import { DbTransferFileStatusEnum } from './enums';
const props = defineProps({
data: {
type: [Object],
},
title: {
type: String,
},
});
const dialogVisible = defineModel<boolean>('visible', { default: false });
const columns = ref([
TableColumn.new('fileKey', '文件').setMinWidth(280).isSlot(),
TableColumn.new('createTime', '执行时间').setMinWidth(180).isTime(),
TableColumn.new('fileDbType', 'sql语言').setMinWidth(90).isSlot(),
TableColumn.new('status', '状态').typeTag(DbTransferFileStatusEnum),
]);
const perms = {
del: 'db:transfer:files:del',
down: 'db:transfer:files:down',
run: 'db:transfer:files:run',
};
const actionBtns = hasPerms([perms.del, perms.down, perms.run]);
const actionWidth = ((actionBtns[perms.run] ? 1 : 0) + 1) * 55;
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
columns.value.push(actionColumn);
}
});
const runFormRef: any = ref(null);
const state = reactive({
query: {
taskId: props.data?.id,
name: null,
pageNum: 1,
pageSize: 10,
},
logsDialog: {
logId: 0,
title: '数据库迁移日志',
visible: false,
data: null as any,
running: false,
},
runDialog: {
title: '指定数据库执行sql文件',
visible: false,
data: null as any,
formRules: {
targetDbId: [
{
required: true,
message: '请选择目标数据库',
trigger: ['change', 'blur'],
},
],
},
runForm: {
id: 0,
dbType: '',
clientId: '',
targetDbId: 0,
targetDbName: '',
targetTagPath: '',
targetInstName: '',
targetDbType: '',
},
loading: false,
cancel: function () {
state.runDialog.visible = false;
state.runDialog.runForm = {} as any;
},
btnOk: function () {
runFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
return false;
}
console.log(state.runDialog.runForm);
if (state.runDialog.runForm.targetDbType !== state.runDialog.runForm.dbType) {
ElMessage.warning(`请选择[${state.runDialog.runForm.dbType}]数据库`);
return false;
}
state.runDialog.runForm.clientId = getClientId();
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
ElMessage.success('保存成功');
state.runDialog.cancel();
await search();
});
},
onSelectRunTargetDb: function (param: any) {
if (param.type !== state.runDialog.runForm.dbType) {
ElMessage.warning(`请选择[${state.runDialog.runForm.dbType}]数据库`);
}
},
},
selectionData: [], // 选中的数据
tableData: [],
});
const search = async () => {
const { total, list } = await dbApi.dbTransferFileList.request(state.query);
state.tableData = list;
pageTableRef.value.total = total;
};
const pageTableRef: Ref<any> = ref(null);
const del = async function () {
try {
await ElMessageBox.confirm(`将会删除sql文件确定删除?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功');
await search();
} catch (err) {
//
}
};
const openLog = function (data: any) {
state.logsDialog.logId = data.logId;
state.logsDialog.visible = true;
state.logsDialog.title = '数据库迁移日志';
state.logsDialog.running = data.state === 1;
};
// 运行sql弹出选择需要运行的库默认运行当前数据库需要保证数据库类型与sql文件一致
const openRun = function (data: any) {
console.log(data);
state.runDialog.runForm = { id: data.id, dbType: data.fileDbType } as any;
console.log(state.runDialog.runForm);
state.runDialog.visible = true;
};
watch(dialogVisible, async (newValue: boolean) => {
if (!newValue) {
return;
}
state.query.taskId = props.data?.id;
state.query.pageNum = 1;
state.query.pageSize = 10;
await search();
});
</script>
<style lang="scss"></style>

View File

@@ -14,11 +14,16 @@
<el-button v-auth="perms.del" :disabled="selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
</template>
<template #taskName="{ data }">
<span :style="`${data.taskName ? '' : 'color:red'}`">
{{ data.taskName || '请设置' }}
</span>
</template>
<template #srcDb="{ data }">
<el-tooltip :content="`${data.srcTagPath} > ${data.srcInstName} > ${data.srcDbName}`">
<span>
<SvgIcon :name="getDbDialect(data.srcDbType).getInfo().icon" :size="18" />
{{ data.srcInstName }}
{{ data.srcDbName }}
</span>
</el-tooltip>
</template>
@@ -26,21 +31,43 @@
<el-tooltip :content="`${data.targetTagPath} > ${data.targetInstName} > ${data.targetDbName}`">
<span>
<SvgIcon :name="getDbDialect(data.targetDbType).getInfo().icon" :size="18" />
{{ data.targetInstName }}
{{ data.targetDbName }}
</span>
</el-tooltip>
</template>
<template #status="{ data }">
<span v-if="actionBtns[perms.status]">
<el-switch
v-model="data.status"
@click="updStatus(data.id, data.status)"
inline-prompt
active-text="启用"
inactive-text="禁用"
:active-value="1"
:inactive-value="-1"
/>
</span>
<span v-else>
<el-tag v-if="data.status == 1" class="ml-2" type="success">启用</el-tag>
<el-tag v-else class="ml-2" type="danger">禁用</el-tag>
</span>
</template>
<template #action="{ data }">
<!-- 删除启停用编辑 -->
<el-button v-if="actionBtns[perms.save]" @click="edit(data)" type="primary" link>编辑</el-button>
<el-button v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
<el-button v-if="actionBtns[perms.log]" type="warning" link @click="log(data)">日志</el-button>
<el-button v-if="data.runningState === 1" @click="stop(data.id)" type="danger" link>停止</el-button>
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1" type="primary" link @click="reRun(data)">运行</el-button>
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1 && data.status === 1" type="success" link @click="reRun(data)"
>运行</el-button
>
<el-button v-if="actionBtns[perms.files] && data.mode === 2" type="success" link @click="openFiles(data)">文件</el-button>
</template>
</page-table>
<db-transfer-edit @val-change="search" :title="editDialog.title" v-model:visible="editDialog.visible" v-model:data="editDialog.data" />
<db-transfer-file :title="filesDialog.title" v-model:visible="filesDialog.visible" v-model:data="filesDialog.data" />
<TerminalLog v-model:log-id="logsDialog.logId" v-model:visible="logsDialog.visible" :title="logsDialog.title" />
</div>
@@ -57,6 +84,7 @@ import { SearchItem } from '@/components/SearchForm';
import { getDbDialect } from '@/views/ops/db/dialect';
import { DbTransferRunningStateEnum } from './enums';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbTransferFile from './DbTransferFile.vue';
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
@@ -66,23 +94,25 @@ const perms = {
status: 'db:transfer:status',
log: 'db:transfer:log',
run: 'db:transfer:run',
files: 'db:transfer:files',
};
const searchItems = [SearchItem.input('name', '名称')];
const columns = ref([
TableColumn.new('srcDb', '源库').setMinWidth(200).isSlot(),
TableColumn.new('targetDb', '目标库').setMinWidth(200).isSlot(),
TableColumn.new('taskName', '任务名').setMinWidth(150).isSlot(),
TableColumn.new('srcDb', '库').setMinWidth(150).isSlot(),
// TableColumn.new('targetDb', '目标库').setMinWidth(150).isSlot(),
TableColumn.new('runningState', '执行状态').typeTag(DbTransferRunningStateEnum),
TableColumn.new('creator', '创建人'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('status', '状态').isSlot(),
TableColumn.new('modifier', '修改人'),
TableColumn.new('updateTime', '修改时间').isTime(),
]);
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run]);
const actionWidth = ((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0)) * 55;
const actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run, perms.files]);
const actionWidth =
((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0) + (actionBtns[perms.files] ? 1 : 0)) * 55;
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
const pageTableRef: Ref<any> = ref(null);
@@ -114,9 +144,15 @@ const state = reactive({
data: null as any,
running: false,
},
filesDialog: {
taskId: 0,
title: '迁移文件列表',
visible: false,
data: null as any,
},
});
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
const { selectionData, query, editDialog, logsDialog, filesDialog } = toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
@@ -131,10 +167,10 @@ const search = () => {
const edit = async (data: any) => {
if (!data) {
state.editDialog.data = null;
state.editDialog.title = '新增数据库迁移任务';
state.editDialog.title = '新增数据库迁移任务(迁移不会对源库造成修改)';
} else {
state.editDialog.data = data;
state.editDialog.title = '修改数据库迁移任务';
state.editDialog.title = '修改数据库迁移任务(迁移不会对源库造成修改)';
}
state.editDialog.visible = true;
};
@@ -178,6 +214,22 @@ const reRun = async (data: any) => {
}, 2000);
};
const openFiles = async (data: any) => {
state.filesDialog.visible = true;
state.filesDialog.title = '迁移文件管理';
state.filesDialog.taskId = data.id;
state.filesDialog.data = data;
};
const updStatus = async (id: any, status: 1 | -1) => {
try {
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
ElMessage.success(`${status === 1 ? '启用' : '禁用'}成功`);
search();
} catch (err) {
//
}
};
const del = async () => {
try {
await ElMessageBox.confirm(`确定删除任务?`, '提示', {

View File

@@ -22,15 +22,6 @@
/>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
@@ -132,7 +123,6 @@ import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { ResourceCodePattern } from '@/common/pattern';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
@@ -161,18 +151,6 @@ const rules = {
trigger: ['change'],
},
],
code: [
{
required: true,
message: '请输入编码',
trigger: ['change', 'blur'],
},
{
pattern: ResourceCodePattern.pattern,
message: ResourceCodePattern.message,
trigger: ['blur'],
},
],
name: [
{
required: true,

View File

@@ -104,7 +104,7 @@ const perms = {
saveDb: 'db:save',
};
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
const searchItems = [SearchItem.input('keyword', '关键字').withPlaceholder('host / 名称 / 编号'), getTagPathSearchItem(TagResourceTypeEnum.Db.value)];
const columns = ref([
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
@@ -118,7 +118,7 @@ const columns = ref([
]);
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms(Object.values(perms));
const actionBtns: any = hasPerms(Object.values(perms));
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
const pageTableRef: Ref<any> = ref(null);

View File

@@ -108,6 +108,16 @@
/>
</el-row>
<el-row>
<el-checkbox
v-model="dbConfig.cacheTable"
label="缓存表信息-[不开启则实时获取表信息]"
:true-value="1"
:false-value="0"
size="small"
/>
</el-row>
<template #reference>
<el-link type="primary" icon="setting" :underline="false"></el-link>
</template>
@@ -149,7 +159,7 @@
<template #label>
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<template #reference>
<span class="font12">{{ dt.label }}</span>
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="font12">{{ dt.label }}</span>
</template>
<template #default>
<el-descriptions :column="1" size="small">
@@ -195,7 +205,6 @@
:db-id="dt.params.id"
:db="dt.params.db"
:db-type="dt.params.type"
:flow-procdef="dt.params.flowProcdef"
:height="state.tablesOpHeight"
/>
</el-tab-pane>
@@ -210,25 +219,31 @@
:dbId="tableCreateDialog.dbId"
:db="tableCreateDialog.db"
:dbType="tableCreateDialog.dbType"
:flow-procdef="tableCreateDialog.flowProcdef"
:version="tableCreateDialog.version"
:data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitEditTableSql"
/>
<el-dialog width="55%" :title="`'${state.chooseTableName}' DDL`" v-model="state.ddlDialog.visible">
<monaco-editor height="400px" language="sql" v-model="state.ddlDialog.ddl" :options="{ readOnly: true }" />
</el-dialog>
<contextmenu ref="tabContextmenuRef" :dropdown="state.tabContextmenu.dropdown" :items="state.tabContextmenu.items" />
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format';
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { getTagTypeCodeByPath, NodeType, TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
@@ -237,7 +252,8 @@ import { useEventListener, useStorage } from '@vueuse/core';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
import { procdefApi } from '@/views/flow/api';
import { format as sqlFormatter } from 'sql-formatter';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
@@ -280,10 +296,10 @@ const SqlIcon = {
};
// node节点点击时触发改变db事件
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
const params = nodeData.params;
if (params.db) {
changeDb(
await changeDb(
{
id: params.id,
host: `${params.host}`,
@@ -291,7 +307,6 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
type: params.type,
tagPath: params.tagPath,
databases: params.dbs,
flowProcdef: params.flowProcdef,
},
params.db
);
@@ -323,7 +338,9 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(as
const params = parentNode.params;
const dbs = (await DbInst.getDbNames(params))?.sort();
const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.DbName.value, resourceCode: params.code });
// 查询数据库版本信息
const version = await dbApi.getCompatibleDbVersion.request({ id: params.id, db: dbs[0] });
return dbs.map((x: any) => {
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
.withParams({
@@ -331,10 +348,10 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(as
id: params.id,
name: params.name,
type: params.type,
version: version || 'unset',
host: `${params.host}:${params.port}`,
dbs: dbs,
db: x,
flowProcdef: flowProcdef,
})
.withIcon(DbIcon);
});
@@ -368,7 +385,12 @@ const getNodeTypeTables = (params: any) => {
let tableKey = `${params.id}.${params.db}.table-menu`;
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu)
.withParams({
...params,
key: tableKey,
})
.withIcon(TableIcon),
new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
];
};
@@ -395,10 +417,10 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
let { id, db, type, flowProcdef, schema } = params;
let { id, db, type, schema, version } = params;
// 获取当前库的所有表信息
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false;
state.reloadStatus = !dbConfig.value.cacheTable;
let dbTableSize = 0;
const tablesNode = tables.map((x: any) => {
const tableSize = x.dataLength + x.indexLength;
@@ -411,7 +433,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
db,
type,
schema,
flowProcdef: flowProcdef,
version,
key: key,
parentKey: parentNode.key,
tableName: x.tableName,
@@ -427,7 +449,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
})
.withNodeDblclickFunc((node: TagTreeNode) => {
const params = node.params;
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: node.key });
addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
});
// 数据库sql模板菜单节点
@@ -455,6 +477,7 @@ const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
new ContextmenuItem('renameTable', '重命名').withIcon('edit').withOnClick((data: any) => onRenameTable(data)),
new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
new ContextmenuItem('ddl', 'DDL').withIcon('Document').withOnClick((data: any) => onGenDdl(data)),
])
.withNodeClickFunc((nodeData: TagTreeNode) => {
const params = nodeData.params;
@@ -471,7 +494,24 @@ const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
new ContextmenuItem('delSql', '删除').withIcon('delete').withOnClick((data: any) => deleteSql(data.params.id, data.params.db, data.params.sqlName)),
]);
const tabContextmenuItems = [
new ContextmenuItem(1, '关闭').withIcon('Close').withOnClick((data: any) => {
onRemoveTab(data.key);
}),
new ContextmenuItem(2, '关闭其他').withIcon('CircleClose').withOnClick((data: any) => {
const tabName = data.key;
const tabNames = [...state.tabs.keys()];
for (let tab of tabNames) {
if (tab !== tabName) {
onRemoveTab(tab);
}
}
}),
];
const tagTreeRef: any = ref(null);
const tabContextmenuRef: any = useTemplateRef('tabContextmenuRef');
const tabs: Map<string, TabInfo> = new Map();
const state = reactive({
@@ -484,6 +524,10 @@ const state = reactive({
activeName: '',
reloadStatus: false,
tabs,
tabContextmenu: {
dropdown: { x: 0, y: 0 },
items: tabContextmenuItems,
},
dataTabsTableHeight: '600px',
tablesOpHeight: '600',
dbServerInfo: {
@@ -495,17 +539,22 @@ const state = reactive({
title: '',
activeName: '',
dbId: 0,
version: '',
db: '',
dbType: '',
flowProcdef: null as any,
data: {},
parentKey: '',
},
chooseTableName: '',
ddlDialog: {
visible: false,
ddl: '',
},
});
const { nowDbInst, tableCreateDialog } = toRefs(state);
const dbConfig = useStorage('dbConfig', { showColumnComment: false, locationTreeNode: false });
const dbConfig = useStorage('dbConfig', DbThemeConfig);
const serverInfoReqParam = ref({
instanceId: 0,
@@ -516,6 +565,7 @@ const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
onMounted(() => {
state.reloadStatus = !dbConfig.value.cacheTable;
autoOpenDb(autoOpenResource.value.dbCodePath);
setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度
@@ -538,7 +588,7 @@ const autoOpenDb = (codePath: string) => {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const typeAndCodes: any = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
@@ -568,8 +618,8 @@ const showDbInfo = async (db: any) => {
};
// 选择数据库,改变当前正在操作的数据库信息
const changeDb = (db: any, dbName: string) => {
state.nowDbInst = DbInst.getOrNewInst(db);
const changeDb = async (db: any, dbName: string) => {
state.nowDbInst = await DbInst.getOrNewInst(db);
state.nowDbInst.databases = db.databases;
state.db = dbName;
};
@@ -579,7 +629,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
if (tableName == '') {
return;
}
changeDb(db, dbName);
await changeDb(db, dbName);
const key = `tableData:${db.id}.${dbName}.${tableName}`;
let tab = state.tabs.get(key);
@@ -608,7 +658,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
ElMessage.warning('请选择数据库实例及对应的schema');
return;
}
changeDb(db, dbName);
await changeDb(db, dbName);
const dbId = db.id;
let label;
@@ -659,7 +709,7 @@ const addTablesOpTab = async (db: any) => {
ElMessage.warning('请选择数据库实例及对应的schema');
return;
}
changeDb(db, dbName);
await changeDb(db, dbName);
const dbId = db.id;
let key = `tablesOp:${dbId}.${dbName}`;
@@ -736,6 +786,15 @@ const onTabChange = () => {
}
};
// 右键点击时:传 x,y 坐标值到子组件中props
const onTabContextmenu = (v: any, e: any) => {
console.log('on tab cm');
const { clientX, clientY } = e;
state.tabContextmenu.dropdown.x = clientX;
state.tabContextmenu.dropdown.y = clientY;
tabContextmenuRef.value.openContextmenu(v);
};
/**
* 定位至当前树节点
*/
@@ -775,7 +834,7 @@ const reloadNode = (nodeKey: string) => {
};
const onEditTable = async (data: any) => {
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdef } = data.params;
let { db, id, tableName, tableComment, type, parentKey, key, version } = data.params;
// data.label就是表名
if (tableName) {
state.tableCreateDialog.title = '修改表';
@@ -792,14 +851,14 @@ const onEditTable = async (data: any) => {
state.tableCreateDialog.activeName = '1';
state.tableCreateDialog.dbId = id;
state.tableCreateDialog.version = version;
state.tableCreateDialog.db = db;
state.tableCreateDialog.dbType = type;
state.tableCreateDialog.flowProcdef = flowProcdef;
state.tableCreateDialog.visible = true;
};
const onDeleteTable = async (data: any) => {
let { db, id, tableName, parentKey, flowProcdef, schema } = data.params;
let { db, id, tableName, parentKey, schema } = data.params;
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -810,20 +869,33 @@ const onDeleteTable = async (data: any) => {
let dialect = getDbDialect(state.nowDbInst.type);
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
if (flowProcdef) {
ElMessage.success('工单提交成功');
return;
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then((res) => {
let success = true;
for (let re of res) {
if (re.errorMsg) {
success = false;
ElMessage.error(`${re.sql} -> 执行失败: ${re.errorMsg}`);
}
}
if (success) {
ElMessage.success('删除成功');
setTimeout(() => {
parentKey && reloadNode(parentKey);
}, 1000);
}
ElMessage.success('删除成功');
setTimeout(() => {
parentKey && reloadNode(parentKey);
}, 1000);
});
};
const onGenDdl = async (data: any) => {
let { db, id, tableName, type } = data.params;
state.chooseTableName = tableName;
let res = await dbApi.tableDdl.request({ id, db, tableName });
state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(type).getInfo().formatSqlDialect as any });
state.ddlDialog.visible = true;
};
const onRenameTable = async (data: any) => {
let { db, id, tableName, parentKey, flowProcdef } = data.params;
let { db, id, tableName, parentKey } = data.params;
let tableData = { db, oldTableName: tableName, tableName };
let value = ref(tableName);
@@ -846,7 +918,6 @@ const onRenameTable = async (data: any) => {
dbId: id as any,
db: db as any,
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
flowProcdef: flowProcdef,
runSuccessCallback: () => {
setTimeout(() => {
parentKey && reloadNode(parentKey);
@@ -906,7 +977,6 @@ const getNowDbInfo = () => {
name: di.name,
type: di.type,
host: di.host,
flowProcdef: di.flowProcdef,
dbName: state.db,
};
};

View File

@@ -1,46 +1,33 @@
<template>
<div class="sync-task-edit">
<el-dialog
:title="title"
v-model="dialogVisible"
:before-close="cancel"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="850px"
>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName" style="height: 450px">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基本信息" :name="basicTab">
<el-form-item>
<el-row>
<el-col :span="11">
<el-col :span="12">
<el-form-item prop="taskName" label="任务名" required>
<el-input v-model.trim="form.taskName" placeholder="请输入同步任务名" auto-complete="off" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-col :span="12">
<el-form-item prop="taskCron" label="cron" required>
<CrontabInput v-model="form.taskCron" />
</el-form-item>
</el-col>
<el-col :span="2">
<el-form-item prop="status" label="状态" label-width="60" required>
<el-switch
v-model="form.status"
inline-prompt
active-text="启用"
inactive-text="禁用"
:active-value="1"
:inactive-value="-1"
/>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="status" label="状态" label-width="60" required>
<el-switch v-model="form.status" inline-prompt active-text="启用" inactive-text="禁用" :active-value="1" :inactive-value="-1" />
</el-form-item>
<el-form-item prop="srcDbId" label="源数据库" required>
<db-select-tree
placeholder="请选择源数据库"
@@ -69,36 +56,73 @@
<monaco-editor height="150px" class="task-sql" language="sql" v-model="form.dataSql" />
</el-form-item>
<el-form-item prop="targetTableName" label="目标库表" required>
<el-select v-model="form.targetTableName" filterable placeholder="请选择目标数据库表">
<el-option
v-for="item in state.targetTableList"
:key="item.tableName"
:label="item.tableName + (item.tableComment && '-' + item.tableComment)"
:value="item.tableName"
/>
</el-select>
<el-form-item>
<el-row class="w100">
<el-col :span="12">
<el-form-item prop="targetTableName" label="目标库表" required>
<el-select v-model="form.targetTableName" filterable placeholder="请选择目标数据库表">
<el-option
v-for="item in state.targetTableList"
:key="item.tableName"
:label="item.tableName + (item.tableComment && '-' + item.tableComment)"
:value="item.tableName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="pageSize" label="分页大小" required>
<el-input type="number" v-model.number="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-row>
<el-col :span="8">
<el-form-item prop="pageSize" label="分页大小" required>
<el-input type="number" v-model.number="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
<el-form-item class="w100" prop="updField">
<template #label>
更新字段
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值支持带别名t.create_time" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值支持带别名t.create_time" placement="top">
<el-form-item prop="updField" label="更新字段" required>
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
</el-form-item>
</el-tooltip>
<el-form-item class="w100" prop="updFieldVal">
<template #label>
更新值
<el-tooltip content="记录更新字段当前值,如:当前时间,当前日期等,下次查询数据时会带上该值条件" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
<el-input v-model.trim="form.updFieldVal" placeholder="更新字段当前最大值" auto-complete="off" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="updFieldVal" label="更新值">
<el-input v-model.trim="form.updFieldVal" placeholder="更新字段当前最大值" auto-complete="off" />
<el-form-item class="w100" prop="updFieldSrc">
<template #label>
值来源
<el-tooltip
content="从查询结果中取更新值的字段名,默认同更新字段,如果查询结果指定了字段别名且与原更新字段不一致,则取这个字段值为当前更新值"
placement="top"
>
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
<el-input v-model.trim="form.updFieldSrc" placeholder="更新值来源" auto-complete="off" />
</el-form-item>
</el-col>
</el-row>
@@ -183,7 +207,18 @@
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</el-drawer>
<!-- <el-dialog
:title="title"
v-model="dialogVisible"
:before-close="cancel"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="850px"
>
</el-dialog> -->
</div>
</template>
@@ -196,6 +231,7 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
import { compatibleDuplicateStrategy, DbType, DuplicateStrategy, getDbDialect } from '@/views/ops/db/dialect';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const props = defineProps({
data: {
@@ -253,6 +289,7 @@ type FormData = {
pageSize?: number;
updField?: string;
updFieldVal?: string;
updFieldSrc?: string;
fieldMap?: { src: string; target: string }[];
status?: 1 | 2;
duplicateStrategy?: -1 | 1 | 2;
@@ -326,9 +363,9 @@ watch(dialogVisible, async (newValue: boolean) => {
const db = dbInfoRes.list[0];
// 初始化实例
db.databases = db.database?.split(' ').sort() || [];
state.srcDbInst = DbInst.getOrNewInst(db);
state.srcDbInst = await DbInst.getOrNewInst(db);
state.form.srcDbType = state.srcDbInst.type;
state.form.srcInstName = db.instanceName;
state.form.srcInstName = db.name;
}
// 初始化target数据源
@@ -338,9 +375,9 @@ watch(dialogVisible, async (newValue: boolean) => {
const db = dbInfoRes.list[0];
// 初始化实例
db.databases = db.database?.split(' ').sort() || [];
state.targetDbInst = DbInst.getOrNewInst(db);
state.targetDbInst = await DbInst.getOrNewInst(db);
state.form.targetDbType = state.targetDbInst.type;
state.form.targetInstName = db.instanceName;
state.form.targetInstName = db.name;
}
if (targetDbId && state.form.targetDbName) {
@@ -397,12 +434,12 @@ const refreshPreviewInsertSql = () => {
const onSelectSrcDb = async (params: any) => {
// 初始化数据源
params.databases = params.dbs; // 数据源里需要这个值
state.srcDbInst = DbInst.getOrNewInst(params);
state.srcDbInst = await DbInst.getOrNewInst(params);
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
};
const onSelectTargetDb = async (params: any) => {
state.targetDbInst = DbInst.getOrNewInst(params);
state.targetDbInst = await DbInst.getOrNewInst(params);
await loadDbTables(params.id, params.db);
};
@@ -465,7 +502,7 @@ const handleGetSrcFields = async () => {
return;
}
let filedMap = {};
let filedMap: any = {};
if (state.form.fieldMap && state.form.fieldMap.length > 0) {
state.form.fieldMap.forEach((a: any) => {
filedMap[a.src] = a.target;

View File

@@ -1,5 +1,5 @@
import Api from '@/common/Api';
import { Base64 } from 'js-base64';
import { AesEncrypt } from '@/common/crypto';
export const dbApi = {
// 获取权限列表
@@ -16,20 +16,7 @@ export const dbApi = {
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
// 获取表即列提示
hintTables: Api.newGet('/dbs/{id}/hint-tables'),
sqlExec: Api.newPost('/dbs/{id}/exec-sql').withBeforeHandler((param: any) => {
// sql编码处理
if (param.sql) {
// 判断是开发环境就打印sql
if (process.env.NODE_ENV === 'development') {
console.log(param.sql);
}
// 非base64编码sql则进行base64编码refreshToken时会重复调用该方法故简单判断下
if (!Base64.isValid(param.sql)) {
param.sql = Base64.encode(param.sql);
}
}
return param;
}),
sqlExec: Api.newPost('/dbs/{id}/exec-sql').withBeforeHandler(async (param: any) => await encryptField(param, 'sql')),
// 保存sql
saveSql: Api.newPost('/dbs/{id}/sql'),
// 获取保存的sql
@@ -39,6 +26,8 @@ export const dbApi = {
deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
// 获取数据库sql执行记录
getSqlExecs: Api.newGet('/dbs/sql-execs'),
// 获取数据库兼容版本
getCompatibleDbVersion: Api.newGet('/dbs/{id}/version'),
instances: Api.newGet('/instances'),
getInstance: Api.newGet('/instances/{instanceId}'),
@@ -73,13 +62,7 @@ export const dbApi = {
// 数据同步相关
datasyncTasks: Api.newGet('/datasync/tasks'),
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler((param: any) => {
// sql编码处理
if (param.dataSql) {
param.dataSql = Base64.encode(param.dataSql);
}
return param;
}),
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
@@ -91,12 +74,31 @@ export const dbApi = {
dbTransferTasks: Api.newGet('/dbTransfer'),
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
};
export const dbSqlExecApi = {
// 根据业务key获取sql执行信息
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
};
const encryptField = async (param: any, field: string) => {
// sql编码处理
if (!param['_encrypted'] && param[field]) {
// 判断是开发环境就打印sql
if (process.env.NODE_ENV === 'development') {
console.log(param[field]);
}
// 使用rsa公钥加密sql
param['_encrypted'] = 1;
param[field] = AesEncrypt(param[field]);
// console.log('解密结果', DesDecrypt(param[field]));
}
return param;
};

View File

@@ -27,25 +27,13 @@ import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
import { computed } from 'vue';
import { DbInst } from '../db';
const props = defineProps({
dbId: {
type: Number,
},
instName: {
type: String,
},
dbName: {
type: String,
},
tagPath: {
type: String,
},
dbType: {
type: String,
},
});
const dbId = defineModel<number>('dbId');
const instName = defineModel<string>('instName');
const dbName = defineModel<string>('dbName');
const tagPath = defineModel<string>('tagPath');
const dbType = defineModel<string>('dbType');
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
const emits = defineEmits(['selectDb']);
/**
* 树节点类型
@@ -63,7 +51,7 @@ class SqlExecNodeType {
const selectNode = computed({
get: () => {
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
},
set: () => {
//
@@ -116,6 +104,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(as
.withParams({
tagPath: params.tagPath,
id: params.id,
code: params.code,
instanceId: params.instanceId,
name: params.name,
type: params.type,
@@ -156,12 +145,12 @@ const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema);
const changeNode = (nodeData: TagTreeNode) => {
const params = nodeData.params;
// postgres
emits('update:dbName', params.db);
emits('update:instName', params.name);
emits('update:dbId', params.id);
emits('update:tagPath', params.tagPath);
emits('update:dbType', params.type);
dbName.value = params.db;
instName.value = params.name;
dbId.value = params.id;
tagPath.value = params.tagPath;
dbType.value = params.type;
emits('selectDb', params);
};
</script>

View File

@@ -28,7 +28,7 @@
:limit="100"
>
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="SQL脚本执行" placement="top">
<el-link type="success" :underline="false" icon="Document"></el-link>
<el-link v-auth="'db:sqlscript:run'" type="success" :underline="false" icon="Document"></el-link>
</el-tooltip>
</el-upload>
</div>
@@ -297,6 +297,8 @@ const onRunSql = async (newTab = false) => {
// 去除字符串前的空格、换行等
sql = sql.replace(/(^\s*)/g, '');
const sqls = splitSql(sql);
// 简单截取前十个字符
const sqlPrefix = sql.slice(0, 10).toLowerCase();
const nonQuery =
@@ -307,28 +309,37 @@ const onRunSql = async (newTab = false) => {
sqlPrefix.startsWith('drop') ||
sqlPrefix.startsWith('create');
// 启用工单审批
if (nonQuery && getNowDbInst().flowProcdef) {
try {
getNowDbInst().promptExeSql(props.dbName, sql, null, () => {
ElMessage.success('工单提交成功');
if (sqls.length == 1) {
let execRemark;
if (nonQuery) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputErrorMessage: '输入执行该sql的备注信息',
});
} catch (e) {
ElMessage.success('工单提交失败');
execRemark = res.value;
}
runSql(sql, execRemark, newTab);
} else {
let isFirst = true;
for (let s of sqls) {
if (isFirst) {
isFirst = false;
runSql(s, '', newTab);
} else {
runSql(s, '', true);
}
}
return;
}
let execRemark;
if (nonQuery) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputErrorMessage: '输入执行该sql的备注信息',
});
execRemark = res.value;
}
};
/**
* 执行单条sql
*
* @param sql 单条sql
* @param newTab 是否新建tab
*/
const runSql = async (sql: string, remark = '', newTab = false) => {
let execRes: ExecResTab;
let i = 0;
let id;
@@ -356,12 +367,16 @@ const onRunSql = async (newTab = false) => {
execRes.errorMsg = '';
execRes.sql = '';
const { data, execute, isFetching, abort } = getNowDbInst().execSql(props.dbName, sql, execRemark);
const { data, execute, isFetching, abort } = getNowDbInst().execSql(props.dbName, sql, remark);
execRes.loading = isFetching;
execRes.abortFn = abort;
await execute();
const colAndData: any = data.value;
const colAndData: any = (data.value as any)[0];
if (colAndData.errorMsg) {
throw { msg: colAndData.errorMsg };
}
if (colAndData.res.length == 0) {
state.tableDataEmptyText = '查无数据';
}
@@ -381,7 +396,8 @@ const onRunSql = async (newTab = false) => {
execRes.data = [];
execRes.tableColumn = [];
execRes.table = '';
execRes.errorMsg = e.msg;
// 要实时响应,故需要用索引改变数据才生效
state.execResTabs[i].errorMsg = e.msg;
return;
} finally {
execRes.sql = sql;
@@ -403,6 +419,64 @@ const onRunSql = async (newTab = false) => {
}
};
function splitSql(sql: string) {
let state = 'normal';
let buffer = '';
let result = [];
let inString = null; // 用于记录当前字符串的引号类型(' 或 "
for (let i = 0; i < sql.length; i++) {
const char = sql[i];
const nextChar = sql[i + 1];
if (state === 'normal') {
if (char === '-' && nextChar === '-') {
state = 'singleLineComment';
i++; // 跳过下一个字符
} else if (char === '/' && nextChar === '*') {
state = 'multiLineComment';
i++; // 跳过下一个字符
} else if (char === "'" || char === '"') {
state = 'string';
inString = char;
buffer += char;
} else if (char === ';') {
if (buffer.trim()) {
result.push(buffer.trim());
}
buffer = '';
} else {
buffer += char;
}
} else if (state === 'string') {
buffer += char;
if (char === '\\') {
// 处理转义字符
buffer += nextChar;
i++;
} else if (char === inString) {
state = 'normal';
inString = null;
}
} else if (state === 'singleLineComment') {
if (char === '\n') {
state = 'normal';
}
} else if (state === 'multiLineComment') {
if (char === '*' && nextChar === '/') {
state = 'normal';
i++; // 跳过下一个字符
}
}
}
if (buffer.trim()) {
result.push(buffer.trim());
}
return result;
}
/**
* 获取sql如果有鼠标选中则返回选中内容否则返回输入框内所有内容
*/

View File

@@ -2,18 +2,7 @@
<div>
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" :close-on-click-modal="false">
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
<el-input
@keyup.enter="runSql"
ref="remarkInputRef"
v-model="remark"
:placeholder="props.flowProcdef ? '执行备注(必填)' : '执行备注(选填)'"
class="mt5"
/>
<div v-if="props.flowProcdef">
<el-divider content-position="left">审批节点</el-divider>
<procdef-tasks :procdef="props.flowProcdef" />
</div>
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="执行备注" class="mt5" />
<template #footer>
<span class="dialog-footer">
@@ -28,13 +17,13 @@
<script lang="ts" setup>
import { toRefs, ref, reactive, onMounted } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { format as sqlFormatter } from 'sql-formatter';
import { SqlExecProps } from './SqlExecBox';
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
import { isTrue } from '@/common/assert';
const props = withDefaults(defineProps<SqlExecProps>(), {});
@@ -58,12 +47,6 @@ onMounted(() => {
* 执行sql
*/
const runSql = async () => {
// 存在流程审批,则备注为必填
if (!state.remark && props.flowProcdef) {
ElMessage.error('请输入执行的备注信息');
return;
}
try {
state.btnLoading = true;
runSuccess = true;
@@ -75,19 +58,15 @@ const runSql = async () => {
sql: state.sqlValue.trim(),
});
// 存在流程审批
if (props.flowProcdef) {
ElMessage.success('工单提交成功');
return;
}
for (let re of res.res) {
if (re.result !== 'success') {
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
throw new Error(re.result);
let isSuccess = true;
for (let re of res) {
if (re.errorMsg) {
isSuccess = false;
ElMessage.error(`${re.sql} \n执行失败: ${re.errorMsg}`);
}
}
isTrue(isSuccess, '存在执行失败sql');
ElMessage.success('执行成功');
} catch (e) {
runSuccess = false;

View File

@@ -25,7 +25,7 @@
:clearable="false"
type="Date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
:placeholder="`选择日期-${placeholder}`"
/>
<el-date-picker
@@ -41,7 +41,7 @@
:clearable="false"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期时间"
:placeholder="`选择日期时间-${placeholder}`"
/>
<el-time-picker
@@ -56,7 +56,7 @@
v-model="itemValue"
:clearable="false"
value-format="HH:mm:ss"
placeholder="选择时间"
:placeholder="`选择时间-${placeholder}`"
/>
</template>

View File

@@ -133,7 +133,7 @@
<el-button id="copyValue" @click="copyGenTxt(state.genTxtDialog.txt)" icon="CopyDocument" type="success" size="small">一键复制</el-button>
</div>
</template>
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
<el-input v-model="state.genTxtDialog.txt" type="textarea" :rows="20" />
</el-dialog>
<DbTableDataForm
@@ -156,7 +156,7 @@
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElInput, ElMessage } from 'element-plus';
import { copyToClipboard } from '@/common/utils/string';
import { DbInst } from '@/views/ops/db/db';
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue';
import { exportCsv, exportFile } from '@/common/utils/export';
@@ -258,12 +258,10 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
return state.table == '';
});
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
.withIcon('edit')
.withOnClick(() => onEditRowData())
.withHideFunc(() => {
return state.table == '';
});
const cmFormView = new ContextmenuItem('formView', '表单视图').withIcon('Document').withOnClick(() => onEditRowData());
// .withHideFunc(() => {
// return state.table == '';
// });
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
.withIcon('tickets')
@@ -363,7 +361,7 @@ const state = reactive({
const { tableHeight, datas } = toRefs(state);
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
const dbConfig = useStorage('dbConfig', DbThemeConfig);
/**
* 行号字段列
@@ -595,7 +593,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
contextmenuRef.value.openContextmenu({ column, rowData: data });
};
@@ -616,10 +614,6 @@ const onDeleteData = async () => {
const db = state.db;
const dbInst = getNowDbInst();
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
// 存在流程则恢复原值,需工单流程审批完后自动执行
if (dbInst.flowProcdef) {
return;
}
emits('dataDelete', deleteDatas);
});
};
@@ -627,12 +621,12 @@ const onDeleteData = async () => {
const onEditRowData = () => {
const selectionDatas = Array.from(selectionRowsMap.values());
if (selectionDatas.length > 1) {
ElMessage.warning('只能编辑一行数据');
ElMessage.warning('只能选择一行数据');
return;
}
const data = selectionDatas[0];
state.tableDataFormDialog.data = { ...data };
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
state.tableDataFormDialog.title = state.table ? `'${props.table}'表单数据` : '表单视图';
state.tableDataFormDialog.visible = true;
};
@@ -648,7 +642,7 @@ const onGenerateJson = async () => {
// 按列字段重新排序对象key
const jsonObj = [];
for (let selectionData of selectionDatas) {
let obj = {};
let obj: any = {};
for (let column of state.columns) {
if (column.show) {
obj[column.title] = selectionData[column.dataKey];
@@ -752,7 +746,7 @@ const submitUpdateFields = async () => {
for (let updateRow of cellUpdateMap.values()) {
const rowData = { ...updateRow.rowData };
let updateColumnValue = {};
let updateColumnValue: any = {};
for (let k of updateRow.columnsMap.keys()) {
const v = updateRow.columnsMap.get(k);
@@ -767,11 +761,6 @@ const submitUpdateFields = async () => {
}
dbInst.promptExeSql(db, res, null, () => {
// 存在流程则恢复原值,需工单流程审批完后自动执行
if (dbInst.flowProcdef) {
cancelUpdateFields();
return;
}
triggerRefresh();
cellUpdateMap.clear();
changeUpdatedField();

View File

@@ -6,10 +6,10 @@
:key="column.columnName"
class="w100 mb5"
:prop="column.columnName"
:required="!column.nullable && !column.isPrimaryKey && !column.isIdentity"
:required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.isIdentity"
>
<template #label>
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
<span class="pointer" :title="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType">
{{ column.columnName }}
</span>
</template>
@@ -17,13 +17,13 @@
<ColumnFormItem
v-model="modelValue[`${column.columnName}`]"
:data-type="dbInst.getDialect().getDataType(column.dataType)"
:placeholder="`${column.columnType} ${column.columnComment}`"
:placeholder="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType"
:column-name="column.columnName"
:disabled="column.isIdentity"
/>
</el-form-item>
</el-form>
<template #footer>
<template #footer v-if="props.tableName">
<span class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirm">确定</el-button>
@@ -99,7 +99,7 @@ const confirm = async () => {
let sql = '';
if (oldValue) {
const updateColumnValue = {};
const updateColumnValue: any = {};
Object.keys(oldValue).forEach((key) => {
// 如果新旧值不相等,则为需要更新的字段
if (oldValue[key] !== modelValue.value[key]) {

View File

@@ -50,22 +50,6 @@
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<!-- 表数据展示配置 -->
<el-popover
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
placement="bottom"
width="auto"
title="展示配置"
trigger="click"
>
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
<template #reference>
<el-link type="primary" icon="setting" :underline="false"></el-link>
</template>
</el-popover>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="font12">提交</el-link>
</el-tooltip>
@@ -258,7 +242,7 @@ import { DbInst } from '@/views/ops/db/db';
import DbTableData from './DbTableData.vue';
import { DbDialect } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import { useEventListener, useStorage } from '@vueuse/core';
import { useEventListener } from '@vueuse/core';
import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
import DbTableDataForm from './DbTableDataForm.vue';
@@ -288,8 +272,6 @@ const condDialogInputRef: Ref = ref(null);
const defaultPageSize = DbInst.DefaultLimit;
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
const state = reactive({
datas: [],
sql: '', // 当前数据tab执行的sql
@@ -404,7 +386,8 @@ const selectData = async () => {
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
state.sql = sql;
const colAndData: any = await dbInst.runSql(db, sql);
const res: any = await dbInst.runSql(db, sql);
const colAndData: any = res[0];
state.datas = colAndData.res;
} finally {
state.loading = false;
@@ -435,7 +418,8 @@ const handleCount = async () => {
const db = props.dbName;
const table = props.tableName;
const dbInst = getNowDbInst();
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
let countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
countRes = countRes[0];
state.total = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
state.showTotal = true;
} catch (e) {

View File

@@ -30,7 +30,7 @@
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
<el-option
v-for="pgsqlType in getDbDialect(dbType).getInfo().columnTypes"
v-for="pgsqlType in getDbDialect(dbType!).getInfo().columnTypes"
:key="pgsqlType.dataType"
:value="pgsqlType.udtName"
:label="pgsqlType.dataType"
@@ -127,7 +127,7 @@
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, watch } from 'vue';
import { computed, reactive, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import SqlExecBox from '../sqleditor/SqlExecBox';
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
@@ -152,15 +152,15 @@ const props = defineProps({
dbType: {
type: String,
},
flowProcdef: {
type: Object,
version: {
type: String,
},
});
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
let dbDialect = getDbDialect(props.dbType);
let dbDialect: any = computed(() => getDbDialect(props.dbType!, props.version));
type ColName = {
prop: string;
@@ -274,7 +274,7 @@ const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
dbDialect = getDbDialect(newValue.dbType);
dbDialect.value = getDbDialect(newValue.dbType!);
});
// 切换到索引tab时刷新索引字段下拉选项
@@ -309,11 +309,11 @@ const addRow = () => {
};
const addIndex = () => {
state.tableData.indexs.res.push(dbDialect.getDefaultIndex());
state.tableData.indexs.res.push(dbDialect.value.getDefaultIndex());
};
const addDefaultRows = () => {
state.tableData.fields.res.push(...dbDialect.getDefaultRows());
state.tableData.fields.res.push(...dbDialect.value.getDefaultRows());
};
const deleteRow = (index: any) => {
@@ -334,8 +334,7 @@ const submit = async () => {
sql: sql,
dbId: props.dbId as any,
db: props.db as any,
dbType: dbDialect.getInfo().formatSqlDialect,
flowProcdef: props.flowProcdef,
dbType: dbDialect.value.getInfo().formatSqlDialect,
runSuccessCallback: () => {
emit('submit-sql', { tableName: state.tableData.tableName });
// cancel();
@@ -371,11 +370,11 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
return data;
}
let oldMap = {},
newMap = {};
oldArr.forEach((a) => (oldMap[a[key]] = a));
let oldMap: any = {},
newMap: any = {};
oldArr.forEach((a: any) => (oldMap[a[key]] = a));
nowArr.forEach((a) => {
nowArr.forEach((a: any) => {
let k = a[key];
newMap[k] = a;
// 取oldName因为修改了name但是oldName不会变
@@ -388,7 +387,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
}
});
oldArr.forEach((a) => {
oldArr.forEach((a: any) => {
let k = a[key];
let newData = newMap[k];
if (!newData) {
@@ -415,21 +414,22 @@ const genSql = () => {
let data = state.tableData;
// 创建表
if (!props.data?.edit) {
let createTable = dbDialect.getCreateTableSql(data);
let createTable = dbDialect.value.getCreateTableSql(data);
let createIndex = '';
if (data.indexs.res.length > 0) {
createIndex = dbDialect.getCreateIndexSql(data);
createIndex = dbDialect.value.getCreateIndexSql(data);
}
return createTable + ';' + createIndex;
} else {
// 修改列
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
let colSql = changeColData.changed ? dbDialect.value.getModifyColumnSql(data, data.tableName, changeColData) : '';
// 修改索引
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
let idxSql = changeIdxData.changed ? dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
let idxSql = changeIdxData.changed ? dbDialect.value.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
// 修改表名,表注释
let tableInfoSql = data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.getModifyTableInfoSql(data) : '';
let tableInfoSql =
data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.value.getModifyTableInfoSql(data) : '';
let sqlArr = [];
colSql && sqlArr.push(colSql);

View File

@@ -109,7 +109,6 @@
:dbId="dbId"
:db="db"
:dbType="dbType"
:flow-procdef="props.flowProcdef"
:data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitSql"
@@ -152,9 +151,6 @@ const props = defineProps({
type: [String],
required: true,
},
flowProcdef: {
type: [Object],
},
});
const state = reactive({
@@ -312,7 +308,6 @@ const dropTable = async (row: any) => {
sql: `DROP TABLE ${tableName}`,
dbId: props.dbId as any,
db: props.db as any,
flowProcdef: props.flowProcdef,
runSuccessCallback: async () => {
await getTables();
},

View File

@@ -9,6 +9,7 @@ import { registerCompletionItemProvider } from '@/components/monaco/completionIt
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
import { DbGetDbNamesMode } from './enums';
import { ElMessage } from 'element-plus';
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
@@ -41,11 +42,8 @@ export class DbInst {
*/
type: string;
/**
* 流程定义,若存在则需要审批执行
*/
flowProcdef: any;
/** 兼容版本 */
version: string;
/**
* dbName -> db
*/
@@ -226,12 +224,18 @@ export class DbInst {
* @param remark 执行备注
*/
async runSql(dbName: string, sql: string, remark: string = '') {
return await dbApi.sqlExec.request({
const res = await dbApi.sqlExec.request({
id: this.id,
db: dbName,
sql: sql.trim(),
remark,
});
for (let re of res) {
if (re.errorMsg) {
ElMessage.error(`${re.sql} -> 执行失败: ${re.errorMsg}`);
}
}
return res;
}
/**
@@ -311,7 +315,7 @@ export class DbInst {
* @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
* @param rowData 表的一行完整数据(需要获取主键信息)
*/
async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
async genUpdateSql(dbName: string, table: string, columnValue: any, rowData: any) {
let schema = '';
let dbArr = dbName.split('/');
if (dbArr.length == 2) {
@@ -360,7 +364,6 @@ export class DbInst {
dbType: this.getDialect().getInfo().formatSqlDialect,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
flowProcdef: this.flowProcdef,
});
};
@@ -378,17 +381,17 @@ export class DbInst {
* @param inst 数据库实例,后端返回的列表接口中的信息
* @returns DbInst
*/
static getOrNewInst(inst: any) {
static async getOrNewInst(inst: any) {
if (!inst) {
throw new Error('inst不能为空');
}
let dbInst = dbInstCache.get(inst.id);
if (dbInst) {
// 更新可能更改的流程定义
if (inst.flowProcdef !== undefined) {
dbInst.flowProcdef = inst.flowProcdef;
dbInstCache.set(dbInst.id, dbInst);
// 可能同一个库关联多个标签,展示需要
if (inst.tagPath) {
dbInst.tagPath = inst.tagPath;
}
return dbInst;
}
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
@@ -399,7 +402,10 @@ export class DbInst {
dbInst.name = inst.name;
dbInst.type = inst.type;
dbInst.databases = inst.databases;
dbInst.flowProcdef = inst.flowProcdef;
if (dbInst.databases?.[0]) {
dbInst.version = await dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] });
}
dbInstCache.set(dbInst.id, dbInst);
return dbInst;
@@ -408,7 +414,6 @@ export class DbInst {
/**
* 获取数据库实例id若不存在则新建一个并缓存
* @param dbId 数据库实例id
* @param dbType 第一次获取时为必传项,即第一次创建时
* @returns 数据库实例
*/
static getInst(dbId?: number): DbInst {
@@ -419,7 +424,26 @@ export class DbInst {
if (dbInst) {
return dbInst;
}
throw new Error('dbInst不存在! 请在合适调用点使用DbInst.newInst()新建该实例');
throw new Error('dbInst不存在! 请在合适调用点使用DbInst.getInstA()新建该实例');
}
/**
* 获取数据库实例信息,若不存在,调接口获取数据库信息
* @param dbId 数据库id
* @returns
*/
static async getInstA(dbId?: number): Promise<DbInst> {
if (!dbId) {
throw new Error('dbId不能为空');
}
let dbInst = dbInstCache.get(dbId);
if (dbInst) {
return Promise.resolve(dbInst);
}
const dbInfoRes = await dbApi.dbs.request({ id: dbId });
const db = dbInfoRes.list[0];
return Promise.resolve(DbInst.getOrNewInst(db));
}
/**
@@ -649,7 +673,7 @@ export function registerDbCompletionItemProvider(dbId: number, db: string, dbs:
triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position);
const dbInst = DbInst.getInst(dbId);
const dbInst = await DbInst.getInstA(dbId);
const { lineNumber, column } = position;
const { startColumn, endColumn } = word;
@@ -842,3 +866,23 @@ function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string)
return tables.length > 0 ? tables[0] : undefined;
}
}
/**
* 数据库主题配置
*/
export const DbThemeConfig = {
/**
* 表数据表头是否显示备注
*/
showColumnComment: true,
/**
* 是否自动定位至树节点
*/
locationTreeNode: true,
/**
* 是否缓存表信息
*/
cacheTable: true,
};

View File

@@ -1,13 +1,14 @@
import { MysqlDialect } from './mysql_dialect';
import { PostgresqlDialect } from './postgres_dialect';
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
import {MysqlDialect} from './mysql_dialect';
import {PostgresqlDialect} from './postgres_dialect';
import {DMDialect} from '@/views/ops/db/dialect/dm_dialect';
import {OracleDialect} from '@/views/ops/db/dialect/oracle_dialect';
import {MariadbDialect} from '@/views/ops/db/dialect/mariadb_dialect';
import {SqliteDialect} from '@/views/ops/db/dialect/sqlite_dialect';
import {MssqlDialect} from '@/views/ops/db/dialect/mssql_dialect';
import {GaussDialect} from '@/views/ops/db/dialect/gauss_dialect';
import {KingbaseEsDialect} from '@/views/ops/db/dialect/kingbaseES_dialect';
import {VastbaseDialect} from '@/views/ops/db/dialect/vastbase_dialect';
import {Oracle11Dialect} from "@/views/ops/db/dialect/oracle11_dialect";
export interface sqlColumnType {
udtName: string;
@@ -37,6 +38,7 @@ export interface IndexDefinition {
indexType: string;
indexComment?: string;
}
export const commonCustomKeywords = ['GROUP BY', 'ORDER BY', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'SELECT * FROM'];
export interface EditorCompletionItem {
@@ -69,7 +71,7 @@ export enum DataType {
}
/** 列数据类型角标 */
export const ColumnTypeSubscript = {
export const ColumnTypeSubscript: any = {
/** 字符串 */
string: 'ab',
/** 数字 */
@@ -212,7 +214,11 @@ export interface DbDialect {
* @param tableName 表名
* @param changeData 改变信息
*/
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
getModifyColumnSql(tableData: any, tableName: string, changeData: {
del: RowDefinition[];
add: RowDefinition[];
upd: RowDefinition[]
}): string;
/**
* 生成编辑索引sql
@@ -249,17 +255,21 @@ export enum DuplicateStrategy {
let mysqlDialect = new MysqlDialect();
let dbType2DialectMap: Map<string, DbDialect> = new Map();
let dbType2DialectVersionMap: Map<string, DbDialect> = new Map();
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
dbType2DialectMap.set(dbType, dd);
};
export const registerDbDialectVersion = (dbType: string, dd: DbDialect) => {
dbType2DialectVersionMap.set(dbType, dd);
};
export const getDbDialectMap = () => {
return dbType2DialectMap;
};
export const getDbDialect = (dbType?: string): DbDialect => {
return dbType2DialectMap.get(dbType!) || mysqlDialect;
export const getDbDialect = (dbType: string, version = ''): DbDialect => {
return dbType2DialectVersionMap.get(dbType + version) || dbType2DialectMap.get(dbType) || mysqlDialect;
};
/**
@@ -282,6 +292,7 @@ export const QuoteEscape = (str: string): string => {
registerDbDialect(DbType.gauss, new GaussDialect());
registerDbDialect(DbType.dm, new DMDialect());
registerDbDialect(DbType.oracle, new OracleDialect());
registerDbDialectVersion(DbType.oracle + '11', new Oracle11Dialect()); // oracle 11g及以前版本的一些语法兼容
registerDbDialect(DbType.sqlite, new SqliteDialect());
registerDbDialect(DbType.mssql, new MssqlDialect());
registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());

View File

@@ -0,0 +1,55 @@
/** oracle 11g 及以前的版本的一些语法兼容 */
import {OracleDialect} from '@/views/ops/db/dialect/oracle_dialect';
import {DialectInfo, RowDefinition} from '@/views/ops/db/dialect/index';
let oracle11DialectInfo: DialectInfo;
export class Oracle11Dialect extends OracleDialect {
getInfo(): DialectInfo {
if (oracle11DialectInfo) {
return oracle11DialectInfo;
}
oracle11DialectInfo = {} as DialectInfo;
Object.assign(oracle11DialectInfo, super.getInfo());
oracle11DialectInfo.name = 'Oracle11x';
return oracle11DialectInfo;
}
// 重写创建自增列sql
genColumnBasicSql(cl: RowDefinition, create: boolean, data = {}): string {
let length = this.getTypeLengthSql(cl);
// 默认值
let defVal = this.getDefaultValueSql(cl, false, data);
// 忽略自增配置11g不支持直接设置自增列需要单独设置自增序列
// 如果有原名以原名为准
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length}`;
return ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
}
getDefaultValueSql(cl: RowDefinition, create?: boolean, data?: any): string {
if (cl.value) {
return ` DEFAULT ${cl.value}`;
} else if (cl.auto_increment) {
return ` DEFAULT ${data.tableName}_${cl.name}_SEQ.NEXTVAL`;
}
return '';
}
getOtherCreateTableSql(data: any): string {
// 通过字段自增信息创建自增序列
let result = '';
data.fields.res.forEach((field: RowDefinition) => {
let seqName = `${data.tableName}_${field.name}_SEQ`;
if (field.auto_increment) {
result += `CREATE SEQUENCE ${seqName} START WITH 1 INCREMENT BY 1 CACHE 20`;
}
});
return result;
}
}

View File

@@ -7,8 +7,8 @@ import {
DuplicateStrategy,
EditorCompletion,
EditorCompletionItem,
QuoteEscape,
IndexDefinition,
QuoteEscape,
RowDefinition,
sqlColumnType,
} from './index';
@@ -85,10 +85,10 @@ const replaceFunctions: EditorCompletionItem[] = [
{ label: 'CURRENT_DATE', insertText: 'CURRENT_DATE', description: '获取当前日期' },
{ label: 'CURRENT_TIMESTAMP', insertText: 'TIMESTAMP', description: '获取当前时间' },
// 转换函数
{ label: 'TO_CHAR', insertText: 'TO_CHAR(d|n[,fmt])', description: '把日期和数字转换为制定格式的字符串' },
{ label: 'TO_CHAR', insertText: `TO_CHAR(d|n, 'yyyy-MM-dd HH24:mi:ss')`, description: '把日期和数字转换为制定格式的字符串' },
{ label: 'TO_DATE', insertText: `TO_DATE(X, 'yyyy-MM-dd HH24:mi:ss')`, description: '把一个字符串以fmt格式转换成一个日期类型' },
{ label: 'TO_NUMBER', insertText: 'TO_NUMBER(X,[,fmt])', description: '把一个字符串以fmt格式转换为一个数字' },
{ label: 'TO_TIMESTAMP', insertText: 'TO_TIMESTAMP(X,[,fmt])', description: '把一个字符串以fmt格式转换为日期类型' },
{ label: 'TO_NUMBER', insertText: `TO_NUMBER(X, 'yyyy-MM-dd HH24:mi:ss')`, description: '把一个字符串以fmt格式转换为一个数字' },
{ label: 'TO_TIMESTAMP', insertText: `TO_TIMESTAMP(X, 'yyyy-MM-dd HH24:mi:ss.ff')`, description: '把一个字符串以fmt格式转换为日期类型' },
// 其他
{ label: 'NVL', insertText: 'NVL(X,VALUE)', description: '如果X为空返回value否则返回X' },
{ label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空返回value1否则返回value2' },
@@ -293,7 +293,7 @@ class OracleDialect implements DbDialect {
return '';
}
genColumnBasicSql(cl: RowDefinition, create: boolean): string {
genColumnBasicSql(cl: RowDefinition, create: boolean, data = {}): string {
let length = this.getTypeLengthSql(cl);
// 默认值
let defVal = this.getDefaultValueSql(cl);
@@ -309,6 +309,11 @@ class OracleDialect implements DbDialect {
return incr ? baseSql : ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
getOtherCreateTableSql(data: any) {
return '';
}
getCreateTableSql(data: any): string {
let schemaArr = data.db.split('/');
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
@@ -322,7 +327,7 @@ class OracleDialect implements DbDialect {
// 创建表结构
let fields: string[] = [];
data.fields.res.forEach((item: any) => {
item.name && fields.push(this.genColumnBasicSql(item, true));
item.name && fields.push(this.genColumnBasicSql(item, true, data));
// 列注释
if (item.remark) {
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${QuoteEscape(item.remark)}'; `;
@@ -344,7 +349,9 @@ class OracleDialect implements DbDialect {
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${QuoteEscape(data.tableComment)}'; `;
}
return createSql + tableCommentSql + columCommentSql;
// 其余建表信息,如:自增字段在老版本的使用方式是创建自增序列
let other = this.getOtherCreateTableSql(data);
return createSql + tableCommentSql + columCommentSql + other;
}
getCreateIndexSql(tableData: any): string {
@@ -391,7 +398,7 @@ class OracleDialect implements DbDialect {
commentArr.push(commentSql);
}
}
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false, tableData)})`);
if (a.pri) {
priArr.add(`${this.quoteIdentifier(a.name)}`);
}
@@ -400,7 +407,7 @@ class OracleDialect implements DbDialect {
if (changeData.add.length > 0) {
changeData.add.forEach((a) => {
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false, tableData)})`);
if (a.remark) {
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${QuoteEscape(a.remark)}'`);
}

View File

@@ -79,7 +79,11 @@ const functions: EditorCompletionItem[] = [
{ label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null' },
{ label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串' },
{ label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值' },
{ label: 'sqlite_compileoption_used', insertText: 'sqlite_compileoption_used(X)', description: '检查SQLite编译时是否使用了指定的编译选项' },
{
label: 'sqlite_compileoption_used',
insertText: 'sqlite_compileoption_used(X)',
description: '检查SQLite编译时是否使用了指定的编译选项',
},
{ label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符' },
{ label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本' },
{ label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串' },
@@ -98,12 +102,21 @@ const functions: EditorCompletionItem[] = [
{ label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。' },
{ label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串' },
{ label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串' },
{ label: 'time', insertText: 'time(time-value[, modifier, ...])', description: '将日期和时间字符串转换为特定的日期和时间格式' },
{
label: 'time',
insertText: 'time(time-value[, modifier, ...])',
description: '将日期和时间字符串转换为特定的日期和时间格式',
},
{ label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
{ label: 'julianday', insertText: 'julianday(time-value[, modifier, ...])', description: '将日期和时间格式化为指定的字符串' },
{
label: 'julianday',
insertText: 'julianday(time-value[, modifier, ...])',
description: '将日期和时间格式化为指定的字符串',
},
];
let sqliteDialectInfo: DialectInfo;
class SqliteDialect implements DbDialect {
getInfo(): DialectInfo {
if (sqliteDialectInfo) {
@@ -124,7 +137,7 @@ class SqliteDialect implements DbDialect {
};
sqliteDialectInfo = {
name: 'Sqlite',
name: 'Sqlite3',
icon: 'iconfont icon-sqlite',
defaultPort: 0,
formatSqlDialect: 'sql',
@@ -135,10 +148,8 @@ class SqliteDialect implements DbDialect {
}
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum,
limit
)};`;
return `SELECT *
FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
}
getPageSql(pageNum: number, limit: number) {
@@ -147,8 +158,28 @@ class SqliteDialect implements DbDialect {
getDefaultRows(): RowDefinition[] {
return [
{ name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
{
name: 'id',
type: 'integer',
length: '',
numScale: '',
value: '',
notNull: true,
pri: true,
auto_increment: true,
remark: '主键ID',
},
{
name: 'creator_id',
type: 'bigint',
length: '20',
numScale: '',
value: '',
notNull: true,
pri: false,
auto_increment: false,
remark: '创建人id',
},
{
name: 'creator',
type: 'varchar',
@@ -171,8 +202,28 @@ class SqliteDialect implements DbDialect {
auto_increment: false,
remark: '创建时间',
},
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
{ name: 'updator', type: 'varchar', length: '100', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改姓名' },
{
name: 'updator_id',
type: 'bigint',
length: '20',
numScale: '',
value: '',
notNull: true,
pri: false,
auto_increment: false,
remark: '修改人id',
},
{
name: 'updator',
type: 'varchar',
length: '100',
numScale: '',
value: '',
notNull: true,
pri: false,
auto_increment: false,
remark: '修改姓名',
},
{
name: 'update_time',
type: 'datetime',
@@ -211,6 +262,7 @@ class SqliteDialect implements DbDialect {
}
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${nullAble} ${defVal} `;
}
getCreateTableSql(data: any): string {
// 创建表结构
let fields: string[] = [];
@@ -219,7 +271,9 @@ class SqliteDialect implements DbDialect {
});
return `CREATE TABLE ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(data.tableName)}
( ${fields.join(',')} )`;
(
${fields.join(',')}
)`;
}
getCreateIndexSql(data: any): string {
@@ -227,13 +281,30 @@ class SqliteDialect implements DbDialect {
let sql = [] as string[];
data.indexs.res.forEach((a: any) => {
sql.push(
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(a.indexName)} ON "${data.tableName}" (${a.columnNames.join(',')})`
`CREATE
${a.unique ? 'UNIQUE' : ''} INDEX
${this.quoteIdentifier(data.db)}
.
${this.quoteIdentifier(a.indexName)}
ON
"${data.tableName}"
(
${a.columnNames.join(',')}
)`
);
});
return sql.join(';');
}
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
getModifyColumnSql(
tableData: any,
tableName: string,
changeData: {
del: RowDefinition[];
add: RowDefinition[];
upd: RowDefinition[];
}
): string {
// sqlite修改表结构需要先删除再创建
// 1.删除旧表索引 DROP INDEX "main"."aa";
@@ -270,16 +341,25 @@ class SqliteDialect implements DbDialect {
});
// 生成sql
sql.push(
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')}) SELECT ${queryFields.join(
','
)} FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')})
SELECT ${queryFields.join(',')}
FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
);
// 5.创建索引
tableData.indexs.res.forEach((a: any) => {
a.indexName &&
sql.push(
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)} ON "${tableName}" (${a.columnNames.join(',')})`
`CREATE
${a.unique ? 'UNIQUE' : ''} INDEX
${this.quoteIdentifier(tableData.db)}
.
${this.quoteIdentifier(a.indexName)}
ON
"${tableName}"
(
${a.columnNames.join(',')}
)`
);
});
@@ -308,7 +388,14 @@ class SqliteDialect implements DbDialect {
if (indexData.length > 0) {
indexData.forEach((a) => {
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} ON ${tableName} (${a.columnNames.join(',')})`);
sql.push(`CREATE
${a.unique ? 'UNIQUE' : ''} INDEX
${this.quoteIdentifier(a.indexName)}
ON
${tableName}
(
${a.columnNames.join(',')}
)`);
});
}
return sql.join(';');

View File

@@ -11,6 +11,7 @@ export const DbSqlExecTypeEnum = {
Delete: EnumValue.of(2, 'DELETE').setTagColor('#F9E2AE'),
Insert: EnumValue.of(3, 'INSERT').setTagColor('#A8DEE0'),
Query: EnumValue.of(4, 'QUERY').setTagColor('#A8DEE0'),
Ddl: EnumValue.of(5, 'DDL').setTagColor('#F9E2AE'),
Other: EnumValue.of(-1, 'OTHER').setTagColor('#F9E2AE'),
};
@@ -42,3 +43,9 @@ export const DbTransferRunningStateEnum = {
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
Stop: EnumValue.of(-2, '手动终止').setTagType('warning'),
};
export const DbTransferFileStatusEnum = {
Running: EnumValue.of(1, '执行中').setTagType('primary'),
Success: EnumValue.of(2, '成功').setTagType('success'),
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
};

View File

@@ -20,16 +20,8 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入机器" auto-complete="off"></el-input>
<el-input v-model.trim="form.name" placeholder="请输入机器名不可重复" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="protocol" label="协议" required>
<el-radio-group v-model="form.protocol" @change="handleChangeProtocol">
@@ -90,7 +82,6 @@ import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vu
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { MachineProtocolEnum } from './enums';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { ResourceCodePattern } from '@/common/pattern';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
@@ -118,18 +109,18 @@ const rules = {
trigger: ['change'],
},
],
code: [
{
required: true,
message: '请输入编码',
trigger: ['change', 'blur'],
},
{
pattern: ResourceCodePattern.pattern,
message: ResourceCodePattern.message,
trigger: ['blur'],
},
],
// code: [
// {
// required: true,
// message: '请输入编码',
// trigger: ['change', 'blur'],
// },
// {
// pattern: ResourceCodePattern.pattern,
// message: ResourceCodePattern.message,
// trigger: ['blur'],
// },
// ],
name: [
{
required: true,

View File

@@ -278,10 +278,8 @@ const perms = {
};
const searchItems = [
SearchItem.input('keyword', '关键字').withPlaceholder('ip / 名称 / 编号'),
getTagPathSearchItem(TagResourceTypeEnum.MachineAuthCert.value),
SearchItem.input('code', '编号'),
SearchItem.input('ip', 'IP'),
SearchItem.input('name', '名称'),
];
const columns = [
@@ -298,7 +296,7 @@ const columns = [
];
// 该用户拥有的的操作列按钮权限使用v-if进行判断v-auth对el-dropdown-item无效
const actionBtns = hasPerms([perms.updateMachine]);
const actionBtns: any = hasPerms([perms.updateMachine]);
const state = reactive({
params: {

View File

@@ -10,6 +10,10 @@
@open="getTermOps()"
>
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
<template #fileKey="{ data }">
<FileInfo :fileKey="data.fileKey" />
</template>
<template #action="{ data }">
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
<el-button @click="showExecCmds(data)" type="primary" link>命令</el-button>
@@ -49,6 +53,8 @@ import 'asciinema-player/dist/bundle/asciinema-player.css';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { formatDate } from '@/common/utils/format';
import { getFileUrl } from '@/common/request';
import FileInfo from '@/components/file/FileInfo.vue';
const props = defineProps({
visible: { type: Boolean },
@@ -62,7 +68,7 @@ const columns = [
TableColumn.new('creator', '操作者').setMinWidth(120),
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
TableColumn.new('fileKey', '文件').isSlot(),
TableColumn.new('action', '操作').isSlot().setMinWidth(120).fixedRight().alignCenter(),
];
@@ -109,14 +115,9 @@ const playRec = async (rec: any) => {
player.dispose();
}
rec.playRecLoding = true;
const content = await machineApi.termOpRec.request({
recId: rec.id,
id: rec.machineId,
});
state.playerDialogVisible = true;
nextTick(() => {
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
player = AsciinemaPlayer.create(getFileUrl(rec.fileKey), playerRef.value, {
autoPlay: true,
speed: 1.0,
idleTimeLimit: 2,

View File

@@ -5,6 +5,7 @@ import { joinClientParams } from '@/common/request';
export const machineApi = {
// 获取权限列表
list: Api.newGet('/machines'),
getByCodes: Api.newGet('/machines/simple'),
tagList: Api.newGet('/machines/tags'),
getMachinePwd: Api.newGet('/machines/{id}/pwd'),
info: Api.newGet('/machines/{id}/sysinfo'),
@@ -46,8 +47,6 @@ export const machineApi = {
delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
// 机器终端操作记录列表
termOpRecs: Api.newGet('/machines/{machineId}/term-recs'),
// 机器终端操作记录详情
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
};
export const cronJobApi = {

View File

@@ -17,14 +17,7 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入名称" auto-complete="off"></el-input>
</el-form-item>
@@ -64,7 +57,6 @@ import { mongoApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { ResourceCodePattern } from '@/common/pattern';
const props = defineProps({
visible: {
@@ -89,18 +81,6 @@ const rules = {
trigger: ['change', 'blur'],
},
],
code: [
{
required: true,
message: '请输入编码',
trigger: ['change', 'blur'],
},
{
pattern: ResourceCodePattern.pattern,
message: ResourceCodePattern.message,
trigger: ['blur'],
},
],
name: [
{
required: true,

View File

@@ -68,7 +68,7 @@ const props = defineProps({
const route = useRoute();
const pageTableRef: Ref<any> = ref(null);
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Mongo.value), SearchItem.input('code', '编号')];
const searchItems = [SearchItem.input('keyword', '关键字').withPlaceholder('host / 名称 / 编号'), getTagPathSearchItem(TagResourceTypeEnum.Mongo.value)];
const columns = [
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),

View File

@@ -201,7 +201,6 @@ import { Splitpanes, Pane } from 'splitpanes';
import { RedisInst } from './redis';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
import { procdefApi } from '@/views/flow/api';
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
@@ -249,13 +248,11 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
// redis实例节点类型
const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const redisInfo = parentNode.params;
const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.Redis.value, resourceCode: redisInfo.code });
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
return new TagTreeNode(x, `db${x}`, NodeTypeDb).withIsLeaf(true).withParams({
id: redisInfo.id,
db: x,
flowProcdef: flowProcdef,
name: `db${x}`,
keys: 0,
});
@@ -288,7 +285,6 @@ const NodeTypeDb = new NodeType(RedisNodeType.Db).withNodeClickFunc((nodeData: T
redisInst.value.id = nodeData.params.id;
redisInst.value.db = Number.parseInt(nodeData.params.db);
redisInst.value.flowProcdef = nodeData.params.flowProcdef;
scan();
});
@@ -366,7 +362,7 @@ const autoOpenRedis = (codePath: string) => {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const typeAndCodes: any = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const redisCode = typeAndCodes[TagResourceTypeEnum.Redis.value][0];

View File

@@ -1,12 +1,13 @@
<template>
<div class="format-viewer-container">
<div class="mb5 fr">
<el-select v-model="selectedView" class="format-selector" size="mini" placeholder="Text">
<el-select v-model="selectedView" class="format-selector" size="small" placeholder="Text">
<template #prefix>
<SvgIcon name="view" />
</template>
<el-option v-for="item of Object.keys(viewers)" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-tag type="primary" :disable-transitions="true" class="ml10">Size: {{ formatByteSize(state.contentSize) }}</el-tag>
</div>
<component ref="viewerRef" :is="components[viewerComponent]" :content="state.content" :name="selectedView"> </component>
@@ -16,6 +17,7 @@
import { ref, reactive, computed, shallowReactive, watch, toRefs, onMounted } from 'vue';
import ViewerText from './ViewerText.vue';
import ViewerJson from './ViewerJson.vue';
import { formatByteSize } from '@/common/utils/format';
const props = defineProps({
content: {
@@ -27,7 +29,7 @@ const props = defineProps({
},
});
const components = shallowReactive({
const components: any = shallowReactive({
ViewerText,
ViewerJson,
});
@@ -35,10 +37,11 @@ const viewerRef: any = ref(null);
const state = reactive({
content: '',
contentSize: 0,
selectedView: 'Text',
});
const viewers = {
const viewers: any = {
Text: {
value: 'ViewerText',
},
@@ -67,6 +70,7 @@ onMounted(() => {
const setContent = (content: string) => {
state.content = content;
state.contentSize = new Blob([content]).size;
try {
JSON.parse(content);
state.selectedView = 'Json';

View File

@@ -21,14 +21,6 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input>
</el-form-item>
@@ -51,11 +43,15 @@
<el-input v-model.trim="form.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="form.mode == 'sentinel'" prop="redisNodePassword" label="节点密码">
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码, 修改操作可不填"
v-model.trim="form.redisNodePassword"
placeholder="请输入Redis节点密码"
autocomplete="new-password"
>
</el-input>
@@ -104,7 +100,6 @@ import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { ResourceCodePattern } from '@/common/pattern';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const props = defineProps({
@@ -129,18 +124,6 @@ const rules = {
trigger: ['blur', 'change'],
},
],
code: [
{
required: true,
message: '请输入编码',
trigger: ['change', 'blur'],
},
{
pattern: ResourceCodePattern.pattern,
message: ResourceCodePattern.message,
trigger: ['blur'],
},
],
name: [
{
required: true,
@@ -186,6 +169,7 @@ const state = reactive({
host: '',
username: null,
password: null,
redisNodePassword: null,
db: '',
remark: '',
sshTunnelMachineId: -1,

View File

@@ -172,7 +172,7 @@ const props = defineProps({
const route = useRoute();
const pageTableRef: Ref<any> = ref(null);
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Redis.value), SearchItem.input('code', '编号')];
const searchItems = [SearchItem.input('keyword', '关键字').withPlaceholder('host / 名称 / 编号'), getTagPathSearchItem(TagResourceTypeEnum.Redis.value)];
const columns = ref([
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-dialog title="待执行cmd" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
<el-input type="textarea" disabled v-model="state.cmdStr" class="mt5" rows="5" />
<el-input type="textarea" disabled v-model="state.cmdStr" class="mt5" :rows="5" />
<el-input @keyup.enter="runCmd" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
<div v-if="props.flowProcdef">

View File

@@ -1,5 +1,5 @@
import { redisApi } from './api';
import showCmdExecBox from './components/CmdExecBox';
// import showCmdExecBox from './components/CmdExecBox';
export class RedisInst {
/**
@@ -12,28 +12,23 @@ export class RedisInst {
*/
db: number;
/**
* 流程定义,若存在则需要审批执行
*/
flowProcdef: any;
/**
* 执行命令
* @param cmd 命令列表如:['SET', 'key', 'value']
* @returns 执行结果
*/
async runCmd(cmd: any[]) {
// 工单流程定义存在,并且为写入命令时,弹窗输入工单相关信息并提交
if (this.flowProcdef && writeCmd[cmd[0].toUpperCase()]) {
showCmdExecBox({
id: this.id,
db: this.db,
flowProcdef: this.flowProcdef,
cmd,
});
// 报错,阻止后续继续执行
throw new Error('提交工单执行');
}
// // 工单流程定义存在,并且为写入命令时,弹窗输入工单相关信息并提交
// if (this.flowProcdef && writeCmd[cmd[0].toUpperCase()]) {
// showCmdExecBox({
// id: this.id,
// db: this.db,
// flowProcdef: this.flowProcdef,
// cmd,
// });
// // 报错,阻止后续继续执行
// throw new Error('提交工单执行');
// }
return await redisApi.runCmd.request({
id: this.id,
@@ -43,96 +38,96 @@ export class RedisInst {
}
}
const writeCmd = {
APPEND: 'APPEND key value',
BLMOVE: 'BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout',
BLPOP: 'BLPOP key [key ...] timeout',
BRPOP: 'BRPOP key [key ...] timeout',
BRPOPLPUSH: 'BRPOPLPUSH source destination timeout',
BZPOPMAX: 'BZPOPMAX key [key ...] timeout',
BZPOPMIN: 'BZPOPMIN key [key ...] timeout',
COPY: 'COPY source destination [DB destination-db] [REPLACE]',
DECR: 'DECR key',
DECRBY: 'DECRBY key decrement',
DEL: 'DEL key [key ...]',
EVAL: 'EVAL script numkeys key [key ...] arg [arg ...]',
EVALSHA: 'EVALSHA sha1 numkeys key [key ...] arg [arg ...]',
EXPIRE: 'EXPIRE key seconds',
EXPIREAT: 'EXPIREAT key timestamp',
FLUSHALL: 'FLUSHALL',
FLUSHDB: 'FLUSHDB',
GEOADD: 'GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]',
GETDEL: 'GETDEL key',
GETSET: 'GETSET key value',
HDEL: 'HDEL key field [field ...]',
HINCRBY: 'HINCRBY key field increment',
HINCRBYFLOAT: 'HINCRBYFLOAT key field increment',
HMSET: 'HMSET key field value [field value ...]',
HSET: 'HSET key field value',
HSETNX: 'HSETNX key field value',
INCR: 'INCR key',
INCRBY: 'INCRBY key increment',
INCRBYFLOAT: 'INCRBYFLOAT key increment',
LINSERT: 'LINSERT key BEFORE|AFTER pivot value',
LMOVE: 'LMOVE source destination LEFT|RIGHT LEFT|RIGHT',
LPOP: 'LPOP key',
LPUSH: 'LPUSH key value [value ...]',
LPUSHX: 'LPUSHX key value',
LREM: 'LREM key count value',
LSET: 'LSET key index value',
LTRIM: 'LTRIM key start stop',
MIGRATE: 'MIGRATE host port key destination-db timeout',
MOVE: 'MOVE key db',
MSET: 'MSET key value [key value ...]',
MSETNX: 'MSETNX key value [key value ...]',
PERSIST: 'PERSIST key',
PEXPIRE: 'PEXPIRE key milliseconds',
PEXPIREAT: 'PEXPIREAT key milliseconds-timestamp',
PSETEX: 'PSETEX key milliseconds value',
PUBLISH: 'PUBLISH channel message',
RENAME: 'RENAME key newkey',
RENAMENX: 'RENAMENX key newkey',
RESTORE: 'RESTORE key ttl serialized-value',
RPOP: 'RPOP key',
RPOPLPUSH: 'RPOPLPUSH source destination',
RPUSH: 'RPUSH key value [value ...]',
RPUSHX: 'RPUSHX key value',
SADD: 'SADD key member [member ...]',
SCRIPT: ['SCRIPT EXISTS script [script ...]', 'SCRIPT FLUSH', 'SCRIPT KILL', 'SCRIPT LOAD script'],
SDIFFSTORE: 'SDIFFSTORE destination key [key ...]',
SET: 'SET key value',
SETBIT: 'SETBIT key offset value',
SETEX: 'SETEX key seconds value',
SETNX: 'SETNX key value',
SETRANGE: 'SETRANGE key offset value',
SINTERSTORE: 'SINTERSTORE destination key [key ...]',
SMOVE: 'SMOVE source destination member',
SORT: 'SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]',
SPOP: 'SPOP key',
SREM: 'SREM key member [member ...]',
SUNIONSTORE: 'SUNIONSTORE destination key [key ...]',
SWAPDB: 'SWAPDB index1 index2',
UNLINK: 'UNLINK key [key ...]',
XADD: 'XADD key ID field string [field string ...]',
XDEL: 'XDEL key ID [ID ...]',
XGROUP: [
'XGROUP CREATE key groupname id|$ [MKSTREAM]',
'XGROUP CREATECONSUMER key groupname consumername',
'XGROUP DELCONSUMER key groupname consumername',
'XGROUP DESTROY key groupname',
'XGROUP SETID key groupname id|$',
],
XTRIM: 'XTRIM key MAXLEN [~] count',
ZADD: 'ZADD key score member [score] [member]',
ZDIFFSTORE: 'ZDIFFSTORE destination numkeys key [key ...]',
ZINCRBY: 'ZINCRBY key increment member',
ZINTERSTORE: 'ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
ZPOPMAX: 'ZPOPMAX key [count]',
ZPOPMIN: 'ZPOPMIN key [count]',
ZRANGESTORE: 'ZRANGESTORE dst src min max [BYSCORE|BYLEX] [REV] [LIMIT offset count]',
ZREM: 'ZREM key member [member ...]',
ZREMRANGEBYLEX: 'ZREMRANGEBYLEX key min max',
ZREMRANGEBYRANK: 'ZREMRANGEBYRANK key start stop',
ZREMRANGEBYSCORE: 'ZREMRANGEBYSCORE key min max',
ZUNIONSTORE: 'ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
};
// const writeCmd = {
// APPEND: 'APPEND key value',
// BLMOVE: 'BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout',
// BLPOP: 'BLPOP key [key ...] timeout',
// BRPOP: 'BRPOP key [key ...] timeout',
// BRPOPLPUSH: 'BRPOPLPUSH source destination timeout',
// BZPOPMAX: 'BZPOPMAX key [key ...] timeout',
// BZPOPMIN: 'BZPOPMIN key [key ...] timeout',
// COPY: 'COPY source destination [DB destination-db] [REPLACE]',
// DECR: 'DECR key',
// DECRBY: 'DECRBY key decrement',
// DEL: 'DEL key [key ...]',
// EVAL: 'EVAL script numkeys key [key ...] arg [arg ...]',
// EVALSHA: 'EVALSHA sha1 numkeys key [key ...] arg [arg ...]',
// EXPIRE: 'EXPIRE key seconds',
// EXPIREAT: 'EXPIREAT key timestamp',
// FLUSHALL: 'FLUSHALL',
// FLUSHDB: 'FLUSHDB',
// GEOADD: 'GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]',
// GETDEL: 'GETDEL key',
// GETSET: 'GETSET key value',
// HDEL: 'HDEL key field [field ...]',
// HINCRBY: 'HINCRBY key field increment',
// HINCRBYFLOAT: 'HINCRBYFLOAT key field increment',
// HMSET: 'HMSET key field value [field value ...]',
// HSET: 'HSET key field value',
// HSETNX: 'HSETNX key field value',
// INCR: 'INCR key',
// INCRBY: 'INCRBY key increment',
// INCRBYFLOAT: 'INCRBYFLOAT key increment',
// LINSERT: 'LINSERT key BEFORE|AFTER pivot value',
// LMOVE: 'LMOVE source destination LEFT|RIGHT LEFT|RIGHT',
// LPOP: 'LPOP key',
// LPUSH: 'LPUSH key value [value ...]',
// LPUSHX: 'LPUSHX key value',
// LREM: 'LREM key count value',
// LSET: 'LSET key index value',
// LTRIM: 'LTRIM key start stop',
// MIGRATE: 'MIGRATE host port key destination-db timeout',
// MOVE: 'MOVE key db',
// MSET: 'MSET key value [key value ...]',
// MSETNX: 'MSETNX key value [key value ...]',
// PERSIST: 'PERSIST key',
// PEXPIRE: 'PEXPIRE key milliseconds',
// PEXPIREAT: 'PEXPIREAT key milliseconds-timestamp',
// PSETEX: 'PSETEX key milliseconds value',
// PUBLISH: 'PUBLISH channel message',
// RENAME: 'RENAME key newkey',
// RENAMENX: 'RENAMENX key newkey',
// RESTORE: 'RESTORE key ttl serialized-value',
// RPOP: 'RPOP key',
// RPOPLPUSH: 'RPOPLPUSH source destination',
// RPUSH: 'RPUSH key value [value ...]',
// RPUSHX: 'RPUSHX key value',
// SADD: 'SADD key member [member ...]',
// SCRIPT: ['SCRIPT EXISTS script [script ...]', 'SCRIPT FLUSH', 'SCRIPT KILL', 'SCRIPT LOAD script'],
// SDIFFSTORE: 'SDIFFSTORE destination key [key ...]',
// SET: 'SET key value',
// SETBIT: 'SETBIT key offset value',
// SETEX: 'SETEX key seconds value',
// SETNX: 'SETNX key value',
// SETRANGE: 'SETRANGE key offset value',
// SINTERSTORE: 'SINTERSTORE destination key [key ...]',
// SMOVE: 'SMOVE source destination member',
// SORT: 'SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]',
// SPOP: 'SPOP key',
// SREM: 'SREM key member [member ...]',
// SUNIONSTORE: 'SUNIONSTORE destination key [key ...]',
// SWAPDB: 'SWAPDB index1 index2',
// UNLINK: 'UNLINK key [key ...]',
// XADD: 'XADD key ID field string [field string ...]',
// XDEL: 'XDEL key ID [ID ...]',
// XGROUP: [
// 'XGROUP CREATE key groupname id|$ [MKSTREAM]',
// 'XGROUP CREATECONSUMER key groupname consumername',
// 'XGROUP DELCONSUMER key groupname consumername',
// 'XGROUP DESTROY key groupname',
// 'XGROUP SETID key groupname id|$',
// ],
// XTRIM: 'XTRIM key MAXLEN [~] count',
// ZADD: 'ZADD key score member [score] [member]',
// ZDIFFSTORE: 'ZDIFFSTORE destination numkeys key [key ...]',
// ZINCRBY: 'ZINCRBY key increment member',
// ZINTERSTORE: 'ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
// ZPOPMAX: 'ZPOPMAX key [count]',
// ZPOPMIN: 'ZPOPMIN key [count]',
// ZRANGESTORE: 'ZRANGESTORE dst src min max [BYSCORE|BYLEX] [REV] [LIMIT offset count]',
// ZREM: 'ZREM key member [member ...]',
// ZREMRANGEBYLEX: 'ZREMRANGEBYLEX key min max',
// ZREMRANGEBYRANK: 'ZREMRANGEBYRANK key start stop',
// ZREMRANGEBYSCORE: 'ZREMRANGEBYSCORE key min max',
// ZUNIONSTORE: 'ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
// };

View File

@@ -52,9 +52,9 @@
/>
<span class="ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
{{ data.code }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag>
</span>

View File

@@ -33,6 +33,7 @@
:before-close="cancelSaveTeam"
:destroy-on-close="true"
:close-on-click-modal="false"
size="40%"
>
<template #header>
<DrawerHeader :header="addTeamDialog.form.id ? '编辑团队' : '添加团队'" :back="cancelSaveTeam" />

View File

@@ -23,6 +23,7 @@ export const tagApi = {
export const resourceAuthCertApi = {
detail: Api.newGet('/auth-certs/detail'),
listByQuery: Api.newGet('/auth-certs'),
getByCodes: Api.newGet('/auth-certs/simple'),
save: Api.newPost('/auth-certs'),
delete: Api.newDelete('/auth-certs/{id}'),
};

Some files were not shown because too many files have changed in this diff Show More