46 Commits

Author SHA1 Message Date
meilin.huang
57361d8241 feat: 支持关联多标签、计划任务立即执行、标签相关操作优化 2023-12-05 23:03:51 +08:00
meilin.huang
b347bd7ef5 feat: 数据库超时时间设置 2023-11-30 15:02:48 +08:00
meilin.huang
070c8ac0da fix: 排序导致条件丢失 2023-11-29 20:13:29 +08:00
meilin.huang
e221c2f42e feat: 新增系统全局分页size配置,可根据屏幕大小自行设置 2023-11-29 17:34:54 +08:00
Coder慌
c7bab3a71b !62 fix:gauss驱动支持ssh
Merge pull request !62 from zongyangleo/dev_1128
2023-11-29 08:40:25 +00:00
刘宗洋
82c17a51a2 fix:libpq驱动支持gaussdb sha256加密登录 2023-11-28 22:49:42 +08:00
meilin.huang
e4447e6bc2 refactor: code review 2023-11-27 17:40:47 +08:00
Coder慌
b9570d9a5f !61 fix: 1、pgsql驱动换成高斯db驱动 2、sql格式化支持传数据库类型
Merge pull request !61 from zongyangleo/dev_1127
2023-11-27 08:58:32 +00:00
刘宗洋
01e8a2c14d fix:
1、pgsql驱动换成高斯db驱动
2、sql格式化支持传数据库类型
2023-11-27 16:55:00 +08:00
meilin.huang
64bd51c3b0 refactor: code review 2023-11-26 21:21:35 +08:00
Coder慌
54ab34df3f !60 feat: 支持pgsql编辑表、索引
Merge pull request !60 from zongyangleo/dev_1126
2023-11-26 02:32:27 +00:00
刘宗洋
206490ba3e feat: 支持pgsql编辑表、索引 2023-11-26 01:47:49 +08:00
meilin.huang
16612d2c9c refactor: 多tab结果集调整 2023-11-24 17:03:08 +08:00
meilin.huang
6b65605360 feat: sql查询支持多tab结果集 2023-11-24 12:12:47 +08:00
meilin.huang
bb37ed3b95 refactor: 数据库操作界面小优化 2023-11-22 12:19:07 +08:00
meilin.huang
d102cc8c08 feat: 数据库表操作支持复制单元格数据菜单 2023-11-20 12:21:27 +08:00
meilin.huang
a6df74d63d refactor: 虚拟表格优化 2023-11-18 21:15:33 +08:00
meilin.huang
f79760943e refactor: 虚拟表格与contextmenu菜单优化 2023-11-18 15:22:25 +08:00
meilin.huang
a40ec21a05 refactor: 数据库表使用虚拟表替换,提升数据量较大时的渲染速度 2023-11-17 13:31:28 +08:00
meilin.huang
43230267b6 refactor: 界面小调整 2023-11-15 12:28:49 +08:00
meilin.huang
0ae99cdaf9 refactor: contextmenu组件优化、标签&资源替换为contextmenu操作 2023-11-14 17:36:51 +08:00
meilin.huang
f234c72514 refactor: DB-数据操作优化 2023-11-13 17:41:03 +08:00
meilin.huang
76527d95bd refactor: 机器相关配置迁移至系统配置、pgsql数据操作完善、新增context-path 2023-11-12 20:14:44 +08:00
meilin.huang
27c53385f2 refactor: 列表操作按钮调整 2023-11-09 12:11:11 +08:00
Coder慌
a1b25e9766 !59 fix: sql代码提示修复:支持跨schema提示
Merge pull request !59 from zongyangleo/dev_1109
2023-11-09 02:13:31 +00:00
刘宗洋
abad0ed481 fix: sql代码提示修复:支持跨schema提示 2023-11-09 09:48:58 +08:00
meilin.huang
eddda41291 feat: 机器列表新增运行状态 & refactor: 登录账号信息存储与context 2023-11-07 21:05:21 +08:00
meilin.huang
d9adf0fd25 fix: 表问题修复 2023-11-03 22:29:01 +08:00
meilin.huang
0ce82b41ba refactor: 标签树展示调整 2023-11-03 17:09:20 +08:00
meilin.huang
37026f3269 feat: 数据库表操作显示表size&其他小优化 2023-11-02 12:46:21 +08:00
meilin.huang
3155380f16 refactor: tooltip延迟显示等 2023-10-31 12:36:04 +08:00
meilin.huang
f2b0f294d8 refactor: machine包调整 2023-10-30 17:34:56 +08:00
meilin.huang
12f63ef3dd refactor: db/redis/mongo连接代码包独立 2023-10-27 17:41:45 +08:00
meilin.huang
a1303b52eb refactor: 新增base.Repo与base.App,重构repo与app层代码 2023-10-26 17:15:49 +08:00
meilin.huang
10f6b03fb5 refactor: code review 2023-10-20 21:31:46 +08:00
may-fly
45d2449221 Merge pull request #74 from kanzihuang/fix-showcreatetable
fix: show create table for postgres
2023-10-20 21:15:54 +08:00
wanli
9e5f146e05 fix: show create table for postgres 2023-10-20 17:37:09 +08:00
meilin.huang
2b91bbe185 refactor: websocket支持单用户多连接 2023-10-19 19:00:23 +08:00
may-fly
747ea6404d Merge pull request #73 from kanzihuang/feat-notify
feature: 每个客户端独立处理后端发送的系统消息
2023-10-18 08:36:21 -05:00
wanli
ccfc6bd1df feature: 每个客户端独立处理后端发送的系统消息 2023-10-18 20:31:27 +08:00
kanzihuang
361eafedae feature: 执行 SQL 脚本时显示执行进度 2023-10-18 15:41:57 +08:00
meilin.huang
a64b894b08 refactor: sqlite依赖替换 2023-10-17 12:32:59 +08:00
may-fly
0ad805c170 Merge pull request #72 from kanzihuang/refactor-dbtype
refactor: 实现 DbType 类型,集中处理部分差异化的数据库操作
2023-10-15 19:41:19 -05:00
kanzihuang
ba82b5b516 refactor: 实现 DbType 类型,集中处理部分差异化的数据库操作 2023-10-15 11:39:42 +08:00
may-fly
f04b82c933 Merge pull request #71 from kanzihuang/fix-exec-postgres-sql-pullrequest
fix: 执行或导入 SQL 脚本支持 PostgreSQL
2023-10-14 09:57:12 -05:00
kanzihuang
23b137ab9b fix: 执行或导入 SQL 脚本支持 PostgreSQL 2023-10-14 21:10:53 +08:00
320 changed files with 11010 additions and 7661 deletions

