mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57361d8241 | ||
|
|
b347bd7ef5 | ||
|
|
070c8ac0da | ||
|
|
e221c2f42e | ||
|
|
c7bab3a71b | ||
|
|
82c17a51a2 | ||
|
|
e4447e6bc2 | ||
|
|
b9570d9a5f | ||
|
|
01e8a2c14d | ||
|
|
64bd51c3b0 | ||
|
|
54ab34df3f | ||
|
|
206490ba3e | ||
|
|
16612d2c9c | ||
|
|
6b65605360 | ||
|
|
bb37ed3b95 | ||
|
|
d102cc8c08 | ||
|
|
a6df74d63d | ||
|
|
f79760943e | ||
|
|
a40ec21a05 | ||
|
|
43230267b6 | ||
|
|
0ae99cdaf9 | ||
|
|
f234c72514 | ||
|
|
76527d95bd | ||
|
|
27c53385f2 | ||
|
|
a1b25e9766 | ||
|
|
abad0ed481 | ||
|
|
eddda41291 | ||
|
|
d9adf0fd25 | ||
|
|
0ce82b41ba | ||
|
|
37026f3269 | ||
|
|
3155380f16 | ||
|
|
f2b0f294d8 | ||
|
|
12f63ef3dd | ||
|
|
a1303b52eb | ||
|
|
10f6b03fb5 | ||
|
|
45d2449221 | ||
|
|
9e5f146e05 | ||
|
|
2b91bbe185 | ||
|
|
747ea6404d | ||
|
|
ccfc6bd1df | ||
|
|
361eafedae | ||
|
|
a64b894b08 | ||
|
|
0ad805c170 | ||
|
|
ba82b5b516 | ||
|
|
f04b82c933 | ||
|
|
23b137ab9b |
@@ -18,7 +18,7 @@ WORKDIR /mayfly
|
||||
# Copy the go source for building server
|
||||
COPY server .
|
||||
|
||||
RUN go mod download
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
||||
|
||||
|
||||
14
README.md
14
README.md
@@ -1,13 +1,13 @@
|
||||
# 🌈mayfly-go
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/objs/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
<a href="https://gitee.com/dromara/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/may-fly/mayfly-go" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/may-fly/mayfly-go.svg?style=social" alt="github star"/>
|
||||
<img src="https://img.shields.io/github/forks/may-fly/mayfly-go.svg?style=social" alt="github fork"/>
|
||||
<a href="https://github.com/dromara/mayfly-go" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/dromara/mayfly-go.svg?style=social" alt="github star"/>
|
||||
<img src="https://img.shields.io/github/forks/dromara/mayfly-go.svg?style=social" alt="github fork"/>
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/mayflygo/mayfly-go/tags" target="_blank">
|
||||
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
|
||||
@@ -100,4 +100,4 @@ http://go.mayfly.run
|
||||
|
||||
#### 💌 支持作者
|
||||
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/may-fly/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/objs/mayfly-go">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"asciinema-player": "^3.6.2",
|
||||
"axios": "^1.5.0",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"countup.js": "^2.7.0",
|
||||
"cropperjs": "^1.5.11",
|
||||
"echarts": "^5.4.0",
|
||||
"element-plus": "^2.4.0",
|
||||
"jsencrypt": "^3.3.1",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.4.3",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.44.0",
|
||||
@@ -24,12 +25,12 @@
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.0",
|
||||
"sql-formatter": "^12.1.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-clipboard3": "^1.0.1",
|
||||
"sql-formatter": "^14.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.3.10",
|
||||
"vue-router": "^4.2.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
@@ -47,11 +48,11 @@
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"prettier": "^2.3.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.11",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.5",
|
||||
"vue-eslint-parser": "^9.3.1"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -40,6 +40,12 @@ const openSetingsDrawer = () => {
|
||||
setingsRef.value.openDrawer();
|
||||
};
|
||||
|
||||
const prefers = matchMedia('(prefers-color-scheme: dark)');
|
||||
const switchDarkFollowOS = () => {
|
||||
// 跟随系统主题
|
||||
themeConfigStores.switchDark(prefers.matches);
|
||||
};
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
@@ -53,9 +59,8 @@ onMounted(() => {
|
||||
if (tc) {
|
||||
themeConfigStores.setThemeConfig({ themeConfig: tc });
|
||||
document.documentElement.style.cssText = getLocal('themeConfigStyle');
|
||||
|
||||
themeConfigStores.switchDark(tc.isDark);
|
||||
}
|
||||
switchDarkFollowOS();
|
||||
|
||||
// 是否开启水印
|
||||
useWatermark().then((res) => {
|
||||
@@ -96,7 +101,7 @@ const refreshWatermarkTime = () => {
|
||||
} else {
|
||||
clearInterval(refreshWatermarkTimeInterval);
|
||||
}
|
||||
}, 10000);
|
||||
}, 60000);
|
||||
};
|
||||
|
||||
// 页面销毁时,关闭监听布局配置
|
||||
|
||||
9
mayfly_go_web/src/common/commonEnum.ts
Normal file
9
mayfly_go_web/src/common/commonEnum.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import EnumValue from './Enum';
|
||||
|
||||
// 标签关联的资源类型
|
||||
export const TagResourceTypeEnum = {
|
||||
Machine: EnumValue.of(1, '机器'),
|
||||
Db: EnumValue.of(2, '数据库'),
|
||||
Redis: EnumValue.of(3, 'redis'),
|
||||
Mongo: EnumValue.of(4, 'mongo'),
|
||||
};
|
||||
@@ -3,6 +3,10 @@ function getBaseApiUrl() {
|
||||
if (path == '/') {
|
||||
return window.location.host;
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
// 去除最后一个/
|
||||
return window.location.host + path.replace(/\/$/, '');
|
||||
}
|
||||
return window.location.host + path;
|
||||
}
|
||||
|
||||
@@ -11,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.5.3',
|
||||
version: 'v1.6.0',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import router from '../router';
|
||||
import Axios from 'axios';
|
||||
import config from './config';
|
||||
import { getToken } from './utils/storage';
|
||||
import { getClientId, getToken } from './utils/storage';
|
||||
import { templateResolve } from './utils/string';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
@@ -54,6 +54,7 @@ service.interceptors.request.use(
|
||||
if (token) {
|
||||
// 设置token
|
||||
config.headers['Authorization'] = token;
|
||||
config.headers['ClientId'] = getClientId();
|
||||
}
|
||||
return config;
|
||||
},
|
||||
@@ -176,7 +177,12 @@ function del(url: string, params: any = null, headers: any = null, options: any
|
||||
|
||||
function getApiUrl(url: string) {
|
||||
// 只是返回api地址而不做请求,用在上传组件之类的
|
||||
return baseUrl + url + '?token=' + getToken();
|
||||
return baseUrl + url + '?' + joinClientParams();
|
||||
}
|
||||
|
||||
// 组装客户端参数,包括 token 和 clientId
|
||||
export function joinClientParams(): string {
|
||||
return `token=${getToken()}&clientId=${getClientId()}`;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import Config from './config';
|
||||
import { ElNotification, NotificationHandle } from 'element-plus';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { createVNode, reactive } from "vue";
|
||||
import { buildProgressProps } from "@/components/progress-notify/progress-notify";
|
||||
import ProgressNotify from '/src/components/progress-notify/progress-notify.vue';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 全局系统消息websocket
|
||||
*/
|
||||
sysMsgSocket() {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
const messageTypes = {
|
||||
0: "error",
|
||||
1: "success",
|
||||
2: "info",
|
||||
}
|
||||
const notifyMap: Map<Number, any> = new Map()
|
||||
|
||||
return SocketBuilder.builder(`${Config.baseWsUrl}/sysmsg?token=${token}`)
|
||||
.message((event: { data: string }) => {
|
||||
const message = JSON.parse(event.data);
|
||||
const type = messageTypes[message.type]
|
||||
switch (message.category) {
|
||||
case "execSqlFileProgress":
|
||||
const content = JSON.parse(message.msg)
|
||||
const id = content.id
|
||||
let progress = notifyMap.get(id)
|
||||
if (content.terminated) {
|
||||
if (progress != undefined) {
|
||||
progress.notification?.close()
|
||||
notifyMap.delete(id)
|
||||
progress = undefined
|
||||
}
|
||||
return
|
||||
}
|
||||
if (progress == undefined) {
|
||||
progress = {
|
||||
props: reactive(buildProgressProps()),
|
||||
notification: undefined,
|
||||
}
|
||||
}
|
||||
progress.props.progress.sqlFileName = content.sqlFileName
|
||||
progress.props.progress.executedStatements = content.executedStatements
|
||||
if (!notifyMap.has(id)) {
|
||||
const vNodeMessage = createVNode(
|
||||
ProgressNotify,
|
||||
progress.props,
|
||||
null,
|
||||
)
|
||||
progress.notification = ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: vNodeMessage,
|
||||
type: type,
|
||||
showClose: false,
|
||||
});
|
||||
notifyMap.set(id, progress)
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: message.msg,
|
||||
type: type,
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.build();
|
||||
},
|
||||
};
|
||||
@@ -59,15 +59,20 @@ export async function useLoginCaptcha(): Promise<boolean> {
|
||||
*/
|
||||
export async function useWatermark(): Promise<any> {
|
||||
const value = await getConfigValue(UseWatermarkConfigKey);
|
||||
const defaultValue = {
|
||||
isUse: true,
|
||||
};
|
||||
if (!value) {
|
||||
return {
|
||||
isUse: true,
|
||||
};
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
const jsonValue = JSON.parse(value);
|
||||
// 将字符串转为bool
|
||||
jsonValue.isUse = convertBool(jsonValue.isUse, true);
|
||||
return jsonValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
const jsonValue = JSON.parse(value);
|
||||
// 将字符串转为bool
|
||||
jsonValue.isUse = convertBool(jsonValue.isUse, true);
|
||||
return jsonValue;
|
||||
}
|
||||
|
||||
function convertBool(value: string, defaultValue: boolean) {
|
||||
|
||||
100
mayfly_go_web/src/common/syssocket.ts
Normal file
100
mayfly_go_web/src/common/syssocket.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import Config from './config';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
|
||||
import { joinClientParams } from './request';
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
* socket连接
|
||||
*/
|
||||
socket: any;
|
||||
|
||||
/**
|
||||
* key -> 消息类别,value -> 消息对应的处理器函数
|
||||
*/
|
||||
categoryHandlers: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
messageTypes = {
|
||||
0: 'error',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化全局系统消息websocket
|
||||
*/
|
||||
init() {
|
||||
// 存在则不需要重新建立连接
|
||||
if (this.socket) {
|
||||
return;
|
||||
}
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
console.log('init system ws');
|
||||
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
|
||||
this.socket = SocketBuilder.builder(sysMsgUrl)
|
||||
.message((event: { data: string }) => {
|
||||
const message = JSON.parse(event.data);
|
||||
// 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理
|
||||
const handler = this.categoryHandlers.get(message.category);
|
||||
if (handler) {
|
||||
handler(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = this.getMsgType(message.type);
|
||||
ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: message.msg,
|
||||
type: type,
|
||||
});
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.close(() => {
|
||||
console.log('close sys socket');
|
||||
this.socket = null;
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
destory() {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.categoryHandlers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息处理函数
|
||||
*
|
||||
* @param category 消息类别
|
||||
* @param handlerFunc 消息处理函数
|
||||
*/
|
||||
registerMsgHandler(category: any, handlerFunc: any) {
|
||||
this.init();
|
||||
if (this.categoryHandlers.has(category)) {
|
||||
console.log(`${category}该类别消息处理器已存在...`);
|
||||
return;
|
||||
}
|
||||
if (typeof handlerFunc != 'function') {
|
||||
throw new Error('message handler需为函数');
|
||||
}
|
||||
this.categoryHandlers.set(category, handlerFunc);
|
||||
}
|
||||
|
||||
getMsgType(msgType: any) {
|
||||
return this.messageTypes[msgType];
|
||||
}
|
||||
}
|
||||
|
||||
// 全局系统消息websocket;
|
||||
const sysSocket = new SysSocket();
|
||||
|
||||
export default sysSocket;
|
||||
17
mayfly_go_web/src/common/use.ts
Normal file
17
mayfly_go_web/src/common/use.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
const vw = ref(document.documentElement.clientWidth);
|
||||
const vh = ref(document.documentElement.clientHeight);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
vw.value = document.documentElement.clientWidth;
|
||||
vh.value = document.documentElement.clientHeight;
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取视图宽高
|
||||
* @returns 视图宽高
|
||||
*/
|
||||
export function useViewport() {
|
||||
return { vw, vh };
|
||||
}
|
||||
@@ -29,15 +29,19 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
cvsData.push(dataValueArr);
|
||||
}
|
||||
const csvString = cvsData.map((e) => e.join(',')).join('\n');
|
||||
exportFile(`${filename}.csv`, csvString);
|
||||
}
|
||||
|
||||
export function exportFile(filename: string, content: string) {
|
||||
// 导出
|
||||
let link = document.createElement('a');
|
||||
let exportContent = '\uFEFF';
|
||||
let blob = new Blob([exportContent + csvString], {
|
||||
let blob = new Blob([exportContent + content], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
link.id = 'download-csv';
|
||||
link.id = 'download-file';
|
||||
link.setAttribute('href', URL.createObjectURL(blob));
|
||||
link.setAttribute('download', `${filename}.csv`);
|
||||
link.setAttribute('download', `${filename}`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
|
||||
@@ -3,21 +3,16 @@
|
||||
* @param size byte size
|
||||
* @returns
|
||||
*/
|
||||
export function formatByteSize(size: any) {
|
||||
const value = Number(size);
|
||||
if (size && !isNaN(value)) {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
|
||||
let index = 0;
|
||||
let k = value;
|
||||
if (value >= 1024) {
|
||||
while (k > 1024) {
|
||||
k = k / 1024;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return `${k.toFixed(2)}${units[index]}`;
|
||||
export function formatByteSize(size: number, fixed = 2) {
|
||||
if (size === 0) {
|
||||
return '0B';
|
||||
}
|
||||
return '-';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const base = 1024;
|
||||
const exponent = Math.floor(Math.log(size) / Math.log(base));
|
||||
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,3 +40,7 @@ export const NextLoading = {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const TokenKey = 'token';
|
||||
const UserKey = 'user';
|
||||
const TagViewsKey = 'tagViews';
|
||||
import { randomUuid } from './string';
|
||||
|
||||
const TokenKey = 'm-token';
|
||||
const UserKey = 'm-user';
|
||||
const TagViewsKey = 'm-tagViews';
|
||||
const ClientIdKey = 'm-clientId';
|
||||
|
||||
// 获取请求token
|
||||
export function getToken(): string {
|
||||
@@ -48,16 +51,33 @@ export function removeTagViews() {
|
||||
removeSession(TagViewsKey);
|
||||
}
|
||||
|
||||
// 获取客户端UUID
|
||||
export function getClientId(): string {
|
||||
let uuid = getSession(ClientIdKey);
|
||||
if (uuid == null) {
|
||||
uuid = randomUuid();
|
||||
setSession(ClientIdKey, uuid);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// 1. localStorage
|
||||
// 设置永久缓存
|
||||
export function setLocal(key: string, val: any) {
|
||||
window.localStorage.setItem(key, JSON.stringify(val));
|
||||
if (typeof val == 'object') {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
// 获取永久缓存
|
||||
export function getLocal(key: string) {
|
||||
let json: any = window.localStorage.getItem(key);
|
||||
return JSON.parse(json);
|
||||
let val: any = window.localStorage.getItem(key);
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除永久缓存
|
||||
@@ -73,13 +93,20 @@ export function clearLocal() {
|
||||
// 2. sessionStorage
|
||||
// 设置临时缓存
|
||||
export function setSession(key: string, val: any) {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(val));
|
||||
if (typeof val == 'object') {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
window.sessionStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
// 获取临时缓存
|
||||
export function getSession(key: string) {
|
||||
let json: any = window.sessionStorage.getItem(key);
|
||||
return JSON.parse(json);
|
||||
let val: any = window.sessionStorage.getItem(key);
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除临时缓存
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import Clipboard from 'clipboard';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 模板字符串解析,如:template = 'hahaha{name}_{id}' ,param = {name: 'hh', id: 1}
|
||||
* 解析后为 hahahahh_1
|
||||
@@ -129,3 +133,49 @@ export function getContentWidth(content: any): number {
|
||||
// }
|
||||
return flexWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns uuid
|
||||
*/
|
||||
export function randomUuid() {
|
||||
return uuidv1();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝文本至剪贴板
|
||||
* @param txt 需要拷贝到剪贴板的文本
|
||||
* @param selector click事件对应的元素selector,默认为 #copyValue
|
||||
* @returns
|
||||
*/
|
||||
export async function copyToClipboard(txt: string, selector: string = '#copyValue') {
|
||||
// navigator clipboard 需要https等安全上下文
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard 向剪贴板写文本
|
||||
try {
|
||||
// 拷贝单元格数据
|
||||
await navigator.clipboard.writeText(txt);
|
||||
ElMessage.success('复制成功');
|
||||
} catch (e: any) {
|
||||
ElMessage.error('复制失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboard = new Clipboard(selector, {
|
||||
text: function () {
|
||||
return txt;
|
||||
},
|
||||
});
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
clipboard.on('error', () => {
|
||||
// 不支持复制
|
||||
ElMessage.error('该浏览器不支持自动复制');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import { useUserInfo } from '@/store/userInfo';
|
||||
* @returns
|
||||
*/
|
||||
export function hasPerm(code: string) {
|
||||
if (!code) {
|
||||
return true;
|
||||
}
|
||||
return useUserInfo().userInfo.permissions.some((v: any) => v === code);
|
||||
}
|
||||
|
||||
|
||||
59
mayfly_go_web/src/components/contextmenu/index.ts
Normal file
59
mayfly_go_web/src/components/contextmenu/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import Contextmenu from './index.vue';
|
||||
|
||||
class ContextmenuItem {
|
||||
clickId: any;
|
||||
|
||||
txt: string;
|
||||
|
||||
icon: string;
|
||||
|
||||
affix: boolean;
|
||||
|
||||
permission: string;
|
||||
|
||||
/**
|
||||
* 是否隐藏回调函数
|
||||
*/
|
||||
hideFunc: (data: any) => boolean;
|
||||
|
||||
onClickFunc: (data: any) => void;
|
||||
|
||||
constructor(clickId: any, txt: string) {
|
||||
this.clickId = clickId;
|
||||
this.txt = txt;
|
||||
}
|
||||
|
||||
withIcon(icon: string) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
withPermission(permission: string) {
|
||||
this.permission = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
withHideFunc(func: (data: any) => boolean) {
|
||||
this.hideFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
withOnClick(func: (data: any) => void) {
|
||||
this.onClickFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否隐藏
|
||||
* @param data 点击数据项
|
||||
* @returns
|
||||
*/
|
||||
isHide(data: any) {
|
||||
if (this.hideFunc) {
|
||||
return this.hideFunc(data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { Contextmenu, ContextmenuItem };
|
||||
@@ -1,36 +1,41 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<transition @enter="onEnter" name="el-zoom-in-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
role="tooltip"
|
||||
data-popper-placement="bottom"
|
||||
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
|
||||
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="state.isShow"
|
||||
v-show="state.isShow && !allHide"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
<li
|
||||
:id="v.clickId"
|
||||
v-auth="v.permission"
|
||||
class="el-dropdown-menu__item"
|
||||
aria-disabled="false"
|
||||
tabindex="-1"
|
||||
:key="k"
|
||||
v-if="!v.affix"
|
||||
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
|
||||
v-if="!v.affix && !v.isHide(state.item)"
|
||||
@click="onCurrentContextmenuClick(v)"
|
||||
>
|
||||
<SvgIcon :name="v.icon" />
|
||||
<span>{{ v.txt }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||
<div v-if="state.arrowLeft > 0" class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutTagsViewContextmenu">
|
||||
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { ContextmenuItem } from './index';
|
||||
import { useViewport } from '@/common/use';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
@@ -44,38 +49,82 @@ const props = defineProps({
|
||||
},
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: [],
|
||||
type: Array<ContextmenuItem>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
const emit = defineEmits(['currentContextmenuClick']);
|
||||
|
||||
const { vw, vh } = useViewport();
|
||||
|
||||
// 定义变量内容
|
||||
const state = reactive({
|
||||
isShow: false,
|
||||
dropdownList: [{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' }],
|
||||
dropdownList: [] as ContextmenuItem[],
|
||||
item: {} as any,
|
||||
arrowLeft: 10,
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 父级传过来的坐标 x,y 值
|
||||
const dropdowns = computed(() => {
|
||||
// 117 为 `Dropdown 下拉菜单` 的宽度
|
||||
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
|
||||
return {
|
||||
x: document.documentElement.clientWidth - 117 - 5,
|
||||
y: props.dropdown.y,
|
||||
};
|
||||
} else {
|
||||
return props.dropdown;
|
||||
// 下拉菜单宽高
|
||||
let contextmenuWidth = 117;
|
||||
let contextmenuHeight = 117;
|
||||
// 下拉菜单元素
|
||||
let ele = null as any;
|
||||
|
||||
const onEnter = (el: any) => {
|
||||
if (ele || el.offsetHeight == 0) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
// 当前项菜单点击
|
||||
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
|
||||
emit('currentContextmenuClick', { id: contextMenuClickId, item: state.item });
|
||||
|
||||
ele = el;
|
||||
contextmenuHeight = el.offsetHeight;
|
||||
contextmenuWidth = el.offsetWidth;
|
||||
setDropdowns(props.dropdown);
|
||||
};
|
||||
|
||||
const setDropdowns = (dropdown: any) => {
|
||||
let { x, y } = dropdown;
|
||||
|
||||
state.arrowLeft = 10;
|
||||
|
||||
// `Dropdown 下拉菜单` 的宽度
|
||||
if (x + contextmenuWidth > vw.value) {
|
||||
state.arrowLeft = contextmenuWidth - (vw.value - x);
|
||||
x = vw.value - contextmenuWidth - 5;
|
||||
}
|
||||
if (y + contextmenuHeight > vh.value) {
|
||||
y = vh.value - contextmenuHeight - 5;
|
||||
state.arrowLeft = 0;
|
||||
}
|
||||
|
||||
state.dropdown.x = x;
|
||||
state.dropdown.y = y;
|
||||
};
|
||||
|
||||
const allHide = computed(() => {
|
||||
for (let item of state.dropdownList) {
|
||||
if (!item.isHide(state.item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 当前项菜单点击
|
||||
const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
||||
// 存在点击事件,则触发该事件函数
|
||||
if (ci.onClickFunc) {
|
||||
ci.onClickFunc(state.item);
|
||||
}
|
||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||
};
|
||||
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
@@ -84,29 +133,34 @@ const openContextmenu = (item: any) => {
|
||||
state.isShow = true;
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeContextmenu = () => {
|
||||
state.isShow = false;
|
||||
};
|
||||
|
||||
// 监听页面监听进行右键菜单的关闭
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('click', closeContextmenu);
|
||||
state.dropdownList = props.items;
|
||||
});
|
||||
|
||||
// 页面卸载时,移除右键菜单监听事件
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('click', closeContextmenu);
|
||||
});
|
||||
// 监听下拉菜单位置
|
||||
|
||||
watch(
|
||||
() => props.dropdown,
|
||||
({ x }) => {
|
||||
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
|
||||
else state.arrowLeft = 10;
|
||||
() => {
|
||||
// 元素置为空,重新在onEnter赋值元素,否则会造成堆栈溢出
|
||||
ele = null;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
(x: any) => {
|
||||
@@ -140,3 +194,4 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.
|
||||
|
||||
@@ -132,7 +132,7 @@ const languageArr = [
|
||||
},
|
||||
];
|
||||
|
||||
const options = {
|
||||
const defaultOptions = {
|
||||
language: 'shell',
|
||||
theme: 'SolarizedLight',
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
@@ -169,7 +169,6 @@ self.MonacoEnvironment = {
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
editorHeight: '500px',
|
||||
languageMode: 'shell',
|
||||
});
|
||||
|
||||
@@ -223,9 +222,9 @@ const initMonacoEditorIns = () => {
|
||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||
// 初始化一些主题
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
options.language = state.languageMode;
|
||||
options.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(options, props.options as any));
|
||||
defaultOptions.language = state.languageMode;
|
||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(defaultOptions, props.options as any));
|
||||
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
>
|
||||
<!-- 这里只获取指定个数的筛选条件 -->
|
||||
<el-input
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
:placeholder="'输入' + item.label + '关键字'"
|
||||
clearable
|
||||
v-if="item.type == 'text'"
|
||||
></el-input>
|
||||
|
||||
<el-select-v2
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
:options="item.options"
|
||||
clearable
|
||||
:placeholder="'选择' + item.label + '关键字'"
|
||||
@@ -41,7 +41,7 @@
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
clearable
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD hh:mm:ss"
|
||||
@@ -185,8 +185,10 @@
|
||||
import { toRefs, watch, reactive, onMounted } from 'vue';
|
||||
import { TableColumn, TableQuery } from './index';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange'])
|
||||
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange']);
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
@@ -195,7 +197,7 @@ const props = defineProps({
|
||||
},
|
||||
inputWidth: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
default: '200px',
|
||||
},
|
||||
// 是否显示选择列
|
||||
showSelection: {
|
||||
@@ -204,7 +206,7 @@ const props = defineProps({
|
||||
},
|
||||
// 当前选择的数据
|
||||
selectionData: {
|
||||
type: Array<any>
|
||||
type: Array<any>,
|
||||
},
|
||||
// 列信息
|
||||
columns: {
|
||||
@@ -236,16 +238,18 @@ const props = defineProps({
|
||||
type: Array<TableQuery>,
|
||||
default: function () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
// 绑定的查询表单
|
||||
queryForm: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
const state = reactive({
|
||||
pageSizes: [] as any, // 可选每页显示的数据量
|
||||
@@ -253,140 +257,153 @@ const state = reactive({
|
||||
pageNum: 1,
|
||||
isOpenMoreQuery: false,
|
||||
defaultQueryCount: 2, // 默认显示的查询参数个数,展开后每行显示查询条件个数为该值加1。第一行用最后一列来占用按钮
|
||||
queryForm: {} as any,
|
||||
queryForm_: {} as any,
|
||||
loadingData: false,
|
||||
// 输入框宽度
|
||||
inputWidth: "200px" as any,
|
||||
inputWidth_: '200px' as any,
|
||||
formatVal: '', // 格式化后的值
|
||||
tableMaxHeight: window.innerHeight - 240 + 'px',
|
||||
})
|
||||
});
|
||||
|
||||
const {
|
||||
pageSizes,
|
||||
isOpenMoreQuery,
|
||||
defaultQueryCount,
|
||||
queryForm,
|
||||
loadingData,
|
||||
inputWidth,
|
||||
formatVal,
|
||||
tableMaxHeight,
|
||||
} = toRefs(state)
|
||||
const { pageSizes, isOpenMoreQuery, defaultQueryCount, queryForm_, inputWidth_, formatVal, loadingData, tableMaxHeight } = toRefs(state);
|
||||
|
||||
watch(() => props.queryForm, (newValue: any) => {
|
||||
state.queryForm = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.pageNum, (newValue: any) => {
|
||||
state.pageNum = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.pageSize, (newValue: any) => {
|
||||
state.pageSize = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.data, (newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach(item => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(props.data);
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.queryForm,
|
||||
(newValue: any) => {
|
||||
state.queryForm_ = newValue;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageNum,
|
||||
(newValue: any) => {
|
||||
state.pageNum = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageSize,
|
||||
(newValue: any) => {
|
||||
state.pageSize = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(props.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const pageSize = props.pageSize;
|
||||
let pageSize = props.pageSize;
|
||||
|
||||
// 如果pageSize设为0,则使用系统全局配置的pageSize
|
||||
if (!pageSize) {
|
||||
pageSize = themeConfig.value.defaultListPageSize;
|
||||
// 可能storage已经存在配置json,则可能没值,需要清storage重试
|
||||
if (!pageSize) {
|
||||
pageSize = 10;
|
||||
}
|
||||
emit('update:pageSize', pageSize);
|
||||
}
|
||||
|
||||
state.pageNum = props.pageNum;
|
||||
state.pageSize = pageSize;
|
||||
state.queryForm = props.queryForm;
|
||||
state.queryForm_ = props.queryForm;
|
||||
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
|
||||
|
||||
// 如果没传输入框宽度,则根据组件size设置默认宽度
|
||||
if (!props.inputWidth) {
|
||||
state.inputWidth = props.size == 'small' ? '150px' : '200px';
|
||||
state.inputWidth_ = props.size == 'small' ? '150px' : '200px';
|
||||
} else {
|
||||
state.inputWidth = props.inputWidth;
|
||||
state.inputWidth_ = props.inputWidth;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const calcuTableHeight = () => {
|
||||
state.tableMaxHeight = window.innerHeight - 240 + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const formatText = (data: any)=> {
|
||||
const formatText = (data: any) => {
|
||||
state.formatVal = '';
|
||||
try {
|
||||
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
state.formatVal = data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getRowQueryItem = (row: number) => {
|
||||
// 第一行需要加个查询等按钮列
|
||||
if (row === 1) {
|
||||
const res = props.query.slice(row - 1, defaultQueryCount.value);
|
||||
// 查询等按钮列
|
||||
res.push(TableQuery.slot("", "", "queryBtns"));
|
||||
return res
|
||||
res.push(TableQuery.slot('', '', 'queryBtns'));
|
||||
return res;
|
||||
}
|
||||
const columnCount = defaultQueryCount.value + 1;
|
||||
return props.query.slice((row - 1) * columnCount - 1, row * columnCount - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
emit('update:selectionData', val);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = () => {
|
||||
emit('update:pageNum', state.pageNum);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = () => {
|
||||
changePageNum(1);
|
||||
emit('update:pageSize', state.pageSize);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const queryData = () => {
|
||||
changePageNum(1);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
// 将查询参数绑定的值置空,并重新粗发查询接口
|
||||
for (let qi of props.query) {
|
||||
state.queryForm[qi.prop] = null;
|
||||
state.queryForm_[qi.prop] = null;
|
||||
}
|
||||
|
||||
changePageNum(1);
|
||||
emit('update:queryForm', state.queryForm);
|
||||
emit('update:queryForm', state.queryForm_);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const changePageNum = (pageNum: number) => {
|
||||
state.pageNum = pageNum;
|
||||
emit('update:pageNum', state.pageNum);
|
||||
}
|
||||
};
|
||||
|
||||
const execQuery = () => {
|
||||
emit('pageChange');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否正在加载数据
|
||||
*/
|
||||
const loading = (loading: boolean) => {
|
||||
state.loadingData = loading;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ loading })
|
||||
defineExpose({ loading });
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.page-table {
|
||||
@@ -420,15 +437,15 @@ defineExpose({ loading })
|
||||
}
|
||||
|
||||
.el-select-v2 {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const buildProgressProps = (): any => {
|
||||
return {
|
||||
progress: {
|
||||
sqlFileName: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
executedStatements: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-descriptions border size="small" :title="`${progress.sqlFileName}`">
|
||||
<el-descriptions border size="small" :title="`${progress.title}`">
|
||||
<el-descriptions-item label="时间">{{ state.elapsedTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已处理">{{ progress.executedStatements }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
size="small"
|
||||
@click="maximize(minimizeTerminal.terminalId)"
|
||||
>
|
||||
<el-tooltip effect="customized" :content="minimizeTerminal.desc" placement="top">
|
||||
<el-tooltip :content="minimizeTerminal.desc" placement="top">
|
||||
<span>
|
||||
{{ minimizeTerminal.title }}
|
||||
</span>
|
||||
|
||||
@@ -61,6 +61,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局设置 -->
|
||||
<el-divider content-position="left">全局设置</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">分页size</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-input-number
|
||||
v-model="themeConfig.defaultListPageSize"
|
||||
controls-position="right"
|
||||
:min="10"
|
||||
:max="50"
|
||||
@change="setLocalThemeConfig"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>
|
||||
</el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局主题 -->
|
||||
<el-divider content-position="left">全局主题</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex">
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
role="tooltip"
|
||||
data-popper-placement="bottom"
|
||||
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="state.isShow"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
<li
|
||||
class="el-dropdown-menu__item"
|
||||
aria-disabled="false"
|
||||
tabindex="-1"
|
||||
:key="k"
|
||||
v-if="!v.affix"
|
||||
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
|
||||
>
|
||||
<SvgIcon :name="v.icon" />
|
||||
<span>{{ v.txt }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutTagsViewContextmenu">
|
||||
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
dropdown: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
const emit = defineEmits(['currentContextmenuClick']);
|
||||
|
||||
// 定义变量内容
|
||||
const state = reactive({
|
||||
isShow: false,
|
||||
dropdownList: [
|
||||
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
|
||||
{ contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
|
||||
{ contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
|
||||
{ contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
|
||||
{
|
||||
contextMenuClickId: 4,
|
||||
txt: '当前页全屏',
|
||||
affix: false,
|
||||
icon: 'full-screen',
|
||||
},
|
||||
],
|
||||
item: {} as any,
|
||||
arrowLeft: 10,
|
||||
});
|
||||
|
||||
// 父级传过来的坐标 x,y 值
|
||||
const dropdowns = computed(() => {
|
||||
// 117 为 `Dropdown 下拉菜单` 的宽度
|
||||
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
|
||||
return {
|
||||
x: document.documentElement.clientWidth - 117 - 5,
|
||||
y: props.dropdown.y,
|
||||
};
|
||||
} else {
|
||||
return props.dropdown;
|
||||
}
|
||||
});
|
||||
// 当前项菜单点击
|
||||
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
|
||||
emit('currentContextmenuClick', { id: contextMenuClickId, path: state.item.path });
|
||||
};
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
item.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
|
||||
closeContextmenu();
|
||||
setTimeout(() => {
|
||||
state.isShow = true;
|
||||
}, 10);
|
||||
};
|
||||
// 关闭右键菜单
|
||||
const closeContextmenu = () => {
|
||||
state.isShow = false;
|
||||
};
|
||||
// 监听页面监听进行右键菜单的关闭
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('click', closeContextmenu);
|
||||
});
|
||||
// 页面卸载时,移除右键菜单监听事件
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('click', closeContextmenu);
|
||||
});
|
||||
// 监听下拉菜单位置
|
||||
watch(
|
||||
() => props.dropdown,
|
||||
({ x }) => {
|
||||
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
|
||||
else state.arrowLeft = 10;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openContextmenu,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-contextmenu {
|
||||
transform-origin: center top;
|
||||
z-index: 2190;
|
||||
position: fixed;
|
||||
.el-dropdown-menu__item {
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
i {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -42,19 +42,19 @@
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
<Contextmenu :dropdown="state.dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
|
||||
<Contextmenu :items="state.contextmenu.items" :dropdown="state.contextmenu.dropdown" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutTagsView">
|
||||
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance, watch } from 'vue';
|
||||
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance } from 'vue';
|
||||
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
|
||||
import screenfull from 'screenfull';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import Sortable from 'sortablejs';
|
||||
import Contextmenu from '@/layout/navBars/tagsView/contextmenu.vue';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
|
||||
import { useTagsViews } from '@/store/tagsViews';
|
||||
import { useKeepALiveNames } from '@/store/keepAliveNames';
|
||||
@@ -73,11 +73,38 @@ const keepAliveNamesStores = useKeepALiveNames();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const contextmenuItems = [
|
||||
new ContextmenuItem(0, '刷新').withIcon('RefreshRight').withOnClick((data: any) => {
|
||||
// path为fullPath
|
||||
let { path } = data;
|
||||
let currentTag = tagsViews.value.find((v: any) => v.path === path);
|
||||
refreshCurrentTagsView(path);
|
||||
router.push({ path, query: currentTag?.query });
|
||||
}),
|
||||
|
||||
new ContextmenuItem(1, '关闭').withIcon('Close').withOnClick((data: any) => closeCurrentTagsView(data.path)),
|
||||
|
||||
new ContextmenuItem(2, '关闭其他').withIcon('CircleClose').withOnClick((data: any) => {
|
||||
let { path } = data;
|
||||
let currentTag = tagsViews.value.find((v: any) => v.path === path);
|
||||
router.push({ path, query: currentTag?.query });
|
||||
closeOtherTagsView(path);
|
||||
}),
|
||||
|
||||
new ContextmenuItem(3, '关闭所有').withIcon('FolderDelete').withOnClick((data: any) => closeAllTagsView(data.path)),
|
||||
|
||||
new ContextmenuItem(4, '当前页全屏').withIcon('full-screen').withOnClick((data: any) => openCurrenFullscreen(data.path)),
|
||||
];
|
||||
|
||||
const state = reactive({
|
||||
routePath: route.fullPath,
|
||||
dropdown: { x: '', y: '' },
|
||||
// dropdown: { x: '', y: '' },
|
||||
tagsRefsIndex: 0,
|
||||
sortable: '' as any,
|
||||
contextmenu: {
|
||||
items: contextmenuItems,
|
||||
dropdown: { x: '', y: '' },
|
||||
},
|
||||
});
|
||||
|
||||
// 动态设置 tagsView 风格样式
|
||||
@@ -239,31 +266,7 @@ const openCurrenFullscreen = (path: string) => {
|
||||
screenfulls.request(element);
|
||||
});
|
||||
};
|
||||
// 当前项右键菜单点击
|
||||
const onCurrentContextmenuClick = (data: any) => {
|
||||
// path为fullPath
|
||||
let { id, path } = data;
|
||||
let currentTag = tagsViews.value.find((v: any) => v.path === path);
|
||||
switch (id) {
|
||||
case 0:
|
||||
refreshCurrentTagsView(path);
|
||||
router.push({ path, query: currentTag?.query });
|
||||
break;
|
||||
case 1:
|
||||
closeCurrentTagsView(path);
|
||||
break;
|
||||
case 2:
|
||||
router.push({ path, query: currentTag?.query });
|
||||
closeOtherTagsView(path);
|
||||
break;
|
||||
case 3:
|
||||
closeAllTagsView(path);
|
||||
break;
|
||||
case 4:
|
||||
openCurrenFullscreen(path);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 判断页面高亮
|
||||
const isActive = (tagView: TagsView) => {
|
||||
return tagView.path === state.routePath;
|
||||
@@ -271,8 +274,8 @@ const isActive = (tagView: TagsView) => {
|
||||
// 右键点击时:传 x,y 坐标值到子组件中(props)
|
||||
const onContextmenu = (v: any, e: any) => {
|
||||
const { clientX, clientY } = e;
|
||||
state.dropdown.x = clientX;
|
||||
state.dropdown.y = clientY;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
contextmenuRef.value.openContextmenu(v);
|
||||
};
|
||||
// 当前的 tagsView 项点击时
|
||||
@@ -371,10 +374,6 @@ const initSortable = () => {
|
||||
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
// 监听非本页面调用 0 刷新当前,1 关闭当前,2 关闭其它,3 关闭全部 4 当前页全屏
|
||||
mittBus.on('onCurrentContextmenuClick', (data: object) => {
|
||||
onCurrentContextmenuClick(data);
|
||||
});
|
||||
// 监听布局配置界面开启/关闭拖拽
|
||||
mittBus.on('openOrCloseSortable', () => {
|
||||
initSortable();
|
||||
@@ -382,8 +381,6 @@ onBeforeMount(() => {
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
// 取消非本页面调用监听
|
||||
mittBus.off('onCurrentContextmenuClick');
|
||||
// 取消监听布局配置界面开启/关闭拖拽
|
||||
mittBus.off('openOrCloseSortable');
|
||||
});
|
||||
@@ -523,8 +520,14 @@ onBeforeRouteUpdate((to) => {
|
||||
-webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
|
||||
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTg5Ljc2MjQgNy4zMzAxNCA1NS4xMjUyKSIgc3Ryb2tlPSJudWxsIiBpZD0ic3ZnXzEiIGZpbGw9IiNGOEVBRTciIGQ9Im02Mi41NzQ0OSwxMTcuNTIwODZjLTU1LjIyOCwwIC0xMDAsLTQ0Ljc3MiAtMTAwLC0xMDBsMCwxMDBsMTAwLDB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogIDxwYXRoIGQ9Im0tMC42Mzc2Niw3LjMxMjI4YzAuMTE5MTksMCAwLjIxNzM3LDAuMDU3OTYgMC40NzY3NiwwLjExOTE5YzAuMjMyLDAuMDU0NzcgMC4yNzMyOSwwLjAzNDkxIDAuMzU3NTcsMC4xMTkxOWMwLjA4NDI4LDAuMDg0MjggMC4zNTc1NywwIDAuNDc2NzYsMGwwLjExOTE5LDBsMC4yMzgzOCwwIiBpZD0ic3ZnXzIiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxwYXRoIGQ9Im0yOC45MjEzNCw2OS4wNTI0NGMwLDAuMTE5MTkgMCwwLjIzODM4IDAsMC4zNTc1N2wwLDAuMTE5MTlsMCwwLjExOTE5IiBpZD0ic3ZnXzMiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNCIgaGVpZ2h0PSIwIiB3aWR0aD0iMS4zMTEwOCIgeT0iNi44MzU1MiIgeD0iLTAuMDQxNzEiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNSIgaGVpZ2h0PSIxLjc4Nzg0IiB3aWR0aD0iMC4xMTkxOSIgeT0iNjguNDU2NSIgeD0iMjguOTIxMzQiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNiIgaGVpZ2h0PSI0Ljg4Njc3IiB3aWR0aD0iMTkuMDcwMzIiIHk9IjUxLjI5MzIxIiB4PSIzNi42Njg2NiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiA8L2c+Cjwvc3ZnPg=='),
|
||||
url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect rx='8' width='100%' height='100%' fill='%23F8EAE7'/></svg>");
|
||||
-webkit-mask-size: 18px 30px, 20px 30px, calc(100% - 30px) calc(100% + 17px);
|
||||
-webkit-mask-position: right bottom, left bottom, center top;
|
||||
-webkit-mask-size:
|
||||
18px 30px,
|
||||
20px 30px,
|
||||
calc(100% - 30px) calc(100% + 17px);
|
||||
-webkit-mask-position:
|
||||
right bottom,
|
||||
left bottom,
|
||||
center top;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@@ -545,14 +548,14 @@ onBeforeRouteUpdate((to) => {
|
||||
|
||||
&:hover {
|
||||
@extend .tgs-style-three-svg;
|
||||
background: var(--el-color-primary-light-9);
|
||||
background: var(--tagsview3-active-background-color);
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active {
|
||||
@extend .tgs-style-three-svg;
|
||||
background: var(--el-color-primary-light-9) !important;
|
||||
background: var(--tagsview3-active-background-color) !important;
|
||||
color: var(--el-color-primary) !important;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { templateResolve } from '@/common/utils/string';
|
||||
import { NextLoading } from '@/common/utils/loading';
|
||||
import { dynamicRoutes, staticRoutes, pathMatch } from './route';
|
||||
import openApi from '@/common/openApi';
|
||||
import sockets from '@/common/sockets';
|
||||
import syssocket from '@/common/syssocket';
|
||||
import pinia from '@/store/index';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
@@ -179,7 +179,6 @@ export async function initRouter() {
|
||||
}
|
||||
}
|
||||
|
||||
let SysWs: any;
|
||||
let loadRouter = false;
|
||||
|
||||
// 路由加载前
|
||||
@@ -204,10 +203,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
resetRoute();
|
||||
NProgress.done();
|
||||
|
||||
if (SysWs) {
|
||||
SysWs.close();
|
||||
SysWs = undefined;
|
||||
}
|
||||
syssocket.destory();
|
||||
return;
|
||||
}
|
||||
if (token && to.path === '/login') {
|
||||
@@ -217,9 +213,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
}
|
||||
|
||||
// 终端不需要连接系统websocket消息
|
||||
if (!SysWs && to.path != '/machine/terminal') {
|
||||
SysWs = sockets.sysMsgSocket();
|
||||
if (to.path != '/machine/terminal') {
|
||||
syssocket.init();
|
||||
}
|
||||
|
||||
// 不存在路由(避免刷新页面找不到路由)并且未加载过(避免token过期,导致获取权限接口报权限不足,无限获取),则重新初始化路由
|
||||
if (useRoutesList().routesList.length == 0 && !loadRouter) {
|
||||
await initRouter();
|
||||
|
||||
@@ -135,6 +135,10 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
globalI18n: 'zh-cn',
|
||||
// 默认全局组件大小,可选值"<|large|default|small>",默认 ''
|
||||
globalComponentSize: '',
|
||||
|
||||
/** 全局设置 */
|
||||
// 默认列表页的分页大小
|
||||
defaultListPageSize: 15,
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
@@ -151,7 +155,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
this.themeConfig.editorTheme = 'vs-dark';
|
||||
} else {
|
||||
body.setAttribute('class', '');
|
||||
this.themeConfig.editorTheme = 'SolarizedLight';
|
||||
this.themeConfig.editorTheme = 'vs';
|
||||
}
|
||||
},
|
||||
// 设置水印配置信息
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
--color-seting-main: #e9eef3;
|
||||
--color-seting-aside: #d3dce6;
|
||||
--color-seting-header: #b3c0d1;
|
||||
|
||||
--tagsview3-active-background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
@@ -24,4 +24,6 @@ html.dark {
|
||||
--bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
|
||||
--bg-columnsMenuBar: var(--next-color-disabled) !important;
|
||||
--bg-columnsMenuBarColor: var(--next-color-bar) !important;
|
||||
|
||||
--tagsview3-active-background-color: var(--next-color-hover);
|
||||
}
|
||||
@@ -345,4 +345,5 @@
|
||||
.el-dialog {
|
||||
border-radius: 6px; /* 设置圆角 */
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
6
mayfly_go_web/src/types/pinia.d.ts
vendored
6
mayfly_go_web/src/types/pinia.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
declare interface UserInfoState<T = any> {
|
||||
userInfo: any;
|
||||
userInfo: T;
|
||||
}
|
||||
|
||||
declare interface ThemeConfigState {
|
||||
@@ -57,6 +57,8 @@ declare interface ThemeConfigState {
|
||||
terminalFontSize: number;
|
||||
terminalFontWeight: string | any;
|
||||
editorTheme: string;
|
||||
|
||||
defaultListPageSize: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,7 +94,7 @@ declare interface TagsView {
|
||||
}
|
||||
|
||||
// TagsView 路由列表
|
||||
declare interface TagsViewsState<T = any> {
|
||||
declare interface TagsViewsState<> {
|
||||
tagsViews: TagsView[];
|
||||
}
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ const loginResDeal = (loginRes: any) => {
|
||||
useUserInfo().setUserInfo(userInfos);
|
||||
|
||||
const token = loginRes.token;
|
||||
// 如果不需要otp校验,则该token即为accessToken,否则为otp校验token
|
||||
// 如果不需要 otp校验,则该token即为accessToken,否则为otp校验token
|
||||
if (loginRes.otp == -1) {
|
||||
signInSuccess(token);
|
||||
return;
|
||||
@@ -385,6 +385,7 @@ const signInSuccess = async (accessToken: string = '') => {
|
||||
}
|
||||
// 存储 token 到浏览器缓存
|
||||
saveToken(accessToken);
|
||||
|
||||
// 初始化路由
|
||||
await initRouter();
|
||||
|
||||
|
||||
45
mayfly_go_web/src/views/ops/component/ResourceTag.vue
Normal file
45
mayfly_go_web/src/views/ops/component/ResourceTag.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
|
||||
<el-popover :show-after="500" @show="getTags" placement="top-start" width="230" trigger="hover">
|
||||
<template #reference>
|
||||
<div>
|
||||
<!-- <el-button type="primary" link size="small">标签</el-button> -->
|
||||
<SvgIcon name="view" :size="16" color="var(--el-color-primary)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tag effect="plain" v-for="tag in tags" :key="tag" class="ml5" type="success" size="small">{{ tag.tagPath }}</el-tag>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [] as any,
|
||||
});
|
||||
|
||||
const { tags } = toRefs(state);
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
|
||||
<el-popover @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover">
|
||||
<el-popover :show-after="500" @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</template>
|
||||
<span v-for="(v, i) in tags" :key="i">
|
||||
<el-tooltip effect="customized" :content="v.remark" placement="top">
|
||||
<el-tooltip :content="v.remark" placement="top">
|
||||
<span class="color-success">{{ v.name }}</span>
|
||||
</el-tooltip>
|
||||
<span v-if="i != state.tags.length - 1" class="color-primary"> / </span>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
ref="treeRef"
|
||||
:style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }"
|
||||
:highlight-current="true"
|
||||
:indent="7"
|
||||
:indent="10"
|
||||
:load="loadNode"
|
||||
:props="treeProps"
|
||||
lazy
|
||||
@@ -21,7 +21,7 @@
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span>
|
||||
<span v-if="data.type == TagTreeNode.TagPath">
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
<tag-info :tag-path="data.label" />
|
||||
</span>
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
<span class="ml3">
|
||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
||||
</span>
|
||||
|
||||
<slot :node="node" :data="data" name="suffix"></slot>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
@@ -40,19 +42,25 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
||||
import { TagTreeNode } from './tag';
|
||||
import { onMounted, reactive, ref, watch, toRefs, onUnmounted } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { useViewport } from '@/common/use';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
tagPathNodeType: {
|
||||
type: [NodeType],
|
||||
required: true,
|
||||
},
|
||||
load: {
|
||||
type: Function,
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
loadContextmenuItems: {
|
||||
type: Function,
|
||||
@@ -70,6 +78,8 @@ const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
||||
const treeRef: any = ref(null);
|
||||
const contextmenuRef = ref();
|
||||
|
||||
const { vh } = useViewport();
|
||||
|
||||
const state = reactive({
|
||||
height: 600 as any,
|
||||
filterText: '',
|
||||
@@ -83,18 +93,18 @@ const state = reactive({
|
||||
const { filterText } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.height) {
|
||||
setHeight();
|
||||
window.onresize = () => setHeight();
|
||||
} else {
|
||||
state.height = props.height;
|
||||
}
|
||||
setHeight();
|
||||
window.addEventListener('resize', setHeight);
|
||||
});
|
||||
|
||||
const setHeight = () => {
|
||||
state.height = window.innerHeight - 157 + 'px';
|
||||
state.height = vh.value - 148 + 'px';
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', setHeight);
|
||||
});
|
||||
|
||||
watch(filterText, (val) => {
|
||||
treeRef.value?.filter(val);
|
||||
});
|
||||
@@ -104,6 +114,18 @@ const filterNode = (value: string, data: any) => {
|
||||
return data.label.includes(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载标签树节点
|
||||
*/
|
||||
const loadTags = async () => {
|
||||
const tags = await tagApi.getResourceTagPaths.request({ resourceType: props.resourceType });
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tags) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, props.tagPathNodeType));
|
||||
}
|
||||
return tagNodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载树节点
|
||||
* @param { Object } node
|
||||
@@ -115,7 +137,13 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
}
|
||||
let nodes = [];
|
||||
try {
|
||||
nodes = await props.load(node);
|
||||
if (node.level == 0) {
|
||||
nodes = await loadTags();
|
||||
} else if (props.load) {
|
||||
nodes = await props.load(node);
|
||||
} else {
|
||||
nodes = await node.data.loadChildren();
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -124,18 +152,23 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
|
||||
const treeNodeClick = (data: any) => {
|
||||
emit('nodeClick', data);
|
||||
if (data.type.nodeClickFunc) {
|
||||
data.type.nodeClickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
if (!props.loadContextmenuItems) {
|
||||
return;
|
||||
}
|
||||
// 加载当前节点是否需要显示右击菜单
|
||||
const items = props.loadContextmenuItems(data);
|
||||
let items = data.type.contextMenuItems;
|
||||
if (!items || items.length == 0) {
|
||||
if (props.loadContextmenuItems) {
|
||||
items = props.loadContextmenuItems(data);
|
||||
}
|
||||
}
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
state.contextmenuItems = items;
|
||||
@@ -173,6 +206,7 @@ defineExpose({
|
||||
overflow: 'auto';
|
||||
position: relative;
|
||||
|
||||
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
|
||||
.el-tree {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div>
|
||||
<el-tree-select
|
||||
v-bind="$attrs"
|
||||
@check="changeTag"
|
||||
v-model="selectTags"
|
||||
@change="changeTag"
|
||||
style="width: 100%"
|
||||
:data="tags"
|
||||
placeholder="请选择关联标签"
|
||||
:render-after-expand="true"
|
||||
:default-expanded-keys="[selectTags]"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
node-key="id"
|
||||
:props="{
|
||||
value: 'id',
|
||||
@@ -33,35 +33,46 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAttrs, toRefs, reactive, onMounted } from 'vue';
|
||||
import { toRefs, reactive, onMounted } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
const attrs = useAttrs();
|
||||
//定义事件
|
||||
const emit = defineEmits(['changeTag', 'update:tagPath']);
|
||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
// 单选则为id,多选为id数组
|
||||
selectTags: null as any,
|
||||
selectTags: [],
|
||||
});
|
||||
|
||||
const { tags, selectTags } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (attrs.modelValue) {
|
||||
state.selectTags = attrs.modelValue;
|
||||
if (props.resourceCode) {
|
||||
const resourceTags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
state.selectTags = resourceTags.map((x: any) => x.tagId);
|
||||
changeTag();
|
||||
}
|
||||
|
||||
state.tags = await tagApi.getTagTrees.request(null);
|
||||
});
|
||||
|
||||
const changeTag = (tag: any, checkInfo: any) => {
|
||||
if (checkInfo.checkedNodes.length > 0) {
|
||||
emit('update:tagPath', tag.codePath);
|
||||
emit('changeTag', tag);
|
||||
} else {
|
||||
emit('update:tagPath', null);
|
||||
}
|
||||
const changeTag = () => {
|
||||
emit('changeTag', state.selectTags);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
|
||||
export class TagTreeNode {
|
||||
/**
|
||||
* 节点id
|
||||
@@ -12,18 +14,26 @@ export class TagTreeNode {
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
type: any;
|
||||
type: NodeType;
|
||||
|
||||
/**
|
||||
* 是否为叶子节点
|
||||
*/
|
||||
isLeaf: boolean = false;
|
||||
|
||||
/**
|
||||
* 额外需要传递的参数
|
||||
*/
|
||||
params: any;
|
||||
|
||||
icon: any;
|
||||
|
||||
static TagPath = -1;
|
||||
|
||||
constructor(key: any, label: string, type?: any) {
|
||||
constructor(key: any, label: string, type?: NodeType) {
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
this.type = type || TagTreeNode.TagPath;
|
||||
this.type = type || new NodeType(TagTreeNode.TagPath);
|
||||
}
|
||||
|
||||
withIsLeaf(isLeaf: boolean) {
|
||||
@@ -35,4 +45,73 @@ export class TagTreeNode {
|
||||
this.params = params;
|
||||
return this;
|
||||
}
|
||||
|
||||
withIcon(icon: any) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
||||
* @returns 子节点信息
|
||||
*/
|
||||
async loadChildren() {
|
||||
if (this.isLeaf) {
|
||||
return [];
|
||||
}
|
||||
if (this.type && this.type.loadNodesFunc) {
|
||||
return await this.type.loadNodesFunc(this);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型,用于加载子节点及点击事件等
|
||||
*/
|
||||
export class NodeType {
|
||||
/**
|
||||
* 节点类型值
|
||||
*/
|
||||
value: number;
|
||||
|
||||
contextMenuItems: ContextmenuItem[];
|
||||
|
||||
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
||||
|
||||
nodeClickFunc: (node: TagTreeNode) => void;
|
||||
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值加载子节点回调函数
|
||||
* @param func 加载子节点回调函数
|
||||
* @returns this
|
||||
*/
|
||||
withLoadNodesFunc(func: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>) {
|
||||
this.loadNodesFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值节点点击事件回调函数
|
||||
* @param func 节点点击事件回调函数
|
||||
* @returns this
|
||||
*/
|
||||
withNodeClickFunc(func: (node: TagTreeNode) => void) {
|
||||
this.nodeClickFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值右击菜单按钮选项
|
||||
* @param contextMenuItems 右击菜单按钮选项
|
||||
* @returns this
|
||||
*/
|
||||
withContextMenuItems(contextMenuItems: ContextmenuItem[]) {
|
||||
this.contextMenuItems = contextMenuItems;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,19 @@
|
||||
width="38%"
|
||||
>
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="tagId" label="标签" required>
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Db.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="instanceId" label="数据库实例" required>
|
||||
@@ -19,7 +30,7 @@
|
||||
:disabled="form.id !== undefined"
|
||||
remote
|
||||
:remote-method="getInstances"
|
||||
@change="getAllDatabase"
|
||||
@change="changeInstance"
|
||||
v-model="form.instanceId"
|
||||
placeholder="请输入实例名称搜索并选择实例"
|
||||
filterable
|
||||
@@ -77,7 +88,8 @@
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -128,6 +140,7 @@ const rules = {
|
||||
};
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
@@ -135,9 +148,9 @@ const state = reactive({
|
||||
databaseList: [] as any,
|
||||
form: {
|
||||
id: null,
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
tagId: [],
|
||||
name: null,
|
||||
code: '',
|
||||
database: '',
|
||||
remark: '',
|
||||
instanceId: null as any,
|
||||
@@ -148,13 +161,14 @@ const state = reactive({
|
||||
|
||||
const { dialogVisible, allDatabases, databaseList, form, btnLoading } = toRefs(state);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
if (!state.dialogVisible) {
|
||||
return;
|
||||
}
|
||||
if (newValue.db) {
|
||||
state.form = { ...newValue.db };
|
||||
|
||||
// 将数据库名使用空格切割,获取所有数据库列表
|
||||
state.databaseList = newValue.db.database.split(' ');
|
||||
} else {
|
||||
@@ -163,6 +177,11 @@ watch(props, (newValue: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
const changeInstance = () => {
|
||||
state.databaseList = [];
|
||||
getAllDatabase();
|
||||
};
|
||||
|
||||
/**
|
||||
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
|
||||
*/
|
||||
|
||||
@@ -46,54 +46,43 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
|
||||
</template>
|
||||
|
||||
<template #database="{ data }">
|
||||
<el-popover placement="right" trigger="click" :width="300">
|
||||
<template #reference>
|
||||
<el-link type="primary" :underline="false" plain @click="selectDb(data.dbs)">查看 </el-link>
|
||||
</template>
|
||||
<el-input v-model="filterDb.param" @keyup="filterSchema" class="w-50 m-2" placeholder="搜索" size="small">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<search-icon />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div
|
||||
class="el-tag--plain el-tag--success"
|
||||
v-for="db in filterDb.list"
|
||||
:key="db"
|
||||
style="border: 1px var(--color-success-light-3) solid; margin-top: 3px; border-radius: 5px; padding: 2px; position: relative"
|
||||
>
|
||||
<el-link type="success" plain size="small" :underline="false">{{ db }}</el-link>
|
||||
<el-link type="primary" plain size="small" :underline="false" @click="showTableInfo(data, db)" style="position: absolute; right: 4px"
|
||||
>操作
|
||||
</el-link>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<template #more="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
<el-button class="ml5" type="primary" @click="onShowSqlExec(data)" link>SQL执行记录</el-button>
|
||||
<template #host="{ data }">
|
||||
{{ `${data.host}:${data.port}` }}
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>编辑</el-button>
|
||||
<el-button v-if="data.type == 'mysql'" class="ml5" type="primary" @click="onDumpDbs(data)" link>导出</el-button>
|
||||
<span v-if="actionBtns[perms.saveDb]">
|
||||
<el-button type="primary" @click="editDb(data)" link>编辑</el-button>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
</span>
|
||||
|
||||
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-dropdown @command="handleMoreActionCommand">
|
||||
<span class="el-dropdown-link-more">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
|
||||
<!-- <el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.saveDb]"> 编辑 </el-dropdown-item> -->
|
||||
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="data.type == DbType.mysql"> 导出 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog width="80%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
|
||||
<db-table-list :db-id="dbId" :db="db" :db-type="state.row.type" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<el-dialog width="720px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="9">
|
||||
<el-form-item label="导出内容: ">
|
||||
@@ -172,18 +161,20 @@ import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { dbApi } from './api';
|
||||
import config from '@/common/config';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { Search as SearchIcon } from '@element-plus/icons-vue';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||
import { DbType } from './dialect';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
const DbTableList = defineAsyncComponent(() => import('./table/DbTableList.vue'));
|
||||
|
||||
const perms = {
|
||||
base: 'db',
|
||||
@@ -194,17 +185,20 @@ const perms = {
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.slot('instanceId', '实例', 'instanceSelect')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('instanceName', '实例名'),
|
||||
TableColumn.new('type', '类型'),
|
||||
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
|
||||
TableColumn.new('username', 'username'),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('database', '数据库').isSlot().setMinWidth(70),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('more', '更多').isSlot().setMinWidth(180).fixedRight(),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(150).fixedRight().alignCenter();
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
||||
|
||||
const route = useRoute();
|
||||
const pageTableRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
@@ -221,10 +215,10 @@ const state = reactive({
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
tagPath: null,
|
||||
tagPath: '',
|
||||
instanceId: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
@@ -236,13 +230,6 @@ const state = reactive({
|
||||
instanceId: 0,
|
||||
},
|
||||
},
|
||||
showDumpInfo: false,
|
||||
dumpInfo: {
|
||||
id: 0,
|
||||
db: '',
|
||||
type: 3,
|
||||
tables: [],
|
||||
},
|
||||
// sql执行记录弹框
|
||||
sqlExecLogDialog: {
|
||||
title: '',
|
||||
@@ -250,10 +237,6 @@ const state = reactive({
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
chooseTableName: '',
|
||||
tableInfoDialog: {
|
||||
visible: false,
|
||||
},
|
||||
exportDialog: {
|
||||
visible: false,
|
||||
dbId: 0,
|
||||
@@ -275,8 +258,7 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { dbId, db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, tableInfoDialog, exportDialog, dbEditDialog, filterDb } =
|
||||
toRefs(state);
|
||||
const { db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -288,6 +270,10 @@ onMounted(async () => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
|
||||
if (route.query.tagPath) {
|
||||
state.query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
let res: any = await dbApi.dbs.request(state.query);
|
||||
// 切割数据库
|
||||
res.list?.forEach((e: any) => {
|
||||
@@ -316,7 +302,7 @@ const onBeforeCloseInfoDialog = () => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await dbApi.dbTags.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Db.value });
|
||||
};
|
||||
|
||||
const getInstances = async (instanceName = '') => {
|
||||
@@ -330,6 +316,24 @@ const getInstances = async (instanceName = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoreActionCommand = (commond: any) => {
|
||||
const data = commond.data;
|
||||
const type = commond.type;
|
||||
switch (type) {
|
||||
case 'detail': {
|
||||
showInfo(data);
|
||||
return;
|
||||
}
|
||||
case 'edit': {
|
||||
editDb(data);
|
||||
return;
|
||||
}
|
||||
case 'dumpDb': {
|
||||
onDumpDbs(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const editDb = async (data: any) => {
|
||||
if (!data) {
|
||||
state.dbEditDialog.data = null;
|
||||
@@ -355,7 +359,9 @@ const deleteDb = async () => {
|
||||
await dbApi.deleteDb.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const onShowSqlExec = async (row: any) => {
|
||||
@@ -406,40 +412,23 @@ const dumpDbs = () => {
|
||||
'href',
|
||||
`${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${
|
||||
state.exportDialog.extName
|
||||
}&token=${getToken()}`
|
||||
}&${joinClientParams()}`
|
||||
);
|
||||
a.click();
|
||||
state.exportDialog.visible = false;
|
||||
};
|
||||
|
||||
const showTableInfo = async (row: any, db: string) => {
|
||||
state.dbId = row.id;
|
||||
state.row = row;
|
||||
state.db = db;
|
||||
state.tableInfoDialog.visible = true;
|
||||
};
|
||||
|
||||
const closeTableInfo = () => {
|
||||
state.showDumpInfo = false;
|
||||
state.tableInfoDialog.visible = false;
|
||||
};
|
||||
|
||||
// 点击查看时初始化数据
|
||||
const selectDb = (row: any) => {
|
||||
state.filterDb.param = '';
|
||||
state.filterDb.cache = row;
|
||||
state.filterDb.list = row;
|
||||
};
|
||||
|
||||
// 输入字符过滤schema
|
||||
const filterSchema = () => {
|
||||
if (state.filterDb.param) {
|
||||
state.filterDb.list = state.filterDb.cache.filter((a) => {
|
||||
return String(a).toLowerCase().indexOf(state.filterDb.param) > -1;
|
||||
});
|
||||
} else {
|
||||
state.filterDb.list = state.filterDb.cache;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
.db-list {
|
||||
.el-transfer-panel {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
.el-dropdown-link-more {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs,watch, reactive, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { toRefs, watch, reactive, onMounted } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum } from './enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -103,13 +103,12 @@ onMounted(async () => {
|
||||
searchSqlExecLog();
|
||||
});
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
watch(props, async () => {
|
||||
await searchSqlExecLog();
|
||||
});
|
||||
|
||||
|
||||
const searchSqlExecLog = async () => {
|
||||
state.query.dbId = props.dbId
|
||||
state.query.dbId = props.dbId;
|
||||
const res = await dbApi.getSqlExecs.request(state.query);
|
||||
state.data = res.list;
|
||||
state.total = res.total;
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="testConn" :loading="state.testConnBtnLoading" type="success">测试连接</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
@@ -153,6 +154,7 @@ const state = reactive({
|
||||
// 原用户名
|
||||
oldUserName: null,
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, pwd, btnLoading } = toRefs(state);
|
||||
@@ -176,6 +178,32 @@ const getDbPwd = async () => {
|
||||
state.pwd = await dbApi.getInstancePwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const getReqForm = async () => {
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
if (!state.form.sshTunnelMachineId) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async () => {
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
state.testConnBtnLoading = true;
|
||||
try {
|
||||
await dbApi.testConn.request(await getReqForm());
|
||||
ElMessage.success('连接成功');
|
||||
} finally {
|
||||
state.testConnBtnLoading = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
if (!state.form.id) {
|
||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||
@@ -185,12 +213,7 @@ const btnOk = async () => {
|
||||
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
if (!state.form.sshTunnelMachineId) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
dbApi.saveInstance.request(reqForm).then(() => {
|
||||
dbApi.saveInstance.request(await getReqForm()).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -20,11 +20,8 @@
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #more="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
@@ -81,16 +78,16 @@ const queryConfig = [TableQuery.text('name', '名称')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('host', 'host:port').setFormatFunc((data: any, _prop: string) => `${data.host}:${data.port}`),
|
||||
TableColumn.new('type', '类型'),
|
||||
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('params', '连接参数'),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('more', '更多').isSlot().setMinWidth(50).fixedRight(),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveInstance]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(65).fixedRight().alignCenter();
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||
|
||||
const pageTableRef: any = ref(null);
|
||||
|
||||
@@ -108,7 +105,7 @@ const state = reactive({
|
||||
query: {
|
||||
name: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
@@ -173,7 +170,9 @@ const deleteInstance = async () => {
|
||||
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,130 +1,369 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row class="mb5">
|
||||
<el-col :span="4">
|
||||
<el-button
|
||||
:disabled="!state.db || !nowDbInst.id"
|
||||
type="primary"
|
||||
icon="plus"
|
||||
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases?.split(' ') }, state.db)"
|
||||
size="small"
|
||||
>新建查询</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="20" v-if="state.db">
|
||||
<el-descriptions :column="4" size="small" border style="height: 10px" class="ml5">
|
||||
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="实例" label-align="right">
|
||||
{{ nowDbInst.id }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.type }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.name }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="库名" label-align="right">{{ state.db }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row type="flex">
|
||||
<el-col :span="4">
|
||||
<tag-tree
|
||||
ref="tagTreeRef"
|
||||
@node-click="nodeClick"
|
||||
:load="loadNode"
|
||||
:load-contextmenu-items="getContextmenuItems"
|
||||
@current-contextmenu-click="onCurrentContextmenuClick"
|
||||
:height="state.tagTreeHeight"
|
||||
>
|
||||
<div class="db-sql-exec">
|
||||
<el-row>
|
||||
<el-col :span="5">
|
||||
<tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type == NodeType.DbInst">
|
||||
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
|
||||
<span v-if="data.type.value == SqlExecNodeType.DbInst">
|
||||
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" />
|
||||
<SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres" :size="18" />
|
||||
|
||||
<SvgIcon name="InfoFilled" v-else />
|
||||
<SvgIcon :name="getDbDialect(data.params.type).getIcon()" :size="18" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
|
||||
<el-form-item label="类型:">{{ data.params.type }}</el-form-item>
|
||||
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
||||
<el-form-item v-if="data.params.remark" label="备注:">{{ data.params.remark }}</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ data.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
{{ `${data.params.host}:${data.params.port}` }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="user">
|
||||
{{ data.params.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">
|
||||
{{ data.params.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
|
||||
<SvgIcon v-if="data.type == NodeType.Db" name="Coin" color="#67c23a" />
|
||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||
</template>
|
||||
|
||||
<SvgIcon name="Calendar" v-if="data.type == NodeType.TableMenu" color="#409eff" />
|
||||
|
||||
<el-tooltip v-if="data.type == NodeType.Table" effect="customized" :content="data.params.tableComment" placement="top-end">
|
||||
<SvgIcon name="Calendar" color="#409eff" />
|
||||
<template #label="{ data }">
|
||||
<el-tooltip placement="left" :show-after="1000" v-if="data.type.value == SqlExecNodeType.Table" :content="data.params.tableComment">
|
||||
{{ data.label }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql" color="#f56c6c" />
|
||||
<template #suffix="{ data }">
|
||||
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
|
||||
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{
|
||||
` ${data.params.dbTableSize}`
|
||||
}}</span>
|
||||
</template>
|
||||
</tag-tree>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-container id="data-exec" class="mt5 ml5">
|
||||
<el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key" :name="dt.key">
|
||||
<table-data
|
||||
v-if="dt.type === TabType.TableData"
|
||||
@gen-insert-sql="onGenerateInsertSql"
|
||||
:data="dt"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
></table-data>
|
||||
|
||||
<query
|
||||
v-else
|
||||
<el-col :span="19">
|
||||
<el-row>
|
||||
<el-col :span="24" v-if="state.db">
|
||||
<el-descriptions :column="4" size="small" border class="ml5">
|
||||
<el-descriptions-item label-align="right" label="操作"
|
||||
><el-button
|
||||
:disabled="!state.db || !nowDbInst.id"
|
||||
type="primary"
|
||||
icon="Search"
|
||||
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases?.split(' ') }, state.db)"
|
||||
size="small"
|
||||
>新建查询</el-button
|
||||
></el-descriptions-item
|
||||
>
|
||||
|
||||
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label-align="right">
|
||||
<template #label>
|
||||
<div>
|
||||
<SvgIcon :name="getDbDialect(nowDbInst.type).getIcon()" :size="18" />
|
||||
实例
|
||||
</div>
|
||||
</template>
|
||||
{{ nowDbInst.id }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.name }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.host }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="库名" label-align="right">{{ state.db }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div id="data-exec" class="mt5 ml5">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
style="width: 100%"
|
||||
v-model="state.activeName"
|
||||
>
|
||||
<el-tab-pane closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference> {{ dt.label }} </template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="tagPath">
|
||||
{{ dt.params.tagPath }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">
|
||||
{{ dt.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
<SvgIcon :name="getDbDialect(dt.params.type).getIcon()" :size="18" />
|
||||
{{ dt.params.host }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="库名">
|
||||
{{ dt.params.dbName }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<db-table-data-op
|
||||
v-if="dt.type === TabType.TableData"
|
||||
:db-id="dt.dbId"
|
||||
:db-name="dt.db"
|
||||
:table-name="dt.params.table"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
></db-table-data-op>
|
||||
|
||||
<db-sql-editor
|
||||
v-if="dt.type === TabType.Query"
|
||||
:db-id="dt.dbId"
|
||||
:db-name="dt.db"
|
||||
:sql-name="dt.params.sqlName"
|
||||
@save-sql-success="reloadSqls"
|
||||
@delete-sql-success="deleteSqlScript(dt)"
|
||||
:data="dt"
|
||||
:editor-height="state.editorHeight"
|
||||
>
|
||||
</query>
|
||||
</db-sql-editor>
|
||||
|
||||
<db-tables-op
|
||||
v-if="dt.type == TabType.TablesOp"
|
||||
:db-id="dt.params.id"
|
||||
:db="dt.params.db"
|
||||
:db-type="dt.params.type"
|
||||
:height="state.tablesOpHeight"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" width="1000px">
|
||||
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, toRefs, onBeforeUnmount } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { DbInst, TabInfo, TabType, registerDbCompletionItemProvider } from './db';
|
||||
import { TagTreeNode } from '../component/tag';
|
||||
import { TagTreeNode, NodeType } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { dbApi } from './api';
|
||||
import { dispposeCompletionItemProvider } from '../../../components/monaco/completionItemProvider';
|
||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { getDbDialect } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
||||
|
||||
const Query = defineAsyncComponent(() => import('./component/tab/Query.vue'));
|
||||
const TableData = defineAsyncComponent(() => import('./component/tab/TableData.vue'));
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
class NodeType {
|
||||
class SqlExecNodeType {
|
||||
static DbInst = 1;
|
||||
static Db = 2;
|
||||
static TableMenu = 3;
|
||||
static SqlMenu = 4;
|
||||
static Table = 5;
|
||||
static Sql = 6;
|
||||
static PgSchemaMenu = 7;
|
||||
static PgSchema = 8;
|
||||
}
|
||||
class ContextmenuClickId {
|
||||
static ReloadTable = 0;
|
||||
}
|
||||
|
||||
const DbIcon = {
|
||||
name: 'Coin',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
// pgsql schema icon
|
||||
const SchemaIcon = {
|
||||
name: 'List',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
const TableIcon = {
|
||||
name: 'Calendar',
|
||||
color: '#409eff',
|
||||
};
|
||||
|
||||
const SqlIcon = {
|
||||
name: 'Files',
|
||||
color: '#f56c6c',
|
||||
};
|
||||
|
||||
// node节点点击时,触发改变db事件
|
||||
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
if (params.db) {
|
||||
changeDb(
|
||||
{ id: params.id, host: `${params.host}`, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.database },
|
||||
params.db
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
return dbs.map((x: any) => {
|
||||
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||
.withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
host: `${params.host}:${params.port}`,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库节点
|
||||
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
if (params.type == 'postgres') {
|
||||
return [new TagTreeNode(`${params.id}.${params.db}.schema-menu`, 'schema', NodeTypePostgresScheamMenu).withParams(params).withIcon(SchemaIcon)];
|
||||
}
|
||||
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// postgres schema模式菜单
|
||||
const NodeTypePostgresScheamMenu = new NodeType(SqlExecNodeType.PgSchemaMenu)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
return schemaNames.map((sn: any) => {
|
||||
// 将db变更为 db/schema;
|
||||
const nParams = { ...params };
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// postgres schema模式
|
||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库表菜单节点
|
||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadTables(data.key)),
|
||||
|
||||
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
|
||||
const params = data.params;
|
||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
|
||||
}),
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
let dbTableSize = 0;
|
||||
const tablesNode = tables.map((x: any) => {
|
||||
dbTableSize += x.dataLength + x.indexLength;
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
||||
})
|
||||
.withIcon(TableIcon);
|
||||
});
|
||||
// 设置父节点参数的表大小
|
||||
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
|
||||
return tablesNode;
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库sql模板菜单节点
|
||||
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const id = params.id;
|
||||
const db = params.db;
|
||||
const dbs = params.dbs;
|
||||
// 加载用户保存的sql脚本
|
||||
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||
return sqls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
})
|
||||
.withIcon(SqlIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 表节点类型
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
||||
});
|
||||
|
||||
// sql模板节点类型
|
||||
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
||||
.withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
addQueryTab({ id: params.id, nodeKey: nodeData.key, dbs: params.dbs }, params.db, params.sqlName);
|
||||
})
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('delSql', '删除').withIcon('delete').withOnClick((data: any) => deleteSql(data.params.id, data.params.db, data.params.sqlName)),
|
||||
]);
|
||||
|
||||
const tagTreeRef: any = ref(null);
|
||||
|
||||
@@ -138,13 +377,9 @@ const state = reactive({
|
||||
activeName: '',
|
||||
reloadStatus: false,
|
||||
tabs,
|
||||
dataTabsTableHeight: '600',
|
||||
dataTabsTableHeight: 600,
|
||||
editorHeight: '600',
|
||||
tagTreeHeight: window.innerHeight - 178 + 'px',
|
||||
genSqlDialog: {
|
||||
visible: false,
|
||||
sql: '',
|
||||
},
|
||||
tablesOpHeight: '600',
|
||||
});
|
||||
|
||||
const { nowDbInst } = toRefs(state);
|
||||
@@ -163,205 +398,60 @@ onBeforeUnmount(() => {
|
||||
* 设置editor高度和数据表高度
|
||||
*/
|
||||
const setHeight = () => {
|
||||
state.editorHeight = window.innerHeight - 518 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 256 + 'px';
|
||||
state.tagTreeHeight = window.innerHeight - 165 + 'px';
|
||||
state.editorHeight = window.innerHeight - 525 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 255;
|
||||
state.tablesOpHeight = window.innerHeight - 220 + 'px';
|
||||
};
|
||||
|
||||
/**
|
||||
* instmap; tagPaht -> info[]
|
||||
*/
|
||||
const instMap: Map<string, any[]> = new Map();
|
||||
|
||||
const getInsts = async () => {
|
||||
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000 });
|
||||
if (!res.total) return;
|
||||
for (const db of res.list) {
|
||||
const tagPath = db.tagPath;
|
||||
let dbInsts = instMap.get(tagPath) || [];
|
||||
dbInsts.push(db);
|
||||
instMap.set(tagPath, dbInsts?.sort());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载树节点
|
||||
* @param {Object} node
|
||||
* @param {Object} resolve
|
||||
*/
|
||||
const loadNode = async (node: any) => {
|
||||
// 一级为tagPath
|
||||
if (node.level === 0) {
|
||||
await getInsts();
|
||||
const tagPaths = instMap.keys();
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tagPaths) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath));
|
||||
}
|
||||
return tagNodes;
|
||||
}
|
||||
|
||||
const data = node.data;
|
||||
const nodeType = data.type;
|
||||
const params = data.params;
|
||||
|
||||
// 点击tagPath -> 加载数据库实例信息列表
|
||||
if (nodeType === TagTreeNode.TagPath) {
|
||||
const dbInfos = instMap.get(data.key);
|
||||
return dbInfos?.map((x: any) => {
|
||||
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.DbInst).withParams(x);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击数据库实例 -> 加载库列表
|
||||
if (nodeType === NodeType.DbInst) {
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
return dbs.map((x: any) => {
|
||||
return new TagTreeNode(`${data.key}.${x}`, x, NodeType.Db).withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 点击数据库 -> 加载 表&Sql 菜单
|
||||
if (nodeType === NodeType.Db) {
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeType.TableMenu).withParams(params),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeType.SqlMenu).withParams(params),
|
||||
];
|
||||
}
|
||||
|
||||
// 点击表菜单 -> 加载表列表
|
||||
if (nodeType === NodeType.TableMenu) {
|
||||
return await getTables(params);
|
||||
}
|
||||
|
||||
if (nodeType === NodeType.SqlMenu) {
|
||||
return await loadSqls(params.id, params.db, params.dbs);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const nodeClick = async (data: any) => {
|
||||
const params = data.params;
|
||||
const nodeKey = data.key;
|
||||
const dataType = data.type;
|
||||
// 点击数据库,修改当前数据库信息
|
||||
if (dataType === NodeType.Db || dataType === NodeType.SqlMenu || dataType === NodeType.TableMenu || dataType === NodeType.DbInst) {
|
||||
changeSchema({ id: params.id, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.database }, params.db);
|
||||
return;
|
||||
}
|
||||
|
||||
// 点击表加载表数据tab
|
||||
if (dataType === NodeType.Table) {
|
||||
await loadTableData({ id: params.id, nodeKey: nodeKey }, params.db, params.tableName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 点击表加载表数据tab
|
||||
if (dataType === NodeType.Sql) {
|
||||
await addQueryTab({ id: params.id, nodeKey: nodeKey, dbs: params.dbs }, params.db, params.sqlName);
|
||||
}
|
||||
};
|
||||
|
||||
const getContextmenuItems = (data: any) => {
|
||||
const dataType = data.type;
|
||||
if (dataType === NodeType.TableMenu) {
|
||||
return [{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 当前右击菜单点击事件
|
||||
const onCurrentContextmenuClick = (clickData: any) => {
|
||||
const clickId = clickData.id;
|
||||
if (clickId == ContextmenuClickId.ReloadTable) {
|
||||
reloadTables(clickData.item.key);
|
||||
}
|
||||
};
|
||||
|
||||
const getTables = async (params: any) => {
|
||||
const { id, db } = params;
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
return tables.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({
|
||||
id,
|
||||
db,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载用户保存的sql脚本
|
||||
*
|
||||
* @param inst
|
||||
* @param schema
|
||||
*/
|
||||
const loadSqls = async (id: any, db: string, dbs: any) => {
|
||||
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||
return sqls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeType.Sql).withIsLeaf(true).withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 选择数据库
|
||||
const changeSchema = (inst: any, schema: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(inst);
|
||||
state.db = schema;
|
||||
// 选择数据库,改变当前正在操作的数据库信息
|
||||
const changeDb = (db: any, dbName: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(db);
|
||||
state.db = dbName;
|
||||
};
|
||||
|
||||
// 加载选中的表数据,即新增表数据操作tab
|
||||
const loadTableData = async (inst: any, schema: string, tableName: string) => {
|
||||
changeSchema(inst, schema);
|
||||
const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||
if (tableName == '') {
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const label = `${inst.id}:\`${schema}\`.${tableName}`;
|
||||
let tab = state.tabs.get(label);
|
||||
state.activeName = label;
|
||||
const key = `${db.id}:\`${dbName}\`.${tableName}`;
|
||||
let tab = state.tabs.get(key);
|
||||
state.activeName = key;
|
||||
// 如果存在该表tab,则直接返回
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = label;
|
||||
tab.treeNodeKey = inst.nodeKey;
|
||||
tab.dbId = inst.id;
|
||||
tab.db = schema;
|
||||
tab.label = tableName;
|
||||
tab.key = key;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = db.id;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.TableData;
|
||||
tab.params = {
|
||||
...getNowDbInfo(),
|
||||
table: tableName,
|
||||
};
|
||||
state.tabs.set(label, tab);
|
||||
state.tabs.set(key, tab);
|
||||
};
|
||||
|
||||
// 新建查询panel
|
||||
const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
|
||||
if (!db || !inst.id) {
|
||||
// 新建查询tab
|
||||
const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
if (!dbName || !db.id) {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = inst.id;
|
||||
const dbId = db.id;
|
||||
let label;
|
||||
let key;
|
||||
// 存在sql模板名,则该模板名只允许一个tab
|
||||
if (sqlName) {
|
||||
label = `查询:${dbId}:${db}.${sqlName}`;
|
||||
label = `查询-${sqlName}`;
|
||||
key = `查询:${dbId}:${dbName}.${sqlName}`;
|
||||
} else {
|
||||
let count = 1;
|
||||
state.tabs.forEach((v) => {
|
||||
@@ -369,24 +459,66 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
label = `新查询${count}:${dbId}:${db}`;
|
||||
label = `新查询-${count}`;
|
||||
key = `新查询${count}:${dbId}:${dbName}`;
|
||||
}
|
||||
state.activeName = label;
|
||||
let tab = state.tabs.get(label);
|
||||
state.activeName = key;
|
||||
let tab = state.tabs.get(key);
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = label;
|
||||
tab.treeNodeKey = inst.nodeKey;
|
||||
tab.key = key;
|
||||
tab.label = label;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = dbId;
|
||||
tab.db = db;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.Query;
|
||||
tab.params = {
|
||||
...getNowDbInfo(),
|
||||
sqlName: sqlName,
|
||||
dbs: inst.dbs,
|
||||
dbs: db.dbs,
|
||||
};
|
||||
state.tabs.set(label, tab);
|
||||
state.tabs.set(key, tab);
|
||||
|
||||
// 注册当前sql编辑框提示词
|
||||
registerDbCompletionItemProvider('sql', tab.dbId, tab.db, tab.params.dbs);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加数据操作tab
|
||||
* @param inst
|
||||
*/
|
||||
const addTablesOpTab = async (db: any) => {
|
||||
const dbName = db.db;
|
||||
if (!db || !db.id) {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let key = `表操作:${dbId}:${dbName}.tablesOp`;
|
||||
state.activeName = key;
|
||||
|
||||
let tab = state.tabs.get(key);
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = key;
|
||||
tab.label = `表操作-${dbName}`;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = dbId;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.TablesOp;
|
||||
tab.params = {
|
||||
...getNowDbInfo(),
|
||||
id: db.id,
|
||||
db: dbName,
|
||||
type: db.type,
|
||||
};
|
||||
state.tabs.set(key, tab);
|
||||
};
|
||||
|
||||
const onRemoveTab = (targetName: string) => {
|
||||
@@ -405,6 +537,7 @@ const onRemoveTab = (targetName: string) => {
|
||||
}
|
||||
state.tabs.delete(targetName);
|
||||
state.activeName = activeName;
|
||||
onTabChange();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -425,18 +558,23 @@ const onTabChange = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async (sql: string) => {
|
||||
state.genSqlDialog.sql = sql;
|
||||
state.genSqlDialog.visible = true;
|
||||
};
|
||||
|
||||
const reloadSqls = (dbId: number, db: string) => {
|
||||
tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db));
|
||||
};
|
||||
|
||||
const deleteSqlScript = (ti: TabInfo) => {
|
||||
reloadSqls(ti.dbId, ti.db);
|
||||
onRemoveTab(ti.key);
|
||||
const deleteSql = async (dbId: any, db: string, sqlName: string) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
|
||||
ElMessage.success('删除成功');
|
||||
reloadSqls(dbId, db);
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||
@@ -447,50 +585,53 @@ const reloadTables = (nodeKey: string) => {
|
||||
state.reloadStatus = true;
|
||||
tagTreeRef.value.reloadNode(nodeKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前操作的数据库信息
|
||||
*/
|
||||
const getNowDbInfo = () => {
|
||||
const di = state.nowDbInst;
|
||||
return {
|
||||
tagPath: di.tagPath,
|
||||
id: di.id,
|
||||
name: di.name,
|
||||
type: di.type,
|
||||
host: di.host,
|
||||
dbName: state.db,
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sql-file-exec {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
.db-sql-exec {
|
||||
.db-table-size {
|
||||
color: #c4c9c4;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.sqlEditor {
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#data-exec {
|
||||
min-height: calc(100vh - 155px);
|
||||
|
||||
.editor-move-resize {
|
||||
cursor: n-resize;
|
||||
height: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 30px;
|
||||
}
|
||||
|
||||
#data-exec {
|
||||
min-height: calc(100vh - 155px);
|
||||
.el-tabs__header {
|
||||
margin: 0 0 5px;
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0 0 5px;
|
||||
.el-tabs__item {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 5px;
|
||||
.el-tabs__nav-next,
|
||||
.el-tabs__nav-prev {
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.instances-pop-form {
|
||||
.el-form-item {
|
||||
margin-bottom: unset;
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,8 +10,8 @@ export const dbApi = {
|
||||
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
||||
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
||||
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
||||
tableMetadata: Api.newGet('/dbs/{id}/t-metadata'),
|
||||
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||
// 获取表即列提示
|
||||
hintTables: Api.newGet('/dbs/{id}/hint-tables'),
|
||||
sqlExec: Api.newPost('/dbs/{id}/exec-sql'),
|
||||
@@ -27,8 +27,9 @@ export const dbApi = {
|
||||
|
||||
// 获取权限列表
|
||||
instances: Api.newGet('/instances'),
|
||||
getInstance: Api.newGet("/instances/{instanceId}"),
|
||||
getInstance: Api.newGet('/instances/{instanceId}'),
|
||||
getAllDatabase: Api.newGet('/instances/{instanceId}/databases'),
|
||||
testConn: Api.newPost('/instances/test-conn'),
|
||||
saveInstance: Api.newPost('/instances'),
|
||||
getInstancePwd: Api.newGet('/instances/{id}/pwd'),
|
||||
deleteInstance: Api.newDelete('/instances/{id}'),
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table
|
||||
@cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
|
||||
@sort-change="(sort: any) => onTableSortChange(sort)"
|
||||
@selection-change="onDataSelectionChange"
|
||||
:data="datas"
|
||||
size="small"
|
||||
:max-height="tableHeight"
|
||||
v-loading="loading"
|
||||
element-loading-text="查询中..."
|
||||
:empty-text="emptyText"
|
||||
highlight-current-row
|
||||
stripe
|
||||
border
|
||||
class="mt5"
|
||||
>
|
||||
<el-table-column v-if="datas.length > 0 && table" type="selection" width="35" />
|
||||
|
||||
<template v-for="(item, index) in columns">
|
||||
<el-table-column
|
||||
min-width="100"
|
||||
:width="DbInst.flexColumnWidth(item.columnName, datas)"
|
||||
align="center"
|
||||
v-if="item.show"
|
||||
:key="index"
|
||||
:prop="item.columnName"
|
||||
:label="item.columnName"
|
||||
show-overflow-tooltip
|
||||
:sortable="sortable"
|
||||
>
|
||||
<template #header v-if="showColumnTip">
|
||||
<el-tooltip raw-content placement="top" effect="customized">
|
||||
<template #content> {{ getColumnTip(item) }} </template>
|
||||
{{ item.columnName }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, reactive, toRefs } from 'vue';
|
||||
import { DbInst, UpdateFieldsMeta, FieldsMeta } from '../db';
|
||||
|
||||
const emits = defineEmits(['sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField'])
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
db: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
},
|
||||
columns: {
|
||||
type: Array<any>,
|
||||
},
|
||||
sortable: {
|
||||
type: [String, Boolean],
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
showColumnTip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '600'
|
||||
}
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
dbId: 0, // 当前选中操作的数据库实例
|
||||
dbType: '',
|
||||
db: '', // 数据库名
|
||||
table: '', // 当前的表名
|
||||
datas: [],
|
||||
columns: [],
|
||||
sortable: false,
|
||||
loading: false,
|
||||
selectionDatas: [] as any,
|
||||
showColumnTip: false,
|
||||
tableHeight: '600',
|
||||
emptyText: '',
|
||||
updatedFields: [] as UpdateFieldsMeta[],// 各个tab表被修改的字段信息
|
||||
});
|
||||
|
||||
const {
|
||||
tableHeight,
|
||||
datas,
|
||||
sortable,
|
||||
loading,
|
||||
showColumnTip,
|
||||
} = toRefs(state);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
setState(newValue);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in DbTable mounted');
|
||||
setState(props);
|
||||
})
|
||||
|
||||
const setState = (props: any) => {
|
||||
state.dbId = props.dbId;
|
||||
state.dbType = props.dbType;
|
||||
state.db = props.db;
|
||||
state.table = props.table;
|
||||
state.datas = props.data;
|
||||
state.tableHeight = props.height;
|
||||
state.sortable = props.sortable;
|
||||
state.loading = props.loading;
|
||||
state.columns = props.columns;
|
||||
state.showColumnTip = props.showColumnTip;
|
||||
state.emptyText = props.emptyText;
|
||||
}
|
||||
|
||||
const getColumnTip = (column: any) => {
|
||||
const comment = column.columnComment;
|
||||
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 表排序字段变更
|
||||
*/
|
||||
const onTableSortChange = async (sort: any) => {
|
||||
if (!sort.prop) {
|
||||
return;
|
||||
}
|
||||
cancelUpdateFields();
|
||||
emits('sortChange', sort);
|
||||
};
|
||||
|
||||
const onDataSelectionChange = (datas: []) => {
|
||||
state.selectionDatas = datas;
|
||||
emits('selectionChange', datas);
|
||||
};
|
||||
|
||||
// 监听单元格点击事件
|
||||
const cellClick = (row: any, column: any, cell: any) => {
|
||||
const property = column.property;
|
||||
// 如果当前操作的表名不存在 或者 当前列的property不存在(如多选框),则不允许修改当前单元格内容
|
||||
if (!state.table || !property) {
|
||||
return;
|
||||
}
|
||||
let div: HTMLElement = cell.children[0];
|
||||
if (div && div.tagName === 'DIV') {
|
||||
// 转为字符串比较,可能存在数字等
|
||||
let text = (row[property] || row[property] == 0 ? row[property] : '') + '';
|
||||
let input = document.createElement('input');
|
||||
input.setAttribute('value', text);
|
||||
// 将表格width也赋值于输入框,避免输入框长度超过表格长度
|
||||
input.setAttribute('style', 'height:23px;text-align:center;border:none;' + div.getAttribute('style'));
|
||||
cell.replaceChildren(input);
|
||||
input.focus();
|
||||
input.addEventListener('blur', async () => {
|
||||
row[property] = input.value;
|
||||
cell.replaceChildren(div);
|
||||
if (input.value !== text) {
|
||||
let currentUpdatedFields = state.updatedFields
|
||||
const dbInst = getNowDbInst();
|
||||
// 主键
|
||||
const primaryKey = await dbInst.loadTableColumn(state.db, state.table);
|
||||
const primaryKeyValue = row[primaryKey.columnName];
|
||||
// 更新字段列信息
|
||||
const updateColumn = await dbInst.loadTableColumn(state.db, state.table, property);
|
||||
const newField = {
|
||||
div, row,
|
||||
fieldName: property,
|
||||
fieldType: updateColumn.columnType,
|
||||
oldValue: text,
|
||||
newValue: input.value
|
||||
} as FieldsMeta;
|
||||
|
||||
// 被修改的字段
|
||||
const primaryKeyFields = currentUpdatedFields.filter((meta) => meta.primaryKey === primaryKeyValue)
|
||||
let hasKey = false;
|
||||
if (primaryKeyFields.length <= 0) {
|
||||
primaryKeyFields[0] = {
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields: [newField]
|
||||
}
|
||||
} else {
|
||||
hasKey = true
|
||||
let hasField = primaryKeyFields[0].fields.some(a => {
|
||||
if (a.fieldName === newField.fieldName) {
|
||||
a.newValue = newField.newValue
|
||||
}
|
||||
return a.fieldName === newField.fieldName
|
||||
})
|
||||
if (!hasField) {
|
||||
primaryKeyFields[0].fields.push(newField)
|
||||
}
|
||||
}
|
||||
let fields = primaryKeyFields[0].fields
|
||||
|
||||
const fieldsParam = fields.filter((a) => {
|
||||
if (a.fieldName === column.property) {
|
||||
a.newValue = input.value
|
||||
}
|
||||
return a.fieldName === column.property
|
||||
})
|
||||
|
||||
const field = fieldsParam.length > 0 && fieldsParam[0] || {} as FieldsMeta
|
||||
if (field.oldValue === input.value) { // 新值=旧值
|
||||
// 删除数据
|
||||
div.classList.remove('update_field_active')
|
||||
let delIndex: number[] = [];
|
||||
currentUpdatedFields.forEach((a, i) => {
|
||||
if (a.primaryKey === primaryKeyValue) {
|
||||
a.fields = a.fields && a.fields.length > 0 ? a.fields.filter(f => f.fieldName !== column.property) : [];
|
||||
a.fields.length <= 0 && delIndex.push(i)
|
||||
}
|
||||
});
|
||||
delIndex.forEach(i => delete currentUpdatedFields[i])
|
||||
currentUpdatedFields = currentUpdatedFields.filter(a => a)
|
||||
} else {
|
||||
// 新增数据
|
||||
div.classList.add('update_field_active')
|
||||
if (hasKey) {
|
||||
currentUpdatedFields.forEach((value, index, array) => {
|
||||
if (value.primaryKey === primaryKeyValue) {
|
||||
array[index].fields = fields
|
||||
}
|
||||
})
|
||||
} else {
|
||||
currentUpdatedFields.push({
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields
|
||||
})
|
||||
}
|
||||
}
|
||||
state.updatedFields = currentUpdatedFields;
|
||||
changeUpdatedField();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
const dbInst = DbInst.getInst(state.dbId)
|
||||
let currentUpdatedFields = state.updatedFields;
|
||||
if (currentUpdatedFields.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const db = state.db;
|
||||
let res = '';
|
||||
let divs: HTMLElement[] = [];
|
||||
currentUpdatedFields.forEach(a => {
|
||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
||||
let primaryKey = a.primaryKey;
|
||||
let primaryKeyType = a.primaryKeyType;
|
||||
let primaryKeyName = a.primaryKeyName;
|
||||
a.fields.forEach(f => {
|
||||
sql += ` ${dbInst.wrapName(f.fieldName)} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`
|
||||
// 如果修改的字段是主键
|
||||
if (f.fieldName === primaryKeyName) {
|
||||
primaryKey = f.oldValue
|
||||
}
|
||||
divs.push(f.div)
|
||||
})
|
||||
sql = sql.substring(0, sql.length - 1)
|
||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
|
||||
res += sql;
|
||||
})
|
||||
|
||||
dbInst.promptExeSql(db, res, () => { }, () => {
|
||||
currentUpdatedFields = [];
|
||||
divs.forEach(a => {
|
||||
a.classList.remove('update_field_active');
|
||||
})
|
||||
state.updatedFields = [];
|
||||
changeUpdatedField();
|
||||
});
|
||||
}
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
state.updatedFields.forEach((a: any) => {
|
||||
a.fields.forEach((b: any) => {
|
||||
b.div.classList.remove('update_field_active')
|
||||
b.row[b.fieldName] = b.oldValue
|
||||
})
|
||||
})
|
||||
state.updatedFields = [];
|
||||
changeUpdatedField();
|
||||
}
|
||||
|
||||
|
||||
const changeUpdatedField = () => {
|
||||
emits('changeUpdatedField', state.updatedFields);
|
||||
}
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(state.dbId);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
submitUpdateFields,
|
||||
cancelUpdateFields
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,712 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<div class="fl">
|
||||
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="format sql" placement="top">
|
||||
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-upload
|
||||
class="sql-file-exec"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="execSqlFileSuccess"
|
||||
:headers="{ Authorization: token }"
|
||||
:action="getUploadSqlFileUrl()"
|
||||
:show-file-list="false"
|
||||
name="file"
|
||||
multiple
|
||||
:limit="100"
|
||||
>
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="SQL脚本执行" placement="top">
|
||||
<el-link type="success" :underline="false" icon="Document"></el-link>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div style="float: right" class="fl">
|
||||
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" :height="state.editorHeight" :id="'MonacoTextarea-' + getKey()" />
|
||||
|
||||
<div class="editor-move-resize" @mousedown="onDragSetHeight">
|
||||
<el-icon>
|
||||
<Minus />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div class="mt5 sql-exec-res">
|
||||
<el-tabs v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTab">
|
||||
<el-tab-pane closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
|
||||
<template #reference>
|
||||
<div>
|
||||
<span>
|
||||
<span v-if="dt.loading">
|
||||
<SvgIcon class="mb2 is-loading" name="Loading" color="var(--el-color-primary)" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<SvgIcon class="mb2" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
|
||||
<SvgIcon class="mb2" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span> 结果{{ dt.id }} </span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions v-if="dt.sql" :column="1" size="small">
|
||||
<el-descriptions-item>
|
||||
<div style="width: 280px">
|
||||
<el-text size="small" truncated :title="dt.sql"> {{ dt.sql }} </el-text>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="耗时 :"> {{ dt.execTime }}ms </el-descriptions-item>
|
||||
<el-descriptions-item label="结果集 :">
|
||||
{{ dt.data?.length }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<el-row>
|
||||
<span v-if="dt.hasUpdatedFileds" class="mt5">
|
||||
<span>
|
||||
<el-link type="success" :underline="false" @click="submitUpdateFields(dt)"><span style="font-size: 12px">提交</span></el-link>
|
||||
</span>
|
||||
<span>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="warning" :underline="false" @click="cancelUpdateFields(dt)"><span style="font-size: 12px">取消</span></el-link>
|
||||
</span>
|
||||
</span>
|
||||
</el-row>
|
||||
<db-table-data
|
||||
v-if="!dt.errorMsg"
|
||||
:ref="(el) => (dt.dbTableRef = el)"
|
||||
:db-id="dbId"
|
||||
:db="dbName"
|
||||
:data="dt.data"
|
||||
:table="dt.table"
|
||||
:columns="dt.tableColumn"
|
||||
:loading="dt.loading"
|
||||
:height="tableDataHeight"
|
||||
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
|
||||
@change-updated-field="changeUpdatedField($event, dt)"
|
||||
@data-delete="onDeleteData($event, dt)"
|
||||
></db-table-data>
|
||||
|
||||
<el-result v-else icon="error" title="执行失败" :sub-title="dt.errorMsg"> </el-result>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { h, nextTick, watch, onMounted, reactive, toRefs, ref } from 'vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import config from '@/common/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor } from 'monaco-editor';
|
||||
|
||||
import DbTableData from '@/views/ops/db/component/table/DbTableData.vue';
|
||||
import { DbInst } from '../../db';
|
||||
import { dbApi } from '../../api';
|
||||
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import syssocket from '@/common/syssocket';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { getDbDialect } from '../../dialect';
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess']);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// sql脚本名,若有则去加载该sql内容
|
||||
sqlName: {
|
||||
type: String,
|
||||
},
|
||||
editorHeight: {
|
||||
type: String,
|
||||
default: '600',
|
||||
},
|
||||
});
|
||||
|
||||
class ExecResTab {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 当前结果集对应的sql
|
||||
*/
|
||||
sql: string;
|
||||
|
||||
loading: boolean;
|
||||
|
||||
dbTableRef: any;
|
||||
|
||||
tableColumn: any[] = [];
|
||||
|
||||
data: any[] = [];
|
||||
|
||||
execTime: number;
|
||||
|
||||
/**
|
||||
* 当前单表操作sql关联的表信息
|
||||
*/
|
||||
table: string;
|
||||
|
||||
/**
|
||||
* 是否有更新字段
|
||||
*/
|
||||
hasUpdatedFileds: boolean;
|
||||
|
||||
errorMsg: string;
|
||||
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
const token = getToken();
|
||||
const monacoEditorRef: any = ref(null);
|
||||
|
||||
let monacoEditor: editor.IStandaloneCodeEditor;
|
||||
|
||||
const state = reactive({
|
||||
token,
|
||||
sql: '', // 当前编辑器的sql内容s
|
||||
sqlName: '' as any, // sql模板名称
|
||||
execResTabs: [] as ExecResTab[],
|
||||
activeTab: 1,
|
||||
editorHeight: '500',
|
||||
tableDataHeight: 255 as any,
|
||||
});
|
||||
|
||||
const { tableDataHeight } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.editorHeight,
|
||||
(newValue: any) => {
|
||||
state.editorHeight = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(props.dbId);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in query mounted');
|
||||
state.editorHeight = props.editorHeight;
|
||||
|
||||
// 默认新建一个结果集tab
|
||||
state.execResTabs.push(new ExecResTab(1));
|
||||
|
||||
state.sqlName = props.sqlName;
|
||||
if (props.sqlName) {
|
||||
const res = await dbApi.getSql.request({ id: props.dbId, type: 1, db: props.dbName, name: props.sqlName });
|
||||
state.sql = res.sql;
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => initMonacoEditor(), 50);
|
||||
});
|
||||
await getNowDbInst().loadDbHints(props.dbName);
|
||||
});
|
||||
|
||||
const onRemoveTab = (targetId: number) => {
|
||||
let activeTab = state.activeTab;
|
||||
const tabs = [...state.execResTabs];
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
const tabId = tabs[i].id;
|
||||
if (tabId !== targetId) {
|
||||
continue;
|
||||
}
|
||||
const nextTab = tabs[i + 1] || tabs[i - 1];
|
||||
if (nextTab) {
|
||||
activeTab = nextTab.id;
|
||||
} else {
|
||||
activeTab = 0;
|
||||
}
|
||||
state.execResTabs.splice(i, 1);
|
||||
state.activeTab = activeTab;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽改变sql编辑区和查询结果区高度
|
||||
*/
|
||||
const onDragSetHeight = () => {
|
||||
document.onmousemove = (e) => {
|
||||
e.preventDefault();
|
||||
//得到鼠标拖动的宽高距离:取绝对值
|
||||
state.editorHeight = `${document.getElementById('MonacoTextarea-' + getKey())!.clientHeight + e.movementY}px`;
|
||||
state.tableDataHeight -= e.movementY;
|
||||
};
|
||||
document.onmouseup = () => {
|
||||
document.onmousemove = null;
|
||||
};
|
||||
};
|
||||
|
||||
const getKey = () => {
|
||||
if (props.sqlName) {
|
||||
return `${props.dbId}:${props.dbName}.${props.sqlName}`;
|
||||
}
|
||||
return props.dbId + ':' + props.dbName;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行sql
|
||||
*/
|
||||
const onRunSql = async (newTab = false) => {
|
||||
// 没有选中的文本,则为全部文本
|
||||
let sql = getSql() as string;
|
||||
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
let execRemark = '';
|
||||
let canRun = true;
|
||||
if (
|
||||
sql.startsWith('update') ||
|
||||
sql.startsWith('UPDATE') ||
|
||||
sql.startsWith('INSERT') ||
|
||||
sql.startsWith('insert') ||
|
||||
sql.startsWith('DELETE') ||
|
||||
sql.startsWith('delete')
|
||||
) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
||||
inputErrorMessage: '请输入执行该sql的备注信息',
|
||||
});
|
||||
execRemark = res.value;
|
||||
if (!execRemark) {
|
||||
canRun = false;
|
||||
}
|
||||
}
|
||||
if (!canRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
let execRes: ExecResTab;
|
||||
let i = 0;
|
||||
let id;
|
||||
// 新tab执行,或者tabs为0,则新建tab执行sql
|
||||
if (newTab || state.execResTabs.length == 0) {
|
||||
// 取最后一个tab的id + 1
|
||||
id = state.execResTabs.length == 0 ? 1 : state.execResTabs[state.execResTabs.length - 1].id + 1;
|
||||
execRes = new ExecResTab(id);
|
||||
state.execResTabs.push(execRes);
|
||||
i = state.execResTabs.length - 1;
|
||||
} else {
|
||||
// 不是新建tab执行,则在当前激活的tab上执行sql
|
||||
i = state.execResTabs.findIndex((x) => x.id == state.activeTab);
|
||||
execRes = state.execResTabs[i];
|
||||
id = execRes.id;
|
||||
}
|
||||
|
||||
state.activeTab = id;
|
||||
const startTime = new Date().getTime();
|
||||
try {
|
||||
execRes.loading = true;
|
||||
execRes.errorMsg = '';
|
||||
execRes.sql = '';
|
||||
|
||||
const colAndData: any = await getNowDbInst().runSql(props.dbName, sql, execRemark);
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集');
|
||||
}
|
||||
// 要实时响应,故需要用索引改变数据才生效
|
||||
state.execResTabs[i].data = colAndData.res;
|
||||
// 兼容表格字段配置
|
||||
state.execResTabs[i].tableColumn = colAndData.colNames.map((x: any) => {
|
||||
return {
|
||||
columnName: x,
|
||||
show: true,
|
||||
};
|
||||
});
|
||||
cancelUpdateFields(execRes);
|
||||
} catch (e: any) {
|
||||
execRes.data = [];
|
||||
execRes.tableColumn = [];
|
||||
execRes.table = '';
|
||||
execRes.errorMsg = e.msg;
|
||||
return;
|
||||
} finally {
|
||||
state.execResTabs[i].loading = false;
|
||||
execRes.sql = sql;
|
||||
execRes.execTime = new Date().getTime() - startTime;
|
||||
}
|
||||
|
||||
// 即只有以该字符串开头的sql才可修改表数据内容
|
||||
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
|
||||
const tableName = sql.split(/from/i)[1];
|
||||
if (tableName) {
|
||||
const tn = tableName.trim().split(' ')[0].split('\n')[0];
|
||||
execRes.table = tn;
|
||||
execRes.table = tn;
|
||||
} else {
|
||||
execRes.table = '';
|
||||
}
|
||||
} else {
|
||||
execRes.table = '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
||||
*/
|
||||
const getSql = () => {
|
||||
let res = '' as string | undefined;
|
||||
// 编辑器还没初始化
|
||||
if (!monacoEditor?.getModel) {
|
||||
return res;
|
||||
}
|
||||
// 选择选中的sql
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (selection) {
|
||||
res = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
}
|
||||
// 整个编辑器的sql
|
||||
if (!res) {
|
||||
return monacoEditor.getModel()?.getValue();
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const saveSql = async () => {
|
||||
const sql = monacoEditor.getModel()?.getValue();
|
||||
notBlank(sql, 'sql内容不能为空');
|
||||
|
||||
let sqlName = state.sqlName;
|
||||
if (!sqlName) {
|
||||
try {
|
||||
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\w+/,
|
||||
inputErrorMessage: '请输入SQL脚本名',
|
||||
});
|
||||
sqlName = input.value;
|
||||
state.sqlName = sqlName;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await dbApi.saveSql.request({ id: props.dbId, db: props.dbName, sql: sql, type: 1, name: sqlName });
|
||||
ElMessage.success('保存成功');
|
||||
// 保存sql脚本成功事件
|
||||
emits('saveSqlSuccess', props.dbId, props.dbName);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化sql
|
||||
*/
|
||||
const formatSql = () => {
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formatDialect = getDbDialect(getNowDbInst().type).getFormatDialect();
|
||||
|
||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||
if (sql) {
|
||||
replaceSelection(sqlFormatter(sql, { language: formatDialect }), selection);
|
||||
return;
|
||||
}
|
||||
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue(), { language: formatDialect }));
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
getNowDbInst().runSql(props.dbName, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 替换选中的内容
|
||||
*/
|
||||
const replaceSelection = (str: string, selection: any) => {
|
||||
const model = monacoEditor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (!selection) {
|
||||
model.setValue(str);
|
||||
return;
|
||||
}
|
||||
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
|
||||
|
||||
const textBeforeSelection = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: startLineNumber,
|
||||
endColumn: startColumn,
|
||||
});
|
||||
|
||||
const textAfterSelection = model.getValueInRange({
|
||||
startLineNumber: endLineNumber,
|
||||
startColumn: endColumn,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: model.getLineMaxColumn(model.getLineCount()),
|
||||
});
|
||||
|
||||
monacoEditor.setValue(textBeforeSelection + str + textAfterSelection);
|
||||
monacoEditor.focus();
|
||||
monacoEditor.setPosition({
|
||||
lineNumber: startLineNumber,
|
||||
column: 0,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* sql文件执行进度通知缓存
|
||||
*/
|
||||
const sqlExecNotifyMap: Map<string, any> = new Map();
|
||||
const beforeUpload = (file: File) => {
|
||||
ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
|
||||
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
|
||||
const content = JSON.parse(message.msg);
|
||||
const id = content.id;
|
||||
let progress = sqlExecNotifyMap.get(id);
|
||||
if (content.terminated) {
|
||||
if (progress != undefined) {
|
||||
progress.notification?.close();
|
||||
sqlExecNotifyMap.delete(id);
|
||||
progress = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress == undefined) {
|
||||
progress = {
|
||||
props: reactive(buildProgressProps()),
|
||||
notification: undefined,
|
||||
};
|
||||
}
|
||||
progress.props.progress.title = content.title;
|
||||
progress.props.progress.executedStatements = content.executedStatements;
|
||||
if (!sqlExecNotifyMap.has(id)) {
|
||||
progress.notification = ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: h(ProgressNotify, progress.props),
|
||||
type: syssocket.getMsgType(message.type),
|
||||
showClose: false,
|
||||
});
|
||||
sqlExecNotifyMap.set(id, progress);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 执行sql成功
|
||||
const execSqlFileSuccess = (res: any) => {
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取sql文件上传执行url
|
||||
const getUploadSqlFileUrl = () => {
|
||||
return `${config.baseApiUrl}/dbs/${props.dbId}/exec-sql-file?db=${props.dbName}&${joinClientParams()}`;
|
||||
};
|
||||
|
||||
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
||||
// 如果存在要更新字段,则显示提交和取消按钮
|
||||
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据删除事件
|
||||
*/
|
||||
const onDeleteData = async (deleteDatas: any, dt: ExecResTab) => {
|
||||
const db = props.dbName;
|
||||
const dbInst = getNowDbInst();
|
||||
const primaryKey = await dbInst.loadTableColumn(db, dt.table);
|
||||
const primaryKeyColumnName = primaryKey.columnName;
|
||||
dt.data = dt.data.filter((d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1));
|
||||
};
|
||||
|
||||
const submitUpdateFields = (dt: ExecResTab) => {
|
||||
dt?.dbTableRef?.submitUpdateFields();
|
||||
};
|
||||
|
||||
const cancelUpdateFields = (dt: ExecResTab) => {
|
||||
dt?.dbTableRef?.cancelUpdateFields();
|
||||
};
|
||||
|
||||
const initMonacoEditor = () => {
|
||||
monacoEditor = monacoEditorRef.value.getEditor();
|
||||
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
// id: 'run-sql-action' + state.ti.key,
|
||||
id: 'run-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '执行SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await onRunSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
// id: 'run-sql-action' + state.ti.key,
|
||||
id: 'run-sql-action-on-newtab' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '新标签执行SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyR, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.6,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await onRunSql(true);
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'format-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '格式化SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 2,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await formatSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'save-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '保存SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 3,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
await saveSql();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sql-file-exec {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.editor-move-resize {
|
||||
cursor: n-resize;
|
||||
height: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sql-exec-res {
|
||||
.el-tabs__header {
|
||||
margin: 0 0 !important;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
font-size: 12px;
|
||||
height: 25px;
|
||||
margin: 0px;
|
||||
padding: 0 6px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,12 @@
|
||||
import { h, render, VNode } from 'vue';
|
||||
import SqlExecDialog from './SqlExecDialog.vue';
|
||||
import {SqlLanguage} from 'sql-formatter/lib/src/sqlFormatter'
|
||||
|
||||
export type SqlExecProps = {
|
||||
sql: string;
|
||||
dbId: number;
|
||||
db: string;
|
||||
dbType?: SqlLanguage;
|
||||
runSuccessCallback?: Function;
|
||||
cancelCallback?: Function;
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<el-dialog :destroy-on-close="true" title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
|
||||
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
|
||||
<el-input ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
|
||||
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, nextTick, reactive } from 'vue';
|
||||
import { dbApi } from '../api';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
|
||||
// import base style
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
@@ -113,7 +113,8 @@ const cancel = () => {
|
||||
const open = (props: SqlExecProps) => {
|
||||
runSuccessCallback = props.runSuccessCallback;
|
||||
cancelCallback = props.cancelCallback;
|
||||
state.sqlValue = sqlFormatter(props.sql);
|
||||
props.dbType = props.dbType || 'mysql';
|
||||
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType });
|
||||
state.dbId = props.dbId;
|
||||
state.db = props.db;
|
||||
state.dialogVisible = true;
|
||||
@@ -1,547 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<div class="fl">
|
||||
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="format sql" placement="top">
|
||||
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-upload
|
||||
class="sql-file-exec"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="execSqlFileSuccess"
|
||||
:headers="{ Authorization: token }"
|
||||
:action="getUploadSqlFileUrl()"
|
||||
:show-file-list="false"
|
||||
name="file"
|
||||
multiple
|
||||
:limit="100"
|
||||
>
|
||||
<el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top">
|
||||
<el-link type="success" :underline="false" icon="Document"></el-link>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-tooltip class="box-item" effect="dark" content="limit" placement="top">
|
||||
<el-link @click="onLimit()" type="success" :underline="false" icon="Operation"> </el-link>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div style="float: right" class="fl">
|
||||
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL </el-button>
|
||||
<el-button v-if="sqlName" @click="deleteSql()" type="danger" icon="delete" plain size="small">删除SQL </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" :height="editorHeight" :id="'MonacoTextarea-' + ti.key" />
|
||||
|
||||
<div class="editor-move-resize" @mousedown="onDragSetHeight">
|
||||
<el-icon>
|
||||
<Minus />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div class="mt5">
|
||||
<el-row>
|
||||
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete" :underline="false"></el-link>
|
||||
|
||||
<span v-if="execRes.data.length > 0">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="success" :underline="false" @click="exportData"><span style="font-size: 12px">导出</span></el-link>
|
||||
</span>
|
||||
<span v-if="hasUpdatedFileds">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span style="font-size: 12px">提交</span></el-link>
|
||||
</span>
|
||||
<span v-if="hasUpdatedFileds">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="warning" :underline="false" @click="cancelUpdateFields"><span style="font-size: 12px">取消</span></el-link>
|
||||
</span>
|
||||
</el-row>
|
||||
<db-table
|
||||
ref="dbTableRef"
|
||||
:db-id="state.ti.dbId"
|
||||
:db="state.ti.db"
|
||||
:data="execRes.data"
|
||||
:table="state.table"
|
||||
:columns="execRes.tableColumn"
|
||||
:loading="loading"
|
||||
:height="tableDataHeight"
|
||||
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
|
||||
@selection-change="onDataSelectionChange"
|
||||
@change-updated-field="changeUpdatedField"
|
||||
></db-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, watch, onMounted, reactive, toRefs, ref, Ref } from 'vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { isTrue, notBlank } from '@/common/assert';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import config from '@/common/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor } from 'monaco-editor';
|
||||
|
||||
import DbTable from '../DbTable.vue';
|
||||
import { TabInfo } from '../../db';
|
||||
import { exportCsv } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
import { dbApi } from '../../api';
|
||||
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: TabInfo,
|
||||
required: true,
|
||||
},
|
||||
// sql脚本名,若有则去加载该sql内容
|
||||
sqlName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
editorHeight: {
|
||||
type: String,
|
||||
default: '600',
|
||||
},
|
||||
});
|
||||
|
||||
const token = getToken();
|
||||
const monacoEditorRef: any = ref(null);
|
||||
const dbTableRef = ref(null) as Ref;
|
||||
|
||||
let monacoEditor: editor.IStandaloneCodeEditor;
|
||||
|
||||
const state = reactive({
|
||||
token,
|
||||
ti: {} as TabInfo,
|
||||
dbs: [],
|
||||
dbId: null, // 当前选中操作的数据库实例
|
||||
table: '', // 当前单表操作sql的表信息
|
||||
sqlName: '',
|
||||
sql: '', // 当前编辑器的sql内容
|
||||
loading: false, // 是否在加载数据
|
||||
execRes: {
|
||||
data: [],
|
||||
tableColumn: [],
|
||||
},
|
||||
selectionDatas: [] as any,
|
||||
editorHeight: '500',
|
||||
tableDataHeight: 250 as any,
|
||||
hasUpdatedFileds: false,
|
||||
});
|
||||
|
||||
const { tableDataHeight, editorHeight, ti, execRes, table, sqlName, loading, hasUpdatedFileds } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.editorHeight,
|
||||
(newValue: any) => {
|
||||
state.editorHeight = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in query mounted');
|
||||
state.ti = props.data;
|
||||
state.editorHeight = props.editorHeight;
|
||||
const params = state.ti.params;
|
||||
state.dbs = params && params.dbs;
|
||||
|
||||
if (params && params.sqlName) {
|
||||
state.sqlName = params.sqlName;
|
||||
const res = await dbApi.getSql.request({ id: state.ti.dbId, type: 1, name: state.sqlName, db: state.ti.db });
|
||||
state.sql = res.sql;
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => initMonacoEditor(), 50);
|
||||
});
|
||||
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
|
||||
});
|
||||
|
||||
const initMonacoEditor = () => {
|
||||
monacoEditor = monacoEditorRef.value.getEditor();
|
||||
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'run-sql-action' + state.ti.key,
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '执行SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await onRunSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'format-sql-action' + state.ti.key,
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '格式化SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 2,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await formatSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽改变sql编辑区和查询结果区高度
|
||||
*/
|
||||
const onDragSetHeight = () => {
|
||||
document.onmousemove = (e) => {
|
||||
e.preventDefault();
|
||||
//得到鼠标拖动的宽高距离:取绝对值
|
||||
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.clientHeight + e.movementY}px`;
|
||||
state.tableDataHeight -= e.movementY;
|
||||
};
|
||||
document.onmouseup = () => {
|
||||
document.onmousemove = null;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行sql
|
||||
*/
|
||||
const onRunSql = async () => {
|
||||
// 没有选中的文本,则为全部文本
|
||||
let sql = getSql() as string;
|
||||
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
let execRemark = '';
|
||||
let canRun = true;
|
||||
if (
|
||||
sql.startsWith('update') ||
|
||||
sql.startsWith('UPDATE') ||
|
||||
sql.startsWith('INSERT') ||
|
||||
sql.startsWith('insert') ||
|
||||
sql.startsWith('DELETE') ||
|
||||
sql.startsWith('delete')
|
||||
) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
||||
inputErrorMessage: '请输入执行该sql的备注信息',
|
||||
});
|
||||
execRemark = res.value;
|
||||
if (!execRemark) {
|
||||
canRun = false;
|
||||
}
|
||||
}
|
||||
if (!canRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.loading = true;
|
||||
|
||||
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集');
|
||||
}
|
||||
state.execRes.data = colAndData.res;
|
||||
// 兼容表格字段配置
|
||||
state.execRes.tableColumn = colAndData.colNames.map((x: any) => {
|
||||
return {
|
||||
columnName: x,
|
||||
show: true,
|
||||
};
|
||||
});
|
||||
cancelUpdateFields();
|
||||
} catch (e: any) {
|
||||
state.execRes.data = [];
|
||||
state.execRes.tableColumn = [];
|
||||
state.table = '';
|
||||
return;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
// 即只有以该字符串开头的sql才可修改表数据内容
|
||||
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
|
||||
state.selectionDatas = [];
|
||||
const tableName = sql.split(/from/i)[1];
|
||||
if (tableName) {
|
||||
const tn = tableName.trim().split(' ')[0].split('\n')[0];
|
||||
state.table = tn;
|
||||
state.table = tn;
|
||||
} else {
|
||||
state.table = '';
|
||||
}
|
||||
} else {
|
||||
state.table = '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
||||
*/
|
||||
const getSql = () => {
|
||||
let res = '' as string | undefined;
|
||||
// 编辑器还没初始化
|
||||
if (!monacoEditor?.getModel) {
|
||||
return res;
|
||||
}
|
||||
// 选择选中的sql
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (selection) {
|
||||
res = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
}
|
||||
// 整个编辑器的sql
|
||||
if (!res) {
|
||||
return monacoEditor.getModel()?.getValue();
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const saveSql = async () => {
|
||||
const sql = monacoEditor.getModel()?.getValue();
|
||||
notBlank(sql, 'sql内容不能为空');
|
||||
|
||||
let sqlName = state.sqlName;
|
||||
const newSql = !sqlName;
|
||||
if (newSql) {
|
||||
try {
|
||||
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\w+/,
|
||||
inputErrorMessage: '请输入SQL脚本名',
|
||||
});
|
||||
sqlName = input.value;
|
||||
state.sqlName = sqlName;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await dbApi.saveSql.request({ id: state.ti.dbId, db: state.ti.db, sql: sql, type: 1, name: sqlName });
|
||||
ElMessage.success('保存成功');
|
||||
// 保存sql脚本成功事件
|
||||
emits('saveSqlSuccess', state.ti.dbId, state.ti.db);
|
||||
};
|
||||
|
||||
const deleteSql = async () => {
|
||||
const sqlName = state.sqlName;
|
||||
notBlank(sqlName, '该sql内容未保存');
|
||||
const { dbId, db } = state.ti;
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
|
||||
ElMessage.success('删除成功');
|
||||
emits('deleteSqlSuccess', dbId, db);
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化sql
|
||||
*/
|
||||
const formatSql = () => {
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||
if (sql) {
|
||||
replaceSelection(sqlFormatter(sql), selection);
|
||||
return;
|
||||
}
|
||||
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue()));
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 替换选中的内容
|
||||
*/
|
||||
const replaceSelection = (str: string, selection: any) => {
|
||||
const model = monacoEditor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (!selection) {
|
||||
model.setValue(str);
|
||||
return;
|
||||
}
|
||||
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
|
||||
|
||||
const textBeforeSelection = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: startLineNumber,
|
||||
endColumn: startColumn,
|
||||
});
|
||||
|
||||
const textAfterSelection = model.getValueInRange({
|
||||
startLineNumber: endLineNumber,
|
||||
startColumn: endColumn,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: model.getLineMaxColumn(model.getLineCount()),
|
||||
});
|
||||
|
||||
monacoEditor.setValue(textBeforeSelection + str + textAfterSelection);
|
||||
monacoEditor.focus();
|
||||
monacoEditor.setPosition({
|
||||
lineNumber: startLineNumber,
|
||||
column: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const onLimit = () => {
|
||||
let position = monacoEditor.getPosition() as monaco.Position;
|
||||
let newText = ' limit 10';
|
||||
monacoEditor?.getModel()?.applyEdits([
|
||||
{
|
||||
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
|
||||
text: newText,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
*/
|
||||
const exportData = () => {
|
||||
const dataList = state.execRes.data as any;
|
||||
isTrue(dataList.length > 0, '没有数据可导出');
|
||||
exportCsv(
|
||||
`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`,
|
||||
state.execRes.tableColumn.map((x: any) => x.columnName),
|
||||
dataList
|
||||
);
|
||||
};
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
|
||||
};
|
||||
|
||||
// 执行sql成功
|
||||
const execSqlFileSuccess = (res: any) => {
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取sql文件上传执行url
|
||||
const getUploadSqlFileUrl = () => {
|
||||
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`;
|
||||
};
|
||||
|
||||
const onDataSelectionChange = (datas: []) => {
|
||||
state.selectionDatas = datas;
|
||||
};
|
||||
|
||||
const changeUpdatedField = (updatedFields: []) => {
|
||||
// 如果存在要更新字段,则显示提交和取消按钮
|
||||
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除数据事件
|
||||
*/
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = state.selectionDatas;
|
||||
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
|
||||
const { db } = state.ti;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
const primaryKeyColumnName = primaryKey.columnName;
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
|
||||
state.execRes.data = state.execRes.data.filter(
|
||||
(d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1)
|
||||
);
|
||||
state.selectionDatas = [];
|
||||
});
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
dbTableRef.value.submitUpdateFields();
|
||||
};
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
dbTableRef.value.cancelUpdateFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sql-file-exec {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.editor-move-resize {
|
||||
cursor: n-resize;
|
||||
height: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
750
mayfly_go_web/src/views/ops/db/component/table/DbTableData.vue
Normal file
750
mayfly_go_web/src/views/ops/db/component/table/DbTableData.vue
Normal file
@@ -0,0 +1,750 @@
|
||||
<template>
|
||||
<div class="db-table-data mt5" :style="{ height: `${tableHeight}px` }">
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-table-v2
|
||||
ref="tableRef"
|
||||
:header-height="30"
|
||||
:row-height="30"
|
||||
:row-class="rowClass"
|
||||
:columns="state.columns"
|
||||
:data="datas"
|
||||
:width="width"
|
||||
:height="height"
|
||||
fixed
|
||||
class="table"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
>
|
||||
<template #header="{ columns }">
|
||||
<div v-for="(column, i) in columns" :key="i">
|
||||
<div
|
||||
:style="{
|
||||
width: `${column.width}px`,
|
||||
height: '100%',
|
||||
lineHeight: '30px',
|
||||
textAlign: 'center',
|
||||
borderRight: 'var(--el-table-border)',
|
||||
}"
|
||||
>
|
||||
<!-- 行号列表头 -->
|
||||
<div v-if="column.key == rowNoColumn.key || !showColumnTip">
|
||||
<el-text tag="b"> {{ column.title }} </el-text>
|
||||
</div>
|
||||
|
||||
<div v-else @contextmenu="headerContextmenuClick($event, column)">
|
||||
<div v-if="showColumnTip" @mouseover="column.showSetting = true" @mouseleave="column.showSetting = false">
|
||||
<el-tooltip :show-after="500" raw-content placement="top">
|
||||
<template #content> {{ getColumnTip(column) }} </template>
|
||||
<el-text tag="b" style="cursor: pointer"> {{ column.title }} </el-text>
|
||||
</el-tooltip>
|
||||
|
||||
<span>
|
||||
<SvgIcon
|
||||
color="var(--el-color-primary)"
|
||||
v-if="column.title == nowSortColumn?.columnName"
|
||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<el-text tag="b" style="cursor: pointer"> {{ column.title }} </el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell="{ rowData, column, rowIndex, columnIndex }">
|
||||
<div @contextmenu="dataContextmenuClick($event, rowIndex, column, rowData)" class="table-data-cell">
|
||||
<!-- 行号列 -->
|
||||
<div v-if="column.key == 'tableDataRowNo'">
|
||||
<el-text tag="b" size="small">
|
||||
{{ rowIndex + 1 }}
|
||||
</el-text>
|
||||
</div>
|
||||
|
||||
<!-- 数据列 -->
|
||||
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
|
||||
<div v-if="canEdit(rowIndex, columnIndex)">
|
||||
<el-input
|
||||
:ref="(el: any) => el?.focus()"
|
||||
@blur="onExitEditMode(rowData, column, rowIndex)"
|
||||
class="w100"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
size="small"
|
||||
v-model="rowData[column.dataKey!]"
|
||||
></el-input>
|
||||
</div>
|
||||
|
||||
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''">
|
||||
<el-text style="color: var(--el-color-info-light-5)" v-if="rowData[column.dataKey!] === null" size="small" truncated>
|
||||
NULL
|
||||
</el-text>
|
||||
|
||||
<el-text v-else :title="rowData[column.dataKey!]" size="small" truncated>
|
||||
{{ rowData[column.dataKey!] }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="loading" #overlay>
|
||||
<div class="el-loading-mask" style="display: flex; align-items: center; justify-content: center">
|
||||
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="42" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div style="text-align: center">
|
||||
<el-empty :style="{ height: `${tableHeight}px` }" :description="state.emptyText" :image-size="100" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-v2>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
|
||||
<el-dialog @close="state.genTxtDialog.visible = false" v-model="state.genTxtDialog.visible" :title="state.genTxtDialog.title" width="1000px">
|
||||
<template #header>
|
||||
<div class="mr15" style="display: flex; justify-content: flex-end">
|
||||
<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-dialog>
|
||||
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, watch, reactive, toRefs } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { ContextmenuItem, Contextmenu } from '@/components/contextmenu';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { exportCsv, exportFile } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
|
||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
db: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
},
|
||||
columns: {
|
||||
type: Array<any>,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
showColumnTip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 600,
|
||||
},
|
||||
});
|
||||
|
||||
const contextmenuRef = ref();
|
||||
const tableRef = ref();
|
||||
|
||||
/** 表头 contextmenu items **/
|
||||
|
||||
const cmHeaderAsc = new ContextmenuItem('asc', '升序').withIcon('top').withOnClick((data: any) => {
|
||||
onTableSortChange({ columnName: data.dataKey, order: 'asc' });
|
||||
});
|
||||
|
||||
const cmHeaderDesc = new ContextmenuItem('desc', '降序').withIcon('bottom').withOnClick((data: any) => {
|
||||
onTableSortChange({ columnName: data.dataKey, order: 'desc' });
|
||||
});
|
||||
|
||||
const cmHeaderFixed = new ContextmenuItem('fixed', '固定')
|
||||
.withIcon('Paperclip')
|
||||
.withOnClick((data: any) => {
|
||||
data.fixed = true;
|
||||
})
|
||||
.withHideFunc((data: any) => data.fixed);
|
||||
|
||||
const cmHeaderCancenFixed = new ContextmenuItem('cancelFixed', '取消固定')
|
||||
.withIcon('Minus')
|
||||
.withOnClick((data: any) => (data.fixed = false))
|
||||
.withHideFunc((data: any) => !data.fixed);
|
||||
|
||||
/** 表数据 contextmenu items **/
|
||||
|
||||
const cmDataCopyCell = new ContextmenuItem('copyValue', '复制')
|
||||
.withIcon('CopyDocument')
|
||||
.withOnClick(async (data: any) => {
|
||||
await copyToClipboard(data.rowData[data.column.dataKey]);
|
||||
})
|
||||
.withHideFunc(() => {
|
||||
// 选中多条则隐藏该复制按钮
|
||||
return selectionRowsMap.size > 1;
|
||||
});
|
||||
|
||||
const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
||||
.withIcon('delete')
|
||||
.withOnClick(() => onDeleteData())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||
.withIcon('tickets')
|
||||
.withOnClick(() => onGenerateInsertSql())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataGenJson = new ContextmenuItem('genJson', '生成JSON').withIcon('tickets').withOnClick(() => onGenerateJson());
|
||||
|
||||
const cmDataExportCsv = new ContextmenuItem('exportCsv', '导出CSV').withIcon('document').withOnClick(() => onExportCsv());
|
||||
|
||||
const cmDataExportSql = new ContextmenuItem('exportSql', '导出SQL')
|
||||
.withIcon('document')
|
||||
.withOnClick(() => onExportSql())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
class NowUpdateCell {
|
||||
rowIndex: number;
|
||||
colIndex: number;
|
||||
oldValue: any;
|
||||
}
|
||||
|
||||
class UpdatedRow {
|
||||
/**
|
||||
* 主键值
|
||||
*/
|
||||
primaryValue: any;
|
||||
|
||||
/**
|
||||
* 行数据
|
||||
*/
|
||||
rowData: any;
|
||||
|
||||
/**
|
||||
* 修改到的列信息, columnName -> tablecelldata
|
||||
*/
|
||||
columnsMap: Map<string, TableCellData> = new Map();
|
||||
}
|
||||
|
||||
class TableCellData {
|
||||
/**
|
||||
* 旧值
|
||||
*/
|
||||
oldValue: any;
|
||||
}
|
||||
|
||||
let nowSortColumn = null as any;
|
||||
|
||||
// 当前正在更新的单元格
|
||||
let nowUpdateCell: NowUpdateCell = null as any;
|
||||
|
||||
// 选中的数据, key->rowIndex value->primaryKeyValue
|
||||
const selectionRowsMap: Map<number, any> = new Map();
|
||||
|
||||
// 更新单元格 key-> rowIndex value -> 更新行
|
||||
const cellUpdateMap: Map<number, UpdatedRow> = new Map();
|
||||
|
||||
const state = reactive({
|
||||
dbId: 0, // 当前选中操作的数据库实例
|
||||
dbType: '',
|
||||
db: '', // 数据库名
|
||||
table: '', // 当前的表名
|
||||
datas: [],
|
||||
columns: [] as any,
|
||||
loading: false,
|
||||
tableHeight: 600,
|
||||
emptyText: '',
|
||||
|
||||
contextmenu: {
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
items: [] as ContextmenuItem[],
|
||||
},
|
||||
|
||||
genTxtDialog: {
|
||||
title: 'SQL',
|
||||
visible: false,
|
||||
txt: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { tableHeight, datas } = toRefs(state);
|
||||
|
||||
/**
|
||||
* 行号字段列
|
||||
*/
|
||||
const rowNoColumn = {
|
||||
title: 'No.',
|
||||
key: 'tableDataRowNo',
|
||||
dataKey: 'tableDataRowNo',
|
||||
width: 45,
|
||||
fixed: true,
|
||||
align: 'center',
|
||||
headerClass: 'table-column',
|
||||
class: 'table-column',
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
setTableData(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.columns,
|
||||
(newValue: any) => {
|
||||
// 赋值列字段值是否隐藏,state.columns多了一列索引列
|
||||
if (newValue.length + 1 == state.columns.length) {
|
||||
for (let i = 0; i < newValue.length; i++) {
|
||||
state.columns[i + 1].hidden = !newValue[i].show;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.table,
|
||||
(newValue: any) => {
|
||||
state.table = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.height,
|
||||
(newValue: any) => {
|
||||
state.tableHeight = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(newValue: any) => {
|
||||
state.loading = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in DbTable mounted');
|
||||
state.tableHeight = props.height;
|
||||
state.loading = props.loading;
|
||||
state.emptyText = props.emptyText;
|
||||
|
||||
state.dbId = props.dbId;
|
||||
state.dbType = props.dbType;
|
||||
state.db = props.db;
|
||||
state.table = props.table;
|
||||
setTableData(props.data);
|
||||
});
|
||||
|
||||
const setTableData = (datas: any) => {
|
||||
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||
selectionRowsMap.clear();
|
||||
cellUpdateMap.clear();
|
||||
state.datas = datas;
|
||||
setTableColumns(props.columns);
|
||||
};
|
||||
|
||||
const setTableColumns = (columns: any) => {
|
||||
state.columns = columns.map((x: any) => {
|
||||
const columnName = x.columnName;
|
||||
return {
|
||||
...x,
|
||||
key: columnName,
|
||||
dataKey: columnName,
|
||||
width: DbInst.flexColumnWidth(columnName, state.datas),
|
||||
title: columnName,
|
||||
align: 'center',
|
||||
headerClass: 'table-column',
|
||||
class: 'table-column',
|
||||
sortable: true,
|
||||
hidden: !x.show,
|
||||
};
|
||||
});
|
||||
if (state.columns.length > 0) {
|
||||
state.columns.unshift(rowNoColumn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前单元格是否允许编辑
|
||||
* @param rowIndex ri
|
||||
* @param colIndex ci
|
||||
*/
|
||||
const canEdit = (rowIndex: number, colIndex: number) => {
|
||||
return state.table && nowUpdateCell && nowUpdateCell.rowIndex == rowIndex && nowUpdateCell.colIndex == colIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断当前单元格是否被更新了
|
||||
* @param rowIndex ri
|
||||
* @param columnName cn
|
||||
*/
|
||||
const isUpdated = (rowIndex: number, columnName: string) => {
|
||||
return cellUpdateMap.get(rowIndex)?.columnsMap.get(columnName);
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断当前行是否被选中
|
||||
* @param rowIndex
|
||||
*/
|
||||
const isSelection = (rowIndex: number): boolean => {
|
||||
return selectionRowsMap.get(rowIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* 选中指定行
|
||||
* @param rowIndex
|
||||
* @param rowData
|
||||
* @param isMultiple 是否允许多选
|
||||
*/
|
||||
const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
|
||||
if (isMultiple) {
|
||||
// 如果重复点击,则取消改选中数据
|
||||
if (selectionRowsMap.get(rowIndex)) {
|
||||
selectionRowsMap.delete(rowIndex);
|
||||
triggerRefresh();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
selectionRowsMap.clear();
|
||||
}
|
||||
selectionRowsMap.set(rowIndex, rowData);
|
||||
triggerRefresh();
|
||||
};
|
||||
|
||||
/**
|
||||
* 行事件处理
|
||||
*/
|
||||
const rowEventHandlers = {
|
||||
onClick: (e: any) => {
|
||||
const event = e.event;
|
||||
const rowIndex = e.rowIndex;
|
||||
const rowData = e.rowData;
|
||||
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
selectionRow(rowIndex, rowData, true);
|
||||
return;
|
||||
}
|
||||
selectionRow(rowIndex, rowData);
|
||||
},
|
||||
};
|
||||
|
||||
const headerContextmenuClick = (event: any, data: any) => {
|
||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancenFixed];
|
||||
contextmenuRef.value.openContextmenu(data);
|
||||
};
|
||||
|
||||
const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: any) => {
|
||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||
|
||||
// 当前行未选中,则单行选中该行
|
||||
if (!isSelection(rowIndex)) {
|
||||
selectionRow(rowIndex, data);
|
||||
}
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||
};
|
||||
|
||||
/**
|
||||
* 表排序字段变更
|
||||
*/
|
||||
const onTableSortChange = async (sort: any) => {
|
||||
nowSortColumn = sort;
|
||||
cancelUpdateFields();
|
||||
emits('sortChange', sort);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除数据事件
|
||||
*/
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = Array.from(selectionRowsMap.values());
|
||||
const db = state.db;
|
||||
const dbInst = getNowDbInst();
|
||||
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
|
||||
emits('dataDelete', deleteDatas);
|
||||
});
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
||||
state.genTxtDialog.title = 'SQL';
|
||||
state.genTxtDialog.visible = true;
|
||||
};
|
||||
|
||||
const onGenerateJson = async () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
// 按列字段重新排序对象key
|
||||
const jsonObj = [];
|
||||
for (let selectionData of selectionDatas) {
|
||||
let obj = {};
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
obj[column.title] = selectionData[column.dataKey];
|
||||
}
|
||||
}
|
||||
jsonObj.push(obj);
|
||||
}
|
||||
state.genTxtDialog.txt = JSON.stringify(jsonObj, null, 4);
|
||||
state.genTxtDialog.title = 'JSON';
|
||||
state.genTxtDialog.visible = true;
|
||||
};
|
||||
|
||||
const copyGenTxt = async (txt: string) => {
|
||||
await copyToClipboard(txt);
|
||||
state.genTxtDialog.visible = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
*/
|
||||
const onExportCsv = () => {
|
||||
const dataList = state.datas as any;
|
||||
let columnNames = [];
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
columnNames.push(column.columnName);
|
||||
}
|
||||
}
|
||||
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
||||
};
|
||||
|
||||
const onExportSql = async () => {
|
||||
const selectionDatas = state.datas;
|
||||
exportFile(
|
||||
`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}.sql`,
|
||||
await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas)
|
||||
);
|
||||
};
|
||||
|
||||
const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex = 0) => {
|
||||
if (!state.table) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
nowUpdateCell = {
|
||||
rowIndex: rowIndex,
|
||||
colIndex: columnIndex,
|
||||
oldValue: rowData[column.dataKey],
|
||||
};
|
||||
};
|
||||
|
||||
const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
|
||||
const oldValue = nowUpdateCell.oldValue;
|
||||
const newValue = rowData[column.dataKey];
|
||||
|
||||
// 未改变单元格值
|
||||
if (oldValue == newValue) {
|
||||
nowUpdateCell = null as any;
|
||||
triggerRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedRow = cellUpdateMap.get(rowIndex);
|
||||
if (!updatedRow) {
|
||||
updatedRow = new UpdatedRow();
|
||||
updatedRow.rowData = rowData;
|
||||
cellUpdateMap.set(rowIndex, updatedRow);
|
||||
}
|
||||
|
||||
const columnName = column.dataKey;
|
||||
let cellData = updatedRow.columnsMap.get(columnName);
|
||||
if (cellData) {
|
||||
// 多次修改情况,可能又修改回原值,则移除该修改单元格
|
||||
if (cellData.oldValue == newValue) {
|
||||
cellUpdateMap.delete(rowIndex);
|
||||
}
|
||||
} else {
|
||||
cellData = new TableCellData();
|
||||
cellData.oldValue = oldValue;
|
||||
updatedRow.columnsMap.set(columnName, cellData);
|
||||
}
|
||||
|
||||
nowUpdateCell = null as any;
|
||||
triggerRefresh();
|
||||
changeUpdatedField();
|
||||
};
|
||||
|
||||
const submitUpdateFields = async () => {
|
||||
const dbInst = getNowDbInst();
|
||||
if (cellUpdateMap.size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = state.db;
|
||||
let res = '';
|
||||
|
||||
for (let updateRow of cellUpdateMap.values()) {
|
||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
||||
const rowData = updateRow.rowData;
|
||||
// 主键列信息
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
let primaryKeyType = primaryKey.columnType;
|
||||
let primaryKeyName = primaryKey.columnName;
|
||||
let primaryKeyValue = rowData[primaryKeyName];
|
||||
|
||||
for (let k of updateRow.columnsMap.keys()) {
|
||||
const v = updateRow.columnsMap.get(k);
|
||||
if (!v) {
|
||||
continue;
|
||||
}
|
||||
// 更新字段列信息
|
||||
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
|
||||
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k])},`;
|
||||
|
||||
// 如果修改的字段是主键
|
||||
if (k === primaryKeyName) {
|
||||
primaryKeyValue = v.oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
|
||||
res += sql;
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(
|
||||
db,
|
||||
res,
|
||||
() => {},
|
||||
() => {
|
||||
triggerRefresh();
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
const updateRows = cellUpdateMap.values();
|
||||
// 恢复原值
|
||||
for (let updateRow of updateRows) {
|
||||
const rowData = updateRow.rowData;
|
||||
updateRow.columnsMap.forEach((v: TableCellData, k: string) => {
|
||||
rowData[k] = v.oldValue;
|
||||
});
|
||||
}
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
};
|
||||
|
||||
const changeUpdatedField = () => {
|
||||
emits('changeUpdatedField', cellUpdateMap);
|
||||
};
|
||||
|
||||
const rowClass = (row: any) => {
|
||||
if (isSelection(row.rowIndex)) {
|
||||
return 'data-selection';
|
||||
}
|
||||
if (row.rowIndex % 2 != 0) {
|
||||
return 'data-spacing';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getColumnTip = (column: any) => {
|
||||
const comment = column.columnComment;
|
||||
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 触发响应式实时刷新,否则需要滑动或移动才能使样式实时生效
|
||||
*/
|
||||
const triggerRefresh = () => {
|
||||
// 改变columns等属性值,才能触发slot中的if条件等, 暂不知为啥
|
||||
if (state.columns[0].opTimes) {
|
||||
state.columns[0].opTimes = state.columns[0].opTimes + 1;
|
||||
} else {
|
||||
state.columns[0].opTimes = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(state.dbId);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
submitUpdateFields,
|
||||
cancelUpdateFields,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.db-table-data {
|
||||
.table {
|
||||
border-left: var(--el-table-border);
|
||||
border-top: var(--el-table-border);
|
||||
}
|
||||
|
||||
.table-column {
|
||||
padding: 0 2px;
|
||||
font-size: 12px;
|
||||
border-right: var(--el-table-border);
|
||||
}
|
||||
|
||||
.table-data-cell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-selection {
|
||||
background-color: var(--el-table-current-row-bg-color);
|
||||
}
|
||||
|
||||
.data-spacing {
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,64 +2,65 @@
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<div class="mt5">
|
||||
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5"> </el-link>
|
||||
<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"
|
||||
>
|
||||
<div v-for="(item, index) in columns" :key="index">
|
||||
<el-checkbox
|
||||
v-model="item.show"
|
||||
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
|
||||
:true-label="true"
|
||||
:false-label="false"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-link icon="Operation" size="small" :underline="false"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
<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"
|
||||
>
|
||||
<div v-for="(item, index) in columns" :key="index">
|
||||
<el-checkbox
|
||||
v-model="item.show"
|
||||
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
|
||||
:true-label="true"
|
||||
:false-label="false"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-link icon="Operation" size="small" :underline="false"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" :underline="false"></el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" :underline="false"></el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-link @click="onDeleteData()" type="danger" icon="delete" :underline="false"></el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-tooltip :show-after="500" class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-tooltip :show-after="500" class="box-item" effect="dark" content="commit" placement="top">
|
||||
<template #content>
|
||||
1. 右击数据/表头可显示操作菜单 <br />
|
||||
2. 按住Ctrl点击数据则为多选 <br />
|
||||
3. 双击单元格可编辑数据
|
||||
</template>
|
||||
<el-link icon="QuestionFilled" :underline="false"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="生成insert sql" placement="top">
|
||||
<el-link @click="onGenerateInsertSql()" type="success" :underline="false">gi</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="导出当前页的csv文件" placement="top">
|
||||
<el-link type="success" :underline="false" @click="exportData"><span class="f12">导出</span></el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
|
||||
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="f12">提交</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider v-if="hasUpdatedFileds" direction="vertical" border-style="dashed" />
|
||||
<el-tooltip v-if="hasUpdatedFileds" class="box-item" effect="dark" content="取消修改" placement="top">
|
||||
<el-link @click="cancelUpdateFields" type="warning" :underline="false" class="f12">取消</el-link>
|
||||
</el-tooltip>
|
||||
<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="f12">提交</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider v-if="hasUpdatedFileds" direction="vertical" border-style="dashed" />
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="取消修改" placement="top">
|
||||
<el-link @click="cancelUpdateFields" type="warning" :underline="false" class="f12">取消</el-link>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input
|
||||
ref="condInputRef"
|
||||
@keyup.enter.native="onSelectByCondition()"
|
||||
v-model="condition"
|
||||
placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可"
|
||||
placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容后回车或点击查询按钮即可"
|
||||
clearable
|
||||
@clear="selectData"
|
||||
size="small"
|
||||
@@ -68,19 +69,30 @@
|
||||
<template #prepend>
|
||||
<el-popover :visible="state.condPopVisible" trigger="click" :width="320" placement="right">
|
||||
<template #reference>
|
||||
<el-link @click.stop="state.condPopVisible = !state.condPopVisible" type="success" :underline="false">选择列</el-link>
|
||||
<el-link @click.stop="chooseCondColumnName" type="success" :underline="false">选择列</el-link>
|
||||
</template>
|
||||
<el-table
|
||||
:data="columns"
|
||||
:data="filterCondColumns"
|
||||
max-height="500"
|
||||
size="small"
|
||||
@row-click="(...event: any) => {
|
||||
onConditionRowClick(event);
|
||||
}
|
||||
@row-click="
|
||||
(...event: any) => {
|
||||
onConditionRowClick(event);
|
||||
}
|
||||
"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<el-table-column property="columnName" label="列名" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column property="columnName" label="列名" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<el-input
|
||||
ref="columnNameSearchInputRef"
|
||||
v-model="state.columnNameSearch"
|
||||
size="small"
|
||||
placeholder="列名: 输入可过滤"
|
||||
clearable
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
@@ -93,21 +105,21 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<db-table
|
||||
<db-table-data
|
||||
ref="dbTableRef"
|
||||
:db-id="state.ti.dbId"
|
||||
:db="state.ti.db"
|
||||
:db-id="dbId"
|
||||
:db="dbName"
|
||||
:data="datas"
|
||||
:table="state.table"
|
||||
:table="tableName"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:height="tableHeight"
|
||||
:show-column-tip="true"
|
||||
:sortable="'custom'"
|
||||
@sort-change="(sort: any) => onTableSortChange(sort)"
|
||||
@selection-change="onDataSelectionChange"
|
||||
@change-updated-field="changeUpdatedField"
|
||||
></db-table>
|
||||
@data-delete="onRefresh"
|
||||
></db-table-data>
|
||||
|
||||
<el-row type="flex" class="mt5" justify="center">
|
||||
<el-pagination
|
||||
@@ -138,7 +150,12 @@
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="19">
|
||||
<el-input ref="conditionInputRef" v-model="conditionDialog.value" :placeholder="conditionDialog.placeholder" />
|
||||
<el-input
|
||||
@keyup.enter.native="onConfirmCondition"
|
||||
ref="oneCondInputRef"
|
||||
v-model="conditionDialog.value"
|
||||
:placeholder="conditionDialog.placeholder"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>
|
||||
@@ -153,6 +170,7 @@
|
||||
<el-form ref="dataForm" :model="addDataDialog.data" label-width="auto" size="small">
|
||||
<el-form-item
|
||||
v-for="column in columns"
|
||||
:key="column.columnName"
|
||||
class="w100"
|
||||
:prop="column.columnName"
|
||||
:label="column.columnName"
|
||||
@@ -179,35 +197,41 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, reactive, toRefs, ref, Ref, onUnmounted } from 'vue';
|
||||
import { isTrue, notEmpty, notBlank } from '@/common/assert';
|
||||
import { onMounted, computed, watch, reactive, toRefs, ref, Ref, onUnmounted } from 'vue';
|
||||
import { notEmpty } from '@/common/assert';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { DbInst, TabInfo } from '../../db';
|
||||
import { exportCsv } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
import DbTable from '../DbTable.vue';
|
||||
|
||||
const emits = defineEmits(['genInsertSql']);
|
||||
const dataForm: any = ref(null);
|
||||
const conditionInputRef: any = ref();
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import DbTableData from './DbTableData.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: TabInfo,
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tableName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tableHeight: {
|
||||
type: [String],
|
||||
default: '600',
|
||||
type: [Number],
|
||||
default: 600,
|
||||
},
|
||||
});
|
||||
|
||||
const dataForm: any = ref(null);
|
||||
const dbTableRef = ref(null) as Ref;
|
||||
const columnNameSearchInputRef = ref(null) as Ref;
|
||||
const oneCondInputRef: any = ref();
|
||||
const condInputRef = ref(null) as Ref;
|
||||
|
||||
const defaultPageSize = DbInst.DefaultLimit;
|
||||
|
||||
const state = reactive({
|
||||
ti: {} as TabInfo,
|
||||
table: '', // 当前的表名
|
||||
datas: [],
|
||||
sql: '', // 当前数据tab执行的sql
|
||||
orderBy: '',
|
||||
@@ -215,11 +239,20 @@ const state = reactive({
|
||||
loading: false, // 是否在加载数据
|
||||
columns: [] as any,
|
||||
pageNum: 1,
|
||||
pageSize: DbInst.DefaultLimit,
|
||||
pageSizes: [20, 40, 80, 100, 200, 300, 400],
|
||||
pageSize: defaultPageSize,
|
||||
pageSizes: [
|
||||
defaultPageSize,
|
||||
defaultPageSize * 2,
|
||||
defaultPageSize * 4,
|
||||
defaultPageSize * 8,
|
||||
defaultPageSize * 20,
|
||||
defaultPageSize * 40,
|
||||
defaultPageSize * 80,
|
||||
],
|
||||
count: 0,
|
||||
selectionDatas: [] as any,
|
||||
condPopVisible: false,
|
||||
columnNameSearch: '',
|
||||
conditionDialog: {
|
||||
title: '',
|
||||
placeholder: '',
|
||||
@@ -235,7 +268,7 @@ const state = reactive({
|
||||
placeholder: '',
|
||||
visible: false,
|
||||
},
|
||||
tableHeight: '600',
|
||||
tableHeight: 600,
|
||||
hasUpdatedFileds: false,
|
||||
});
|
||||
|
||||
@@ -248,14 +281,15 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(props.dbId);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in table data mounted');
|
||||
state.ti = props.data;
|
||||
state.tableHeight = props.tableHeight;
|
||||
state.table = state.ti.params.table;
|
||||
notBlank(state.table, 'TableData组件params.table信息不能为空');
|
||||
|
||||
const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table);
|
||||
const columns = await getNowDbInst().loadColumns(props.dbName, props.tableName);
|
||||
columns.forEach((x: any) => {
|
||||
x.show = true;
|
||||
});
|
||||
@@ -277,8 +311,6 @@ const handlerWindowClick = () => {
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
// 查询条件置空
|
||||
state.condition = '';
|
||||
state.pageNum = 1;
|
||||
await selectData();
|
||||
};
|
||||
@@ -295,12 +327,13 @@ const pageChange = async () => {
|
||||
*/
|
||||
const selectData = async () => {
|
||||
state.loading = true;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const { db } = state.ti;
|
||||
const dbInst = getNowDbInst();
|
||||
const db = props.dbName;
|
||||
const table = props.tableName;
|
||||
try {
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(state.table, state.condition));
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
state.count = countRes.res[0].count;
|
||||
let sql = dbInst.getDefaultSelectSql(state.table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
state.sql = sql;
|
||||
if (state.count > 0) {
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
@@ -320,20 +353,34 @@ const handleSizeChange = async (size: any) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
* 选择条件列
|
||||
*/
|
||||
const exportData = () => {
|
||||
const dataList = state.datas as any;
|
||||
isTrue(dataList.length > 0, '没有数据可导出');
|
||||
let columnNames = [];
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
columnNames.push(column.columnName);
|
||||
}
|
||||
const chooseCondColumnName = () => {
|
||||
state.condPopVisible = !state.condPopVisible;
|
||||
if (state.condPopVisible) {
|
||||
columnNameSearchInputRef.value.clear();
|
||||
columnNameSearchInputRef.value.focus();
|
||||
}
|
||||
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
||||
};
|
||||
|
||||
/**
|
||||
* 过滤条件列名
|
||||
*/
|
||||
const filterCondColumns = computed(() => {
|
||||
const columns = state.columns;
|
||||
const columnNameSearch = state.columnNameSearch;
|
||||
if (!columnNameSearch) {
|
||||
return columns;
|
||||
}
|
||||
return columns.filter((data: any) => {
|
||||
let tnMatch = true;
|
||||
if (columnNameSearch) {
|
||||
tnMatch = data.columnName.toLowerCase().includes(columnNameSearch.toLowerCase());
|
||||
}
|
||||
return tnMatch;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 条件查询,点击列信息后显示输入对应的值
|
||||
*/
|
||||
@@ -344,7 +391,7 @@ const onConditionRowClick = (event: any) => {
|
||||
state.conditionDialog.columnRow = row;
|
||||
state.conditionDialog.visible = true;
|
||||
setTimeout(() => {
|
||||
conditionInputRef.value.focus();
|
||||
oneCondInputRef.value.focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
@@ -359,6 +406,7 @@ const onConfirmCondition = () => {
|
||||
condition += `${row.columnName} ${conditionDialog.condition} `;
|
||||
state.condition = condition + DbInst.wrapColumnValue(row.columnType, conditionDialog.value);
|
||||
onCancelCondition();
|
||||
condInputRef.value.focus();
|
||||
};
|
||||
|
||||
const onCancelCondition = () => {
|
||||
@@ -374,7 +422,7 @@ const onCancelCondition = () => {
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
|
||||
getNowDbInst().runSql(props.dbName, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
@@ -382,17 +430,15 @@ const onSelectByCondition = async () => {
|
||||
notEmpty(state.condition, '条件不能为空');
|
||||
state.pageNum = 1;
|
||||
await selectData();
|
||||
condInputRef.value.blur();
|
||||
};
|
||||
|
||||
/**
|
||||
* 表排序字段变更
|
||||
*/
|
||||
const onTableSortChange = async (sort: any) => {
|
||||
if (!sort.prop) {
|
||||
return;
|
||||
}
|
||||
const sortType = sort.order == 'descending' ? 'DESC' : 'ASC';
|
||||
state.orderBy = `ORDER BY ${sort.prop} ${sortType}`;
|
||||
const sortType = sort.order == 'desc' ? 'DESC' : 'ASC';
|
||||
state.orderBy = `ORDER BY ${sort.columnName} ${sortType}`;
|
||||
await onRefresh();
|
||||
};
|
||||
|
||||
@@ -400,27 +446,9 @@ const onDataSelectionChange = (datas: []) => {
|
||||
state.selectionDatas = datas;
|
||||
};
|
||||
|
||||
const changeUpdatedField = (updatedFields: []) => {
|
||||
const changeUpdatedField = (updatedFields: any) => {
|
||||
// 如果存在要更新字段,则显示提交和取消按钮
|
||||
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除数据事件
|
||||
*/
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = state.selectionDatas;
|
||||
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
|
||||
const { db } = state.ti;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
|
||||
onRefresh();
|
||||
});
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async () => {
|
||||
isTrue(state.selectionDatas && state.selectionDatas.length > 0, '请先选择数据');
|
||||
emits('genInsertSql', state.ti.getNowDbInst().genInsertSql(state.ti.db, state.table, state.selectionDatas));
|
||||
state.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
@@ -432,7 +460,7 @@ const cancelUpdateFields = () => {
|
||||
};
|
||||
|
||||
const onShowAddDataDialog = async () => {
|
||||
state.addDataDialog.title = `添加'${state.table}'表数据`;
|
||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||
state.addDataDialog.visible = true;
|
||||
};
|
||||
|
||||
@@ -445,7 +473,7 @@ const closeAddDataDialog = () => {
|
||||
const addRow = async () => {
|
||||
dataForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const dbInst = getNowDbInst();
|
||||
const data = state.addDataDialog.data;
|
||||
// key: 字段名,value: 字段名提示
|
||||
let obj: any = {};
|
||||
@@ -458,8 +486,8 @@ const addRow = async () => {
|
||||
}
|
||||
let columnNames = Object.keys(obj).join(',');
|
||||
let values = Object.values(obj).join(',');
|
||||
let sql = `INSERT INTO ${dbInst.wrapName(state.table)} (${columnNames}) VALUES (${values});`;
|
||||
dbInst.promptExeSql(state.ti.db, sql, null, () => {
|
||||
let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
||||
dbInst.promptExeSql(props.dbName, sql, null, () => {
|
||||
closeAddDataDialog();
|
||||
onRefresh();
|
||||
});
|
||||
@@ -471,8 +499,4 @@ const addRow = async () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
</style>
|
||||
<style lang="scss"></style>
|
||||
@@ -13,26 +13,6 @@
|
||||
<el-input style="width: 80%" v-model="tableData.tableComment" size="small"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="characterSet" label="charset">
|
||||
<el-select filterable style="width: 80%" v-model="tableData.characterSet" size="small">
|
||||
<el-option v-for="item in characterSetNameList" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="characterSet" label="collation">
|
||||
<el-select filterable style="width: 80%" v-model="tableData.collation" size="small">
|
||||
<el-option
|
||||
v-for="item in collationNameList"
|
||||
:key="item"
|
||||
:label="tableData.characterSet + '_' + item"
|
||||
:value="tableData.characterSet + '_' + item"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-tabs v-model="activeName">
|
||||
@@ -42,24 +22,42 @@
|
||||
<template #default="scope">
|
||||
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> </el-input>
|
||||
|
||||
<el-select v-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||
<el-option v-for="typeValue in columnTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
|
||||
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||
<el-option
|
||||
v-for="pgsqlType in state.columnTypeList"
|
||||
:key="pgsqlType.dataType"
|
||||
:value="pgsqlType.udtName"
|
||||
:label="pgsqlType.dataType"
|
||||
>
|
||||
<span v-if="pgsqlType.dataType === pgsqlType.udtName"
|
||||
>{{ pgsqlType.dataType }}{{ pgsqlType.desc && ':' + pgsqlType.desc }}</span
|
||||
>
|
||||
<span v-else>{{ pgsqlType.dataType }},别名:{{ pgsqlType.udtName }} {{ pgsqlType.desc }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-input v-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
|
||||
|
||||
<el-input v-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
|
||||
|
||||
<el-checkbox v-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
|
||||
<el-input v-else-if="item.prop === 'numScale'" size="small" v-model="scope.row.numScale"> </el-input>
|
||||
|
||||
<el-checkbox v-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
|
||||
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
|
||||
|
||||
<el-checkbox v-if="item.prop === 'auto_increment'" size="small" v-model="scope.row.auto_increment"> </el-checkbox>
|
||||
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
|
||||
|
||||
<el-input v-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
|
||||
<el-checkbox
|
||||
v-else-if="item.prop === 'auto_increment'"
|
||||
size="small"
|
||||
v-model="scope.row.auto_increment"
|
||||
:disabled="dbType === DbType.postgresql"
|
||||
>
|
||||
</el-checkbox>
|
||||
|
||||
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
|
||||
|
||||
<el-link
|
||||
v-if="item.prop === 'action'"
|
||||
v-else-if="item.prop === 'action'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@@ -133,10 +131,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, toRefs, reactive, ref } from 'vue';
|
||||
import { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service';
|
||||
import { reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SqlExecBox from '../component/SqlExecBox';
|
||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import { getDbDialect, DbType } from '../../dialect/index';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -154,20 +152,23 @@ const props = defineProps({
|
||||
db: {
|
||||
type: String,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||
|
||||
const dbDialect = getDbDialect(props.dbType);
|
||||
|
||||
const formRef: any = ref();
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
btnloading: false,
|
||||
activeName: '1',
|
||||
columnTypeList: TYPE_LIST,
|
||||
columnTypeList: dbDialect.getColumnTypes(),
|
||||
indexTypeList: ['BTREE'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
|
||||
characterSetNameList: CHARACTER_SET_NAME_LIST,
|
||||
collationNameList: COLLATION_SUFFIX_LIST,
|
||||
tableData: {
|
||||
fields: {
|
||||
colNames: [
|
||||
@@ -183,6 +184,10 @@ const state = reactive({
|
||||
prop: 'length',
|
||||
label: '长度',
|
||||
},
|
||||
{
|
||||
prop: 'numScale',
|
||||
label: '小数点',
|
||||
},
|
||||
{
|
||||
prop: 'value',
|
||||
label: '默认值',
|
||||
@@ -209,18 +214,17 @@ const state = reactive({
|
||||
label: '操作',
|
||||
},
|
||||
],
|
||||
res: [
|
||||
{
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
length: '',
|
||||
notNull: false,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '',
|
||||
},
|
||||
],
|
||||
res: [] as {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
length: string;
|
||||
numScale: string;
|
||||
notNull: boolean;
|
||||
pri: boolean;
|
||||
auto_increment: boolean;
|
||||
remark: string;
|
||||
}[],
|
||||
},
|
||||
indexs: {
|
||||
colNames: [
|
||||
@@ -250,25 +254,21 @@ const state = reactive({
|
||||
},
|
||||
],
|
||||
columns: [{ name: '', remark: '' }],
|
||||
res: [
|
||||
{
|
||||
indexName: '',
|
||||
columnNames: [],
|
||||
unique: false,
|
||||
indexType: 'BTREE',
|
||||
indexComment: '',
|
||||
},
|
||||
],
|
||||
res: [] as {
|
||||
indexName: string;
|
||||
columnNames: string[];
|
||||
unique: boolean;
|
||||
indexType: 'BTREE';
|
||||
indexComment: string;
|
||||
}[],
|
||||
},
|
||||
characterSet: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
tableName: '',
|
||||
tableComment: '',
|
||||
height: 550,
|
||||
height: 450,
|
||||
},
|
||||
});
|
||||
|
||||
const { dialogVisible, btnloading, activeName, columnTypeList, indexTypeList, characterSetNameList, collationNameList, tableData } = toRefs(state);
|
||||
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
@@ -285,6 +285,7 @@ const addRow = () => {
|
||||
type: '',
|
||||
value: '',
|
||||
length: '',
|
||||
numScale: '',
|
||||
notNull: false,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
@@ -303,15 +304,107 @@ const addIndex = () => {
|
||||
};
|
||||
|
||||
const addDefaultRows = () => {
|
||||
state.tableData.fields.res.push(
|
||||
{ name: 'id', type: 'bigint', length: '20', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{ name: 'creator', type: 'varchar', length: '100', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人姓名' },
|
||||
{ name: 'create_time', type: 'datetime', length: '', value: 'CURRENT_TIMESTAMP', notNull: true, pri: false, auto_increment: false, remark: '创建时间' },
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{ name: 'updator', type: 'varchar', length: '100', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人姓名' },
|
||||
{ name: 'update_time', type: 'datetime', length: '', value: 'CURRENT_TIMESTAMP', notNull: true, pri: false, auto_increment: false, remark: '修改时间' }
|
||||
);
|
||||
if (props.dbType === DbType.mysql) {
|
||||
state.tableData.fields.res.push(
|
||||
{ name: 'id', type: 'bigint', length: '20', 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',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人姓名',
|
||||
},
|
||||
{
|
||||
name: 'create_time',
|
||||
type: 'datetime',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
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',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改时间',
|
||||
}
|
||||
);
|
||||
} else if (props.dbType === DbType.postgresql) {
|
||||
state.tableData.fields.res.push(
|
||||
{ name: 'id', type: 'bigserial', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'int8', length: '', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人姓名',
|
||||
},
|
||||
{
|
||||
name: 'create_time',
|
||||
type: 'timestamp',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建时间',
|
||||
},
|
||||
{ name: 'updator_id', type: 'int8', length: '', 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: 'timestamp',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改时间',
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRow = (index: any) => {
|
||||
@@ -332,6 +425,7 @@ const submit = async () => {
|
||||
sql: sql,
|
||||
dbId: props.dbId as any,
|
||||
db: props.db as any,
|
||||
dbType: dbDialect.getFormatDialect(),
|
||||
runSuccessCallback: () => {
|
||||
emit('submit-sql', { tableName: state.tableData.tableName });
|
||||
// cancel();
|
||||
@@ -399,129 +493,24 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
||||
};
|
||||
|
||||
const genSql = () => {
|
||||
const genColumnBasicSql = (cl: any) => {
|
||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : "'" + cl.value + "'") : '';
|
||||
let defVal = `${val ? 'DEFAULT ' + val : ''}`;
|
||||
let length = cl.length ? `(${cl.length})` : '';
|
||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
|
||||
};
|
||||
|
||||
let data = state.tableData;
|
||||
// 创建表
|
||||
if (!props.data?.edit) {
|
||||
if (state.activeName === '1') {
|
||||
// 创建表结构
|
||||
let primary_key = '';
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item) => {
|
||||
item.name && fields.push(genColumnBasicSql(item));
|
||||
if (item.pri) {
|
||||
primary_key += `${item.name},`;
|
||||
}
|
||||
});
|
||||
|
||||
return `CREATE TABLE ${data.tableName}
|
||||
( ${fields.join(',')}
|
||||
${primary_key ? `, PRIMARY KEY (${primary_key.slice(0, -1)})` : ''}
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=${data.characterSet} COLLATE =${data.collation} COMMENT='${data.tableComment}';`;
|
||||
return dbDialect.getCreateTableSql(data);
|
||||
} else if (state.activeName === '2' && data.indexs.res.length > 0) {
|
||||
// 创建索引
|
||||
let sql = `ALTER TABLE ${data.tableName}`;
|
||||
state.tableData.indexs.res.forEach((a) => {
|
||||
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`;
|
||||
});
|
||||
return sql.substring(0, sql.length - 1) + ';';
|
||||
return dbDialect.getCreateIndexSql(data);
|
||||
}
|
||||
} else {
|
||||
// 修改
|
||||
let addSql = '',
|
||||
updSql = '',
|
||||
delSql = '';
|
||||
if (state.activeName === '1') {
|
||||
// 修改列
|
||||
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name');
|
||||
if (changeData.add.length > 0) {
|
||||
addSql = `ALTER TABLE ${data.tableName}`;
|
||||
changeData.add.forEach((a) => {
|
||||
addSql += ` ADD ${genColumnBasicSql(a)},`;
|
||||
});
|
||||
addSql = addSql.substring(0, addSql.length - 1);
|
||||
addSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
updSql = `ALTER TABLE ${data.tableName}`;
|
||||
changeData.upd.forEach((a) => {
|
||||
updSql += ` MODIFY ${genColumnBasicSql(a)},`;
|
||||
});
|
||||
updSql = updSql.substring(0, updSql.length - 1);
|
||||
updSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
delSql += ` ALTER TABLE ${data.tableName} DROP COLUMN ${a.name}; `;
|
||||
});
|
||||
}
|
||||
return addSql + updSql + delSql;
|
||||
return dbDialect.getModifyColumnSql(data.tableName, changeData);
|
||||
} else if (state.activeName === '2') {
|
||||
// 修改索引
|
||||
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
|
||||
// 搜集修改和删除的索引,添加到drop index xx
|
||||
// 收集新增和修改的索引,添加到ADD xx
|
||||
// ALTER TABLE `test1`
|
||||
// DROP INDEX `test1_name_uindex`,
|
||||
// DROP INDEX `test1_column_name4_index`,
|
||||
// ADD UNIQUE INDEX `test1_name_uindex`(`id`) USING BTREE COMMENT 'ASDASD',
|
||||
// ADD INDEX `111`(`column_name4`) USING BTREE COMMENT 'zasf';
|
||||
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
dropIndexNames.push(a.indexName);
|
||||
addIndexs.push(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
dropIndexNames.push(a.indexName);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
addIndexs.push(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (dropIndexNames.length > 0 || addIndexs.length > 0) {
|
||||
let sql = `ALTER TABLE ${data.tableName} `;
|
||||
if (dropIndexNames.length > 0) {
|
||||
dropIndexNames.forEach((a) => {
|
||||
sql += `DROP INDEX ${a},`;
|
||||
});
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
}
|
||||
|
||||
if (addIndexs.length > 0) {
|
||||
if (dropIndexNames.length > 0) {
|
||||
sql += ',';
|
||||
}
|
||||
addIndexs.forEach((a) => {
|
||||
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${
|
||||
a.indexComment
|
||||
}',`;
|
||||
});
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
return dbDialect.getModifyIndexSql(data.tableName, changeData);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -537,6 +526,7 @@ const reset = () => {
|
||||
type: '',
|
||||
value: '',
|
||||
length: '',
|
||||
numScale: '',
|
||||
notNull: false,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
@@ -565,8 +555,11 @@ const indexChanges = (row: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
let prefix = row.unique ? 'udx_' : 'idx_';
|
||||
row.indexName = prefix + name;
|
||||
let suffix = row.unique ? 'udx' : 'idx';
|
||||
let commentSuffix = row.unique ? '唯一索引' : '普通索引';
|
||||
// 以表名为前缀
|
||||
row.indexName = `${tableData.value.tableName}_${name}_${suffix}`.replaceAll(' ', '');
|
||||
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
|
||||
};
|
||||
|
||||
const oldData = { indexs: [] as any[], fields: [] as any[] };
|
||||
@@ -592,6 +585,7 @@ watch(
|
||||
type,
|
||||
value: a.columnDefault || '',
|
||||
length,
|
||||
numScale: a.numScale,
|
||||
notNull: a.nullable !== 'YES',
|
||||
pri: a.columnKey === 'PRI',
|
||||
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="db-table">
|
||||
<el-row class="mb10">
|
||||
<el-row class="mb5">
|
||||
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right" trigger="click">
|
||||
<template #reference>
|
||||
<el-button class="ml5" type="success" size="small">导出</el-button>
|
||||
@@ -30,7 +30,7 @@
|
||||
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" height="65vh">
|
||||
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" :height="height">
|
||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
|
||||
@@ -63,8 +63,8 @@
|
||||
{{ formatByteSize(scope.row.indexLength) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="createTime" label="创建时间" min-width="150"> </el-table-column>
|
||||
<el-table-column label="更多信息" min-width="140">
|
||||
<el-table-column v-if="dbType === DbType.mysql" property="createTime" label="创建时间" min-width="150"> </el-table-column>
|
||||
<el-table-column label="更多信息" min-width="160">
|
||||
<template #default="scope">
|
||||
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
||||
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
|
||||
@@ -104,32 +104,38 @@
|
||||
<el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl" size="small"> </el-input>
|
||||
</el-dialog>
|
||||
|
||||
<db-table-edit
|
||||
<db-table-op
|
||||
:title="tableCreateDialog.title"
|
||||
:active-name="tableCreateDialog.activeName"
|
||||
:dbId="dbId"
|
||||
:db="db"
|
||||
:dbType="dbType"
|
||||
:data="tableCreateDialog.data"
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitSql"
|
||||
>
|
||||
</db-table-edit>
|
||||
</db-table-op>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, computed, onMounted, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, toRefs, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { dbApi } from '../api';
|
||||
import SqlExecBox from '../component/SqlExecBox';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import config from '@/common/config';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { DbType } from '../../dialect/index';
|
||||
|
||||
const DbTableEdit = defineAsyncComponent(() => import('./DbTableEdit.vue'));
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: [String],
|
||||
default: '65vh',
|
||||
},
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
@@ -175,7 +181,7 @@ const state = reactive({
|
||||
visible: false,
|
||||
activeName: '1',
|
||||
type: '',
|
||||
enableEditTypes: ['mysql'], // 支持"编辑表"的数据库类型
|
||||
enableEditTypes: [DbType.mysql, DbType.postgresql], // 支持"编辑表"的数据库类型
|
||||
data: {
|
||||
// 修改表时,传递修改数据
|
||||
edit: false,
|
||||
@@ -209,7 +215,7 @@ onMounted(async () => {
|
||||
getTables();
|
||||
});
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
watch(props, async () => {
|
||||
await getTables();
|
||||
});
|
||||
|
||||
@@ -239,6 +245,7 @@ const getTables = async () => {
|
||||
state.tables = [];
|
||||
state.tables = await dbApi.tableInfos.request({ id: props.dbId, db: props.db });
|
||||
} catch (e) {
|
||||
//
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
@@ -259,7 +266,7 @@ const dump = (db: string) => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute(
|
||||
'href',
|
||||
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&token=${getToken()}`
|
||||
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&${joinClientParams()}`
|
||||
);
|
||||
a.click();
|
||||
state.showDumpInfo = false;
|
||||
@@ -317,7 +324,9 @@ const dropTable = async (row: any) => {
|
||||
state.tables = await dbApi.tableInfos.request({ id: props.dbId, db: props.db });
|
||||
},
|
||||
});
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
// 打开编辑表
|
||||
@@ -1,14 +1,15 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { dbApi } from './api';
|
||||
import { getTextWidth } from '@/common/utils/string';
|
||||
import SqlExecBox from './component/SqlExecBox';
|
||||
import SqlExecBox from './component/sqleditor/SqlExecBox';
|
||||
|
||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
|
||||
import { language as addSqlLanguage } from './lang/mysql.js';
|
||||
import { language as addSqlLanguage } from './dialect/mysql_dialect';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor, languages, Position } from 'monaco-editor';
|
||||
|
||||
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import { getDbDialect } from './dialect';
|
||||
|
||||
const sqlCompletionKeywords = [...sqlLanguage.keywords, ...addSqlLanguage.keywords];
|
||||
const sqlCompletionOperators = [...sqlLanguage.operators, ...addSqlLanguage.operators];
|
||||
@@ -28,6 +29,11 @@ export class DbInst {
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* ip:port
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* 实例名
|
||||
*/
|
||||
@@ -39,17 +45,17 @@ export class DbInst {
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* schema -> db
|
||||
* dbName -> db
|
||||
*/
|
||||
dbs: Map<string, Db> = new Map();
|
||||
|
||||
/** 数据库schema,多个用空格隔开 */
|
||||
/** 数据库,多个用空格隔开 */
|
||||
databases: string;
|
||||
|
||||
/**
|
||||
* 默认查询分页数量
|
||||
*/
|
||||
static DefaultLimit = 20;
|
||||
static DefaultLimit = 25;
|
||||
|
||||
/**
|
||||
* 获取指定数据库实例,若不存在则新建并缓存
|
||||
@@ -88,11 +94,57 @@ export class DbInst {
|
||||
db.columnsMap?.clear();
|
||||
db.tableHints = null;
|
||||
console.log(`load tables -> dbName: ${dbName}`);
|
||||
tables = await dbApi.tableMetadata.request({ id: this.id, db: dbName });
|
||||
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
|
||||
db.tables = tables;
|
||||
return tables;
|
||||
}
|
||||
|
||||
async loadTableSuggestions(dbName: string, range: any, reload?: boolean) {
|
||||
const tables = await this.loadTables(dbName, reload);
|
||||
// 表名联想
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
tables?.forEach((tableMeta: any, index: any) => {
|
||||
const { tableName, tableComment } = tableMeta;
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: tableName + ' - ' + tableComment,
|
||||
description: 'table',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.File,
|
||||
detail: tableComment,
|
||||
insertText: tableName + ' ',
|
||||
range,
|
||||
sortText: 300 + index + '',
|
||||
});
|
||||
});
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
/** 加载列信息提示 */
|
||||
async loadTableColumnSuggestions(db: string, tableName: string, range: any) {
|
||||
let dbHits = await this.loadDbHints(db);
|
||||
let columns = dbHits[tableName];
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
columns?.forEach((a: string, index: any) => {
|
||||
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
|
||||
const nameAndComment = a.split(' ');
|
||||
const fieldName = nameAndComment[0];
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: a,
|
||||
description: 'column',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
|
||||
insertText: fieldName, // create_time
|
||||
range,
|
||||
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
|
||||
});
|
||||
});
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表的所有列信息
|
||||
* @param table 表名
|
||||
@@ -165,14 +217,7 @@ export class DbInst {
|
||||
|
||||
// 获取指定表的默认查询sql
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||
const baseSql = `SELECT * FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''}`;
|
||||
if (this.type == 'mysql') {
|
||||
return `${baseSql} LIMIT ${(pageNum - 1) * limit}, ${limit};`;
|
||||
}
|
||||
if (this.type == 'postgres') {
|
||||
return `${baseSql} OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
|
||||
}
|
||||
return baseSql;
|
||||
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,11 +226,12 @@ export class DbInst {
|
||||
* @param table 表名
|
||||
* @param datas 要生成的数据
|
||||
*/
|
||||
genInsertSql(dbName: string, table: string, datas: any[]): string {
|
||||
async genInsertSql(dbName: string, table: string, datas: any[]) {
|
||||
if (!datas) {
|
||||
return '';
|
||||
}
|
||||
const columns = this.getDb(dbName).getColumns(table);
|
||||
|
||||
const columns = await this.loadColumns(dbName, table);
|
||||
const sqls = [];
|
||||
for (let data of datas) {
|
||||
let colNames = [];
|
||||
@@ -205,8 +251,8 @@ export class DbInst {
|
||||
* @param table 表名
|
||||
* @param datas 要删除的记录
|
||||
*/
|
||||
genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
|
||||
const primaryKey = this.getDb(db).getColumn(table);
|
||||
async genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
|
||||
const primaryKey = await this.loadTableColumn(db, table);
|
||||
const primaryKeyColumnName = primaryKey.columnName;
|
||||
const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(',');
|
||||
return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
|
||||
@@ -232,13 +278,7 @@ export class DbInst {
|
||||
* @returns
|
||||
*/
|
||||
wrapName = (name: string) => {
|
||||
if (this.type == 'mysql') {
|
||||
return `\`${name}\``;
|
||||
}
|
||||
if (this.type == 'postgres') {
|
||||
return `"${name}"`;
|
||||
}
|
||||
return name;
|
||||
return getDbDialect(this.type).wrapName(name);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -258,6 +298,7 @@ export class DbInst {
|
||||
dbInst = new DbInst();
|
||||
dbInst.tagPath = inst.tagPath;
|
||||
dbInst.id = inst.id;
|
||||
dbInst.host = inst.host;
|
||||
dbInst.name = inst.name;
|
||||
dbInst.type = inst.type;
|
||||
dbInst.databases = inst.databases;
|
||||
@@ -337,7 +378,7 @@ export class DbInst {
|
||||
}
|
||||
|
||||
// 获取列名称的长度 加上排序图标长度
|
||||
const columnWidth: number = getTextWidth(prop) + 40;
|
||||
const columnWidth: number = getTextWidth(prop) + 23;
|
||||
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
|
||||
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
|
||||
return columnWidth;
|
||||
@@ -345,7 +386,6 @@ export class DbInst {
|
||||
|
||||
// 获取该列中最长的数据(内容)
|
||||
let maxWidthText = '';
|
||||
let maxWidthValue;
|
||||
// 获取该列中最长的数据(内容)
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let nowValue = tableData[i][prop];
|
||||
@@ -356,7 +396,6 @@ export class DbInst {
|
||||
let nowText = nowValue + '';
|
||||
if (nowText.length > maxWidthText.length) {
|
||||
maxWidthText = nowText;
|
||||
maxWidthValue = nowValue;
|
||||
}
|
||||
}
|
||||
const contentWidth: number = getTextWidth(maxWidthText) + 15;
|
||||
@@ -407,11 +446,18 @@ export enum TabType {
|
||||
* 查询框
|
||||
*/
|
||||
Query,
|
||||
|
||||
/**
|
||||
* 表操作
|
||||
*/
|
||||
TablesOp,
|
||||
}
|
||||
|
||||
export class TabInfo {
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* tab唯一key。与label、name都一致
|
||||
* tab唯一key。与name都一致
|
||||
*/
|
||||
key: string;
|
||||
|
||||
@@ -449,33 +495,6 @@ export class TabInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改表字段所需数据 */
|
||||
export type UpdateFieldsMeta = {
|
||||
// 主键值
|
||||
primaryKey: string;
|
||||
// 主键名
|
||||
primaryKeyName: string;
|
||||
// 主键类型
|
||||
primaryKeyType: string;
|
||||
// 新值
|
||||
fields: FieldsMeta[];
|
||||
};
|
||||
|
||||
export type FieldsMeta = {
|
||||
// 字段所在div
|
||||
div: HTMLElement;
|
||||
// 字段名
|
||||
fieldName: string;
|
||||
// 字段所在的表格行数据
|
||||
row: any;
|
||||
// 字段类型
|
||||
fieldType: string;
|
||||
// 原值
|
||||
oldValue: string;
|
||||
// 新值
|
||||
newValue: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册数据库表、字段等信息提示
|
||||
*
|
||||
@@ -484,7 +503,7 @@ export type FieldsMeta = {
|
||||
* @param db 库名
|
||||
* @param dbs 该库所有库名
|
||||
*/
|
||||
export function registerDbCompletionItemProvider(language: string, dbId: number, db: string, dbs: [] = []) {
|
||||
export function registerDbCompletionItemProvider(language: string, dbId: number, db: string, dbs: any[] = []) {
|
||||
registerCompletionItemProvider(language, {
|
||||
triggerCharacters: ['.', ' '],
|
||||
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
|
||||
@@ -520,29 +539,6 @@ export function registerDbCompletionItemProvider(language: string, dbId: number,
|
||||
let lastToken = tokens[tokens.length - 1].toLowerCase();
|
||||
const secondToken = (tokens.length > 2 && tokens[tokens.length - 2].toLowerCase()) || '';
|
||||
|
||||
// console.log("光标前文本:=>" + textBeforePointerMulti)
|
||||
// console.log("最后输入的:=>" + lastToken)
|
||||
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
|
||||
let alias = '';
|
||||
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
|
||||
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
|
||||
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
|
||||
if (lastToken.trim().startsWith('.')) {
|
||||
alias = secondToken;
|
||||
}
|
||||
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
|
||||
let aliasArr = lastToken.split(',');
|
||||
if (aliasArr.length > 1) {
|
||||
lastToken = aliasArr[aliasArr.length - 1];
|
||||
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
|
||||
if (lastToken.trim().startsWith('.')) {
|
||||
alias = secondToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取光标所在行之前的所有文本内容
|
||||
const textBeforeCursor = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
@@ -581,39 +577,62 @@ export function registerDbCompletionItemProvider(language: string, dbId: number,
|
||||
sqlStatement = textBeforeCursor + textAfterCursor;
|
||||
}
|
||||
|
||||
const tableName = getTableName4SqlCtx(sqlStatement, alias);
|
||||
// 提出到表名,则将表对应的字段也添加进提示建议
|
||||
if (tableName) {
|
||||
let dbHits = await dbInst.loadDbHints(db);
|
||||
let columns = dbHits[tableName];
|
||||
columns?.forEach((a: string, index: any) => {
|
||||
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
|
||||
const nameAndComment = a.split(' ');
|
||||
const fieldName = nameAndComment[0];
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
|
||||
// 库名提示
|
||||
if (dbs && dbs.length > 0) {
|
||||
dbs.forEach((a: any) => {
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: a,
|
||||
description: 'column',
|
||||
description: 'schema',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
|
||||
insertText: fieldName, // create_time
|
||||
kind: monaco.languages.CompletionItemKind.Folder,
|
||||
insertText: a,
|
||||
range,
|
||||
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 若存在字段提示,并且有别名,则提示字段即可,不完善后续的表名以及函数等
|
||||
if (suggestions.length > 0 && alias) {
|
||||
return {
|
||||
suggestions: suggestions,
|
||||
};
|
||||
let alias = '';
|
||||
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
|
||||
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
|
||||
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
|
||||
if (lastToken.trim().startsWith('.')) {
|
||||
alias = secondToken;
|
||||
}
|
||||
|
||||
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
|
||||
let aliasArr = lastToken.split(',');
|
||||
if (aliasArr.length > 1) {
|
||||
lastToken = aliasArr[aliasArr.length - 1];
|
||||
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
|
||||
if (lastToken.trim().startsWith('.')) {
|
||||
alias = secondToken;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是【库.表名联想】.前的字符串是库名
|
||||
if (dbs.indexOf(alias) >= 0) {
|
||||
return await dbInst.loadTableSuggestions(alias, range);
|
||||
}
|
||||
// 表下列名联想 .前的字符串是表名或表别名
|
||||
const sqlInfo = getTableName4SqlCtx(sqlStatement, alias, db);
|
||||
// 提出到表名,则将表对应的字段也添加进提示建议
|
||||
if (sqlInfo) {
|
||||
return await dbInst.loadTableColumnSuggestions(sqlInfo.db, sqlInfo.tableName, range);
|
||||
}
|
||||
}
|
||||
|
||||
const tables = await dbInst.loadTables(db);
|
||||
// 空格触发也会提示字段信息
|
||||
const sqlInfo = getTableName4SqlCtx(sqlStatement, alias, db);
|
||||
if (sqlInfo) {
|
||||
const columnSuggestions = await dbInst.loadTableColumnSuggestions(sqlInfo.db, sqlInfo.tableName, range);
|
||||
suggestions.push(...columnSuggestions.suggestions);
|
||||
}
|
||||
|
||||
// 表名联想
|
||||
// 当前库的表名联想
|
||||
const tables = await dbInst.loadTables(db);
|
||||
tables.forEach((tableMeta: any, index: any) => {
|
||||
const { tableName, tableComment } = tableMeta;
|
||||
suggestions.push({
|
||||
@@ -697,21 +716,6 @@ export function registerDbCompletionItemProvider(language: string, dbId: number,
|
||||
});
|
||||
});
|
||||
|
||||
// 库名提示
|
||||
if (dbs && dbs.length > 0) {
|
||||
dbs.forEach((a: any) => {
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: a,
|
||||
description: 'schema',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Folder,
|
||||
insertText: a,
|
||||
range,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 默认提示
|
||||
return {
|
||||
suggestions: suggestions,
|
||||
@@ -720,31 +724,33 @@ export function registerDbCompletionItemProvider(language: string, dbId: number,
|
||||
});
|
||||
}
|
||||
|
||||
function getTableName4SqlCtx(sql: string, alias: string = '') {
|
||||
function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string): { tableName: string; tableAlias: string; db: string } | undefined {
|
||||
// 去除多余的换行、空格和制表符
|
||||
sql = sql.replace(/[\r\n\s\t]+/g, ' ');
|
||||
|
||||
// 提取所有可能的表名和别名
|
||||
const regex = /(?:(?:FROM|JOIN|UPDATE)\s+(\S+)\s+(?:AS\s+)?(\S+))/gi;
|
||||
const regex = /(?:FROM|JOIN|UPDATE)\s+(\S+)\s+(?:AS\s+)?(\S+)/gi;
|
||||
let matches;
|
||||
const tables = [];
|
||||
|
||||
// 使用正则表达式匹配所有的表和别名
|
||||
while ((matches = regex.exec(sql)) !== null) {
|
||||
const tableName = matches[1].replace(/[`"]/g, '');
|
||||
let tableName = matches[1].replace(/[`"]/g, '');
|
||||
let db = defaultDb;
|
||||
if (tableName.indexOf('.') >= 0) {
|
||||
let info = tableName.split('.');
|
||||
db = info[0];
|
||||
tableName = info[1];
|
||||
}
|
||||
const tableAlias = matches[2] ? matches[2].replace(/[`"]/g, '') : tableName;
|
||||
tables.push({ tableName, tableAlias });
|
||||
tables.push({ tableName, tableAlias, db });
|
||||
}
|
||||
|
||||
// console.log('sql....', sql);
|
||||
// console.log('alias....', alias);
|
||||
// console.log('parset tables...', tables);
|
||||
if (alias) {
|
||||
// 如果指定了别名参数,则返回对应的表名
|
||||
const table = tables.find((t) => t.tableAlias === alias);
|
||||
return table ? table.tableName : '';
|
||||
return tables.find((t) => t.tableAlias === alias);
|
||||
} else {
|
||||
// 如果未指定别名参数,则返回第一个表名
|
||||
return tables.length > 0 ? tables[0].tableName : '';
|
||||
return tables.length > 0 ? tables[0] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
87
mayfly_go_web/src/views/ops/db/dialect/index.ts
Normal file
87
mayfly_go_web/src/views/ops/db/dialect/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { MysqlDialect } from './mysql_dialect';
|
||||
import { PostgresqlDialect } from './postgres_dialect';
|
||||
|
||||
export interface sqlColumnType {
|
||||
udtName: string;
|
||||
dataType: string;
|
||||
desc: string;
|
||||
space: string;
|
||||
range?: string;
|
||||
}
|
||||
|
||||
export const DbType = {
|
||||
mysql: 'mysql',
|
||||
postgresql: 'postgres',
|
||||
};
|
||||
|
||||
export interface DbDialect {
|
||||
/**
|
||||
* 获取格式化sql对应的dialect名称
|
||||
*/
|
||||
getFormatDialect(): string;
|
||||
|
||||
/**
|
||||
* 获取图标信息
|
||||
*/
|
||||
getIcon(): string;
|
||||
|
||||
/**
|
||||
* 获取默认查询sql
|
||||
* @param table 表名
|
||||
* @param condition 条件
|
||||
* @param orderBy 排序
|
||||
* @param pageNum 页数
|
||||
* @param limit 条数
|
||||
*/
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
|
||||
|
||||
/**
|
||||
* 包裹数据库表名、字段名等,避免使用关键字为字段名或表名时报错
|
||||
* @param name 名称
|
||||
*/
|
||||
wrapName(name: string): string;
|
||||
|
||||
/**
|
||||
* 生成字段类型列表
|
||||
* */
|
||||
getColumnTypes(): sqlColumnType[];
|
||||
|
||||
/**
|
||||
* 生成创建表sql
|
||||
* @param tableData 建表数据
|
||||
*/
|
||||
getCreateTableSql(tableData: any): string;
|
||||
|
||||
/**
|
||||
* 生成创建索引sql
|
||||
* @param tableData
|
||||
*/
|
||||
getCreateIndexSql(tableData: any): string;
|
||||
|
||||
/**
|
||||
* 生成编辑列sql
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变信息
|
||||
*/
|
||||
getModifyColumnSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
|
||||
|
||||
/**
|
||||
* 生成编辑索引sql
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变数据
|
||||
*/
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
|
||||
}
|
||||
|
||||
let mysqlDialect = new MysqlDialect();
|
||||
let postgresDialect = new PostgresqlDialect();
|
||||
|
||||
export const getDbDialect = (dbType: string | undefined): DbDialect => {
|
||||
if (dbType === DbType.mysql) {
|
||||
return mysqlDialect;
|
||||
}
|
||||
if (dbType === DbType.postgresql) {
|
||||
return postgresDialect;
|
||||
}
|
||||
throw new Error('不支持的数据库');
|
||||
};
|
||||
229
mayfly_go_web/src/views/ops/db/dialect/mysql_dialect.ts
Normal file
229
mayfly_go_web/src/views/ops/db/dialect/mysql_dialect.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { DbDialect, sqlColumnType } from './index';
|
||||
|
||||
export { MYSQL_TYPE_LIST, MysqlDialect, language };
|
||||
|
||||
const MYSQL_TYPE_LIST = [
|
||||
'bigint',
|
||||
'binary',
|
||||
'blob',
|
||||
'char',
|
||||
'datetime',
|
||||
'date',
|
||||
'decimal',
|
||||
'double',
|
||||
'enum',
|
||||
'float',
|
||||
'int',
|
||||
'json',
|
||||
'longblob',
|
||||
'longtext',
|
||||
'mediumblob',
|
||||
'mediumtext',
|
||||
'set',
|
||||
'smallint',
|
||||
'text',
|
||||
'time',
|
||||
'timestamp',
|
||||
'tinyint',
|
||||
'varbinary',
|
||||
'varchar',
|
||||
];
|
||||
|
||||
class MysqlDialect implements DbDialect {
|
||||
getFormatDialect() {
|
||||
return 'mysql';
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return 'iconfont icon-op-mysql';
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} LIMIT ${
|
||||
(pageNum - 1) * limit
|
||||
}, ${limit};`;
|
||||
}
|
||||
|
||||
wrapName = (name: string) => {
|
||||
return `\`${name}\``;
|
||||
};
|
||||
|
||||
getColumnTypes(): sqlColumnType[] {
|
||||
return MYSQL_TYPE_LIST.map((a) => ({ udtName: a, dataType: a, desc: '', space: '' }));
|
||||
}
|
||||
|
||||
genColumnBasicSql(cl: any): string {
|
||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
let length = cl.length ? `(${cl.length})` : '';
|
||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
|
||||
}
|
||||
getCreateTableSql(data: any): string {
|
||||
// 创建表结构
|
||||
let pks = [] as string[];
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
if (item.pri) {
|
||||
pks.push(item.name);
|
||||
}
|
||||
});
|
||||
|
||||
return `CREATE TABLE ${data.tableName}
|
||||
( ${fields.join(',')}
|
||||
${pks ? `, PRIMARY KEY (${pks.join(',')})` : ''}
|
||||
) COMMENT='${data.tableComment}';`;
|
||||
}
|
||||
|
||||
getCreateIndexSql(data: any): string {
|
||||
// 创建索引
|
||||
let sql = `ALTER TABLE ${data.tableName}`;
|
||||
data.indexs.res.forEach((a: any) => {
|
||||
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`;
|
||||
});
|
||||
return sql.substring(0, sql.length - 1) + ';';
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
let addSql = '',
|
||||
updSql = '',
|
||||
delSql = '';
|
||||
if (changeData.add.length > 0) {
|
||||
addSql = `ALTER TABLE ${tableName}`;
|
||||
changeData.add.forEach((a) => {
|
||||
addSql += ` ADD ${this.genColumnBasicSql(a)},`;
|
||||
});
|
||||
addSql = addSql.substring(0, addSql.length - 1);
|
||||
addSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
updSql = `ALTER TABLE ${tableName}`;
|
||||
let arr = [] as string[];
|
||||
changeData.upd.forEach((a) => {
|
||||
arr.push(` MODIFY ${this.genColumnBasicSql(a)}`);
|
||||
});
|
||||
updSql += arr.join(',');
|
||||
updSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
delSql += ` ALTER TABLE ${tableName} DROP COLUMN ${a.name}; `;
|
||||
});
|
||||
}
|
||||
return addSql + updSql + delSql;
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 搜集修改和删除的索引,添加到drop index xx
|
||||
// 收集新增和修改的索引,添加到ADD xx
|
||||
// ALTER TABLE `test1`
|
||||
// DROP INDEX `test1_name_uindex`,
|
||||
// DROP INDEX `test1_column_name4_index`,
|
||||
// ADD UNIQUE INDEX `test1_name_uindex`(`id`) USING BTREE COMMENT 'ASDASD',
|
||||
// ADD INDEX `111`(`column_name4`) USING BTREE COMMENT 'zasf';
|
||||
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
dropIndexNames.push(a.indexName);
|
||||
addIndexs.push(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
dropIndexNames.push(a.indexName);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
addIndexs.push(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (dropIndexNames.length > 0 || addIndexs.length > 0) {
|
||||
let sql = `ALTER TABLE ${tableName} `;
|
||||
if (dropIndexNames.length > 0) {
|
||||
dropIndexNames.forEach((a) => {
|
||||
sql += `DROP INDEX ${a},`;
|
||||
});
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
}
|
||||
|
||||
if (addIndexs.length > 0) {
|
||||
if (dropIndexNames.length > 0) {
|
||||
sql += ',';
|
||||
}
|
||||
addIndexs.forEach((a) => {
|
||||
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${
|
||||
a.indexComment
|
||||
}',`;
|
||||
});
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// src/basic-languages/mysql/mysql.ts
|
||||
var language = {
|
||||
keywords: ['GROUP BY', 'ORDER BY', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'SELECT * FROM'],
|
||||
operators: [],
|
||||
builtinFunctions: [],
|
||||
builtinVariables: [],
|
||||
replaceFunctions: [
|
||||
// 自定义修改函数提示
|
||||
|
||||
/** 字符串相关函数 */
|
||||
{ label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
|
||||
{ label: 'ASCII', insertText: 'ASCII(char)', description: '返回字符的ASCII值' },
|
||||
{ label: 'BIT_LENGTH', insertText: 'BIT_LENGTH(str1)', description: '多字符串合并' },
|
||||
{ label: 'INSTR', insertText: 'INSTR(str,substr)', description: '返回字符串substr所在str位置' },
|
||||
{ label: 'LEFT', insertText: 'LEFT(str,len)', description: '返回字符串str的左端len个字符' },
|
||||
{ label: 'RIGHT', insertText: 'RIGHT(str,len)', description: '返回字符串str的右端len个字符' },
|
||||
{ label: 'MID', insertText: 'MID(str,pos,len)', description: '返回字符串str的位置pos起len个字符' },
|
||||
{ label: 'SUBSTRING', insertText: 'SUBSTRING(exp, start, length)', description: '截取字符串' },
|
||||
{ label: 'REPLACE', insertText: 'REPLACE(str,from_str,to_str)', description: '替换字符串' },
|
||||
{ label: 'REPEAT', insertText: 'REPEAT(str,count)', description: '重复字符串count遍' },
|
||||
{ label: 'UPPER', insertText: 'UPPER(str)', description: '返回大写的字符串' },
|
||||
{ label: 'LOWER', insertText: 'LOWER(str)', description: '返回小写的字符串' },
|
||||
{ label: 'TRIM', insertText: 'TRIM(str)', description: '去除字符串首尾空格' },
|
||||
/** 数学相关函数 */
|
||||
{ label: 'ABS', insertText: 'ABS(n)', description: '返回n的绝对值' },
|
||||
{ label: 'FLOOR', insertText: 'FLOOR(n)', description: '返回不大于n的最大整数' },
|
||||
{ label: 'CEILING', insertText: 'CEILING(n)', description: '返回不小于n的最小整数值' },
|
||||
{ label: 'ROUND', insertText: 'ROUND(n,d)', description: '返回n的四舍五入值,保留d(默认0)位小数' },
|
||||
{ label: 'RAND', insertText: 'RAND()', description: '返回在范围0到1.0内的随机浮点值' },
|
||||
|
||||
/** 日期函数 */
|
||||
{ label: 'DATE', insertText: "DATE('date')", description: '返回指定表达式的日期部分' },
|
||||
{ label: 'WEEK', insertText: "WEEK('date')", description: '返回指定日期是一年中的第几周' },
|
||||
{ label: 'MONTH', insertText: "MONTH('date')", description: '返回指定日期的月份' },
|
||||
{ label: 'QUARTER', insertText: "QUARTER('date')", description: '返回指定日期是一年的第几个季度' },
|
||||
{ label: 'YEAR', insertText: "YEAR('date')", description: '返回指定日期的年份' },
|
||||
{ label: 'DATE_ADD', insertText: "DATE_ADD('date', interval 1 day)", description: '日期函数加减运算' },
|
||||
{ label: 'DATE_SUB', insertText: "DATE_SUB('date', interval 1 day)", description: '日期函数加减运算' },
|
||||
{ label: 'DATE_FORMAT', insertText: "DATE_FORMAT('date', '%Y-%m-%d %h:%i:%s')", description: '' },
|
||||
{ label: 'CURDATE', insertText: 'CURDATE()', description: '返回当前日期' },
|
||||
{ label: 'CURTIME', insertText: 'CURTIME()', description: '返回当前时间' },
|
||||
{ label: 'NOW', insertText: 'NOW()', description: '返回当前日期时间' },
|
||||
{ label: 'DATEDIFF', insertText: 'DATEDIFF(expr1,expr2)', description: '返回结束日expr1和起始日expr2之间的天数' },
|
||||
{ label: 'UNIX_TIMESTAMP', insertText: 'UNIX_TIMESTAMP()', description: '返回指定时间(默认当前)unix时间戳' },
|
||||
{ label: 'FROM_UNIXTIME', insertText: 'FROM_UNIXTIME(timestamp)', description: '把时间戳格式为年月日时分秒' },
|
||||
|
||||
/** 逻辑函数 */
|
||||
{ label: 'IFNULL', insertText: 'IFNULL(expression, alt_value)', description: '表达式为空取第二个参数值,否则取表达式值' },
|
||||
{ label: 'IF', insertText: 'IF(expr1, expr2, expr3)', description: 'expr1为true则取expr2,否则取expr3' },
|
||||
{ label: 'CASE', insertText: '(CASE \n WHEN expr1 THEN expr2 \n ELSE expr3) col', description: 'CASE WHEN THEN ELSE' },
|
||||
],
|
||||
};
|
||||
280
mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts
Normal file
280
mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { DbDialect, sqlColumnType } from './index';
|
||||
|
||||
export { PostgresqlDialect, GAUSS_TYPE_LIST };
|
||||
|
||||
const GAUSS_TYPE_LIST: sqlColumnType[] = [
|
||||
// 数值 - 整数型
|
||||
{ udtName: 'int1', dataType: 'tinyint', desc: '微整数,别名为INT1', space: '1字节', range: '0 ~ +255' },
|
||||
{ udtName: 'int2', dataType: 'smallint', desc: '小范围整数,别名为INT2。', space: '2字节', range: '-32,768 ~ +32,767' },
|
||||
{ udtName: 'int4', dataType: 'integer', desc: '常用的整数,别名为INT4。', space: '4字节', range: '-2,147,483,648 ~ +2,147,483,647' },
|
||||
{ udtName: 'int8', dataType: 'bigint', desc: '大范围的整数,别名为INT8。', space: '8字节', range: '很大' },
|
||||
|
||||
// 数值 - 任意精度型
|
||||
{
|
||||
udtName: 'numeric',
|
||||
dataType: 'numeric',
|
||||
desc: '精度(总位数)取值范围为[1,1000],标度(小数位数)取值范围为[0,精度]。',
|
||||
space: '每四位(十进制位)占用两个字节,然后在整个数据上加上八个字节的额外开销',
|
||||
range: '未指定精度的情况下,小数点前最大131,072位,小数点后最大16,383位',
|
||||
},
|
||||
// 数值 - 任意精度型
|
||||
{ udtName: 'decimal', dataType: 'decimal', desc: '等同于number类型', space: '等同于number类型' },
|
||||
|
||||
// 数值 - 序列整型
|
||||
{ udtName: 'smallserial', dataType: 'smallserial', desc: '二字节序列整型。', space: '2字节', range: '-32,768 ~ +32,767' },
|
||||
{ udtName: 'serial', dataType: 'serial', desc: '四字节序列整型。', space: '4字节', range: '-2,147,483,648 ~ +2,147,483,647' },
|
||||
{ udtName: 'bigserial', dataType: 'bigserial', desc: '八字节序列整型', space: '8字节', range: '-9,223,372,036,854,775,808 ~ +9,223,372,036,854,775,807' },
|
||||
{
|
||||
udtName: 'largeserial',
|
||||
dataType: 'largeserial',
|
||||
desc: '默认插入十六字节序列整型,实际数值类型和numeric相同',
|
||||
space: '变长类型,每四位(十进制位)占用两个字节,然后在整个数据上加上八个字节的额外开销。',
|
||||
range: '小数点前最大131,072位,小数点后最大16,383位。',
|
||||
},
|
||||
|
||||
// 数值 - 浮点类型(不常用 就不列出来了)
|
||||
|
||||
// 货币类型
|
||||
{ udtName: 'money', dataType: 'money', desc: '货币金额', space: '8字节', range: '-92233720368547758.08 ~ +92233720368547758.07' },
|
||||
|
||||
// 布尔类型
|
||||
{ udtName: 'bool', dataType: 'bool', desc: '布尔类型', space: '1字节', range: 'true:真 , false:假 , null:未知(unknown)' },
|
||||
|
||||
// 字符类型
|
||||
{ udtName: 'char', dataType: 'char', desc: '定长字符串,不足补空格。n是指字节长度,如不带精度n,默认精度为1。', space: '最大为10MB' },
|
||||
{ udtName: 'character', dataType: 'character', desc: '定长字符串,不足补空格。n是指字节长度,如不带精度n,默认精度为1。', space: '最大为10MB' },
|
||||
{ udtName: 'nchar', dataType: 'nchar', desc: '定长字符串,不足补空格。n是指字节长度,如不带精度n,默认精度为1。', space: '最大为10MB' },
|
||||
{ udtName: 'varchar', dataType: 'varchar', desc: '变长字符串。PG兼容模式下,n是字符长度。其他兼容模式下,n是指字节长度。', space: '最大为10MB。' },
|
||||
{ udtName: 'text', dataType: 'text', desc: '变长字符串。', space: '最大稍微小于1GB-1。' },
|
||||
{ udtName: 'clob', dataType: 'clob', desc: '文本大对象。是TEXT类型的别名。', space: '最大稍微小于32TB-1。' },
|
||||
|
||||
//特殊字符类型 用的很少,先屏蔽了
|
||||
// { udtName: 'name', dataType: 'name', desc: '用于对象名的内部类型。', space: '64字节。' },
|
||||
// { udtName: '"char"', dataType: '"char"', desc: '单字节内部类型。', space: '1字节。' },
|
||||
|
||||
// 二进制类型
|
||||
{ udtName: 'bytea', dataType: 'bytea', desc: '变长的二进制字符串', space: '4字节加上实际的二进制字符串。最大为1GB减去8203字节(即1073733621字节)。' },
|
||||
|
||||
// 日期/时间类型
|
||||
{ udtName: 'date', dataType: 'date', desc: '日期', space: '4字节' },
|
||||
{ udtName: 'time', dataType: 'time', desc: 'TIME [(p)] 只用于一日内时间,p表示小数点后的精度,取值范围为0~6。', space: '8-12字节' },
|
||||
{ udtName: 'timestamp', dataType: 'timestamp', desc: 'TIMESTAMP[(p)]日期和时间,p表示小数点后的精度,取值范围为0~6', space: '8字节' },
|
||||
// 带时区的时间戳用的少,先屏蔽了
|
||||
//{ udtName: 'TIMESTAMPTZ', dataType: 'TIMESTAMP WITH TIME ZONE', desc: '带时区的时间戳', space: '8字节' },
|
||||
{
|
||||
udtName: 'interval',
|
||||
dataType: 'interval',
|
||||
desc: '时间间隔', // 可以跟参数:YEAR,MONTH,DAY,HOUR,MINUTE,SECOND,DAY TO HOUR,DAY TO MINUTE,DAY TO SECOND,HOUR TO MINUTE,HOUR TO SECOND,MINUTE TO SECOND
|
||||
space: '精度取值范围为0~6,且参数为SECOND,DAY TO SECOND,HOUR TO SECOND或MINUTE TO SECOND时,参数p才有效',
|
||||
},
|
||||
// 几何类型
|
||||
{ udtName: 'point', dataType: 'point', desc: '平面中的点, 如:(x,y)', space: '16字节' },
|
||||
{ udtName: 'lseg', dataType: 'lseg', desc: '(有限)线段, 如:((x1,y1),(x2,y2))', space: '32字节' },
|
||||
{ udtName: 'box', dataType: 'box', desc: '矩形, 如:((x1,y1),(x2,y2))', space: '32字节' },
|
||||
{ udtName: 'path', dataType: 'path', desc: '闭合路径(与多边形类似), 如:((x1,y1),...)', space: '16+16n字节' },
|
||||
{ udtName: 'path', dataType: 'path', desc: '开放路径(与多边形类似), 如:[(x1,y1),...]', space: '16+16n字节' },
|
||||
{ udtName: 'polygon', dataType: 'polygon', desc: '多边形(与闭合路径相似), 如:((x1,y1),...)', space: '40+16n字节' },
|
||||
{ udtName: 'circle', dataType: 'polygon', desc: '圆,如:<(x,y),r> (圆心和半径)', space: '24 字节' },
|
||||
|
||||
// 网络地址类型
|
||||
{ udtName: 'cidr', dataType: 'cidr', desc: 'IPv4网络', space: '7字节' },
|
||||
{ udtName: 'inet', dataType: 'inet', desc: 'IPv4主机和网络', space: '7字节' },
|
||||
{ udtName: 'macaddr', dataType: 'macaddr', desc: 'MAC地址', space: '6字节' },
|
||||
];
|
||||
|
||||
class PostgresqlDialect implements DbDialect {
|
||||
getFormatDialect() {
|
||||
return 'postgresql';
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return 'iconfont icon-op-postgres';
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} OFFSET ${
|
||||
(pageNum - 1) * limit
|
||||
} LIMIT ${limit};`;
|
||||
}
|
||||
|
||||
wrapName = (name: string) => {
|
||||
return name;
|
||||
};
|
||||
|
||||
getColumnTypes(): sqlColumnType[] {
|
||||
return GAUSS_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName));
|
||||
}
|
||||
|
||||
matchType(text: string, arr: string[]): boolean {
|
||||
if (!text || !arr || arr.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (text.indexOf(arr[i]) > -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getDefaultValueSql(cl: any): string {
|
||||
if (cl.value && cl.value.length > 0) {
|
||||
// 哪些字段默认值需要加引号
|
||||
let marks = false;
|
||||
if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
|
||||
// 默认值是now()的time或date不需要加引号
|
||||
if (cl.value.toLowerCase().replace(' ', '') === 'CURRENT_TIMESTAMP' && this.matchType(cl.type, ['time', 'date'])) {
|
||||
marks = false;
|
||||
} else {
|
||||
marks = true;
|
||||
}
|
||||
}
|
||||
// 哪些函数不需要加引号
|
||||
if (this.matchType(cl.value, ['nextval'])) {
|
||||
marks = false;
|
||||
}
|
||||
return ` DEFAULT ${marks ? "'" : ''}${cl.value}${marks ? "'" : ''}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getTypeLengthSql(cl: any) {
|
||||
// 哪些字段可以指定长度
|
||||
if (cl.length && this.matchType(cl.type, ['char', 'time', 'bit', 'num', 'decimal'])) {
|
||||
// 哪些字段类型可以指定小数点
|
||||
if (cl.numScale && this.matchType(cl.type, ['num', 'decimal'])) {
|
||||
return `(${cl.length}, ${cl.numScale})`;
|
||||
} else {
|
||||
return `(${cl.length})`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
genColumnBasicSql(cl: any): string {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
let createSql = '';
|
||||
let tableCommentSql = '';
|
||||
let columCommentSql = '';
|
||||
|
||||
// 创建表结构
|
||||
let pks = [] as string[];
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
if (item.pri) {
|
||||
pks.push(item.name);
|
||||
}
|
||||
// 列注释
|
||||
if (item.remark) {
|
||||
columCommentSql += ` comment on column ${data.tableName}.${item.name} is '${item.remark}'; `;
|
||||
}
|
||||
});
|
||||
// 建表
|
||||
createSql = `CREATE TABLE ${data.tableName}
|
||||
(
|
||||
${fields.join(',')}
|
||||
${pks ? `, PRIMARY KEY (${pks.join(',')})` : ''}
|
||||
);`;
|
||||
// 表注释
|
||||
if (data.tableComment) {
|
||||
tableCommentSql = ` comment on table ${data.tableName} is '${data.tableComment}'; `;
|
||||
}
|
||||
|
||||
return createSql + tableCommentSql + columCommentSql;
|
||||
}
|
||||
|
||||
getCreateIndexSql(tableData: any): string {
|
||||
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
||||
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
||||
// 创建索引
|
||||
let sql: string[] = [];
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} USING btree ("${a.columnNames.join('","')})"`);
|
||||
if (a.indexComment) {
|
||||
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
|
||||
}
|
||||
});
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
let sql: string[] = [];
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
let typeLength = this.getTypeLengthSql(a);
|
||||
let defaultSql = this.getDefaultValueSql(a);
|
||||
sql.push(`ALTER TABLE ${tableName} add ${a.name} ${a.type}${typeLength} ${defaultSql}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
let typeLength = this.getTypeLengthSql(a);
|
||||
sql.push(`ALTER TABLE ${tableName} alter column ${a.name} type ${a.type}${typeLength}`);
|
||||
let defaultSql = this.getDefaultValueSql(a);
|
||||
if (defaultSql) {
|
||||
sql.push(`alter table ${tableName} alter column ${a.name} set ${defaultSql}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`ALTER TABLE ${tableName} DROP COLUMN ${a.name}`);
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 不能直接修改索引名或字段、需要先删后加
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
dropIndexNames.push(a.indexName);
|
||||
addIndexs.push(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
dropIndexNames.push(a.indexName);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
addIndexs.push(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (dropIndexNames.length > 0 || addIndexs.length > 0) {
|
||||
let sql: string[] = [];
|
||||
if (dropIndexNames.length > 0) {
|
||||
dropIndexNames.forEach((a) => {
|
||||
sql.push(`DROP INDEX ${a}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (addIndexs.length > 0) {
|
||||
addIndexs.forEach((a) => {
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')})`);
|
||||
if (a.indexComment) {
|
||||
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// src/basic-languages/mysql/mysql.ts
|
||||
var language = {
|
||||
keywords: [
|
||||
"GROUP BY",
|
||||
"ORDER BY",
|
||||
"LEFT JOIN",
|
||||
"RIGHT JOIN",
|
||||
"INNER JOIN",
|
||||
"SELECT * FROM",
|
||||
],
|
||||
operators: [
|
||||
],
|
||||
builtinFunctions: [
|
||||
],
|
||||
builtinVariables: [],
|
||||
replaceFunctions:[ // 自定义修改函数提示
|
||||
|
||||
/** 字符串相关函数 */
|
||||
{ label: 'CONCAT', insertText:'CONCAT(str1,str2,...)', description: '多字符串合并' },
|
||||
{ label: 'ASCII', insertText:'ASCII(char)', description: '返回字符的ASCII值' },
|
||||
{ label: 'BIT_LENGTH', insertText:'BIT_LENGTH(str1)', description: '多字符串合并' },
|
||||
{ label: 'INSTR', insertText:'INSTR(str,substr)', description: '返回字符串substr所在str位置' },
|
||||
{ label: 'LEFT', insertText:'LEFT(str,len)', description: '返回字符串str的左端len个字符' },
|
||||
{ label: 'RIGHT', insertText:'RIGHT(str,len)', description: '返回字符串str的右端len个字符' },
|
||||
{ label: 'MID', insertText:'MID(str,pos,len)', description: '返回字符串str的位置pos起len个字符' },
|
||||
{ label: 'SUBSTRING', insertText:'SUBSTRING(exp, start, length)', description: '截取字符串' },
|
||||
{ label: 'REPLACE', insertText:'REPLACE(str,from_str,to_str)', description: '替换字符串' },
|
||||
{ label: 'REPEAT', insertText:'REPEAT(str,count)', description: '重复字符串count遍' },
|
||||
{ label: 'UPPER', insertText:'UPPER(str)', description: '返回大写的字符串' },
|
||||
{ label: 'LOWER', insertText:'LOWER(str)', description: '返回小写的字符串' },
|
||||
{ label: 'TRIM', insertText:'TRIM(str)', description: '去除字符串首尾空格' },
|
||||
/** 数学相关函数 */
|
||||
{ label: 'ABS', insertText:'ABS(n)', description: '返回n的绝对值' },
|
||||
{ label: 'FLOOR', insertText:'FLOOR(n)', description: '返回不大于n的最大整数' },
|
||||
{ label: 'CEILING', insertText:'CEILING(n)', description: '返回不小于n的最小整数值' },
|
||||
{ label: 'ROUND', insertText:'ROUND(n,d)', description: '返回n的四舍五入值,保留d(默认0)位小数' },
|
||||
{ label: 'RAND', insertText:'RAND()', description: '返回在范围0到1.0内的随机浮点值' },
|
||||
|
||||
/** 日期函数 */
|
||||
{ label: 'DATE', insertText:'DATE(\'date\')', description: '返回指定表达式的日期部分' },
|
||||
{ label: 'WEEK', insertText:'WEEK(\'date\')', description: '返回指定日期是一年中的第几周' },
|
||||
{ label: 'MONTH', insertText:'MONTH(\'date\')', description: '返回指定日期的月份' },
|
||||
{ label: 'QUARTER', insertText:'QUARTER(\'date\')', description: '返回指定日期是一年的第几个季度' },
|
||||
{ label: 'YEAR', insertText:'YEAR(\'date\')', description: '返回指定日期的年份' },
|
||||
{ label: 'DATE_ADD', insertText:'DATE_ADD(\'date\', interval 1 day)', description: '日期函数加减运算' },
|
||||
{ label: 'DATE_SUB', insertText:'DATE_SUB(\'date\', interval 1 day)', description: '日期函数加减运算' },
|
||||
{ label: 'DATE_FORMAT', insertText:'DATE_FORMAT(\'date\', \'%Y-%m-%d %h:%i:%s\')', description: '' },
|
||||
{ label: 'CURDATE', insertText:'CURDATE()', description: '返回当前日期' },
|
||||
{ label: 'CURTIME', insertText:'CURTIME()', description: '返回当前时间' },
|
||||
{ label: 'NOW', insertText:'NOW()', description: '返回当前日期时间' },
|
||||
{ label: 'DATEDIFF', insertText:'DATEDIFF(expr1,expr2)', description: '返回结束日expr1和起始日expr2之间的天数' },
|
||||
{ label: 'UNIX_TIMESTAMP', insertText:'UNIX_TIMESTAMP()', description: '返回指定时间(默认当前)unix时间戳' },
|
||||
{ label: 'FROM_UNIXTIME', insertText:'FROM_UNIXTIME(timestamp)', description: '把时间戳格式为年月日时分秒' },
|
||||
|
||||
/** 逻辑函数 */
|
||||
{ label: 'IFNULL', insertText:'IFNULL(expression, alt_value)', description: '表达式为空取第二个参数值,否则取表达式值' },
|
||||
{ label: 'IF', insertText:'IF(expr1, expr2, expr3)', description: 'expr1为true则取expr2,否则取expr3' },
|
||||
{ label: 'CASE', insertText:'(CASE \n WHEN expr1 THEN expr2 \n ELSE expr3) col', description: 'CASE WHEN THEN ELSE' },
|
||||
]
|
||||
};
|
||||
export {
|
||||
language
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
export const TYPE_LIST = [
|
||||
'bigint',
|
||||
'binary',
|
||||
'blob',
|
||||
'char',
|
||||
'datetime',
|
||||
'date',
|
||||
'decimal',
|
||||
'double',
|
||||
'enum',
|
||||
'float',
|
||||
'int',
|
||||
'json',
|
||||
'longblob',
|
||||
'longtext',
|
||||
'mediumblob',
|
||||
'mediumtext',
|
||||
'set',
|
||||
'smallint',
|
||||
'text',
|
||||
'time',
|
||||
'timestamp',
|
||||
'tinyint',
|
||||
'varbinary',
|
||||
'varchar',
|
||||
];
|
||||
|
||||
export const CHARACTER_SET_NAME_LIST = [
|
||||
'armscii8',
|
||||
'ascii',
|
||||
'big5',
|
||||
'binary',
|
||||
'cp1250',
|
||||
'cp1251',
|
||||
'cp1256',
|
||||
'cp1257',
|
||||
'cp850',
|
||||
'cp852',
|
||||
'cp866',
|
||||
'cp932',
|
||||
'dec8',
|
||||
'eucjpms',
|
||||
'euckr',
|
||||
'gb18030',
|
||||
'gb2312',
|
||||
'gbk',
|
||||
'geostd8',
|
||||
'greek',
|
||||
'hebrew',
|
||||
'hp8',
|
||||
'keybcs2',
|
||||
'koi8r',
|
||||
'koi8u',
|
||||
'latin1',
|
||||
'latin2',
|
||||
'latin5',
|
||||
'latin7',
|
||||
'macce',
|
||||
'macroman',
|
||||
'sjis',
|
||||
'swe7',
|
||||
'tis620',
|
||||
'ucs2',
|
||||
'ujis',
|
||||
'utf16',
|
||||
'utf16le',
|
||||
'utf32',
|
||||
'utf8',
|
||||
'utf8mb4',
|
||||
];
|
||||
|
||||
export const COLLATION_SUFFIX_LIST = [
|
||||
'unicode_ci',
|
||||
'bin',
|
||||
'croatian_ci',
|
||||
'czech_ci',
|
||||
'danish_ci',
|
||||
'esperanto_ci',
|
||||
'estonian_ci',
|
||||
'general_ci',
|
||||
'german2_ci',
|
||||
'hungarian_ci',
|
||||
'icelandic_ci',
|
||||
'latvian_ci',
|
||||
'lithuanian_ci',
|
||||
'persian_ci',
|
||||
'polish_ci',
|
||||
'roman_ci',
|
||||
'romanian_ci',
|
||||
'sinhala_ci',
|
||||
'slovak_ci',
|
||||
'slovenian_ci',
|
||||
'spanish2_ci',
|
||||
'spanish_ci',
|
||||
'swedish_ci',
|
||||
'turkish_ci',
|
||||
'unicode_520_ci',
|
||||
'vietnamese_ci',
|
||||
];
|
||||
@@ -4,8 +4,19 @@
|
||||
<el-form :model="form" ref="machineForm" :rules="rules" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form-item prop="tagId" label="标签">
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签">
|
||||
<tag-tree-select
|
||||
multiple
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
|
||||
@@ -24,7 +35,7 @@
|
||||
<el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password"> </el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="认证方式">
|
||||
<el-form-item label="认证方式" required>
|
||||
<el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType" placeholder="请选认证方式">
|
||||
<el-option key="1" label="密码" :value="1"> </el-option>
|
||||
<el-option key="2" label="授权凭证" :value="2"> </el-option>
|
||||
@@ -71,9 +82,10 @@
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { machineApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import AuthCertSelect from './authcert/AuthCertSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -95,7 +107,7 @@ const rules = {
|
||||
{
|
||||
required: true,
|
||||
message: '请选择标签',
|
||||
trigger: ['blur', 'change'],
|
||||
trigger: ['change'],
|
||||
},
|
||||
],
|
||||
name: [
|
||||
@@ -126,17 +138,11 @@ const rules = {
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入授权密码',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const machineForm: any = ref(null);
|
||||
const authCertSelectRef: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
@@ -146,14 +152,14 @@ const state = reactive({
|
||||
authType: 1,
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
ip: null,
|
||||
port: 22,
|
||||
name: null,
|
||||
authCertId: null as any,
|
||||
username: '',
|
||||
password: '',
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
tagId: [],
|
||||
remark: '',
|
||||
sshTunnelMachineId: null as any,
|
||||
enableRecorder: -1,
|
||||
@@ -173,6 +179,7 @@ watch(props, async (newValue: any) => {
|
||||
state.tabActiveName = 'basic';
|
||||
if (newValue.machine) {
|
||||
state.form = { ...newValue.machine };
|
||||
|
||||
// 如果凭证类型为公共的,则表示使用授权凭证认证
|
||||
const authCertId = (state.form as any).authCertId;
|
||||
if (authCertId > 0) {
|
||||
@@ -181,7 +188,7 @@ watch(props, async (newValue: any) => {
|
||||
state.authType = 1;
|
||||
}
|
||||
} else {
|
||||
state.form = { port: 22 } as any;
|
||||
state.form = { port: 22, tagId: [] } as any;
|
||||
state.authType = 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,19 +24,42 @@
|
||||
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #ipPort="{ data }">
|
||||
<el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" :underline="false">
|
||||
{{ `${data.ip}:${data.port}` }}
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<template #stat="{ data }">
|
||||
<span v-if="!data.stat">-</span>
|
||||
<div v-else>
|
||||
<el-row>
|
||||
<el-text size="small" style="font-size: 10px">
|
||||
内存(可用/总):
|
||||
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
|
||||
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
|
||||
</span>
|
||||
</el-text>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-text style="font-size: 10px" size="small">
|
||||
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
|
||||
</el-text>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #fs="{ data }">
|
||||
<span v-if="!data.stat?.fsInfos">-</span>
|
||||
<div v-else>
|
||||
<el-row v-for="i in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
|
||||
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||
</el-text>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #status="{ data }">
|
||||
<el-switch
|
||||
v-auth:disabled="'machine:update'"
|
||||
@@ -52,9 +75,13 @@
|
||||
></el-switch>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Machine.value" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<span v-auth="'machine:terminal'">
|
||||
<el-tooltip effect="customized" content="按住ctrl则为新标签打开" placement="top">
|
||||
<el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top">
|
||||
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>终端</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
@@ -159,15 +186,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
// 组件
|
||||
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
|
||||
@@ -179,6 +209,7 @@ const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const pageTableRef: any = ref(null);
|
||||
const terminalDialogRef: any = ref(null);
|
||||
|
||||
@@ -193,11 +224,13 @@ const perms = {
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.text('ip', 'IP'), TableQuery.text('name', '名称')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(45),
|
||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
|
||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
||||
]);
|
||||
@@ -209,10 +242,10 @@ const state = reactive({
|
||||
tags: [] as any,
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
ip: null,
|
||||
name: null,
|
||||
tagPath: null,
|
||||
tagPath: '',
|
||||
},
|
||||
// 列表数据
|
||||
data: {
|
||||
@@ -327,7 +360,7 @@ const closeCli = async (row: any) => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await machineApi.tagList.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Machine.value });
|
||||
};
|
||||
|
||||
const openFormDialog = async (machine: any) => {
|
||||
@@ -358,7 +391,9 @@ const deleteMachine = async () => {
|
||||
await machineApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('操作成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const serviceManager = (row: any) => {
|
||||
@@ -399,6 +434,9 @@ const showFileManage = (selectionData: any) => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
if (route.query.tagPath) {
|
||||
state.params.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
const res = await machineApi.list.request(state.params);
|
||||
state.data = res;
|
||||
} finally {
|
||||
@@ -406,6 +444,18 @@ const search = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getStatsFontClass = (availavle: number, total: number) => {
|
||||
const p = availavle / total;
|
||||
if (p < 0.1) {
|
||||
return 'color-danger';
|
||||
}
|
||||
if (p < 0.2) {
|
||||
return 'color-warning';
|
||||
}
|
||||
|
||||
return 'color-success';
|
||||
};
|
||||
|
||||
const showInfo = (info: any) => {
|
||||
state.infoDialog.data = info;
|
||||
state.infoDialog.visible = true;
|
||||
|
||||
@@ -7,31 +7,44 @@
|
||||
:before-close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
width="800"
|
||||
>
|
||||
<page-table
|
||||
height="100%"
|
||||
v-model:query-form="query"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:total="total"
|
||||
v-model:page-size="query.pageSize"
|
||||
v-model:page-num="query.pageNum"
|
||||
@pageChange="getTermOps()"
|
||||
>
|
||||
<template #action="{ data }">
|
||||
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="playerDialogVisible"
|
||||
:before-close="handleClosePlayer"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
width="70%"
|
||||
>
|
||||
<div class="toolbar">
|
||||
<el-select @change="getUsers" v-model="operateDate" placeholder="操作日期" filterable>
|
||||
<el-option v-for="item in operateDates" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
<el-select class="ml10" @change="getRecs" filterable v-model="user" placeholder="请选择操作人">
|
||||
<el-option v-for="item in users" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
<el-select class="ml10" @change="playRec" filterable v-model="rec" placeholder="请选择操作记录">
|
||||
<el-option v-for="item in recs" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
快捷键-> space[空格键]: 暂停/播放
|
||||
</div>
|
||||
<div ref="playerRef" id="rc-player"></div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, ref, reactive } from 'vue';
|
||||
import { toRefs, watch, ref, reactive, nextTick } from 'vue';
|
||||
import { machineApi } from './api';
|
||||
import * as AsciinemaPlayer from 'asciinema-player';
|
||||
import 'asciinema-player/dist/bundle/asciinema-player.css';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean },
|
||||
@@ -41,67 +54,75 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
|
||||
|
||||
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('action', '操作').isSlot().setMinWidth(60).fixedRight().alignCenter(),
|
||||
];
|
||||
|
||||
const playerRef = ref(null);
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
title: '',
|
||||
machineId: 0,
|
||||
operateDates: [],
|
||||
users: [],
|
||||
recs: [],
|
||||
operateDate: '',
|
||||
user: '',
|
||||
rec: '',
|
||||
data: [],
|
||||
total: 0,
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
|
||||
playerDialogVisible: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, title, operateDates, operateDate, users, recs, user, rec } = toRefs(state);
|
||||
const { dialogVisible, query, data, total, playerDialogVisible } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
if (visible) {
|
||||
state.machineId = newValue.machineId;
|
||||
state.title = newValue.title;
|
||||
await getOperateDate();
|
||||
await getTermOps();
|
||||
}
|
||||
state.dialogVisible = visible;
|
||||
});
|
||||
|
||||
const getOperateDate = async () => {
|
||||
const res = await machineApi.recDirNames.request({ path: state.machineId });
|
||||
state.operateDates = res as any;
|
||||
};
|
||||
|
||||
const getUsers = async (operateDate: string) => {
|
||||
state.users = [];
|
||||
state.user = '';
|
||||
state.recs = [];
|
||||
state.rec = '';
|
||||
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${operateDate}` });
|
||||
state.users = res as any;
|
||||
};
|
||||
|
||||
const getRecs = async (user: string) => {
|
||||
state.recs = [];
|
||||
state.rec = '';
|
||||
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${state.operateDate}/${user}` });
|
||||
state.recs = res as any;
|
||||
const getTermOps = async () => {
|
||||
const res = await machineApi.termOpRecs.request({ id: state.machineId, ...state.query });
|
||||
state.data = res.list;
|
||||
state.total = res.total;
|
||||
};
|
||||
|
||||
let player: any = null;
|
||||
|
||||
const playRec = async (rec: string) => {
|
||||
if (player) {
|
||||
player.dispose();
|
||||
const playRec = async (rec: any) => {
|
||||
try {
|
||||
if (player) {
|
||||
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, {
|
||||
autoPlay: true,
|
||||
speed: 1.0,
|
||||
idleTimeLimit: 2,
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
rec.playRecLoding = false;
|
||||
}
|
||||
const content = await machineApi.recDirNames.request({
|
||||
isFile: '1',
|
||||
path: `${state.machineId}/${state.operateDate}/${state.user}/${rec}`,
|
||||
});
|
||||
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
|
||||
autoPlay: true,
|
||||
speed: 1.0,
|
||||
idleTimeLimit: 2,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClosePlayer = () => {
|
||||
state.playerDialogVisible = false;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,12 +132,8 @@ const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
emit('update:machineId', null);
|
||||
emit('cancel');
|
||||
state.operateDates = [];
|
||||
state.users = [];
|
||||
state.recs = [];
|
||||
state.operateDate = '';
|
||||
state.user = '';
|
||||
state.rec = '';
|
||||
state.data = [];
|
||||
state.total = 0;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@@ -124,5 +141,15 @@ const handleClose = () => {
|
||||
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
#rc-player {
|
||||
.ap-terminal {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.ap-player {
|
||||
height: 550px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
<el-link @click="onRefresh" icon="refresh" :underline="false" type="success"></el-link>
|
||||
</template>
|
||||
<el-descriptions-item label="主机名">
|
||||
{{ stats.Hostname }}
|
||||
{{ stats.hostname }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运行时间">
|
||||
{{ stats.Uptime }}
|
||||
{{ stats.uptime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="总任务">
|
||||
{{ stats.TotalProcs }}
|
||||
{{ stats.totalProcs }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运行中任务">
|
||||
{{ stats.RunningProcs }}
|
||||
{{ stats.runningProcs }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负载"> {{ stats.Load1 }} {{ stats.Load5 }} {{ stats.Load10 }} </el-descriptions-item>
|
||||
<el-descriptions-item label="负载"> {{ stats.load1 }} {{ stats.load5 }} {{ stats.load10 }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
|
||||
@@ -35,16 +35,16 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :lg="8" :md="8">
|
||||
<span style="font-size: 16px; font-weight: 700">磁盘</span>
|
||||
<el-table :data="stats.FSInfos" stripe max-height="250" style="width: 100%" border>
|
||||
<el-table-column prop="MountPoint" label="挂载点" min-width="100" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="Used" label="可使用" min-width="70" show-overflow-tooltip>
|
||||
<el-table :data="stats.fSInfos" stripe max-height="250" style="width: 100%" border>
|
||||
<el-table-column prop="mountPoint" label="挂载点" min-width="100" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="used" label="可使用" min-width="70" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ formatByteSize(scope.row.Free) }}
|
||||
{{ formatByteSize(scope.row.free) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Used" label="已使用" min-width="70" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ formatByteSize(scope.row.Used) }}
|
||||
{{ formatByteSize(scope.row.used) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -54,16 +54,16 @@
|
||||
<span style="font-size: 16px; font-weight: 700">网卡</span>
|
||||
<el-table :data="netInter" stripe max-height="250" style="width: 100%" border>
|
||||
<el-table-column prop="name" label="网卡" min-width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="IPv4" label="IPv4" min-width="130" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="IPv6" label="IPv6" min-width="130" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="Rx" label="接收(rx)" min-width="110" show-overflow-tooltip>
|
||||
<el-table-column prop="ipv4" label="IPv4" min-width="130" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="ipv6" label="IPv6" min-width="130" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="rx" label="接收(rx)" min-width="110" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ formatByteSize(scope.row.Rx) }}
|
||||
{{ formatByteSize(scope.row.rx) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Tx" label="发送(tx)" min-width="110" show-overflow-tooltip>
|
||||
<el-table-column prop="tx" label="发送(tx)" min-width="110" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ formatByteSize(scope.row.Tx) }}
|
||||
{{ formatByteSize(scope.row.tx) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -84,9 +84,6 @@ const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
},
|
||||
machineId: {
|
||||
type: Number,
|
||||
},
|
||||
@@ -134,11 +131,12 @@ const onRefresh = async () => {
|
||||
};
|
||||
|
||||
const initMemStats = () => {
|
||||
const mem = state.stats.memInfo;
|
||||
const data = [
|
||||
{ name: '可用内存', value: state.stats.MemAvailable },
|
||||
{ name: '可用内存', value: mem.available },
|
||||
{
|
||||
name: '已用内存',
|
||||
value: state.stats.MemTotal - state.stats.MemAvailable,
|
||||
value: mem.total - mem.available,
|
||||
},
|
||||
];
|
||||
const option = {
|
||||
@@ -192,20 +190,20 @@ const initMemStats = () => {
|
||||
};
|
||||
|
||||
const initCpuStats = () => {
|
||||
const cpu = state.stats.CPU;
|
||||
const cpu = state.stats.cpu;
|
||||
const data = [
|
||||
{ name: 'Idle', value: cpu.Idle },
|
||||
{ name: 'Idle', value: cpu.idle },
|
||||
{
|
||||
name: 'Iowait',
|
||||
value: cpu.Iowait,
|
||||
value: cpu.iowait,
|
||||
},
|
||||
{
|
||||
name: 'System',
|
||||
value: cpu.System,
|
||||
value: cpu.system,
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: cpu.User,
|
||||
value: cpu.user,
|
||||
},
|
||||
];
|
||||
const option = {
|
||||
@@ -283,7 +281,7 @@ const initEchartsResize = () => {
|
||||
|
||||
const parseNetInter = () => {
|
||||
state.netInter = [];
|
||||
const netInter = state.stats.NetIntf;
|
||||
const netInter = state.stats.netIntf;
|
||||
const keys = Object.keys(netInter);
|
||||
const values = Object.values(netInter);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Api from '@/common/Api';
|
||||
import config from '@/common/config';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
|
||||
export const machineApi = {
|
||||
// 获取权限列表
|
||||
@@ -33,7 +33,7 @@ export const machineApi = {
|
||||
cpFile: Api.newPost('/machines/{machineId}/files/{fileId}/cp'),
|
||||
renameFile: Api.newPost('/machines/{machineId}/files/{fileId}/rename'),
|
||||
mvFile: Api.newPost('/machines/{machineId}/files/{fileId}/mv'),
|
||||
uploadFile: Api.newPost('/machines/{machineId}/files/{fileId}/upload?token={token}'),
|
||||
uploadFile: Api.newPost('/machines/{machineId}/files/{fileId}/upload?' + joinClientParams()),
|
||||
fileContent: Api.newGet('/machines/{machineId}/files/{fileId}/read'),
|
||||
createFile: Api.newPost('/machines/{machineId}/files/{id}/create-file'),
|
||||
// 修改文件内容
|
||||
@@ -43,7 +43,10 @@ export const machineApi = {
|
||||
// 删除配置的文件or目录
|
||||
delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
|
||||
terminal: Api.newGet('/api/machines/{id}/terminal'),
|
||||
recDirNames: Api.newGet('/machines/rec/names'),
|
||||
// 机器终端操作记录列表
|
||||
termOpRecs: Api.newGet('/machines/{id}/term-recs'),
|
||||
// 机器终端操作记录详情
|
||||
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
|
||||
};
|
||||
|
||||
export const authCertApi = {
|
||||
@@ -59,9 +62,10 @@ export const cronJobApi = {
|
||||
relateCronJobIds: Api.newGet('/machine-cronjobs/cronjob-ids'),
|
||||
save: Api.newPost('/machine-cronjobs'),
|
||||
delete: Api.newDelete('/machine-cronjobs/{id}'),
|
||||
run: Api.newPost('/machine-cronjobs/run/{key}'),
|
||||
execList: Api.newGet('/machine-cronjobs/execs'),
|
||||
};
|
||||
|
||||
export function getMachineTerminalSocketUrl(machineId: any) {
|
||||
return `${config.baseWsUrl}/machines/${machineId}/terminal?token=${getToken()}`;
|
||||
return `${config.baseWsUrl}/machines/${machineId}/terminal?${joinClientParams()}`;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import { AuthMethodEnum } from '../enums';
|
||||
const state = reactive({
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
queryConfig: [TableQuery.text('name', '凭证名称')],
|
||||
@@ -105,7 +105,9 @@ const deleteAc = async (data: any) => {
|
||||
await authCertApi.delete.request({ id: data.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
ref="pageTableRef"
|
||||
:query="queryConfig"
|
||||
v-model:query-form="params"
|
||||
:data="data.list"
|
||||
:data="state.data.list"
|
||||
:columns="columns"
|
||||
:total="data.total"
|
||||
:total="state.data.total"
|
||||
v-model:page-size="params.pageSize"
|
||||
v-model:page-num="params.pageNum"
|
||||
@pageChange="search()"
|
||||
@@ -88,7 +88,7 @@ const state = reactive({
|
||||
|
||||
const machineMap: Map<number, any> = new Map();
|
||||
|
||||
const { dialogVisible, params, data } = toRefs(state);
|
||||
const { dialogVisible, params } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button :disabled="data.status == CronJobStatusEnum.Disable.value" v-auth="perms.saveCronJob" type="primary" @click="runCronJob(data)" link
|
||||
>执行</el-button
|
||||
>
|
||||
<el-button v-auth="perms.saveCronJob" type="primary" @click="openFormDialog(data)" link>编辑</el-button>
|
||||
<el-button type="primary" @click="showExec(data)" link>执行记录</el-button>
|
||||
</template>
|
||||
@@ -69,7 +72,7 @@ const columns = ref([
|
||||
const state = reactive({
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
ip: null,
|
||||
name: null,
|
||||
},
|
||||
@@ -111,6 +114,11 @@ const openFormDialog = async (data: any) => {
|
||||
state.cronJobEdit.visible = true;
|
||||
};
|
||||
|
||||
const runCronJob = async (data: any) => {
|
||||
await cronJobApi.run.request({ key: data.key });
|
||||
ElMessage.success('执行成功');
|
||||
};
|
||||
|
||||
const deleteCronJob = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】计划任务信息? 该操作将同时删除执行记录`, '提示', {
|
||||
@@ -121,7 +129,9 @@ const deleteCronJob = async () => {
|
||||
await cronJobApi.delete.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('操作成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -274,11 +274,12 @@ import { ref, toRefs, reactive, onMounted, computed } from 'vue';
|
||||
import { ElMessage, ElMessageBox, ElInput } from 'element-plus';
|
||||
import { machineApi } from '../api';
|
||||
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import config from '@/common/config';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import MachineFileContent from './MachineFileContent.vue';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
|
||||
const props = defineProps({
|
||||
machineId: { type: Number },
|
||||
@@ -607,7 +608,7 @@ const deleteFile = async (files: any) => {
|
||||
|
||||
const downloadFile = (data: any) => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('href', `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/read?type=1&path=${data.path}&token=${token}`);
|
||||
a.setAttribute('href', `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/read?type=1&path=${data.path}&${joinClientParams()}`);
|
||||
a.click();
|
||||
};
|
||||
|
||||
@@ -628,7 +629,7 @@ function getFolder(e: any) {
|
||||
// 上传操作
|
||||
machineApi.uploadFile
|
||||
.request(form, {
|
||||
url: `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/upload-folder?token=${token}`,
|
||||
url: `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/upload-folder?${joinClientParams()}`,
|
||||
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
||||
onUploadProgress: onUploadProgress,
|
||||
baseURL: '',
|
||||
@@ -669,7 +670,7 @@ const getUploadFile = (content: any) => {
|
||||
params.append('token', token);
|
||||
machineApi.uploadFile
|
||||
.request(params, {
|
||||
url: `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/upload?token=${token}`,
|
||||
url: `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/upload?${joinClientParams()}`,
|
||||
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
||||
onUploadProgress: onUploadProgress,
|
||||
baseURL: '',
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<tag-tree @node-click="nodeClick" :load="loadNode">
|
||||
<el-col :span="5">
|
||||
<tag-tree :resource-type="TagResourceTypeEnum.Mongo.value" :tag-path-node-type="NodeTypeTagPath">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type == NodeType.Mongo">
|
||||
<el-popover placement="right-start" title="mongo实例信息" trigger="hover" :width="210">
|
||||
<span v-if="data.type.value == MongoNodeType.Mongo">
|
||||
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<SvgIcon name="iconfont icon-op-mongo" :size="18" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="auto" :size="'small'">
|
||||
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
||||
<el-form-item label="链接:">{{ data.params.uri }}</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ data.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="链接">
|
||||
{{ data.params.uri }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
|
||||
<SvgIcon v-if="data.type == NodeType.Dbs" name="Coin" color="#67c23a" />
|
||||
<SvgIcon v-if="data.type.value == MongoNodeType.Dbs" name="Coin" color="#67c23a" />
|
||||
|
||||
<SvgIcon v-if="data.type == NodeType.Coll || data.type == NodeType.CollMenu" name="Document" class="color-primary" />
|
||||
<SvgIcon
|
||||
v-if="data.type.value == MongoNodeType.Coll || data.type.value == MongoNodeType.CollMenu"
|
||||
name="Document"
|
||||
class="color-primary"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #label="{ data }">
|
||||
<span v-if="data.type == NodeType.Dbs">
|
||||
{{ data.params.dbName }}
|
||||
<span v-if="data.type.value == MongoNodeType.Dbs">
|
||||
{{ data.params.database }}
|
||||
<span style="color: #8492a6; font-size: 13px"> [{{ formatByteSize(data.params.size) }}] </span>
|
||||
</span>
|
||||
|
||||
@@ -34,7 +42,7 @@
|
||||
</tag-tree>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="20">
|
||||
<el-col :span="19">
|
||||
<div id="mongo-tab" class="ml5" style="border: 1px solid var(--el-border-color-light, #ebeef5); margin-top: 1px">
|
||||
<el-row v-if="nowColl">
|
||||
<el-descriptions :column="10" size="small" border>
|
||||
@@ -161,9 +169,11 @@ import { computed, defineAsyncComponent, reactive, ref, toRefs } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { isTrue, notBlank } from '@/common/assert';
|
||||
import { TagTreeNode } from '../component/tag';
|
||||
import { TagTreeNode, NodeType } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
|
||||
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
|
||||
|
||||
@@ -175,13 +185,66 @@ const perms = {
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
class NodeType {
|
||||
class MongoNodeType {
|
||||
static Mongo = 1;
|
||||
static Dbs = 2;
|
||||
static CollMenu = 3;
|
||||
static Coll = 4;
|
||||
}
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const res = await mongoApi.mongoList.request({ tagPath: parentNode.key });
|
||||
if (!res.total) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const mongoInfos = res.list;
|
||||
await sleep(100);
|
||||
return mongoInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeMongo).withParams(x);
|
||||
});
|
||||
});
|
||||
|
||||
const NodeTypeMongo = new NodeType(MongoNodeType.Mongo).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const inst = parentNode.params;
|
||||
// 点击mongo -> 加载mongo数据库列表
|
||||
const res = await mongoApi.databases.request({ id: inst.id });
|
||||
return res.Databases.map((x: any) => {
|
||||
const database = x.Name;
|
||||
return new TagTreeNode(`${inst.id}.${database}`, database, NodeTypeDbs).withParams({
|
||||
id: inst.id,
|
||||
database,
|
||||
size: x.SizeOnDisk,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const NodeTypeDbs = new NodeType(MongoNodeType.Dbs).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
// 点击数据库列表 -> 加载数据库下拥有的菜单列表
|
||||
return [new TagTreeNode(`${params.id}.${params.database}.mongo-coll`, '集合', NodeTypeCollMenu).withParams(params)];
|
||||
});
|
||||
|
||||
const NodeTypeCollMenu = new NodeType(MongoNodeType.CollMenu).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const { id, database } = parentNode.params;
|
||||
// 点击数据库集合节点 -> 加载集合列表
|
||||
const colls = await mongoApi.collections.request({ id, database });
|
||||
return colls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${database}.${x}`, x, NodeTypeColl).withIsLeaf(true).withParams({
|
||||
id,
|
||||
database,
|
||||
collection: x,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const NodeTypeColl = new NodeType(MongoNodeType.Coll).withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const { id, database, collection } = nodeData.params;
|
||||
changeCollection(id, database, collection);
|
||||
});
|
||||
|
||||
const findParamInputRef: any = ref(null);
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
@@ -220,108 +283,6 @@ const nowColl = computed(() => {
|
||||
return getNowDataTab();
|
||||
});
|
||||
|
||||
/**
|
||||
* instmap; tagPaht -> mongo info[]
|
||||
*/
|
||||
const instMap: Map<string, any[]> = new Map();
|
||||
|
||||
const getInsts = async () => {
|
||||
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000 });
|
||||
if (!res.total) return;
|
||||
for (const mongoInfo of res.list) {
|
||||
const tagPath = mongoInfo.tagPath;
|
||||
let mongoInsts = instMap.get(tagPath) || [];
|
||||
mongoInsts.push(mongoInfo);
|
||||
instMap.set(tagPath, mongoInsts);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载文件树节点
|
||||
* @param {Object} node
|
||||
* @param {Object} resolve
|
||||
*/
|
||||
const loadNode = async (node: any) => {
|
||||
// 一级为tagPath
|
||||
if (node.level === 0) {
|
||||
await getInsts();
|
||||
const tagPaths = instMap.keys();
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tagPaths) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath));
|
||||
}
|
||||
return tagNodes;
|
||||
}
|
||||
|
||||
const data = node.data;
|
||||
const params = data.params;
|
||||
const nodeType = data.type;
|
||||
|
||||
// 点击标签 -> 显示mongo信息列表
|
||||
if (nodeType === TagTreeNode.TagPath) {
|
||||
const mongoInfos = instMap.get(data.key);
|
||||
return mongoInfos?.map((x: any) => {
|
||||
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Mongo).withParams(x);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击mongo -> 加载mongo数据库列表
|
||||
if (nodeType === NodeType.Mongo) {
|
||||
return await getDatabases(params);
|
||||
}
|
||||
|
||||
// 点击数据库列表 -> 加载数据库下拥有的菜单列表
|
||||
if (nodeType === NodeType.Dbs) {
|
||||
return [new TagTreeNode(`${params.id}.${params.dbName}.mongo-coll`, '集合', NodeType.CollMenu).withParams(params)];
|
||||
}
|
||||
|
||||
// 点击数据库集合节点 -> 加载集合列表
|
||||
if (nodeType === NodeType.CollMenu) {
|
||||
return await getCollections(params.id, params.dbName);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取实例的所有库信息
|
||||
* @param inst 实例信息
|
||||
*/
|
||||
const getDatabases = async (inst: any) => {
|
||||
const res = await mongoApi.databases.request({ id: inst.id });
|
||||
return res.Databases.map((x: any) => {
|
||||
const dbName = x.Name;
|
||||
return new TagTreeNode(`${inst.id}.${dbName}`, dbName, NodeType.Dbs).withParams({
|
||||
id: inst.id,
|
||||
dbName,
|
||||
size: x.SizeOnDisk,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取集合列表信息
|
||||
* @param inst
|
||||
*/
|
||||
const getCollections = async (id: any, database: string) => {
|
||||
const colls = await mongoApi.collections.request({ id, database });
|
||||
return colls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${database}.${x}`, x, NodeType.Coll).withIsLeaf(true).withParams({
|
||||
id,
|
||||
database,
|
||||
collection: x,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const nodeClick = async (data: any) => {
|
||||
// 点击集合
|
||||
if (data.type === NodeType.Coll) {
|
||||
const { id, database, collection } = data.params;
|
||||
await changeCollection(id, database, collection);
|
||||
}
|
||||
};
|
||||
|
||||
const changeCollection = async (id: any, schema: string, collection: string) => {
|
||||
const label = `${id}:\`${schema}\`.${collection}`;
|
||||
let dataTab = state.dataTabs[label];
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form-item prop="tagId" label="标签" required>
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Mongo.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
@@ -32,6 +43,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="testConn" :loading="state.testConnBtnLoading" type="success">测试连接</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
@@ -44,8 +56,9 @@
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { mongoApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -87,18 +100,21 @@ const rules = {
|
||||
};
|
||||
|
||||
const mongoForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
tabActiveName: 'basic',
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
name: null,
|
||||
uri: null,
|
||||
sshTunnelMachineId: null as any,
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
tagId: [],
|
||||
},
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, btnLoading } = toRefs(state);
|
||||
@@ -116,15 +132,35 @@ watch(props, async (newValue: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
const getReqForm = () => {
|
||||
const reqForm = { ...state.form };
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async () => {
|
||||
mongoForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
state.testConnBtnLoading = true;
|
||||
try {
|
||||
await mongoApi.testConn.request(getReqForm());
|
||||
ElMessage.success('连接成功');
|
||||
} finally {
|
||||
state.testConnBtnLoading = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
mongoForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
// reqForm.uri = await RsaEncrypt(reqForm.uri);
|
||||
mongoApi.saveMongo.request(reqForm).then(() => {
|
||||
mongoApi.saveMongo.request(getReqForm()).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -25,10 +25,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Mongo.value" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -57,24 +54,28 @@
|
||||
import { mongoApi } from './api';
|
||||
import { defineAsyncComponent, ref, toRefs, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const MongoEdit = defineAsyncComponent(() => import('./MongoEdit.vue'));
|
||||
const MongoDbs = defineAsyncComponent(() => import('./MongoDbs.vue'));
|
||||
const MongoRunCommand = defineAsyncComponent(() => import('./MongoRunCommand.vue'));
|
||||
|
||||
const pageTableRef: any = ref(null);
|
||||
const route = useRoute();
|
||||
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('uri', '连接uri'),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(20).alignCenter(),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('creator', '创建人'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(145).fixedRight().alignCenter(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(170).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
const state = reactive({
|
||||
@@ -88,8 +89,8 @@ const state = reactive({
|
||||
selectionData: [],
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tagPath: null,
|
||||
pageSize: 0,
|
||||
tagPath: '',
|
||||
},
|
||||
mongoEditDialog: {
|
||||
visible: false,
|
||||
@@ -126,12 +127,19 @@ const deleteMongo = async () => {
|
||||
await mongoApi.deleteMongo.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
|
||||
if (route.query.tagPath) {
|
||||
state.query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
|
||||
const res = await mongoApi.mongoList.request(state.query);
|
||||
state.list = res.list;
|
||||
state.total = res.total;
|
||||
@@ -141,7 +149,7 @@ const search = async () => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await mongoApi.mongoTags.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Mongo.value });
|
||||
};
|
||||
|
||||
const editMongo = async (data: any) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import Api from '@/common/Api';
|
||||
export const mongoApi = {
|
||||
mongoList: Api.newGet('/mongos'),
|
||||
mongoTags: Api.newGet('/mongos/tags'),
|
||||
testConn: Api.newPost('/mongos/test-conn'),
|
||||
saveMongo: Api.newPost('/mongos'),
|
||||
deleteMongo: Api.newDelete('/mongos/{id}'),
|
||||
databases: Api.newGet('/mongos/{id}/databases'),
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<el-col :span="5">
|
||||
<el-row type="flex" justify="space-between">
|
||||
<el-col :span="24" class="flex-auto">
|
||||
<tag-tree @node-click="nodeClick" :load="loadNode">
|
||||
<tag-tree :resource-type="TagResourceTypeEnum.Redis.value" :tag-path-node-type="NodeTypeTagPath">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type == NodeType.Redis">
|
||||
<el-popover placement="right-start" title="redis实例信息" trigger="hover" :width="210">
|
||||
<span v-if="data.type.value == RedisNodeType.Redis">
|
||||
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<SvgIcon name="iconfont icon-op-redis" :size="18" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="auto" :size="'small'">
|
||||
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
||||
<el-form-item label="模式:">{{ data.params.mode }}</el-form-item>
|
||||
<el-form-item label="链接:">{{ data.params.host }}</el-form-item>
|
||||
<el-form-item label="备注:">{{ data.params.remark }}</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ data.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="模式">
|
||||
{{ data.params.mode }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
{{ data.params.host }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" label-align="right">
|
||||
{{ data.params.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
|
||||
<SvgIcon v-if="data.type == NodeType.Db" name="Coin" color="#67c23a" />
|
||||
<SvgIcon v-if="data.type.value == RedisNodeType.Db" name="Coin" color="#67c23a" />
|
||||
</template>
|
||||
</tag-tree>
|
||||
</el-col>
|
||||
@@ -120,27 +128,11 @@
|
||||
</el-tree>
|
||||
|
||||
<!-- right context menu -->
|
||||
<div ref="rightMenuRef" class="key-list-right-menu">
|
||||
<!-- folder right menu -->
|
||||
<div v-if="!state.rightClickNode?.isLeaf"></div>
|
||||
<!-- key right menu -->
|
||||
<div v-else>
|
||||
<el-row>
|
||||
<el-link @click="showKeyDetail(state.rightClickNode.key, true)" type="primary" icon="plus" :underline="false"
|
||||
>新tab打开</el-link
|
||||
>
|
||||
</el-row>
|
||||
<el-row class="mt5">
|
||||
<el-link @click="delKey(state.rightClickNode.key)" v-auth="'redis:data:del'" type="danger" icon="delete" :underline="false"
|
||||
>删除</el-link
|
||||
>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="13" style="border-left: 1px solid var(--el-card-border-color)">
|
||||
<el-col :span="12" style="border-left: 1px solid var(--el-card-border-color)">
|
||||
<div class="ml5">
|
||||
<el-tabs @tab-remove="removeDataTab" style="width: 100%" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
||||
@@ -184,20 +176,96 @@ import { redisApi } from './api';
|
||||
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { isTrue, notBlank, notNull } from '@/common/assert';
|
||||
import { TagTreeNode } from '../component/tag';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { TagTreeNode, NodeType } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { sleep } from '../../../common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
||||
|
||||
const contextmenuRef = ref();
|
||||
|
||||
const cmCopyKey = new ContextmenuItem('copyValue', '复制')
|
||||
.withIcon('CopyDocument')
|
||||
.withHideFunc((data: any) => !data.isLeaf)
|
||||
.withOnClick(async (data: any) => await copyToClipboard(data.key));
|
||||
|
||||
const cmNewTabOpen = new ContextmenuItem('newTabOpenKey', '新tab打开')
|
||||
.withIcon('plus')
|
||||
.withHideFunc((data: any) => !data.isLeaf)
|
||||
.withOnClick((data: any) => showKeyDetail(data.key, true));
|
||||
|
||||
const cmDelKey = new ContextmenuItem('delKey', '删除')
|
||||
.withIcon('delete')
|
||||
.withPermission('redis:data:del')
|
||||
.withHideFunc((data: any) => !data.isLeaf)
|
||||
.withOnClick((data: any) => delKey(data.key));
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
class NodeType {
|
||||
class RedisNodeType {
|
||||
static Redis = 1;
|
||||
static Db = 2;
|
||||
}
|
||||
|
||||
// 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(`${parentNode.key}.${x.id}`, x.name, NodeTypeRedis).withParams(x);
|
||||
});
|
||||
});
|
||||
|
||||
// redis实例节点类型
|
||||
const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const redisInfo = parentNode.params;
|
||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||
return new TagTreeNode(x, `db${x}`, NodeTypeDb).withIsLeaf(true).withParams({
|
||||
id: redisInfo.id,
|
||||
db: x,
|
||||
name: `db${x}`,
|
||||
keys: 0,
|
||||
});
|
||||
});
|
||||
|
||||
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} [${e.params.keys}]`;
|
||||
});
|
||||
return dbs;
|
||||
});
|
||||
|
||||
// 库节点类型
|
||||
const NodeTypeDb = new NodeType(RedisNodeType.Db).withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
resetScanParam();
|
||||
state.scanParam.id = nodeData.params.id;
|
||||
state.scanParam.db = nodeData.params.db;
|
||||
scan();
|
||||
});
|
||||
|
||||
const treeProps = {
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
@@ -207,7 +275,6 @@ const treeProps = {
|
||||
const defaultCount = 250;
|
||||
|
||||
const keyTreeRef: any = ref(null);
|
||||
const rightMenuRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
@@ -239,6 +306,13 @@ const state = reactive({
|
||||
},
|
||||
},
|
||||
dbsize: 0,
|
||||
contextmenu: {
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
items: [cmCopyKey, cmNewTabOpen, cmDelKey],
|
||||
},
|
||||
});
|
||||
|
||||
const { scanParam, keyTreeData, newKeyDialog } = toRefs(state);
|
||||
@@ -253,98 +327,34 @@ const setHeight = () => {
|
||||
state.keyTreeHeight = window.innerHeight - 174 + 'px';
|
||||
};
|
||||
|
||||
/**
|
||||
* instmap; tagPaht -> redis info[]
|
||||
*/
|
||||
const instMap: Map<string, any[]> = new Map();
|
||||
// /**
|
||||
// * instmap; tagPaht -> redis info[]
|
||||
// */
|
||||
// const instMap: Map<string, any[]> = new Map();
|
||||
|
||||
const getInsts = async () => {
|
||||
const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
|
||||
if (!res.total) return;
|
||||
for (const redisInfo of res.list) {
|
||||
const tagPath = redisInfo.tagPath;
|
||||
let redisInsts = instMap.get(tagPath) || [];
|
||||
redisInsts.push(redisInfo);
|
||||
instMap.set(tagPath, redisInsts);
|
||||
}
|
||||
};
|
||||
// const getInsts = async () => {
|
||||
// const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
|
||||
// if (!res.total) return;
|
||||
// for (const redisInfo of res.list) {
|
||||
// const tagPath = redisInfo.tagPath;
|
||||
// let redisInsts = instMap.get(tagPath) || [];
|
||||
// redisInsts.push(redisInfo);
|
||||
// instMap.set(tagPath, redisInsts);
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* 加载文件树节点
|
||||
* @param {Object} node
|
||||
* @param {Object} resolve
|
||||
*/
|
||||
const loadNode = async (node: any) => {
|
||||
// 一级为tagPath
|
||||
if (node.level === 0) {
|
||||
await getInsts();
|
||||
const tagPaths = instMap.keys();
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tagPaths) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath));
|
||||
}
|
||||
return tagNodes;
|
||||
}
|
||||
|
||||
const data = node.data;
|
||||
// 点击tagPath -> 加载数据库信息列表
|
||||
if (data.type === TagTreeNode.TagPath) {
|
||||
const redisInfos = instMap.get(data.key);
|
||||
return redisInfos?.map((x: any) => {
|
||||
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Redis).withParams(x);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击redis实例 -> 加载库列表
|
||||
if (data.type === NodeType.Redis) {
|
||||
return await getDbs(data.params);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const nodeClick = (data: any) => {
|
||||
// 点击库事件
|
||||
if (data.type === NodeType.Db) {
|
||||
resetScanParam();
|
||||
state.scanParam.id = data.params.id;
|
||||
state.scanParam.db = data.params.db;
|
||||
scan();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有库信息
|
||||
* @param redisInfo redis信息
|
||||
*/
|
||||
const getDbs = async (redisInfo: any) => {
|
||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||
return new TagTreeNode(x, `db${x}`, NodeType.Db).withIsLeaf(true).withParams({
|
||||
id: redisInfo.id,
|
||||
db: x,
|
||||
name: `db${x}`,
|
||||
keys: 0,
|
||||
});
|
||||
});
|
||||
|
||||
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} [${e.params.keys}]`;
|
||||
});
|
||||
return dbs;
|
||||
};
|
||||
// /**
|
||||
// * 加载标签树节点
|
||||
// */
|
||||
// const loadTags = async () => {
|
||||
// await getInsts();
|
||||
// const tagPaths = instMap.keys();
|
||||
// const tagNodes = [];
|
||||
// for (let tagPath of tagPaths) {
|
||||
// tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
|
||||
// }
|
||||
// return tagNodes;
|
||||
// };
|
||||
|
||||
const scan = async (appendKey = false) => {
|
||||
isTrue(state.scanParam.id != null, '请先选择redis');
|
||||
@@ -412,7 +422,8 @@ const expandAllKeyNode = (nodes: any) => {
|
||||
};
|
||||
|
||||
const handleKeyTreeNodeClick = async (data: any) => {
|
||||
hideAllMenus();
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
// 目录则不做处理
|
||||
if (data.type == 1) {
|
||||
return;
|
||||
@@ -471,7 +482,7 @@ const removeDataTab = (targetName: string) => {
|
||||
delete state.dataTabs[targetName];
|
||||
};
|
||||
|
||||
const keyTreeNodeExpand = (data: any, node: any, component: any) => {
|
||||
const keyTreeNodeExpand = (data: any, node: any) => {
|
||||
state.keyTreeExpanded.add(data.key);
|
||||
// async sort nodes
|
||||
if (!node.customSorted) {
|
||||
@@ -480,45 +491,16 @@ const keyTreeNodeExpand = (data: any, node: any, component: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const keyTreeNodeCollapse = (data: any, node: any, component: any) => {
|
||||
const keyTreeNodeCollapse = (data: any) => {
|
||||
state.keyTreeExpanded.delete(data.key);
|
||||
};
|
||||
|
||||
const rightClickNode = (event: any, data: any, node: any) => {
|
||||
hideAllMenus();
|
||||
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
contextmenuRef.value.openContextmenu(node);
|
||||
keyTreeRef.value.setCurrentKey(node.key);
|
||||
state.rightClickNode = node;
|
||||
|
||||
// nextTick for dom render
|
||||
nextTick(() => {
|
||||
let top = event.clientY;
|
||||
const menu = rightMenuRef.value;
|
||||
menu.style.display = 'block';
|
||||
|
||||
// position in bottom
|
||||
if (document.body.clientHeight - top < menu.clientHeight) {
|
||||
top -= menu.clientHeight;
|
||||
}
|
||||
|
||||
menu.style.left = `${event.clientX}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
|
||||
document.addEventListener('click', hideAllMenus, { once: true });
|
||||
});
|
||||
};
|
||||
|
||||
const hideAllMenus = () => {
|
||||
let menus: any = document.querySelectorAll('.key-list-right-menu');
|
||||
|
||||
if (menus.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.rightClickNode = null;
|
||||
for (const menu of menus) {
|
||||
menu.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
const searchKey = async () => {
|
||||
@@ -629,12 +611,6 @@ const delKey = (key: string) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.instances-pop-form {
|
||||
.el-form-item {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.key-list-vtree {
|
||||
height: calc(100vh - 250px);
|
||||
}
|
||||
@@ -655,21 +631,4 @@ const delKey = (key: string) => {
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
/* right menu style start */
|
||||
.key-list-right-menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 5px;
|
||||
z-index: 99999;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
border: 2px solid lightgrey;
|
||||
background: #fafafa;
|
||||
}
|
||||
.dark-mode .key-list-right-menu {
|
||||
background: #263238;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, watch, ref, shallowReactive, reactive, computed, toRefs, onMounted } from 'vue';
|
||||
import { defineAsyncComponent, watch, ref, shallowReactive, reactive, computed, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import KeyHeader from './KeyHeader.vue';
|
||||
|
||||
@@ -107,8 +107,6 @@ watch(
|
||||
onMounted(() => {
|
||||
setKeyInfo(props.keyInfo);
|
||||
});
|
||||
|
||||
const {} = toRefs(state);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.key-tab-container {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="key-header-item key-ttl-input">
|
||||
<el-input type="number" v-model.number="ki.timed" placeholder="单位(秒),负数永久" title="点击修改过期时间">
|
||||
<template #prepend>
|
||||
<span slot="prepend">TTL</span>
|
||||
<span>TTL</span>
|
||||
</template>
|
||||
|
||||
<template #suffix>
|
||||
@@ -37,8 +37,8 @@
|
||||
|
||||
<!-- del & refresh btn -->
|
||||
<div class="key-header-item key-header-btn-con">
|
||||
<el-button slot="reference" type="success" @click="refreshKey" icon="refresh" title="刷新"></el-button>
|
||||
<el-button v-auth="'redis:data:del'" slot="reference" type="danger" @click="delKey" icon="delete" title="删除"></el-button>
|
||||
<el-button type="success" @click="refreshKey" icon="refresh" title="刷新"></el-button>
|
||||
<el-button v-auth="'redis:data:del'" type="danger" @click="delKey" icon="delete" title="删除"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -74,6 +74,7 @@ const state = reactive({
|
||||
timed: -1,
|
||||
} as any,
|
||||
oldKey: '',
|
||||
memuse: 0,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -114,14 +114,14 @@ const getListValue = async (resetTableData = false) => {
|
||||
state.loadMoreDisable = state.values.length === state.total;
|
||||
};
|
||||
|
||||
const lset = async (row: any, rowIndex: number) => {
|
||||
await redisApi.setListValue.request({
|
||||
...getBaseReqParam(),
|
||||
index: (state.pageNum - 1) * state.pageSize + rowIndex,
|
||||
value: row.value,
|
||||
});
|
||||
ElMessage.success('数据保存成功');
|
||||
};
|
||||
// const lset = async (row: any, rowIndex: number) => {
|
||||
// await redisApi.setListValue.request({
|
||||
// ...getBaseReqParam(),
|
||||
// index: (state.pageNum - 1) * state.pageSize + rowIndex,
|
||||
// value: row.value,
|
||||
// });
|
||||
// ElMessage.success('数据保存成功');
|
||||
// };
|
||||
|
||||
const showEditDialog = (row: any) => {
|
||||
state.editDialog.dataRow = row;
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
<el-form :model="form" ref="redisForm" :rules="rules" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form-item prop="tagId" label="标签" required>
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Redis.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input>
|
||||
@@ -73,6 +84,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="testConn" :loading="state.testConnBtnLoading" type="success">测试连接</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
@@ -86,8 +98,9 @@ import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -142,13 +155,15 @@ const rules = {
|
||||
};
|
||||
|
||||
const redisForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
tabActiveName: 'basic',
|
||||
form: {
|
||||
id: null,
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
code: '',
|
||||
tagId: [],
|
||||
name: null,
|
||||
mode: 'standalone',
|
||||
host: '',
|
||||
@@ -161,6 +176,7 @@ const state = reactive({
|
||||
dbList: [0],
|
||||
pwd: '',
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, dbList, pwd, btnLoading } = toRefs(state);
|
||||
@@ -195,19 +211,40 @@ const getPwd = async () => {
|
||||
state.pwd = await redisApi.getRedisPwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const getReqForm = async () => {
|
||||
const reqForm = { ...state.form };
|
||||
if (reqForm.mode == 'sentinel' && reqForm.host.split('=').length != 2) {
|
||||
ElMessage.error('sentinel模式host需为: mastername=sentinelhost:sentinelport模式');
|
||||
return;
|
||||
}
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async () => {
|
||||
redisForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
state.testConnBtnLoading = true;
|
||||
try {
|
||||
await redisApi.testConn.request(await getReqForm());
|
||||
ElMessage.success('连接成功');
|
||||
} finally {
|
||||
state.testConnBtnLoading = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
redisForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
if (reqForm.mode == 'sentinel' && reqForm.host.split('=').length != 2) {
|
||||
ElMessage.error('sentinel模式host需为: mastername=sentinelhost:sentinelport模式');
|
||||
return;
|
||||
}
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
redisApi.saveRedis.request(reqForm).then(() => {
|
||||
redisApi.saveRedis.request(await getReqForm()).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -25,20 +25,14 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #more="{ data }">
|
||||
<el-button @click="showDetail(data)" link>详情</el-button>
|
||||
|
||||
<el-button v-if="data.mode === 'standalone' || data.mode === 'sentinel'" type="primary" @click="showInfoDialog(data)" link>单机信息</el-button>
|
||||
<el-button @click="onShowClusterInfo(data)" v-if="data.mode === 'cluster'" type="primary" link>集群信息</el-button>
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Redis.value" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-if="data.mode === 'standalone' || data.mode === 'sentinel'" type="primary" @click="showInfoDialog(data)" link>单机信息</el-button>
|
||||
<el-button @click="onShowClusterInfo(data)" v-if="data.mode === 'cluster'" type="primary" link>集群信息</el-button>
|
||||
|
||||
<el-button @click="showDetail(data)" link>详情</el-button>
|
||||
<el-button type="primary" link @click="editRedis(data)">编辑</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
@@ -170,21 +164,24 @@ import { ref, toRefs, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import RedisEdit from './RedisEdit.vue';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const pageTableRef: any = ref(null);
|
||||
const route = useRoute();
|
||||
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('host', 'host:port'),
|
||||
TableColumn.new('mode', 'mode'),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('more', '更多').isSlot().setMinWidth(155).fixedRight(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(65).fixedRight().alignCenter(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
const state = reactive({
|
||||
@@ -193,9 +190,9 @@ const state = reactive({
|
||||
total: 0,
|
||||
selectionData: [],
|
||||
query: {
|
||||
tagPath: null,
|
||||
tagPath: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
detailDialog: {
|
||||
visible: false,
|
||||
@@ -246,7 +243,9 @@ const deleteRedis = async () => {
|
||||
await redisApi.delRedis.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const showInfoDialog = async (redis: any) => {
|
||||
@@ -271,6 +270,11 @@ const onShowClusterInfo = async (redis: any) => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
|
||||
if (route.query.tagPath) {
|
||||
state.query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
|
||||
const res = await redisApi.redisList.request(state.query);
|
||||
state.redisTable = res.list;
|
||||
state.total = res.total;
|
||||
@@ -280,7 +284,7 @@ const search = async () => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await redisApi.redisTags.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Redis.value });
|
||||
};
|
||||
|
||||
const editRedis = async (data: any) => {
|
||||
|
||||
@@ -6,11 +6,13 @@ export const redisApi = {
|
||||
getRedisPwd: Api.newGet('/redis/{id}/pwd'),
|
||||
redisInfo: Api.newGet('/redis/{id}/info'),
|
||||
clusterInfo: Api.newGet('/redis/{id}/cluster-info'),
|
||||
testConn: Api.newPost('/redis/test-conn'),
|
||||
saveRedis: Api.newPost('/redis'),
|
||||
delRedis: Api.newDelete('/redis/{id}'),
|
||||
|
||||
keyInfo: Api.newGet('/redis/{id}/{db}/key-info'),
|
||||
keyTtl: Api.newGet('/redis/{id}/{db}/key-ttl'),
|
||||
keyMemuse: Api.newGet('/redis/{id}/{db}/key-memuse'),
|
||||
renameKey: Api.newPost('/redis/{id}/{db}/rename-key'),
|
||||
expireKey: Api.newPost('/redis/{id}/{db}/expire-key'),
|
||||
persistKey: Api.newDelete('/redis/{id}/{db}/persist-key'),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="menu">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="filterTag" placeholder="输入标签关键字过滤" style="width: 200px; margin-right: 10px" />
|
||||
<el-button v-auth="'tag:save'" type="primary" icon="plus" @click="showSaveTabDialog(null)">添加</el-button>
|
||||
<el-input v-model="filterTag" placeholder="输入关键字过滤(右击进行操作)" style="width: 220px; margin-right: 10px" />
|
||||
<el-button v-auth="'tag:save'" type="primary" icon="plus" @click="showSaveTagDialog(null)">添加</el-button>
|
||||
<div style="float: right">
|
||||
<el-tooltip effect="dark" placement="top">
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
1. 用于将资产进行归类
|
||||
<br />2. 可在团队管理中进行分配,用于资源隔离 <br />3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源
|
||||
@@ -26,8 +26,10 @@
|
||||
:data="data"
|
||||
@node-expand="handleNodeExpand"
|
||||
@node-collapse="handleNodeCollapse"
|
||||
@node-contextmenu="nodeContextmenu"
|
||||
@node-click="treeNodeClick"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
:expand-on-click-node="false"
|
||||
:expand-on-click-node="true"
|
||||
:filter-node-method="filterNode"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
@@ -39,44 +41,6 @@
|
||||
<span style="color: #3c8dbc">】</span>
|
||||
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag>
|
||||
</span>
|
||||
|
||||
<el-link @click.prevent="info(data)" style="margin-left: 25px" icon="view" type="info" :underline="false" />
|
||||
|
||||
<el-link v-auth="'tag:save'" @click.prevent="showEditTagDialog(data)" class="ml5" type="primary" icon="edit" :underline="false" />
|
||||
|
||||
<el-link v-auth="'tag:save'" @click.prevent="showSaveTabDialog(data)" icon="circle-plus" :underline="false" type="success" class="ml5" />
|
||||
|
||||
<!-- <el-link
|
||||
v-auth="'resource:changeStatus'"
|
||||
@click.prevent="changeStatus(data, -1)"
|
||||
v-if="data.status === 1 && data.type === enums.ResourceTypeEnum.PERMISSION.value"
|
||||
icon="circle-close"
|
||||
:underline="false"
|
||||
type="warning"
|
||||
class="ml5"
|
||||
/>
|
||||
|
||||
<el-link
|
||||
v-auth="'resource:changeStatus'"
|
||||
@click.prevent="changeStatus(data, 1)"
|
||||
v-if="data.status === -1 && data.type === enums.ResourceTypeEnum.PERMISSION.value"
|
||||
type="success"
|
||||
icon="circle-check"
|
||||
:underline="false"
|
||||
plain
|
||||
class="ml5"
|
||||
/> -->
|
||||
|
||||
<el-link
|
||||
v-auth="'tag:del'"
|
||||
@click.prevent="deleteTag(data)"
|
||||
v-if="data.children == null"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:underline="false"
|
||||
plain
|
||||
class="ml5"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
@@ -114,6 +78,26 @@
|
||||
<el-descriptions-item label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="`[ ${resourceDialog.tagPath} ] 关联的资源`" v-model="resourceDialog.visible" width="500px">
|
||||
<el-table max-height="300" :data="resourceDialog.data">
|
||||
<el-table-column property="resourceType" label="资源类型" min-width="50" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ EnumValue.getLabelByValue(TagResourceTypeEnum, scope.row.resourceType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column property="count" label="数量" min-width="50" show-overflow-tooltip> </el-table-column>
|
||||
|
||||
<el-table-column label="操作" min-width="50" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-button @click="showResources(scope.row.resourceType, resourceDialog.tagPath)" link type="success">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -122,6 +106,10 @@ import { toRefs, ref, watch, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { tagApi } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
|
||||
import { TagResourceTypeEnum } from '../../../common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
interface Tree {
|
||||
id: number;
|
||||
@@ -130,9 +118,41 @@ interface Tree {
|
||||
children?: Tree[];
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const tagForm: any = ref(null);
|
||||
const tagTreeRef: any = ref(null);
|
||||
const filterTag = ref('');
|
||||
const contextmenuRef = ref();
|
||||
|
||||
const contextmenuInfo = new ContextmenuItem('info', '详情').withIcon('view').withOnClick((data: any) => info(data));
|
||||
|
||||
const contextmenuAdd = new ContextmenuItem('addTag', '添加子标签')
|
||||
.withIcon('circle-plus')
|
||||
.withPermission('tag:save')
|
||||
.withOnClick((data: any) => showSaveTagDialog(data));
|
||||
|
||||
const contextmenuEdit = new ContextmenuItem('edit', '编辑')
|
||||
.withIcon('edit')
|
||||
.withPermission('tag:save')
|
||||
.withOnClick((data: any) => showEditTagDialog(data));
|
||||
|
||||
const contextmenuDel = new ContextmenuItem('delete', '删除')
|
||||
.withIcon('delete')
|
||||
.withPermission('tag:del')
|
||||
.withHideFunc((data: any) => {
|
||||
// 存在子标签,则不允许删除
|
||||
return data.children;
|
||||
})
|
||||
.withOnClick((data: any) => deleteTag(data));
|
||||
|
||||
const contextmenuShowRelateResource = new ContextmenuItem('showRelateResources', '查看关联资源')
|
||||
.withIcon('view')
|
||||
.withHideFunc((data: any) => {
|
||||
// 存在子标签,则不允许查看关联资源
|
||||
return data.children;
|
||||
})
|
||||
.withOnClick((data: any) => showRelateResource(data));
|
||||
|
||||
const state = reactive({
|
||||
data: [],
|
||||
@@ -147,11 +167,24 @@ const state = reactive({
|
||||
// 资源类型选择是否选
|
||||
data: null as any,
|
||||
},
|
||||
resourceDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
tagPath: '',
|
||||
data: null as any,
|
||||
},
|
||||
// 展开的节点
|
||||
defaultExpandedKeys: [] as any,
|
||||
contextmenu: {
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
items: [contextmenuInfo, contextmenuEdit, contextmenuAdd, contextmenuDel, contextmenuShowRelateResource],
|
||||
},
|
||||
});
|
||||
|
||||
const { data, saveTabDialog, infoDialog, defaultExpandedKeys } = toRefs(state);
|
||||
const { data, saveTabDialog, infoDialog, resourceDialog, defaultExpandedKeys } = toRefs(state);
|
||||
|
||||
const props = {
|
||||
label: 'name',
|
||||
@@ -188,15 +221,28 @@ const search = async () => {
|
||||
state.data = res;
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
contextmenuRef.value.openContextmenu(data);
|
||||
};
|
||||
|
||||
const treeNodeClick = () => {
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
const info = async (data: any) => {
|
||||
state.infoDialog.data = data;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const showSaveTabDialog = (data: any) => {
|
||||
const showSaveTagDialog = (data: any) => {
|
||||
if (data) {
|
||||
state.saveTabDialog.form.pid = data.id;
|
||||
state.saveTabDialog.title = `新增 [${data.codePath}] 子标签信息`;
|
||||
state.saveTabDialog.title = `新增[ ${data.codePath} ]子标签信息`;
|
||||
} else {
|
||||
state.saveTabDialog.title = '新增根标签信息';
|
||||
}
|
||||
@@ -212,6 +258,49 @@ const showEditTagDialog = (data: any) => {
|
||||
state.saveTabDialog.visible = true;
|
||||
};
|
||||
|
||||
const showRelateResource = async (data: any) => {
|
||||
const resourceMap = new Map();
|
||||
state.resourceDialog.tagPath = data.codePath;
|
||||
const tagResources = await tagApi.getTagResources.request({ tagId: data.id });
|
||||
for (let tagResource of tagResources) {
|
||||
const resourceType = tagResource.resourceType;
|
||||
const exist = resourceMap.get(resourceType);
|
||||
if (exist) {
|
||||
exist.count = exist.count + 1;
|
||||
} else {
|
||||
resourceMap.set(resourceType, { resourceType, count: 1, tagPath: tagResource.tagPath });
|
||||
}
|
||||
}
|
||||
state.resourceDialog.data = Array.from(resourceMap.values());
|
||||
state.resourceDialog.visible = true;
|
||||
};
|
||||
|
||||
const showResources = (resourceType: any, tagPath: string) => {
|
||||
state.resourceDialog.visible = false;
|
||||
setTimeout(() => {
|
||||
let toPath = '';
|
||||
if (resourceType == TagResourceTypeEnum.Machine.value) {
|
||||
toPath = '/machine/machines';
|
||||
}
|
||||
if (resourceType == TagResourceTypeEnum.Db.value) {
|
||||
toPath = '/dbms/dbs';
|
||||
}
|
||||
if (resourceType == TagResourceTypeEnum.Redis.value) {
|
||||
toPath = '/redis/manage';
|
||||
}
|
||||
if (resourceType == TagResourceTypeEnum.Mongo.value) {
|
||||
toPath = '/mongo/mongo-manage';
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: toPath,
|
||||
query: {
|
||||
tagPath,
|
||||
},
|
||||
});
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const saveTag = async () => {
|
||||
tagForm.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
@@ -300,3 +389,4 @@ const removeDeafultExpandId = (id: any) => {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@/components/contextmenu
|
||||
|
||||
@@ -157,7 +157,7 @@ const state = reactive({
|
||||
},
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
queryConfig: [TableQuery.text('name', '团队名称')],
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const tagApi = {
|
||||
getAccountTags: Api.newGet('/tag-trees/account-has'),
|
||||
listByQuery: Api.newGet('/tag-trees/query'),
|
||||
getTagTrees: Api.newGet('/tag-trees'),
|
||||
saveTagTree: Api.newPost('/tag-trees'),
|
||||
delTagTree: Api.newDelete('/tag-trees/{id}'),
|
||||
|
||||
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
|
||||
getTagResources: Api.newGet('/tag-trees/resources'),
|
||||
|
||||
getTeams: Api.newGet('/teams'),
|
||||
saveTeam: Api.newPost('/teams'),
|
||||
delTeam: Api.newDelete('/teams/{id}'),
|
||||
|
||||
@@ -179,7 +179,7 @@ import { dateFormat } from '@/common/utils/date';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import config from '@/common/config';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
|
||||
const { userInfo } = storeToRefs(useUserInfo());
|
||||
const state = reactive({
|
||||
@@ -248,7 +248,7 @@ const bindOAuth2 = () => {
|
||||
var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置;
|
||||
// 小窗口打开oauth2鉴权
|
||||
let oauthWindow = window.open(
|
||||
config.baseApiUrl + '/auth/oauth2/bind?token=' + getToken(),
|
||||
`${config.baseApiUrl}/auth/oauth2/bind?${joinClientParams()}`,
|
||||
'oauth2',
|
||||
`height=${height},width=${width},top=${iTop},left=${iLeft},location=no`
|
||||
);
|
||||
|
||||
@@ -129,7 +129,7 @@ const state = reactive({
|
||||
query: {
|
||||
username: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
@@ -252,7 +252,9 @@ const deleteAccount = async () => {
|
||||
await accountApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -99,7 +99,7 @@ const paramsFormRef: any = ref(null);
|
||||
const state = reactive({
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
total: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="menu">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<span style="font-size: 14px"> <SvgIcon name="info-filled" />红色、橙色字体表示禁用状态 </span>
|
||||
<span style="font-size: 14px"> <SvgIcon name="info-filled" />红色、橙色字体表示禁用状态 (右击资源进行操作) </span>
|
||||
</div>
|
||||
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="addResource(false)">添加</el-button>
|
||||
</div>
|
||||
@@ -14,8 +14,10 @@
|
||||
:data="data"
|
||||
@node-expand="handleNodeExpand"
|
||||
@node-collapse="handleNodeCollapse"
|
||||
@node-contextmenu="nodeContextmenu"
|
||||
@node-click="treeNodeClick"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
:expand-on-click-node="false"
|
||||
:expand-on-click-node="true"
|
||||
draggable
|
||||
:allow-drop="allowDrop"
|
||||
@node-drop="handleDrop"
|
||||
@@ -34,43 +36,6 @@
|
||||
<span :style="data.status == 1 ? 'color: #67c23a;' : 'color: #f67c6c;'">{{ data.name }}</span>
|
||||
<span style="color: #3c8dbc">】</span>
|
||||
</span>
|
||||
|
||||
<el-link @click.prevent="info(data)" style="margin-left: 25px" icon="view" type="info" :underline="false" />
|
||||
|
||||
<el-link v-auth="perms.updateResource" @click.prevent="editResource(data)" class="ml5" type="primary" icon="edit" :underline="false" />
|
||||
|
||||
<el-link
|
||||
v-auth="perms.addResource"
|
||||
@click.prevent="addResource(data)"
|
||||
v-if="data.type === menuTypeValue"
|
||||
icon="circle-plus"
|
||||
:underline="false"
|
||||
type="success"
|
||||
class="ml5"
|
||||
/>
|
||||
|
||||
<el-link
|
||||
v-auth="perms.changeStatus"
|
||||
@click.prevent="changeStatus(data, -1)"
|
||||
v-if="data.status === 1"
|
||||
icon="circle-close"
|
||||
:underline="false"
|
||||
type="warning"
|
||||
class="ml5"
|
||||
/>
|
||||
|
||||
<el-link
|
||||
v-auth="perms.changeStatus"
|
||||
@click.prevent="changeStatus(data, 1)"
|
||||
v-if="data.status === -1"
|
||||
type="success"
|
||||
icon="circle-check"
|
||||
:underline="false"
|
||||
plain
|
||||
class="ml5"
|
||||
/>
|
||||
|
||||
<el-link v-auth="perms.delResource" @click.prevent="deleteMenu(data)" type="danger" icon="delete" :underline="false" plain class="ml5" />
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
@@ -123,17 +88,20 @@
|
||||
<el-descriptions-item label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, onMounted } from 'vue';
|
||||
import { ref, toRefs, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import ResourceEdit from './ResourceEdit.vue';
|
||||
import { ResourceTypeEnum } from '../enums';
|
||||
import { resourceApi } from '../api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
|
||||
const menuTypeValue = ResourceTypeEnum.Menu.value;
|
||||
const permissionTypeValue = ResourceTypeEnum.Permission.value;
|
||||
@@ -150,7 +118,46 @@ const props = {
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
const contextmenuRef = ref();
|
||||
|
||||
const contextmenuInfo = new ContextmenuItem('info', '详情').withIcon('View').withOnClick((data: any) => info(data));
|
||||
|
||||
const contextmenuAdd = new ContextmenuItem('add', '添加子资源')
|
||||
.withIcon('circle-plus')
|
||||
.withPermission(perms.addResource)
|
||||
.withHideFunc((data: any) => data.type !== menuTypeValue)
|
||||
.withOnClick((data: any) => addResource(data));
|
||||
|
||||
const contextmenuEdit = new ContextmenuItem('edit', '编辑')
|
||||
.withIcon('edit')
|
||||
.withPermission(perms.updateResource)
|
||||
.withOnClick((data: any) => editResource(data));
|
||||
|
||||
const contextmenuEnable = new ContextmenuItem('enable', '启用')
|
||||
.withIcon('circle-check')
|
||||
.withPermission(perms.updateResource)
|
||||
.withHideFunc((data: any) => data.status === 1)
|
||||
.withOnClick((data: any) => changeStatus(data, 1));
|
||||
|
||||
const contextmenuDisable = new ContextmenuItem('disable', '禁用')
|
||||
.withIcon('circle-close')
|
||||
.withPermission(perms.updateResource)
|
||||
.withHideFunc((data: any) => data.status === -1)
|
||||
.withOnClick((data: any) => changeStatus(data, -1));
|
||||
|
||||
const contextmenuDel = new ContextmenuItem('delete', '删除')
|
||||
.withIcon('delete')
|
||||
.withPermission(perms.delResource)
|
||||
.withOnClick((data: any) => deleteMenu(data));
|
||||
|
||||
const state = reactive({
|
||||
contextmenu: {
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
items: [contextmenuInfo, contextmenuAdd, contextmenuEdit, contextmenuEnable, contextmenuDisable, contextmenuDel],
|
||||
},
|
||||
//弹出框对象
|
||||
dialogForm: {
|
||||
type: null,
|
||||
@@ -193,6 +200,19 @@ const search = async () => {
|
||||
state.data = res;
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
contextmenuRef.value.openContextmenu(data);
|
||||
};
|
||||
|
||||
const treeNodeClick = () => {
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
const deleteMenu = (data: any) => {
|
||||
ElMessageBox.confirm(`此操作将删除 [${data.name}], 是否继续?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -378,3 +398,4 @@ const info = async (data: any) => {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@/components/contextmenu
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
<el-form-item prop="code" label="角色code" required>
|
||||
<el-input :disabled="form.id != null" v-model="form.code" placeholder="COMMON开头则为所有账号共有角色" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态" required>
|
||||
<el-select v-model="form.status" placeholder="请选择状态" class="w100">
|
||||
<el-option v-for="item in RoleStatusEnum" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色描述">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入角色描述"></el-input>
|
||||
</el-form-item>
|
||||
@@ -25,6 +30,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, watch } from 'vue';
|
||||
import { roleApi } from '../api';
|
||||
import { RoleStatusEnum } from '../enums';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
|
||||
@@ -20,12 +20,9 @@
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #showmore="{ data }">
|
||||
<el-link @click.prevent="showResources(data)" type="info">菜单&权限</el-link>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-if="actionBtns[perms.updateRole]" @click="editRole(data)" type="primary" link>编辑</el-button>
|
||||
<el-button @click="showResources(data)" type="info" link>权限详情</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveRoleResource]" @click="editResource(data)" type="success" link>权限分配</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
@@ -73,16 +70,15 @@ const columns = ref([
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('modifier', '更新账号'),
|
||||
TableColumn.new('updateTime', '更新时间').isTime(),
|
||||
TableColumn.new('showmore', '查看更多').isSlot().setMinWidth(150),
|
||||
]);
|
||||
|
||||
const actionBtns = hasPerms([perms.updateRole, perms.saveRoleResource]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight().alignCenter();
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(260).fixedRight().alignCenter();
|
||||
|
||||
const state = reactive({
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
total: 0,
|
||||
@@ -157,7 +153,9 @@ const deleteRole = async (data: any) => {
|
||||
});
|
||||
ElMessage.success('删除成功!');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const showResources = async (row: any) => {
|
||||
|
||||
@@ -6,6 +6,25 @@
|
||||
<span class="custom-tree-node">
|
||||
<span v-if="data.type == ResourceTypeEnum.Menu.value">{{ node.label }}</span>
|
||||
<span v-if="data.type == ResourceTypeEnum.Permission.value" style="color: #67c23a">{{ node.label }}</span>
|
||||
|
||||
<el-popover :show-after="500" placement="right-start" title="资源分配信息" trigger="hover" :width="200">
|
||||
<template #reference>
|
||||
<el-link style="margin-left: 25px" icon="InfoFilled" type="info" :underline="false" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="资源名称">
|
||||
{{ data.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分配账号">
|
||||
{{ data.creator }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分配时间">
|
||||
{{ dateFormat(data.createTime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
@@ -14,9 +33,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, toRefs, reactive, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { toRefs, reactive, watch } from 'vue';
|
||||
import { ResourceTypeEnum } from '../enums';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -33,8 +52,6 @@ const props = defineProps({
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'update:resources']);
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
@@ -52,26 +69,6 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const info = (info: any) => {
|
||||
ElMessageBox.alert(
|
||||
'<strong style="margin-right: 18px">资源名称:</strong>' +
|
||||
info.name +
|
||||
' <br/><strong style="margin-right: 18px">分配账号:</strong>' +
|
||||
info.creator +
|
||||
' <br/><strong style="margin-right: 18px">分配时间:</strong>' +
|
||||
proxy.$filters.dateFormat(info.createTime) +
|
||||
'',
|
||||
'分配信息',
|
||||
{
|
||||
type: 'info',
|
||||
dangerouslyUseHTMLString: true,
|
||||
closeOnClickModal: true,
|
||||
showConfirmButton: false,
|
||||
}
|
||||
).catch(() => {});
|
||||
return;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
emit('update:visible', false);
|
||||
emit('update:resources', []);
|
||||
|
||||
@@ -43,7 +43,7 @@ const state = reactive({
|
||||
creatorId: null,
|
||||
description: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
queryConfig: [
|
||||
TableQuery.slot('creatorId', '操作人', 'selectAccount'),
|
||||
|
||||
@@ -42,9 +42,9 @@ const viteConfig: UserConfig = {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[hash].[name].js`,
|
||||
chunkFileNames: `assets/[hash].[name].js`,
|
||||
assetFileNames: `assets/[name].[hash].[ext]`,
|
||||
entryFileNames: `assets/[name]-[hash].js`,
|
||||
chunkFileNames: `assets/[name]-[hash].js`,
|
||||
assetFileNames: `assets/[name]-[hash].[ext]`,
|
||||
compact: true,
|
||||
manualChunks: {
|
||||
vue: ['vue', 'vue-router', 'pinia'],
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
|
||||
integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==
|
||||
|
||||
"@babel/parser@^7.23.5":
|
||||
version "7.23.5"
|
||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.5.tgz#37dee97c4752af148e1d38c34b856b2507660563"
|
||||
integrity sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==
|
||||
|
||||
"@babel/runtime@^7.21.0":
|
||||
version "7.21.5"
|
||||
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
|
||||
@@ -19,125 +24,125 @@
|
||||
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz"
|
||||
integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
|
||||
|
||||
"@element-plus/icons-vue@^2.0.6":
|
||||
version "2.0.9"
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.0.9.tgz"
|
||||
integrity sha512-okdrwiVeKBmW41Hkl0eMrXDjzJwhQMuKiBOu17rOszqM+LS/yBYpNQNV5Jvoh06Wc+89fMmb/uhzf8NZuDuUaQ==
|
||||
|
||||
"@element-plus/icons-vue@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz#7ad90d08a8c0d5fd3af31c4f73264ca89614397a"
|
||||
integrity sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==
|
||||
|
||||
"@esbuild/android-arm64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.11.tgz#fa6f0cc7105367cb79cc0a8bf32bf50cb1673e45"
|
||||
integrity sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==
|
||||
"@element-plus/icons-vue@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz#1f635ad5fdd5c85ed936481525570e82b5a8307a"
|
||||
integrity sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==
|
||||
|
||||
"@esbuild/android-arm@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.11.tgz#ae84a410696c9f549a15be94eaececb860bacacb"
|
||||
integrity sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==
|
||||
"@esbuild/android-arm64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz#fb7130103835b6d43ea499c3f30cfb2b2ed58456"
|
||||
integrity sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==
|
||||
|
||||
"@esbuild/android-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.11.tgz#0e58360bbc789ad0d68174d32ba20e678c2a16b6"
|
||||
integrity sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==
|
||||
"@esbuild/android-arm@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.19.8.tgz#b46e4d9e984e6d6db6c4224d72c86b7757e35bcb"
|
||||
integrity sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==
|
||||
|
||||
"@esbuild/darwin-arm64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz#fcdcd2ef76ca656540208afdd84f284072f0d1f9"
|
||||
integrity sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==
|
||||
"@esbuild/android-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.19.8.tgz#a13db9441b5a4f4e4fec4a6f8ffacfea07888db7"
|
||||
integrity sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==
|
||||
|
||||
"@esbuild/darwin-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz#c5ac602ec0504a8ff81e876bc8a9811e94d69d37"
|
||||
integrity sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==
|
||||
"@esbuild/darwin-arm64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz#49f5718d36541f40dd62bfdf84da9c65168a0fc2"
|
||||
integrity sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.11.tgz#7012fb06ee3e6e0d5560664a65f3fefbcc46db2e"
|
||||
integrity sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==
|
||||
"@esbuild/darwin-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz#75c5c88371eea4bfc1f9ecfd0e75104c74a481ac"
|
||||
integrity sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==
|
||||
|
||||
"@esbuild/freebsd-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.11.tgz#c5de1199f70e1f97d5c8fca51afa9bf9a2af5969"
|
||||
integrity sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==
|
||||
"@esbuild/freebsd-arm64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz#9d7259fea4fd2b5f7437b52b542816e89d7c8575"
|
||||
integrity sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==
|
||||
|
||||
"@esbuild/linux-arm64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.11.tgz#2a6d3a74e0b8b5f294e22b4515b29f76ebd42660"
|
||||
integrity sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==
|
||||
"@esbuild/freebsd-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz#abac03e1c4c7c75ee8add6d76ec592f46dbb39e3"
|
||||
integrity sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==
|
||||
|
||||
"@esbuild/linux-arm@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.11.tgz#5175bd61b793b436e4aece6328aa0d9be07751e1"
|
||||
integrity sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==
|
||||
"@esbuild/linux-arm64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz#c577932cf4feeaa43cb9cec27b89cbe0df7d9098"
|
||||
integrity sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==
|
||||
|
||||
"@esbuild/linux-ia32@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.11.tgz#20ee6cfd65a398875f321a485e7b2278e5f6f67b"
|
||||
integrity sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==
|
||||
"@esbuild/linux-arm@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz#d6014d8b98b5cbc96b95dad3d14d75bb364fdc0f"
|
||||
integrity sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==
|
||||
|
||||
"@esbuild/linux-loong64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.11.tgz#8e7b251dede75083bf44508dab5edce3f49d052b"
|
||||
integrity sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==
|
||||
"@esbuild/linux-ia32@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz#2379a0554307d19ac4a6cdc15b08f0ea28e7a40d"
|
||||
integrity sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==
|
||||
|
||||
"@esbuild/linux-mips64el@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.11.tgz#a3125eb48538ac4932a9d05089b157f94e443165"
|
||||
integrity sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==
|
||||
"@esbuild/linux-loong64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz#e2a5bbffe15748b49356a6cd7b2d5bf60c5a7123"
|
||||
integrity sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==
|
||||
|
||||
"@esbuild/linux-ppc64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.11.tgz#842abadb7a0995bd539adee2be4d681b68279499"
|
||||
integrity sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==
|
||||
"@esbuild/linux-mips64el@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz#1359331e6f6214f26f4b08db9b9df661c57cfa24"
|
||||
integrity sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==
|
||||
|
||||
"@esbuild/linux-riscv64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.11.tgz#7ce6e6cee1c72d5b4d2f4f8b6fcccf4a9bea0e28"
|
||||
integrity sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==
|
||||
"@esbuild/linux-ppc64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz#9ba436addc1646dc89dae48c62d3e951ffe70951"
|
||||
integrity sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==
|
||||
|
||||
"@esbuild/linux-s390x@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.11.tgz#98fbc794363d02ded07d300df2e535650b297b96"
|
||||
integrity sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==
|
||||
"@esbuild/linux-riscv64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz#fbcf0c3a0b20f40b5fc31c3b7695f0769f9de66b"
|
||||
integrity sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==
|
||||
|
||||
"@esbuild/linux-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.11.tgz#f8458ec8cf74c8274e4cacd00744d8446cac52eb"
|
||||
integrity sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==
|
||||
"@esbuild/linux-s390x@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz#989e8a05f7792d139d5564ffa7ff898ac6f20a4a"
|
||||
integrity sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==
|
||||
|
||||
"@esbuild/netbsd-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.11.tgz#a7b2f991b8293748a7be42eac1c4325faf0c7cca"
|
||||
integrity sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==
|
||||
"@esbuild/linux-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz#b187295393a59323397fe5ff51e769ec4e72212b"
|
||||
integrity sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.11.tgz#3e50923de84c54008f834221130fd23646072b2f"
|
||||
integrity sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==
|
||||
"@esbuild/netbsd-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz#c1ec0e24ea82313cb1c7bae176bd5acd5bde7137"
|
||||
integrity sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==
|
||||
|
||||
"@esbuild/sunos-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.11.tgz#ae47a550b0cd395de03606ecfba03cc96c7c19e2"
|
||||
integrity sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==
|
||||
"@esbuild/openbsd-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz#0c5b696ac66c6d70cf9ee17073a581a28af9e18d"
|
||||
integrity sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==
|
||||
|
||||
"@esbuild/win32-arm64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.11.tgz#05d364582b7862d7fbf4698ef43644f7346dcfcc"
|
||||
integrity sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==
|
||||
"@esbuild/sunos-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz#2a697e1f77926ff09fcc457d8f29916d6cd48fb1"
|
||||
integrity sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==
|
||||
|
||||
"@esbuild/win32-ia32@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.11.tgz#a3372095a4a1939da672156a3c104f8ce85ee616"
|
||||
integrity sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==
|
||||
"@esbuild/win32-arm64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz#ec029e62a2fca8c071842ecb1bc5c2dd20b066f1"
|
||||
integrity sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==
|
||||
|
||||
"@esbuild/win32-x64@0.18.11":
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz#6526c7e1b40d5b9f0a222c6b767c22f6fb97aa57"
|
||||
integrity sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==
|
||||
"@esbuild/win32-ia32@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz#cbb9a3146bde64dc15543e48afe418c7a3214851"
|
||||
integrity sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==
|
||||
|
||||
"@esbuild/win32-x64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz#c8285183dbdb17008578dbacb6e22748709b4822"
|
||||
integrity sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.4.0"
|
||||
@@ -202,7 +207,7 @@
|
||||
resolved "https://registry.npmmirror.com/@humanwhocodes/object-schema/download/@humanwhocodes/object-schema-1.2.1.tgz"
|
||||
integrity sha1-tSBSnsIdjllFoYUd/Rwy6U45/0U=
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.13":
|
||||
"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.15":
|
||||
version "1.4.15"
|
||||
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||
@@ -233,6 +238,66 @@
|
||||
resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz"
|
||||
integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.0.tgz#c08a454d70605aacad17530a953791ea385e37d5"
|
||||
integrity sha512-keHkkWAe7OtdALGoutLY3utvthkGF+Y17ws9LYT8pxMBYXaCoH/8dXS2uzo6e8+sEhY7y/zi5RFo22Dy2lFpDw==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.0.tgz#e0cf96960405947c1a09a389467e6aa10ae1a226"
|
||||
integrity sha512-y3Kt+34smKQNWilicPbBz/MXEY7QwDzMFNgwEWeYiOhUt9MTWKjHqe3EVkXwT2fR7izOvHpDWZ0o2IyD9SWX7A==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.0.tgz#6d2f53021fbb9fdecf60bfb6fd5d999aef8385e9"
|
||||
integrity sha512-oLzzxcUIHltHxOCmaXl+pkIlU+uhSxef5HfntW7RsLh1eHm+vJzjD9Oo4oUKso4YuP4PpbFJNlZjJuOrxo8dPg==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.0.tgz#b7d0a4bbe6fc493efa269a60a66dc070ac10e2bd"
|
||||
integrity sha512-+ANnmjkcOBaV25n0+M0Bere3roeVAnwlKW65qagtuAfIxXF9YxUneRyAn/RDcIdRa7QrjRNJL3jR7T43ObGe8Q==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.0.tgz#12fad1802f500a0196ab0bb4dbb776aaabdedcc7"
|
||||
integrity sha512-tBTSIkjSVUyrekddpkAqKOosnj1Fc0ZY0rJL2bIEWPKqlEQk0paORL9pUIlt7lcGJi3LzMIlUGXvtNi1Z6MOCQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.0.tgz#6de1caa2c9952d16dafa21dd26da9562d4ea2112"
|
||||
integrity sha512-Ed8uJI3kM11de9S0j67wAV07JUNhbAqIrDYhQBrQW42jGopgheyk/cdcshgGO4fW5Wjq97COCY/BHogdGvKVNQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.0.tgz#ef9cae3d22c8c44ff4f271e308bf1c013348bdc0"
|
||||
integrity sha512-mZoNQ/qK4D7SSY8v6kEsAAyDgznzLLuSFCA3aBHZTmf3HP/dW4tNLTtWh9+LfyO0Z1aUn+ecpT7IQ3WtIg3ViQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.0.tgz#e9071050bed7c64a9fd964cde3c8bd139bf8e489"
|
||||
integrity sha512-rouezFHpwCqdEXsqAfNsTgSWO0FoZ5hKv5p+TGO5KFhyN/dvYXNMqMolOb8BkyKcPqjYRBeT+Z6V3aM26rPaYg==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.0.tgz#a4c7f5e0c363b2c34f6a7566b1c9da00bf0b96d0"
|
||||
integrity sha512-Bbm+fyn3S6u51urfj3YnqBXg5vI2jQPncRRELaucmhBVyZkbWClQ1fEsRmdnCPpQOQfkpg9gZArvtMVkOMsh1w==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.0.tgz#9a7bfc660ac088d447858fc5223984deb979a55a"
|
||||
integrity sha512-+MRMcyx9L2kTrTUzYmR61+XVsliMG4odFb5UmqtiT8xOfEicfYAGEuF/D1Pww1+uZkYhBqAHpvju7VN+GnC3ng==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.0.tgz#7d5fb96e9f0120451da1fece5c74d2bb373f8925"
|
||||
integrity sha512-rxfeE6K6s/Xl2HGeK6cO8SiQq3k/3BYpw7cfhW5Bk2euXNEpuzi2cc7llxx1si1QgwfjNtdRNTGqdBzGlFZGFw==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.0.tgz#15841505c7ec1648020941d04ca0210f88c59e3a"
|
||||
integrity sha512-QqmCsydHS172Y0Kc13bkMXvipbJSvzeglBncJG3LsYJSiPlxYACz7MmJBs4A8l1oU+jfhYEIC/+AUSlvjmiX/g==
|
||||
|
||||
"@types/antlr4@4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.npmmirror.com/@types/antlr4/-/antlr4-4.7.0.tgz"
|
||||
@@ -375,6 +440,16 @@
|
||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz#8ae96573236cdb12de6850a6d929b5537ec85390"
|
||||
integrity sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==
|
||||
|
||||
"@vue/compiler-core@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.10.tgz#9ca4123a1458df43db641aaa8b7d1e636aa22545"
|
||||
integrity sha512-doe0hODR1+i1menPkRzJ5MNR6G+9uiZHIknK3Zn5OcIztu6GGw7u0XUzf3AgB8h/dfsZC9eouzoLo3c3+N/cVA==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.5"
|
||||
"@vue/shared" "3.3.10"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-core@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128"
|
||||
@@ -385,6 +460,14 @@
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-dom@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz#183811252be6aff4ac923f783124bb1590301907"
|
||||
integrity sha512-NCrqF5fm10GXZIK0GrEAauBqdy+F2LZRt3yNHzrYjpYBuRssQbuPLtSnSNjyR9luHKkWSH8we5LMB3g+4z2HvA==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/compiler-dom@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz#f56e09b5f4d7dc350f981784de9713d823341151"
|
||||
@@ -393,7 +476,23 @@
|
||||
"@vue/compiler-core" "3.3.4"
|
||||
"@vue/shared" "3.3.4"
|
||||
|
||||
"@vue/compiler-sfc@3.3.4", "@vue/compiler-sfc@^3.3.4":
|
||||
"@vue/compiler-sfc@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz#8eb97d42f276089ec58fd0565ef3a813bceeaa87"
|
||||
integrity sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.5"
|
||||
"@vue/compiler-core" "3.3.10"
|
||||
"@vue/compiler-dom" "3.3.10"
|
||||
"@vue/compiler-ssr" "3.3.10"
|
||||
"@vue/reactivity-transform" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.5"
|
||||
postcss "^8.4.32"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-sfc@^3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz#b19d942c71938893535b46226d602720593001df"
|
||||
integrity sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==
|
||||
@@ -409,6 +508,14 @@
|
||||
postcss "^8.1.10"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-ssr@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.10.tgz#5a1b14a358cb3960a4edbce0ade90548e452fcaa"
|
||||
integrity sha512-12iM4jA4GEbskwXMmPcskK5wImc2ohKm408+o9iox3tfN9qua8xL0THIZtoe9OJHnXP4eOWZpgCAAThEveNlqQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/compiler-ssr@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777"
|
||||
@@ -422,6 +529,17 @@
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
|
||||
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
|
||||
|
||||
"@vue/reactivity-transform@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.10.tgz#b045776cc954bb57883fd305db7a200d42993768"
|
||||
integrity sha512-0xBdk+CKHWT+Gev8oZ63Tc0qFfj935YZx+UAynlutnrDZ4diFCVFMWixn65HzjE3S1iJppWOo6Tt1OzASH7VEg==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.5"
|
||||
"@vue/compiler-core" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.5"
|
||||
|
||||
"@vue/reactivity-transform@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
|
||||
@@ -433,37 +551,42 @@
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.0"
|
||||
|
||||
"@vue/reactivity@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.4.tgz#a27a29c6cd17faba5a0e99fbb86ee951653e2253"
|
||||
integrity sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==
|
||||
"@vue/reactivity@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.10.tgz#78fe3da319276d9e6d0f072037532928c472a287"
|
||||
integrity sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==
|
||||
dependencies:
|
||||
"@vue/shared" "3.3.4"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/runtime-core@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.4.tgz#4bb33872bbb583721b340f3088888394195967d1"
|
||||
integrity sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==
|
||||
"@vue/runtime-core@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.10.tgz#d7b78c5c0500b856cf9447ef81d4a1b1438fd5bb"
|
||||
integrity sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.3.4"
|
||||
"@vue/shared" "3.3.4"
|
||||
"@vue/reactivity" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/runtime-dom@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz#992f2579d0ed6ce961f47bbe9bfe4b6791251566"
|
||||
integrity sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==
|
||||
"@vue/runtime-dom@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz#130dfffb8fee8051671aaf80c5104d2020544950"
|
||||
integrity sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==
|
||||
dependencies:
|
||||
"@vue/runtime-core" "3.3.4"
|
||||
"@vue/shared" "3.3.4"
|
||||
csstype "^3.1.1"
|
||||
"@vue/runtime-core" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
csstype "^3.1.2"
|
||||
|
||||
"@vue/server-renderer@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.4.tgz#ea46594b795d1536f29bc592dd0f6655f7ea4c4c"
|
||||
integrity sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==
|
||||
"@vue/server-renderer@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.10.tgz#f23d151f0e5021ebdc730052d9934c9178486742"
|
||||
integrity sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.3.4"
|
||||
"@vue/shared" "3.3.4"
|
||||
"@vue/compiler-ssr" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/shared@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.10.tgz#1583a8d85a957d8b819078c465d2a11db7914b2f"
|
||||
integrity sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==
|
||||
|
||||
"@vue/shared@3.3.4":
|
||||
version "3.3.4"
|
||||
@@ -492,16 +615,11 @@
|
||||
dependencies:
|
||||
vue-demi "*"
|
||||
|
||||
acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn@^8.7.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.npmmirror.com/acorn/download/acorn-8.7.0.tgz"
|
||||
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
|
||||
|
||||
acorn@^8.8.0:
|
||||
version "8.8.2"
|
||||
resolved "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||
@@ -570,10 +688,10 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267"
|
||||
integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==
|
||||
axios@^1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2"
|
||||
integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
@@ -589,6 +707,11 @@ binary-extensions@^2.0.0:
|
||||
resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.nlark.com/brace-expansion/download/brace-expansion-1.1.11.tgz"
|
||||
@@ -632,10 +755,10 @@ chalk@^4.0.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
clipboard@^2.0.6:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.10.tgz"
|
||||
integrity sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==
|
||||
clipboard@^2.0.11:
|
||||
version "2.0.11"
|
||||
resolved "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5"
|
||||
integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==
|
||||
dependencies:
|
||||
good-listener "^1.2.2"
|
||||
select "^1.1.2"
|
||||
@@ -689,12 +812,17 @@ cross-spawn@^7.0.2:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
csstype@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.1.tgz"
|
||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||
|
||||
csstype@^3.1.1:
|
||||
csstype@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
@@ -765,21 +893,21 @@ dt-sql-parser@^4.0.0-beta.3.2:
|
||||
"@types/antlr4" "4.7.0"
|
||||
antlr4 "4.7.2"
|
||||
|
||||
echarts@^5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.npmmirror.com/echarts/-/echarts-5.4.0.tgz#a9a8e5367293a397408d3bf3e2638b869249ce04"
|
||||
integrity sha512-uPsO9VRUIKAdFOoH3B0aNg7NRVdN7aM39/OjovjO9MwmWsAkfGyeXJhK+dbRi51iDrQWliXV60/XwLA7kg3z0w==
|
||||
echarts@^5.4.3:
|
||||
version "5.4.3"
|
||||
resolved "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz#f5522ef24419164903eedcfd2b506c6fc91fb20c"
|
||||
integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==
|
||||
dependencies:
|
||||
tslib "2.3.0"
|
||||
zrender "5.4.0"
|
||||
zrender "5.4.4"
|
||||
|
||||
element-plus@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.4.0.tgz#e79249ac4c0a606d377c2f31ad553aa992286fe3"
|
||||
integrity sha512-yJEa8LXkGOOgkfkeqMMEdeX/Dc8EH9qPcRuX91dlhSXxgCKKbp9tH3QFTOG99ibZsrN/Em62nh7ddvbc7I1frw==
|
||||
element-plus@^2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.4.3.tgz#ff21d0207d71752eb6a47a46609bc667f222841f"
|
||||
integrity sha512-b3q26j+lM4SBqiyzw8HybybGnP2pk4MWgrnzzzYW5qKQUgV6EG1Zg7nMCfgCVccI8tNvZoTiUHb2mFaiB9qT8w==
|
||||
dependencies:
|
||||
"@ctrl/tinycolor" "^3.4.1"
|
||||
"@element-plus/icons-vue" "^2.0.6"
|
||||
"@element-plus/icons-vue" "^2.3.1"
|
||||
"@floating-ui/dom" "^1.0.1"
|
||||
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
|
||||
"@types/lodash" "^4.14.182"
|
||||
@@ -794,33 +922,33 @@ element-plus@^2.4.0:
|
||||
memoize-one "^6.0.0"
|
||||
normalize-wheel-es "^1.2.0"
|
||||
|
||||
esbuild@^0.18.10:
|
||||
version "0.18.11"
|
||||
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.11.tgz#cbf94dc3359d57f600a0dbf281df9b1d1b4a156e"
|
||||
integrity sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==
|
||||
esbuild@^0.19.3:
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.19.8.tgz#ad05b72281d84483fa6b5345bd246c27a207b8f1"
|
||||
integrity sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==
|
||||
optionalDependencies:
|
||||
"@esbuild/android-arm" "0.18.11"
|
||||
"@esbuild/android-arm64" "0.18.11"
|
||||
"@esbuild/android-x64" "0.18.11"
|
||||
"@esbuild/darwin-arm64" "0.18.11"
|
||||
"@esbuild/darwin-x64" "0.18.11"
|
||||
"@esbuild/freebsd-arm64" "0.18.11"
|
||||
"@esbuild/freebsd-x64" "0.18.11"
|
||||
"@esbuild/linux-arm" "0.18.11"
|
||||
"@esbuild/linux-arm64" "0.18.11"
|
||||
"@esbuild/linux-ia32" "0.18.11"
|
||||
"@esbuild/linux-loong64" "0.18.11"
|
||||
"@esbuild/linux-mips64el" "0.18.11"
|
||||
"@esbuild/linux-ppc64" "0.18.11"
|
||||
"@esbuild/linux-riscv64" "0.18.11"
|
||||
"@esbuild/linux-s390x" "0.18.11"
|
||||
"@esbuild/linux-x64" "0.18.11"
|
||||
"@esbuild/netbsd-x64" "0.18.11"
|
||||
"@esbuild/openbsd-x64" "0.18.11"
|
||||
"@esbuild/sunos-x64" "0.18.11"
|
||||
"@esbuild/win32-arm64" "0.18.11"
|
||||
"@esbuild/win32-ia32" "0.18.11"
|
||||
"@esbuild/win32-x64" "0.18.11"
|
||||
"@esbuild/android-arm" "0.19.8"
|
||||
"@esbuild/android-arm64" "0.19.8"
|
||||
"@esbuild/android-x64" "0.19.8"
|
||||
"@esbuild/darwin-arm64" "0.19.8"
|
||||
"@esbuild/darwin-x64" "0.19.8"
|
||||
"@esbuild/freebsd-arm64" "0.19.8"
|
||||
"@esbuild/freebsd-x64" "0.19.8"
|
||||
"@esbuild/linux-arm" "0.19.8"
|
||||
"@esbuild/linux-arm64" "0.19.8"
|
||||
"@esbuild/linux-ia32" "0.19.8"
|
||||
"@esbuild/linux-loong64" "0.19.8"
|
||||
"@esbuild/linux-mips64el" "0.19.8"
|
||||
"@esbuild/linux-ppc64" "0.19.8"
|
||||
"@esbuild/linux-riscv64" "0.19.8"
|
||||
"@esbuild/linux-s390x" "0.19.8"
|
||||
"@esbuild/linux-x64" "0.19.8"
|
||||
"@esbuild/netbsd-x64" "0.19.8"
|
||||
"@esbuild/openbsd-x64" "0.19.8"
|
||||
"@esbuild/sunos-x64" "0.19.8"
|
||||
"@esbuild/win32-arm64" "0.19.8"
|
||||
"@esbuild/win32-ia32" "0.19.8"
|
||||
"@esbuild/win32-x64" "0.19.8"
|
||||
|
||||
escape-html@^1.0.3:
|
||||
version "1.0.3"
|
||||
@@ -832,23 +960,18 @@ escape-string-regexp@^4.0.0:
|
||||
resolved "https://registry.nlark.com/escape-string-regexp/download/escape-string-regexp-4.0.0.tgz"
|
||||
integrity sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=
|
||||
|
||||
eslint-plugin-vue@^8.2.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.npmmirror.com/eslint-plugin-vue/download/eslint-plugin-vue-8.3.0.tgz"
|
||||
integrity sha512-IIuLHw4vQxGlHcoP2dG6t/2OVdQf2qoyAzEGAxreU1afZOHGA7y3TWq8I+r3ZA6Wjs6xpeUWGHlT31QGr9Rb5g==
|
||||
eslint-plugin-vue@^9.17.0:
|
||||
version "9.17.0"
|
||||
resolved "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz#4501547373f246547083482838b4c8f4b28e5932"
|
||||
integrity sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==
|
||||
dependencies:
|
||||
eslint-utils "^3.0.0"
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
natural-compare "^1.4.0"
|
||||
semver "^7.3.5"
|
||||
vue-eslint-parser "^8.0.1"
|
||||
|
||||
eslint-scope@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmmirror.com/eslint-scope/download/eslint-scope-6.0.0.tgz?cache=0&sync_timestamp=1637466831846&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-scope%2Fdownload%2Feslint-scope-6.0.0.tgz"
|
||||
integrity sha1-nPRbE8Wsjz1MUPRqUSH2Gz4xiXg=
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
nth-check "^2.1.1"
|
||||
postcss-selector-parser "^6.0.13"
|
||||
semver "^7.5.4"
|
||||
vue-eslint-parser "^9.3.1"
|
||||
xml-name-validator "^4.0.0"
|
||||
|
||||
eslint-scope@^7.1.1:
|
||||
version "7.1.1"
|
||||
@@ -870,11 +993,6 @@ eslint-visitor-keys@^2.0.0:
|
||||
resolved "https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-2.1.0.tgz?cache=0&sync_timestamp=1636378510206&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-2.1.0.tgz"
|
||||
integrity sha1-9lMoJZMFknOSyTjtROsKXJsr0wM=
|
||||
|
||||
eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/eslint-visitor-keys/download/eslint-visitor-keys-3.1.0.tgz?cache=0&sync_timestamp=1636378395014&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-3.1.0.tgz"
|
||||
integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==
|
||||
|
||||
eslint-visitor-keys@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
|
||||
@@ -936,15 +1054,6 @@ eslint@^8.35.0:
|
||||
strip-json-comments "^3.1.0"
|
||||
text-table "^0.2.0"
|
||||
|
||||
espree@^9.0.0:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.npmmirror.com/espree/download/espree-9.3.0.tgz"
|
||||
integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==
|
||||
dependencies:
|
||||
acorn "^8.7.0"
|
||||
acorn-jsx "^5.3.1"
|
||||
eslint-visitor-keys "^3.1.0"
|
||||
|
||||
espree@^9.3.1:
|
||||
version "9.5.1"
|
||||
resolved "https://registry.npmmirror.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4"
|
||||
@@ -1096,6 +1205,16 @@ fsevents@~2.3.2:
|
||||
resolved "https://registry.npmmirror.com/fsevents/download/fsevents-2.3.2.tgz"
|
||||
integrity sha1-ilJveLj99GI7cJ4Ll1xSwkwC/Ro=
|
||||
|
||||
fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
get-stdin@=8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmmirror.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
|
||||
integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmmirror.com/glob-parent/download/glob-parent-5.1.2.tgz"
|
||||
@@ -1245,7 +1364,7 @@ js-yaml@^4.1.0:
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsencrypt@^3.3.1:
|
||||
jsencrypt@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.3.2.tgz#b0f1a2278810c7ba1cb8957af11195354622df7c"
|
||||
integrity sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==
|
||||
@@ -1309,6 +1428,13 @@ magic-string@^0.30.0:
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.13"
|
||||
|
||||
magic-string@^0.30.5:
|
||||
version "0.30.5"
|
||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
|
||||
integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||
|
||||
memoize-one@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmmirror.com/memoize-one/download/memoize-one-6.0.0.tgz?cache=0&sync_timestamp=1634697208428&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fmemoize-one%2Fdownload%2Fmemoize-one-6.0.0.tgz"
|
||||
@@ -1392,10 +1518,10 @@ nanoid@^3.1.30:
|
||||
resolved "https://registry.npmmirror.com/nanoid/download/nanoid-3.1.30.tgz"
|
||||
integrity sha1-Y/k8xUjSoRPcXfvGO/oJ4rm2Q2I=
|
||||
|
||||
nanoid@^3.3.6:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -1427,6 +1553,13 @@ nprogress@^0.2.0:
|
||||
resolved "https://registry.npm.taobao.org/nprogress/download/nprogress-0.2.0.tgz"
|
||||
integrity sha1-y480xTIT2JVyP8urkH6UIq28r7E=
|
||||
|
||||
nth-check@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.nlark.com/once/download/once-1.4.0.tgz"
|
||||
@@ -1510,6 +1643,14 @@ pinia@^2.1.7:
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
vue-demi ">=0.14.5"
|
||||
|
||||
postcss-selector-parser@^6.0.13:
|
||||
version "6.0.13"
|
||||
resolved "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b"
|
||||
integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss@^8.1.10:
|
||||
version "8.4.5"
|
||||
resolved "https://registry.npmmirror.com/postcss/download/postcss-8.4.5.tgz"
|
||||
@@ -1519,12 +1660,12 @@ postcss@^8.1.10:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.1"
|
||||
|
||||
postcss@^8.4.27:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
postcss@^8.4.32:
|
||||
version "8.4.32"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
|
||||
integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
@@ -1533,10 +1674,10 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.2.1.tgz"
|
||||
integrity sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=
|
||||
|
||||
prettier@^2.3.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmmirror.com/prettier/download/prettier-2.5.1.tgz"
|
||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
||||
prettier@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmmirror.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
|
||||
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
@@ -1548,10 +1689,10 @@ punycode@^2.1.0:
|
||||
resolved "https://registry.nlark.com/punycode/download/punycode-2.1.1.tgz"
|
||||
integrity sha1-tYsBCsQMIsVldhbI0sLALHv0eew=
|
||||
|
||||
qrcode.vue@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmmirror.com/qrcode.vue/-/qrcode.vue-3.4.0.tgz#4513ff1a4734cb7184086c2fd439f0d462c6d281"
|
||||
integrity sha512-4XeImbv10Fin16Fl2DArCMhGyAdvIg2jb7vDT+hZiIAMg/6H6mz9nUZr/dR8jBcun5VzNzkiwKhiqOGbloinwA==
|
||||
qrcode.vue@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.npmmirror.com/qrcode.vue/-/qrcode.vue-3.4.1.tgz#dd8141da9c4ea07ee56b111cd13eadf123af822a"
|
||||
integrity sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
@@ -1610,11 +1751,23 @@ rimraf@^3.0.2:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rollup@^3.27.1:
|
||||
version "3.28.0"
|
||||
resolved "https://registry.npmmirror.com/rollup/-/rollup-3.28.0.tgz#a3c70004b01934760c0cb8df717c7a1d932389a2"
|
||||
integrity sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==
|
||||
rollup@^4.2.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.6.0.tgz#4f966f6dd3f6bafd01b864d68ba078d308b864fa"
|
||||
integrity sha512-R8i5Her4oO1LiMQ3jKf7MUglYV/mhQ5g5OKeld5CnkmPdIGo79FDDQYqPhq/PCVuTQVuxsWgIbDy9F+zdHn80w==
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.6.0"
|
||||
"@rollup/rollup-android-arm64" "4.6.0"
|
||||
"@rollup/rollup-darwin-arm64" "4.6.0"
|
||||
"@rollup/rollup-darwin-x64" "4.6.0"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.6.0"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.6.0"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.6.0"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.6.0"
|
||||
"@rollup/rollup-linux-x64-musl" "4.6.0"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.6.0"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.6.0"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.6.0"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
@@ -1643,13 +1796,6 @@ select@^1.1.2:
|
||||
resolved "https://registry.nlark.com/select/download/select-1.1.2.tgz"
|
||||
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.3.5"
|
||||
resolved "https://registry.nlark.com/semver/download/semver-7.3.5.tgz?cache=0&sync_timestamp=1618846864940&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-7.3.5.tgz"
|
||||
integrity sha1-C2Ich5NI2JmOSw5L6Us/EuYBjvc=
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.3.6:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.npmmirror.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
|
||||
@@ -1703,12 +1849,13 @@ source-map-js@^1.0.1:
|
||||
resolved "https://registry.npmmirror.com/source-map-js/download/source-map-js-1.0.1.tgz?cache=0&sync_timestamp=1636400753943&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fsource-map-js%2Fdownload%2Fsource-map-js-1.0.1.tgz"
|
||||
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
|
||||
|
||||
sql-formatter@^12.1.2:
|
||||
version "12.1.2"
|
||||
resolved "https://registry.npmmirror.com/sql-formatter/-/sql-formatter-12.1.2.tgz#6dfd042caaa468316123832751a05c77d2d1ef87"
|
||||
integrity sha512-SoFn+9ZflUt8+HYZ/PaifXt1RptcDUn8HXqsWmfXdPV3WeHPgT0qOSJXxHU24d7NOVt9X40MLqf263fNk79XqA==
|
||||
sql-formatter@^14.0.0:
|
||||
version "14.0.0"
|
||||
resolved "https://registry.npmmirror.com/sql-formatter/-/sql-formatter-14.0.0.tgz#07a1714c49d7d280ff2f6f09c64eebfba82b7053"
|
||||
integrity sha512-VcHYMRvZqg3RNjjxNB/puT9O1hR5QLXTvgTaBtxXcvmRQwSnH9M+oW2Ti+uFuVVU8HoNlOjU2uKHv8c0FQNsdQ==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
get-stdin "=8.0.0"
|
||||
nearley "^2.20.1"
|
||||
|
||||
strip-ansi@^6.0.1:
|
||||
@@ -1769,10 +1916,10 @@ type-fest@^0.20.2:
|
||||
resolved "https://registry.npmmirror.com/type-fest/download/type-fest-0.20.2.tgz"
|
||||
integrity sha1-G/IH9LKPkVg2ZstfvTJ4hzAc1fQ=
|
||||
|
||||
typescript@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.0.2.tgz#891e1a90c5189d8506af64b9ef929fca99ba1ee5"
|
||||
integrity sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==
|
||||
typescript@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43"
|
||||
integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
@@ -1781,23 +1928,26 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
vite@^4.4.11:
|
||||
version "4.4.11"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-4.4.11.tgz#babdb055b08c69cfc4c468072a2e6c9ca62102b0"
|
||||
integrity sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==
|
||||
dependencies:
|
||||
esbuild "^0.18.10"
|
||||
postcss "^8.4.27"
|
||||
rollup "^3.27.1"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
vue-clipboard3@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/vue-clipboard3/-/vue-clipboard3-1.0.1.tgz"
|
||||
integrity sha512-iJ2vrizowfA73W3pcxMAKhYSvfekJrQ3FhbveVe9esS1Vfu+xW3Fgc0UKE8N4Q6DyRtcAoNlef8txmD8tK8dIg==
|
||||
uuid@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
|
||||
vite@^5.0.5:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-5.0.5.tgz#3eebe3698e3b32cea36350f58879258fec858a3c"
|
||||
integrity sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==
|
||||
dependencies:
|
||||
clipboard "^2.0.6"
|
||||
esbuild "^0.19.3"
|
||||
postcss "^8.4.32"
|
||||
rollup "^4.2.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vue-demi@*:
|
||||
version "0.13.11"
|
||||
@@ -1809,19 +1959,6 @@ vue-demi@>=0.14.5:
|
||||
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz#676d0463d1a1266d5ab5cba932e043d8f5f2fbd9"
|
||||
integrity sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==
|
||||
|
||||
vue-eslint-parser@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmmirror.com/vue-eslint-parser/download/vue-eslint-parser-8.0.1.tgz"
|
||||
integrity sha1-JeCLIKQUVRUx8+GfmZkC4ez0XxM=
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
eslint-scope "^6.0.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
espree "^9.0.0"
|
||||
esquery "^1.4.0"
|
||||
lodash "^4.17.21"
|
||||
semver "^7.3.5"
|
||||
|
||||
vue-eslint-parser@^9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz#429955e041ae5371df5f9e37ebc29ba046496182"
|
||||
@@ -1842,16 +1979,16 @@ vue-router@^4.2.5:
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue@^3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"
|
||||
integrity sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==
|
||||
vue@^3.3.10:
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.3.10.tgz#6e19c1982ee655a14babe1610288b90005f02ab1"
|
||||
integrity sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.3.4"
|
||||
"@vue/compiler-sfc" "3.3.4"
|
||||
"@vue/runtime-dom" "3.3.4"
|
||||
"@vue/server-renderer" "3.3.4"
|
||||
"@vue/shared" "3.3.4"
|
||||
"@vue/compiler-dom" "3.3.10"
|
||||
"@vue/compiler-sfc" "3.3.10"
|
||||
"@vue/runtime-dom" "3.3.10"
|
||||
"@vue/server-renderer" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
which@^2.0.1:
|
||||
version "2.0.2"
|
||||
@@ -1870,6 +2007,11 @@ wrappy@1:
|
||||
resolved "https://registry.nlark.com/wrappy/download/wrappy-1.0.2.tgz?cache=0&sync_timestamp=1619133505879&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fwrappy%2Fdownload%2Fwrappy-1.0.2.tgz"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
xml-name-validator@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
||||
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
|
||||
|
||||
xterm-addon-fit@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz#48ca99015385141918f955ca7819e85f3691d35f"
|
||||
@@ -1900,9 +2042,9 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zrender@5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.4.0.tgz#d4f76e527b2e3bbd7add2bdaf27a16af85785576"
|
||||
integrity sha512-rOS09Z2HSVGFs2dn/TuYk5BlCaZcVe8UDLLjj1ySYF828LATKKdxuakSZMvrDz54yiKPDYVfjdKqcX8Jky3BIA==
|
||||
zrender@5.4.4:
|
||||
version "5.4.4"
|
||||
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz#8854f1d95ecc82cf8912f5a11f86657cb8c9e261"
|
||||
integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==
|
||||
dependencies:
|
||||
tslib "2.3.0"
|
||||
|
||||
@@ -2,13 +2,13 @@ server:
|
||||
# debug release test
|
||||
model: release
|
||||
port: 18888
|
||||
# 上下文路径, 若设置了该值, 则请求地址为ip:port/context-path
|
||||
# context-path: /mayfly
|
||||
cors: true
|
||||
tls:
|
||||
enable: false
|
||||
key-file: ./default.key
|
||||
cert-file: ./default.pem
|
||||
# 机器终端操作回放文件存储路径
|
||||
machine-rec-path: ./rec
|
||||
jwt:
|
||||
# jwt key,不设置默认使用随机字符串
|
||||
key:
|
||||
|
||||
@@ -3,33 +3,33 @@ module mayfly-go
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
gitee.com/liuzongyang/libpq v1.0.9
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.5
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.14.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231007020222-b91ee5ef3b31
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/golang-jwt/jwt/v5 v5.1.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
|
||||
github.com/mojocn/base64Captcha v1.3.5 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.2.1
|
||||
github.com/redis/go-redis/v9 v9.3.0
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.mongodb.org/mongo-driver v1.12.1 // mongo
|
||||
golang.org/x/crypto v0.14.0 // ssh
|
||||
golang.org/x/oauth2 v0.13.0
|
||||
golang.org/x/crypto v0.16.0 // ssh
|
||||
golang.org/x/oauth2 v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/driver/sqlite v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
@@ -41,14 +41,17 @@ require (
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -63,7 +66,9 @@ require (
|
||||
github.com/montanaflynn/stats v0.7.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
@@ -73,13 +78,17 @@ require (
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
|
||||
golang.org/x/net v0.16.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
|
||||
google.golang.org/grpc v1.52.3 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
vitess.io/vitess v0.17.3 // indirect
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func InitRouter() *gin.Engine {
|
||||
})
|
||||
|
||||
// 设置静态资源
|
||||
setStatic(router)
|
||||
setStatic(serverConfig.ContextPath, router)
|
||||
|
||||
// 是否允许跨域
|
||||
if serverConfig.Cors {
|
||||
@@ -42,7 +42,7 @@ func InitRouter() *gin.Engine {
|
||||
}
|
||||
|
||||
// 设置路由组
|
||||
api := router.Group("/api")
|
||||
api := router.Group(serverConfig.ContextPath + "/api")
|
||||
{
|
||||
common_router.Init(api)
|
||||
|
||||
@@ -61,16 +61,17 @@ func InitRouter() *gin.Engine {
|
||||
return router
|
||||
}
|
||||
|
||||
func setStatic(router *gin.Engine) {
|
||||
func setStatic(contextPath string, router *gin.Engine) {
|
||||
// 使用embed打包静态资源至二进制文件中
|
||||
fsys, _ := fs.Sub(static.Static, "static")
|
||||
fileServer := http.FileServer(http.FS(fsys))
|
||||
handler := WrapStaticHandler(fileServer)
|
||||
router.GET("/", handler)
|
||||
router.GET("/favicon.ico", handler)
|
||||
router.GET("/config.js", handler)
|
||||
handler := WrapStaticHandler(http.StripPrefix(contextPath, fileServer))
|
||||
|
||||
router.GET(contextPath+"/", handler)
|
||||
router.GET(contextPath+"/favicon.ico", handler)
|
||||
router.GET(contextPath+"/config.js", handler)
|
||||
// 所有/assets/**开头的都是静态资源文件
|
||||
router.GET("/assets/*file", handler)
|
||||
router.GET(contextPath+"/assets/*file", handler)
|
||||
|
||||
// 设置静态资源
|
||||
if staticConfs := config.Conf.Server.Static; staticConfs != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mayfly-go/internal/auth/api/form"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/captcha"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/otp"
|
||||
"mayfly-go/pkg/req"
|
||||
@@ -49,7 +51,7 @@ func (a *AccountLogin) Login(rc *req.Ctx) {
|
||||
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
|
||||
|
||||
account := &sysentity.Account{Username: username}
|
||||
err = a.AccountApp.GetAccount(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
|
||||
err = a.AccountApp.GetBy(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
|
||||
|
||||
failCountKey := fmt.Sprintf("account:login:failcount:%s", username)
|
||||
nowFailCount := cache.GetInt(failCountKey)
|
||||
@@ -60,11 +62,11 @@ func (a *AccountLogin) Login(rc *req.Ctx) {
|
||||
if err != nil || !cryptox.CheckPwdHash(originPwd, account.Password) {
|
||||
nowFailCount++
|
||||
cache.SetStr(failCountKey, strconv.Itoa(nowFailCount), time.Minute*time.Duration(loginFailMin))
|
||||
panic(biz.NewBizErr(fmt.Sprintf("用户名或密码错误【当前登录失败%d次】", nowFailCount)))
|
||||
panic(errorx.NewBiz(fmt.Sprintf("用户名或密码错误【当前登录失败%d次】", nowFailCount)))
|
||||
}
|
||||
|
||||
// 校验密码强度(新用户第一次登录密码与账号名一致)
|
||||
biz.IsTrueBy(utils.CheckAccountPasswordLever(originPwd), biz.NewBizErrCode(401, "您的密码安全等级较低,请修改后重新登录"))
|
||||
biz.IsTrueBy(utils.CheckAccountPasswordLever(originPwd), errorx.NewBizCode(401, "您的密码安全等级较低,请修改后重新登录"))
|
||||
rc.ResData = LastLoginCheck(account, accountLoginSecurity, clientIp)
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
|
||||
|
||||
if !otp.Validate(otpVerify.Code, otpSecret) {
|
||||
cache.SetStr(failCountKey, strconv.Itoa(failCount+1), time.Minute*time.Duration(10))
|
||||
panic(biz.NewBizErr("双因素认证授权码不正确"))
|
||||
panic(errorx.NewBiz("双因素认证授权码不正确"))
|
||||
}
|
||||
|
||||
// 如果是未注册状态,则更新account表的otpSecret信息
|
||||
@@ -106,7 +108,7 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
|
||||
update := &sysentity.Account{OtpSecret: otpSecret}
|
||||
update.Id = accountId
|
||||
update.OtpSecretEncrypt()
|
||||
a.AccountApp.Update(update)
|
||||
biz.ErrIsNil(a.AccountApp.Update(context.Background(), update))
|
||||
}
|
||||
|
||||
la := &sysentity.Account{Username: otpInfo.Username}
|
||||
@@ -118,6 +120,7 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
|
||||
}
|
||||
|
||||
func (a *AccountLogin) Logout(rc *req.Ctx) {
|
||||
req.GetPermissionCodeRegistery().Remove(rc.LoginAccount.Id)
|
||||
ws.CloseClient(rc.LoginAccount.Id)
|
||||
la := rc.GetLoginAccount()
|
||||
req.GetPermissionCodeRegistery().Remove(la.Id)
|
||||
ws.CloseClient(ws.UserId(la.Id))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/auth/config"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
@@ -40,7 +41,9 @@ func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.Acc
|
||||
// 默认为不校验otp
|
||||
otpStatus := OtpStatusNone
|
||||
// 访问系统使用的token
|
||||
accessToken := req.CreateToken(account.Id, username)
|
||||
accessToken, err := req.CreateToken(account.Id, username)
|
||||
biz.ErrIsNilAppendErr(err, "token创建失败: %s")
|
||||
|
||||
// 若系统配置中设置开启otp双因素校验,则进行otp校验
|
||||
if accountLoginSecurity.UseOtp {
|
||||
otpInfo, otpurl, otpToken := useOtp(account, accountLoginSecurity.OtpIssuer, accessToken)
|
||||
@@ -106,7 +109,7 @@ func saveLogin(account *sysentity.Account, ip string) {
|
||||
updateAccount.Id = account.Id
|
||||
updateAccount.LastLoginIp = ip
|
||||
// 偷懒为了方便直接获取accountApp
|
||||
sysapp.GetAccountApp().Update(updateAccount)
|
||||
biz.ErrIsNil(sysapp.GetAccountApp().Update(context.TODO(), updateAccount))
|
||||
|
||||
// 创建登录消息
|
||||
loginMsg := &msgentity.Msg{
|
||||
@@ -117,5 +120,5 @@ func saveLogin(account *sysentity.Account, ip string) {
|
||||
loginMsg.CreateTime = &now
|
||||
loginMsg.Creator = account.Username
|
||||
loginMsg.CreatorId = account.Id
|
||||
msgapp.GetMsgApp().Create(loginMsg)
|
||||
msgapp.GetMsgApp().Create(context.TODO(), loginMsg)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"mayfly-go/internal/auth/api/form"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/captcha"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
@@ -69,7 +71,7 @@ func (a *LdapLogin) Login(rc *req.Ctx) {
|
||||
if err != nil {
|
||||
nowFailCount++
|
||||
cache.SetStr(failCountKey, strconv.Itoa(nowFailCount), time.Minute*time.Duration(loginFailMin))
|
||||
panic(biz.NewBizErr(fmt.Sprintf("用户名或密码错误【当前登录失败%d次】", nowFailCount)))
|
||||
panic(errorx.NewBiz(fmt.Sprintf("用户名或密码错误【当前登录失败%d次】", nowFailCount)))
|
||||
}
|
||||
|
||||
rc.ResData = LastLoginCheck(account, accountLoginSecurity, clientIp)
|
||||
@@ -77,7 +79,7 @@ func (a *LdapLogin) Login(rc *req.Ctx) {
|
||||
|
||||
func (a *LdapLogin) getUser(userName string, cols ...string) (*sysentity.Account, error) {
|
||||
account := &sysentity.Account{Username: userName}
|
||||
if err := a.AccountApp.GetAccount(account, cols...); err != nil {
|
||||
if err := a.AccountApp.GetBy(account, cols...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
@@ -87,10 +89,10 @@ func (a *LdapLogin) createUser(userName, displayName string) {
|
||||
account := &sysentity.Account{Username: userName}
|
||||
account.SetBaseInfo(nil)
|
||||
account.Name = displayName
|
||||
a.AccountApp.Create(account)
|
||||
biz.ErrIsNil(a.AccountApp.Create(context.TODO(), account))
|
||||
// 将 LADP 用户本地密码设置为空,不允许本地登录
|
||||
account.Password = cryptox.PwdHash("")
|
||||
a.AccountApp.Update(account)
|
||||
biz.ErrIsNil(a.AccountApp.Update(context.TODO(), account))
|
||||
}
|
||||
|
||||
func (a *LdapLogin) getOrCreateUserWithLdap(userName string, password string, cols ...string) (*sysentity.Account, error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user