View File

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

View File

@@ -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这将是对我极大的鼓励与支持。

View File

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

View File

@@ -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);
};
// 页面销毁时,关闭监听布局配置

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -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];
}
/**

View File

@@ -40,3 +40,7 @@ export const NextLoading = {
});
},
};
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -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;
}
}
// 移除临时缓存

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
export const buildProgressProps = (): any => {
return {
progress: {
sqlFileName: {
title: {
type: String,
},
executedStatements: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';
}
},
// 设置水印配置信息

View File

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

View File

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

View File

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

View File

@@ -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[];
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
};
/**
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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: '', // tabsql
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>

View File

@@ -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*/,

View File

@@ -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) {
//
}
};
//

View File

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

View 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('不支持的数据库');
};

View 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' },
],
};

View 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: '时间间隔', // 可以跟参数YEARMONTHDAYHOURMINUTESECONDDAY TO HOURDAY TO MINUTEDAY TO SECONDHOUR TO MINUTEHOUR TO SECONDMINUTE TO SECOND
space: '精度取值范围为0~6且参数为SECONDDAY TO SECONDHOUR 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 '';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {
//
}
};
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -157,7 +157,7 @@ const state = reactive({
},
query: {
pageNum: 1,
pageSize: 10,
pageSize: 0,
name: null,
},
queryConfig: [TableQuery.text('name', '团队名称')],

View File

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

View File

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

View File

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

View File

@@ -99,7 +99,7 @@ const paramsFormRef: any = ref(null);
const state = reactive({
query: {
pageNum: 1,
pageSize: 10,
pageSize: 0,
name: null,
},
total: 0,

View File

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

View File

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

View File

@@ -20,12 +20,9 @@
>
</template>
<template #showmore="{ data }">
<el-link @click.prevent="showResources(data)" type="info">菜单&权限</el-link>
</template>
<template #action="{ data }">
<el-button v-if="actionBtns[perms.updateRole]" @click="editRole(data)" type="primary" link>编辑</el-button>
<el-button @click="showResources(data)" type="info" link>权限详情</el-button>
<el-button v-if="actionBtns[perms.saveRoleResource]" @click="editResource(data)" type="success" link>权限分配</el-button>
</template>
</page-table>
@@ -73,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) => {

View File

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

View File

@@ -43,7 +43,7 @@ const state = reactive({
creatorId: null,
description: null,
pageNum: 1,
pageSize: 10,
pageSize: 0,
},
queryConfig: [
TableQuery.slot('creatorId', '操作人', 'selectAccount'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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