25 Commits

Author SHA1 Message Date
meilin.huang
2deb3109c2 feat: dbms表数据新增表单视图 2024-07-19 17:06:11 +08:00
meilin.huang
a80221a950 fix: 数据库实例删除等问题修复 2024-07-05 13:14:31 +08:00
meilin.huang
10630847df fix: 工单流程信息展示问题修复 2024-06-24 17:17:57 +08:00
meilin.huang
f43851698e fix: 资源关联多标签删除、数据库实例删除等问题修复与数据库等名称过滤优化 2024-06-07 12:31:40 +08:00
Coder慌
73884bb693 !122 fix: mysql导出修复
Merge pull request !122 from zongyangleo/dev_1.8.6_fix
2024-06-05 04:17:03 +00:00
zongyangleo
1b5bb1de8b fix: mysql导出修复 2024-06-01 13:35:31 +08:00
meilin.huang
4814793546 fix: 修复数据库表数据横向滚动后切换tab导致表头错位&数据取消居中显示 2024-05-31 12:12:40 +08:00
meilin.huang
d85bbff270 release v1.8.6 2024-05-23 17:18:22 +08:00
meilin.huang
bb1522f4dc refactor: 数据库管理迁移至数据库实例-库管理、机器管理-文件支持用户和组信息查看 2024-05-21 12:34:26 +08:00
zongyangleo
a7632fbf58 !121 fix: rdp ssh
* fix: rdp ssh
2024-05-21 04:06:13 +00:00
meilin.huang
c4cb4234fd fix: 问题修复 2024-05-16 17:26:32 +08:00
meilin.huang
89e12678eb refactor: 引入dayjs、新增refreshToken无感刷新、团队新增有效期、数据库等问题修复 2024-05-13 19:55:43 +08:00
meilin.huang
137ebb8e9e fix: 数据库表新增数据表单全必填问题修复等 2024-05-11 12:09:55 +08:00
meilin.huang
05625bd8c1 feat: 1.8.3 2024-05-10 19:59:49 +08:00
meilin.huang
4afeac5fdd refactor: 代码优化与数据库表列显示配置优化 2024-05-09 21:29:34 +08:00
meilin.huang
1d0e91f1af refactor: 机器计划任务与流程定义关联至标签 2024-05-08 21:04:25 +08:00
Coder慌
cf5111a325 !118 修复空数组分隔异常
Merge pull request !118 from 蒋小小/N/A
2024-05-07 11:59:12 +00:00
meilin.huang
78957a8ebd refactor: base.repo与app重构优化 2024-05-05 14:53:30 +08:00
meilin.huang
4ed892a656 feat: 文档更新与sqlite文件更新 2024-04-29 17:09:41 +08:00
meilin.huang
3486b07003 fix: 修复机器列表查询与放开vnc 2024-04-29 12:50:49 +08:00
meilin.huang
a5cd7caf19 refactor: base.repo与base.app精简优化 2024-04-29 12:29:56 +08:00
meilin.huang
f2c7ef78c0 refactor: 精简base.repo与base.app等 2024-04-28 23:45:57 +08:00
meilin.huang
653953ee76 feat: 机器新增命令过滤配置、首页功能完善(操作记录与快捷操作) 2024-04-27 01:35:21 +08:00
meilin.huang
a831614d5a fix: sql脚本问题修复等 2024-04-23 11:35:45 +08:00
蒋小小
29fd5a25d2 修复空数组分隔异常
Signed-off-by: 蒋小小 <bwcx_jzy@163.com>
2024-04-20 17:33:19 +00:00
301 changed files with 6305 additions and 4531 deletions

View File

@@ -22,7 +22,7 @@
### 介绍 ### 介绍
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台** web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
### 开发语言与主要框架 ### 开发语言与主要框架
@@ -45,56 +45,61 @@ http://go.mayfly.run
### 系统核心功能截图 ### 系统核心功能截图
##### 记录操作记录 #### 首页
![记录操作记录](https://objs.gitee.io/mayfly-go-docs/home/log.jpg "屏幕截图.png") ![首页](https://foruda.gitee.com/images/1714378104294194769/149fd257_1240250.png "屏幕截图")
#### 机器操作 #### 机器操作
##### 状态查看 ##### 状态查看
![状态查看](https://objs.gitee.io/mayfly-go-docs/home/machine-status.jpg "屏幕截图.png") ![机器状态查看](https://foruda.gitee.com/images/1714378556642584686/93c46ec0_1240250.png "屏幕截图")
##### ssh 终端 ##### ssh 终端
![ssh终端](https://objs.gitee.io/mayfly-go-docs/home/machine-ssh.jpg "屏幕截图.png") ![终端操作](https://foruda.gitee.com/images/1714378353790214943/2864ba66_1240250.png "屏幕截图")
##### 文件操作 ##### 文件操作
![文件操作](https://objs.gitee.io/mayfly-go-docs/home/file-dir.jpg "屏幕截图.png") ![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件操作](https://objs.gitee.io/mayfly-go-docs/home/file-content-update.jpg "屏幕截图.png")
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### 数据库操作 #### 数据库操作
##### sql 编辑器 ##### sql 编辑器
![sql编辑器](https://objs.gitee.io/mayfly-go-docs/home/dbms-sql-editor.jpg "屏幕截图.png") ![sql编辑器](https://foruda.gitee.com/images/1714378747473077515/3c9387c0_1240250.png "屏幕截图")
##### 在线增删改查数据 ##### 在线增删改查数据
![选表查数据](https://objs.gitee.io/mayfly-go-docs/home/dbms-show-table-data.jpg "屏幕截图.png") ![选表查数据](https://foruda.gitee.com/images/1714378625059063750/3951e5a8_1240250.png "屏幕截图")
#### Redis 操作 #### Redis 操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/redis-data-list.jpg "屏幕截图.png") ![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### Mongo 操作 #### Mongo 操作
![数据](https://objs.gitee.io/mayfly-go-docs/home/mongo-op.jpg "屏幕截图.png") ![mongo操作](https://foruda.gitee.com/images/1714378916425714642/77fc0ed9_1240250.png "屏幕截图")
##### 系统管理 #### 工单流程审批
![流程审批](https://foruda.gitee.com/images/1714379057627690037/ad136862_1240250.png "屏幕截图")
#### 系统管理
##### 账号管理 ##### 账号管理
![账号管理](https://images.gitee.com/uploads/images/2021/0607/173919_a8d7dc18_1240250.png "屏幕截图.png") ![账号管理](https://foruda.gitee.com/images/1714379179491881231/c6d802ae_1240250.png "屏幕截图")
##### 角色管理 ##### 角色管理
![角色管理](https://images.gitee.com/uploads/images/2021/0607/174028_3654fb28_1240250.png "屏幕截图.png") ![角色管理](https://foruda.gitee.com/images/1714379269408676381/6ac1e85c_1240250.png "屏幕截图")
##### 资源管理 ##### 菜单资源管理
![资源管理](https://images.gitee.com/uploads/images/2021/0607/174436_e9e1535c_1240250.png "屏幕截图.png") ![菜单资源管理](https://foruda.gitee.com/images/1714379321338009940/a00d6a02_1240250.png "屏幕截图")
**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go **其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go

View File

@@ -14,7 +14,7 @@ services:
restart: always restart: always
server: server:
image: mayfly-go:v1.3.1 image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@@ -10,20 +10,20 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.11.0",
"asciinema-player": "^3.7.0", "asciinema-player": "^3.8.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"countup.js": "^2.8.0",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.1",
"echarts": "^5.5.0", "dayjs": "^1.11.11",
"element-plus": "^2.7.1", "echarts": "^5.5.1",
"element-plus": "^2.7.7",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"monaco-editor": "^0.47.0", "monaco-editor": "^0.50.0",
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.12.2",
"monaco-themes": "^0.4.4", "monaco-themes": "^0.4.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@@ -34,8 +34,8 @@
"sql-formatter": "^15.0.2", "sql-formatter": "^15.0.2",
"trzsz": "^1.1.5", "trzsz": "^1.1.5",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.4.23", "vue": "^3.4.32",
"vue-router": "^4.3.2", "vue-router": "^4.4.0",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0", "xterm-addon-search": "^0.13.0",
@@ -48,16 +48,16 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4", "@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.5",
"@vue/compiler-sfc": "^3.4.23", "@vue/compiler-sfc": "^3.4.32",
"code-inspector-plugin": "^0.4.5", "code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.35.0", "eslint": "^8.35.0",
"eslint-plugin-vue": "^9.25.0", "eslint-plugin-vue": "^9.25.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"sass": "^1.75.0", "sass": "^1.77.8",
"typescript": "^5.4.5", "typescript": "^5.5.3",
"vite": "^5.2.10", "vite": "^5.3.4",
"vue-eslint-parser": "^9.4.2" "vue-eslint-parser": "^9.4.2"
}, },
"browserslist": [ "browserslist": [

View File

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

View File

@@ -2,6 +2,7 @@ import request from './request';
export default { export default {
login: (param: any) => request.post('/auth/accounts/login', param), login: (param: any) => request.post('/auth/accounts/login', param),
refreshToken: (param: any) => request.get('/auth/accounts/refreshToken', param),
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param), otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
getPublicKey: () => request.get('/common/public-key'), getPublicKey: () => request.get('/common/public-key'),
getConfigValue: (params: any) => request.get('/sys/configs/value', params), getConfigValue: (params: any) => request.get('/sys/configs/value', params),

View File

@@ -38,6 +38,7 @@ export enum ResultEnum {
PARAM_ERROR = 405, PARAM_ERROR = 405,
SERVER_ERROR = 500, SERVER_ERROR = 500,
NO_PERMISSION = 501, NO_PERMISSION = 501,
ACCESS_TOKEN_INVALID = 502, // accessToken失效
} }
export const baseUrl: string = config.baseApiUrl; export const baseUrl: string = config.baseApiUrl;

View File

@@ -1,31 +0,0 @@
export function dateFormat2(fmt: string, date: Date) {
let ret;
const opt = {
'y+': date.getFullYear().toString(), // 年
'M+': (date.getMonth() + 1).toString(), // 月
'd+': date.getDate().toString(), // 日
'H+': date.getHours().toString(), // 时
'm+': date.getMinutes().toString(), // 分
's+': date.getSeconds().toString(), // 秒
'S+': date.getMilliseconds() ? date.getMilliseconds().toString() : '', // 毫秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (const k in opt) {
ret = new RegExp('(' + k + ')').exec(fmt);
if (ret) {
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
}
}
return fmt;
}
export function dateStrFormat(fmt: string, dateStr: string) {
return dateFormat2(fmt, new Date(dateStr));
}
export function dateFormat(dateStr: string) {
if (!dateStr) {
return '';
}
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
}

View File

@@ -1,3 +1,18 @@
import dayjs from 'dayjs';
/**
* 格式化日期
* @param date 日期 字符串 Date 时间戳等
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
* @returns 格式化后内容
*/
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
if (!date) {
return '';
}
return dayjs(date).format(format);
}
/** /**
* 格式化字节单位 * 格式化字节单位
* @param size byte size * @param size byte size
@@ -46,110 +61,6 @@ export function convertToBytes(sizeStr: string) {
return bytes; return bytes;
} }
/*
* 年(Y) 可用1-4个占位符
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
* 星期(W) 可用1-3个占位符
* 季度(q为阿拉伯数字Q为中文数字)可用1或4个占位符
*
* let date = new Date()
* formatDate(date, "YYYY-mm-dd HH:MM:SS") // 2020-02-09 14:04:23
* formatDate(date, "YYYY-mm-dd HH:MM:SS Q") // 2020-02-09 14:09:03 一
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW") // 2020-02-09 14:45:12 星期日
* formatDate(date, "YYYY-mm-dd HH:MM:SS QQQQ") // 2020-02-09 14:09:36 第一季度
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-02-09 14:46:12 星期日 第一季度
*/
export function formatDate(date: Date, format: string) {
let we = date.getDay(); // 星期
let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
const opt: any = {
'Y+': date.getFullYear().toString(), // 年
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始要+1)
'd+': date.getDate().toString(), // 日
'H+': date.getHours().toString(), // 时
'M+': date.getMinutes().toString(), // 分
'S+': date.getSeconds().toString(), // 秒
'q+': qut, // 季度
};
// 中文数字 (星期)
const week: any = {
'0': '日',
'1': '一',
'2': '二',
'3': '三',
'4': '四',
'5': '五',
'6': '六',
};
// 中文数字(季度)
const quarter: any = {
'1': '一',
'2': '二',
'3': '三',
'4': '四',
};
if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
for (let k in opt) {
let r = new RegExp('(' + k + ')').exec(format);
// 若输入的长度不为1则前面补零
if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
}
return format;
}
/**
* 10秒 10 * 1000
* 1分 60 * 1000
* 1小时 60 * 60 * 1000
* 24小时60 * 60 * 24 * 1000
* 3天 60 * 60* 24 * 1000 * 3
*
* let data = new Date()
* formatPast(data) // 刚刚
* formatPast(data - 11 * 1000) // 11秒前
* formatPast(data - 2 * 60 * 1000) // 2分钟前
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
* formatPast(data - 60 * 60 * 71 * 1000) // 2天前
* formatPast("2020-06-01") // 2020-06-01
* formatPast("2020-06-01", "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-06-01 08:00:00 星期一 第二季度
*/
export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
// 传入格式处理、存储转换值
let t: any, s: any;
// 获取js 时间戳
let time: any = new Date().getTime();
// 是否是对象
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param);
// 当前时间戳 - 传入时间戳
time = Number.parseInt(`${time - t}`);
if (time < 10000) {
// 10秒内
return '刚刚';
} else if (time < 60000 && time >= 10000) {
// 超过10秒少于1分钟内
s = Math.floor(time / 1000);
return `${s}秒前`;
} else if (time < 3600000 && time >= 60000) {
// 超过1分钟少于1小时
s = Math.floor(time / 60000);
return `${s}分钟前`;
} else if (time < 86400000 && time >= 3600000) {
// 超过1小时少于24小时
s = Math.floor(time / 3600000);
return `${s}小时前`;
} else if (time < 259200000 && time >= 86400000) {
// 超过1天少于3天内
s = Math.floor(time / 86400000);
return `${s}天前`;
} else {
// 超过3天
let date = typeof param === 'string' || 'object' ? new Date(param) : param;
return formatDate(date, format);
}
}
/** /**
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位) * 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
* *

View File

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

View File

@@ -1,6 +1,7 @@
import { randomUuid } from './string'; import { randomUuid } from './string';
const TokenKey = 'm-token'; const TokenKey = 'm-token';
const RefreshTokenKey = 'm-refresh-token';
const UserKey = 'm-user'; const UserKey = 'm-user';
const TagViewsKey = 'm-tagViews'; const TagViewsKey = 'm-tagViews';
const ClientIdKey = 'm-clientId'; const ClientIdKey = 'm-clientId';
@@ -15,6 +16,14 @@ export function saveToken(token: string) {
setLocal(TokenKey, token); setLocal(TokenKey, token);
} }
export function getRefreshToken(): string {
return getLocal(RefreshTokenKey);
}
export function saveRefreshToken(refreshToken: string) {
return setLocal(RefreshTokenKey, refreshToken);
}
// 获取登录用户基础信息 // 获取登录用户基础信息
export function getUser() { export function getUser() {
return getLocal(UserKey); return getLocal(UserKey);
@@ -39,6 +48,7 @@ export function getThemeConfig() {
export function clearUser() { export function clearUser() {
removeLocal(TokenKey); removeLocal(TokenKey);
removeLocal(UserKey); removeLocal(UserKey);
removeLocal(RefreshTokenKey);
} }
export function getTagViews() { export function getTagViews() {

View File

@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
return width; return width;
} }
/**
* 获取内容所需要占用的宽度
*/
export function getContentWidth(content: any): number {
if (!content) {
return 50;
}
// 以下分配的单位长度可根据实际需求进行调整
let flexWidth = 0;
for (const char of content) {
if (flexWidth > 500) {
break;
}
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
// 小写字母、数字字符
flexWidth += 9.3;
continue;
}
if (char >= 'A' && char <= 'Z') {
flexWidth += 9;
continue;
}
if (char >= '\u4e00' && char <= '\u9fa5') {
// 如果是中文字符为字符分配16个单位宽度
flexWidth += 20;
} else {
// 其他种类字符
flexWidth += 8;
}
}
// if (flexWidth > 450) {
// // 设置最大宽度
// flexWidth = 450;
// }
return flexWidth;
}
/** /**
* *
* @returns uuid * @returns uuid
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
clipboard.destroy(); clipboard.destroy();
}); });
} }
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
keyword = keyword?.toLowerCase();
return fields.filter((field) => {
for (let valueExtractFunc of valueExtractFuncs) {
const value = valueExtractFunc(field)?.toLowerCase();
if (isPrefixSubsequence(keyword, value)) {
return true;
}
}
return false;
});
}
/**
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> trueprefix=uname2 -> false
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
* @param targetTemplate 目标模板
* @returns 是否匹配
*/
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
let i = 0; // 指向prefix的索引
let j = 0; // 指向targetTemplate的索引
while (i < prefix.length && j < targetTemplate.length) {
if (prefix[i] === targetTemplate[j]) {
// 字符匹配,两个指针都向前移动
i++;
}
j++; // 目标字符串指针始终向前移动
}
// 如果prefix的所有字符都被找到返回true
return i === prefix.length;
}

View File

@@ -159,7 +159,7 @@
@current-change="handlePageNumChange" @current-change="handlePageNumChange"
@size-change="handlePageSizeChange" @size-change="handlePageSizeChange"
style="text-align: right" style="text-align: right"
layout="prev, pager, next, total, sizes, jumper" layout="prev, pager, next, total, sizes"
:total="total" :total="total"
v-model:current-page="queryForm.pageNum" v-model:current-page="queryForm.pageNum"
v-model:page-size="queryForm.pageSize" v-model:page-size="queryForm.pageSize"

View File

@@ -1,5 +1,5 @@
import EnumValue from '@/common/Enum'; import EnumValue from '@/common/Enum';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { getValueByPath } from '@/common/utils/object'; import { getValueByPath } from '@/common/utils/object';
import { getTextWidth } from '@/common/utils/string'; import { getTextWidth } from '@/common/utils/string';
@@ -172,7 +172,7 @@ export class TableColumn {
*/ */
isTime(): TableColumn { isTime(): TableColumn {
this.setFormatFunc((data: any, prop: string) => { this.setFormatFunc((data: any, prop: string) => {
return dateFormat(getValueByPath(data, prop)); return formatDate(getValueByPath(data, prop));
}); });
return this; return this;
} }

View File

@@ -24,8 +24,33 @@
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" /> <SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
</div> </div>
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" /> <clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
<el-dialog
v-if="!state.fullscreen"
destroy-on-close
:title="state.filesystemDialog.title"
v-model="state.filesystemDialog.visible"
:close-on-click-modal="false"
width="70%"
>
<machine-file
:machine-id="state.filesystemDialog.machineId"
:auth-cert-name="state.filesystemDialog.authCertName"
:protocol="state.filesystemDialog.protocol"
:file-id="state.filesystemDialog.fileId"
:path="state.filesystemDialog.path"
/>
</el-dialog>
</div> </div>
<el-dialog destroy-on-close :title="state.filesystemDialog.title" v-model="state.filesystemDialog.visible" :close-on-click-modal="false" width="70%">
<el-dialog
v-if="!state.fullscreen"
destroy-on-close
:title="state.filesystemDialog.title"
v-model="state.filesystemDialog.visible"
:close-on-click-modal="false"
width="70%"
>
<machine-file <machine-file
:machine-id="state.filesystemDialog.machineId" :machine-id="state.filesystemDialog.machineId"
:auth-cert-name="state.filesystemDialog.authCertName" :auth-cert-name="state.filesystemDialog.authCertName"

View File

@@ -67,7 +67,7 @@ const state = reactive({
search: null as any, search: null as any,
weblinks: null as any, weblinks: null as any,
}, },
status: TerminalStatus.NoConnected, status: -11,
}); });
onMounted(() => { onMounted(() => {
@@ -96,6 +96,7 @@ onBeforeUnmount(() => {
}); });
function init() { function init() {
state.status = TerminalStatus.NoConnected;
if (term) { if (term) {
console.log('重新连接...'); console.log('重新连接...');
close(); close();
@@ -105,7 +106,7 @@ function init() {
}); });
} }
function initTerm() { async function initTerm() {
term = new Terminal({ term = new Terminal({
fontSize: themeConfig.value.terminalFontSize || 15, fontSize: themeConfig.value.terminalFontSize || 15,
fontWeight: themeConfig.value.terminalFontWeight || 'normal', fontWeight: themeConfig.value.terminalFontWeight || 'normal',
@@ -155,6 +156,7 @@ function initSocket() {
state.status = TerminalStatus.Connected; state.status = TerminalStatus.Connected;
focus(); focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令 // 如果有初始要执行的命令,则发送执行命令
if (props.cmd) { if (props.cmd) {
@@ -209,7 +211,6 @@ function loadAddon() {
// tell trzsz the terminal columns has been changed // tell trzsz the terminal columns has been changed
trzsz.setTerminalColumns(size.cols); trzsz.setTerminalColumns(size.cols);
}); });
window.addEventListener('resize', () => state.addon.fit.fit());
// enable drag files or directories to upload // enable drag files or directories to upload
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault()); terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
terminalRef.value.addEventListener('drop', (event: any) => { terminalRef.value.addEventListener('drop', (event: any) => {

View File

@@ -1,6 +1,15 @@
import EnumValue from '@/common/Enum';
export enum TerminalStatus { export enum TerminalStatus {
Error = -1, Error = -1,
NoConnected = 0, NoConnected = 0,
Connected = 1, Connected = 1,
Disconnected = 2, Disconnected = 2,
} }
export const TerminalStatusEnum = {
Error: EnumValue.of(TerminalStatus.Error, '连接出错').setExtra({ iconColor: 'var(--el-color-error)' }),
NoConnected: EnumValue.of(TerminalStatus.NoConnected, '未连接').setExtra({ iconColor: 'var(--el-color-primary)' }),
Connected: EnumValue.of(TerminalStatus.Connected, '连接成功').setExtra({ iconColor: 'var(--el-color-success)' }),
Disconnected: EnumValue.of(TerminalStatus.Disconnected, '连接失败').setExtra({ iconColor: 'var(--el-color-error)' }),
};

View File

@@ -1,5 +1,5 @@
import router from '@/router'; import router from '@/router';
import { getClientId, getToken } from '@/common/utils/storage'; import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string'; import { templateResolve } from '@/common/utils/string';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { createFetch } from '@vueuse/core'; import { createFetch } from '@vueuse/core';
@@ -8,6 +8,7 @@ import { Result, ResultEnum } from '@/common/request';
import config from '@/common/config'; import config from '@/common/config';
import { unref } from 'vue'; import { unref } from 'vue';
import { URL_401 } from '@/router/staticRouter'; import { URL_401 } from '@/router/staticRouter';
import openApi from '@/common/openApi';
const baseUrl: string = config.baseApiUrl; const baseUrl: string = config.baseApiUrl;
@@ -88,61 +89,104 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
return { return {
execute: async function () { execute: async function () {
try { return execUaf(uaf);
await uaf.execute(true);
} catch (e: any) {
const rejectPromise = Promise.reject(e);
if (e?.name == 'AbortError') {
console.log('请求已取消');
return rejectPromise;
}
const respStatus = uaf.response.value?.status;
if (respStatus == 404) {
ElMessage.error('请求接口不存在');
return rejectPromise;
}
if (respStatus == 500) {
ElMessage.error('服务器响应异常');
return rejectPromise;
}
console.error(e);
ElMessage.error('网络请求错误');
return rejectPromise;
}
const result: Result = uaf.data.value as any;
if (!result) {
ElMessage.error('网络请求失败');
return Promise.reject(result);
}
// 如果返回为成功结果则将结果的data赋值给响应式data
if (result.code === ResultEnum.SUCCESS) {
uaf.data.value = result.data;
return;
}
// 如果提示没有权限,则跳转至无权限页面
if (result.code === ResultEnum.NO_PERMISSION) {
router.push({
path: URL_401,
});
return Promise.reject(result);
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
uaf.error.value = new Error(result.msg);
}
return Promise.reject(result);
}, },
isFetching: uaf.isFetching, isFetching: uaf.isFetching,
data: uaf.data, data: uaf.data,
abort: uaf.abort, abort: uaf.abort,
}; };
} }
let refreshingToken = false;
let queue: any[] = [];
async function execUaf(uaf: any) {
try {
await uaf.execute(true);
} catch (e: any) {
const rejectPromise = Promise.reject(e);
if (e?.name == 'AbortError') {
console.log('请求已取消');
return rejectPromise;
}
const respStatus = uaf.response.value?.status;
if (respStatus == 404) {
ElMessage.error('请求接口不存在');
return rejectPromise;
}
if (respStatus == 500) {
ElMessage.error('服务器响应异常');
return rejectPromise;
}
console.error(e);
ElMessage.error('网络请求错误');
return rejectPromise;
}
const result: Result = uaf.data.value as any;
if (!result) {
ElMessage.error('网络请求失败');
return Promise.reject(result);
}
const resultCode = result.code;
// 如果返回为成功结果则将结果的data赋值给响应式data
if (resultCode === ResultEnum.SUCCESS) {
uaf.data.value = result.data;
return;
}
// 如果是accessToken失效则使用refreshToken刷新token
if (resultCode == ResultEnum.ACCESS_TOKEN_INVALID) {
if (refreshingToken) {
// 请求加入队列等待, 防止并发多次请求refreshToken
return new Promise((resolve) => {
queue.push(() => {
resolve(execUaf(uaf));
});
});
}
try {
refreshingToken = true;
const res = await openApi.refreshToken({ refresh_token: getRefreshToken() });
saveToken(res.token);
saveRefreshToken(res.refresh_token);
// 重新缓存后端用户权限code
await openApi.getPermissions();
// 执行accessToken失效的请求
queue.forEach((resolve: any) => {
resolve();
});
} catch (e: any) {
clearUser();
} finally {
refreshingToken = false;
queue = [];
}
await execUaf(uaf);
return;
}
// 如果提示没有权限,则跳转至无权限页面
if (resultCode === ResultEnum.NO_PERMISSION) {
router.push({
path: URL_401,
});
return Promise.reject(result);
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && resultCode != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
uaf.error.value = new Error(result.msg);
}
return Promise.reject(result);
}

View File

@@ -1,20 +0,0 @@
import { saveThemeConfig } from '@/common/utils/storage';
import { isDark } from './user.vue';
export const switchDark = () => {
themeConfig.value.isDark = isDark.value;
if (isDark.value) {
themeConfig.value.editorTheme = 'vs-dark';
} else {
themeConfig.value.editorTheme = 'vs';
}
// 如果终端主题不是自定义主题,则切换主题
if (themeConfig.value.terminalTheme != 'custom') {
if (isDark.value) {
themeConfig.value.terminalTheme = 'dark';
} else {
themeConfig.value.terminalTheme = 'solarizedLight';
}
}
saveThemeConfig(themeConfig.value);
};

View File

@@ -0,0 +1,29 @@
import { defineStore } from 'pinia';
/**
* 自动打开资源
*/
export const useAutoOpenResource = defineStore('autoOpenResource', {
state: () => ({
autoOpenResource: {
machineCodePath: '',
dbCodePath: '',
redisCodePath: '',
mongoCodePath: '',
},
}),
actions: {
setMachineCodePath(codePath: string) {
this.autoOpenResource.machineCodePath = codePath;
},
setDbCodePath(codePath: string) {
this.autoOpenResource.dbCodePath = codePath;
},
setRedisCodePath(codePath: string) {
this.autoOpenResource.redisCodePath = codePath;
},
setMongoCodePath(codePath: string) {
this.autoOpenResource.mongoCodePath = codePath;
},
},
});

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { dateFormat2 } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { getSysStyleConfig } from '@/common/sysconfig'; import { getSysStyleConfig } from '@/common/sysconfig';
import { getLocal, getThemeConfig } from '@/common/utils/storage'; import { getLocal, getThemeConfig } from '@/common/utils/storage';
@@ -114,7 +114,7 @@ export const useThemeConfig = defineStore('themeConfig', {
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns // 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
layout: 'classic', layout: 'classic',
terminalTheme: 'solarizedLight', terminalTheme: 'light',
// ssh终端字体颜色 // ssh终端字体颜色
terminalForeground: '#C5C8C6', terminalForeground: '#C5C8C6',
// ssh终端背景色 // ssh终端背景色
@@ -191,7 +191,7 @@ export const useThemeConfig = defineStore('themeConfig', {
}, },
// 设置水印时间为当前时间 // 设置水印时间为当前时间
setWatermarkNowTime() { setWatermarkNowTime() {
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date()); this.themeConfig.watermarkText[1] = formatDate(new Date());
}, },
// 切换暗黑模式 // 切换暗黑模式
switchDark(isDark: boolean) { switchDark(isDark: boolean) {
@@ -207,7 +207,7 @@ export const useThemeConfig = defineStore('themeConfig', {
if (isDark) { if (isDark) {
this.themeConfig.terminalTheme = 'dark'; this.themeConfig.terminalTheme = 'dark';
} else { } else {
this.themeConfig.terminalTheme = 'solarizedLight'; this.themeConfig.terminalTheme = 'light';
} }
} }
}, },

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false"> <el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header> <template #header>
<DrawerHeader :header="title" :back="cancel" /> <DrawerHeader :header="title" :back="cancel" />
</template> </template>
@@ -21,6 +21,10 @@
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input> <el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
</el-form-item> </el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联资源">
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypeEnum.DbName.value, TagResourceTypeEnum.Redis.value]" />
</el-form-item>
<el-divider content-position="left">审批节点</el-divider> <el-divider content-position="left">审批节点</el-divider>
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%"> <el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
@@ -70,6 +74,8 @@ import AccountSelectFormItem from '@/views/system/account/components/AccountSele
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { randomUuid } from '../../common/utils/string'; import { randomUuid } from '../../common/utils/string';
import { ProcdefStatus } from './enums'; import { ProcdefStatus } from './enums';
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
data: { data: {
@@ -115,6 +121,7 @@ const state = reactive({
remark: null, remark: null,
// 流程的审批节点任务 // 流程的审批节点任务
tasks: '', tasks: '',
codePaths: [],
}, },
sortable: '' as any, sortable: '' as any,
}); });
@@ -126,6 +133,7 @@ const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save
watch(props, (newValue: any) => { watch(props, (newValue: any) => {
if (newValue.data) { if (newValue.data) {
state.form = { ...newValue.data }; state.form = { ...newValue.data };
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
const tasks = JSON.parse(state.form.tasks); const tasks = JSON.parse(state.form.tasks);
tasks.forEach((t: any) => { tasks.forEach((t: any) => {
t.userId = Number.parseInt(t.userId); t.userId = Number.parseInt(t.userId);
@@ -160,25 +168,26 @@ const deleteTask = (idx: any) => {
}; };
const btnOk = async () => { const btnOk = async () => {
formRef.value.validate(async (valid: boolean) => { try {
if (!valid) { await formRef.value.validate();
ElMessage.error('表单填写有误'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
const checkRes = checkTasks(); }
if (checkRes.err) {
ElMessage.error(checkRes.err);
return false;
}
state.form.tasks = JSON.stringify(checkRes.tasks); const checkRes = checkTasks();
await saveFlowDefExec(); if (checkRes.err) {
ElMessage.success('操作成功'); ElMessage.error(checkRes.err);
emit('val-change', state.form); return false;
//重置表单域 }
formRef.value.resetFields();
state.form = {} as any; state.form.tasks = JSON.stringify(checkRes.tasks);
}); await saveFlowDefExec();
ElMessage.success('操作成功');
emit('val-change', state.form);
//重置表单域
formRef.value.resetFields();
state.form = {} as any;
}; };
const checkTasks = () => { const checkTasks = () => {

View File

@@ -18,6 +18,10 @@
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link> <el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
</template> </template>
<template #codePaths="{ data }">
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
</template>
<template #action="{ data }"> <template #action="{ data }">
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button> <el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button>
</template> </template>
@@ -42,6 +46,7 @@ import { SearchItem } from '@/components/SearchForm';
import ProcdefEdit from './ProcdefEdit.vue'; import ProcdefEdit from './ProcdefEdit.vue';
import ProcdefTasks from './components/ProcdefTasks.vue'; import ProcdefTasks from './components/ProcdefTasks.vue';
import { ProcdefStatus } from './enums'; import { ProcdefStatus } from './enums';
import TagCodePath from '../ops/component/TagCodePath.vue';
const perms = { const perms = {
save: 'flow:procdef:save', save: 'flow:procdef:save',
@@ -55,6 +60,7 @@ const columns = [
TableColumn.new('status', '状态').typeTag(ProcdefStatus), TableColumn.new('status', '状态').typeTag(ProcdefStatus),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60), TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
TableColumn.new('codePaths', '关联资源').isSlot().setMinWidth('250px'),
TableColumn.new('creator', '创建账号'), TableColumn.new('creator', '创建账号'),
TableColumn.new('createTime', '创建时间').isTime(), TableColumn.new('createTime', '创建时间').isTime(),
]; ];

View File

@@ -17,11 +17,11 @@
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" /> <AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
<!-- {{ procinst.creator }} --> <!-- {{ procinst.creator }} -->
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item> <el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
<div v-if="procinst.duration"> <div v-if="procinst.duration">
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item> <el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item> <el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
</div> </div>
<el-descriptions-item label="流程状态"> <el-descriptions-item label="流程状态">
@@ -86,11 +86,11 @@ import { procinstApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue'; import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums'; import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
import { dateFormat } from '@/common/utils/date';
import ProcdefTasks from './components/ProcdefTasks.vue'; import ProcdefTasks from './components/ProcdefTasks.vue';
import { formatTime } from '@/common/utils/format'; import { formatTime } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumTag from '@/components/enumtag/EnumTag.vue';
import AccountInfo from '@/views/system/account/components/AccountInfo.vue'; import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
import { formatDate } from '@/common/utils/format';
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue')); const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue')); const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue'));

View File

@@ -2,7 +2,7 @@ import Api from '@/common/Api';
export const procdefApi = { export const procdefApi = {
list: Api.newGet('/flow/procdefs'), list: Api.newGet('/flow/procdefs'),
getByKey: Api.newGet('/flow/procdefs/{key}'), getByResource: Api.newGet('/flow/procdefs/{resourceType}/{resourceCode}'),
save: Api.newPost('/flow/procdefs'), save: Api.newPost('/flow/procdefs'),
del: Api.newDelete('/flow/procdefs/{id}'), del: Api.newDelete('/flow/procdefs/{id}'),
}; };

View File

@@ -3,7 +3,7 @@
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey"> <el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
<template #description> <template #description>
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div> <div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
<div v-if="task.completeTime">{{ `${dateFormat(task.completeTime)}` }}</div> <div v-if="task.completeTime">{{ `${formatDate(task.completeTime)}` }}</div>
<div v-if="task.remark">{{ task.remark }}</div> <div v-if="task.remark">{{ task.remark }}</div>
</template> </template>
</el-step> </el-step>
@@ -14,8 +14,7 @@
import { toRefs, reactive, watch, onMounted } from 'vue'; import { toRefs, reactive, watch, onMounted } from 'vue';
import { accountApi } from '../../system/api'; import { accountApi } from '../../system/api';
import { ProcinstTaskStatus } from '../enums'; import { ProcinstTaskStatus } from '../enums';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { procdefApi } from '../api';
import { ElSteps, ElStep } from 'element-plus'; import { ElSteps, ElStep } from 'element-plus';
const props = defineProps({ const props = defineProps({
@@ -23,8 +22,8 @@ const props = defineProps({
tasks: { tasks: {
type: [String, Object], type: [String, Object],
}, },
procdefKey: { procdef: {
type: String, type: [Object],
}, },
// 流程实例任务列表 // 流程实例任务列表
procinstTasks: { procinstTasks: {
@@ -54,7 +53,7 @@ watch(
); );
watch( watch(
() => props.procdefKey, () => props.procdef,
async (newValue: any) => { async (newValue: any) => {
if (newValue) { if (newValue) {
parseTasksByKey(newValue); parseTasksByKey(newValue);
@@ -63,15 +62,14 @@ watch(
); );
onMounted(() => { onMounted(() => {
if (props.procdefKey) { if (props.procdef) {
parseTasksByKey(props.procdefKey); parseTasksByKey(props.procdef);
return; return;
} }
parseTasks(props.tasks); parseTasks(props.tasks);
}); });
const parseTasksByKey = async (key: string) => { const parseTasksByKey = async (procdef: any) => {
const procdef = await procdefApi.getByKey.request({ key });
parseTasks(procdef.tasks); parseTasks(procdef.tasks);
}; };

View File

@@ -1,16 +1,13 @@
<template> <template>
<div> <div>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item> <el-descriptions-item :span="3" label="标签"><TagCodePath :path="db.codePaths" /></el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
<el-descriptions-item :span="1" label="名称">{{ db?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item> <el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型"> <el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }} <SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item> <el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
<el-descriptions-item label="表"> <el-descriptions-item label="表">
@@ -33,7 +30,9 @@ import { dbApi } from '@/views/ops/db/api';
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums'; import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { getDbDialect } from '@/views/ops/db/dialect'; import { getDbDialect } from '@/views/ops/db/dialect';
import ResourceTags from '@/views/ops/component/ResourceTags.vue'; import { tagApi } from '@/views/ops/tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
const props = defineProps({ const props = defineProps({
// 业务key // 业务key
@@ -74,6 +73,10 @@ const getDbSqlExec = async (bizKey: string) => {
state.sqlExec = res.list?.[0]; state.sqlExec = res.list?.[0];
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId }); const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
state.db = dbRes.list?.[0]; state.db = dbRes.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
state.db.codePaths = res.map((item: any) => item.codePath);
});
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,11 +1,10 @@
<template> <template>
<div> <div>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item> <el-descriptions-item :span="3" label="标签"><TagCodePath :path="redis.codePaths" /></el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
<el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></el-descriptions-item> <el-descriptions-item :span="2" label="编号">{{ redis?.code }}</el-descriptions-item>
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item> <el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item> <el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
@@ -22,8 +21,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, watch, onMounted } from 'vue'; import { toRefs, reactive, watch, onMounted } from 'vue';
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
import { redisApi } from '@/views/ops/redis/api'; import { redisApi } from '@/views/ops/redis/api';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
import { tagApi } from '@/views/ops/tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
// 业务表单 // 业务表单
@@ -75,6 +76,10 @@ const parseRunCmdForm = async (bizForm: string) => {
return; return;
} }
state.redis = res.list?.[0]; state.redis = res.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Redis.value, codes: state.redis.code }).then((res) => {
state.redis.codePaths = res.map((item: any) => item.codePath);
});
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,137 +1,541 @@
<template> <template>
<div class="home-container"> <div class="home-container personal">
<el-row :gutter="15"> <el-row :gutter="15">
<el-col :sm="6" class="mb15"> <!-- 个人信息 -->
<div @click="toPage({ id: 'personal' })" class="home-card-item home-card-first"> <el-col :xs="24" :sm="16">
<div class="flex-margin flex"> <el-card shadow="hover" header="个人信息">
<img :src="userInfo.photo" /> <div class="personal-user">
<div class="home-card-first-right ml15"> <div class="personal-user-left">
<div class="flex-margin"> <el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
<div class="home-card-first-right-title">{{ `${currentTime}, ${userInfo.username}` }}</div> <img :src="userInfo.photo" />
</div> </el-upload>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb18"
>{{ currentTime }}{{ userInfo.name }}生活变的再糟糕也不妨碍我变得更好
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">用户名</div>
<div class="personal-item-value">{{ userInfo.username }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">角色</div>
<div class="personal-item-value">{{ roleInfo }}</div>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录IP</div>
<div class="personal-item-value">{{ userInfo.lastLoginIp }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录时间</div>
<div class="personal-item-value">{{ formatDate(userInfo.lastLoginTime) }}</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div> </div>
</div> </div>
</div> </el-card>
</el-col> </el-col>
<el-col :sm="3" class="mb15" v-for="(v, k) in topCardItemList as any" :key="k">
<div @click="toPage(v)" class="home-card-item home-card-item-box" :style="{ background: v.color }"> <!-- 消息通知 -->
<div class="home-card-item-flex"> <el-col :xs="24" :sm="8" class="pl15 personal-info">
<div class="home-card-item-title pb3">{{ v.title }}</div> <el-card shadow="hover">
<div class="home-card-item-title-num pb6" :id="v.id"></div> <template #header>
<span>消息通知</span>
<span @click="showMsgs" class="personal-info-more">更多</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in state.msgs as any" :key="k" class="personal-info-li">
<a class="personal-info-li-title">{{ `[${getMsgTypeDesc(v.type)}] ${v.msg}` }}</a>
</li>
</ul>
</div> </div>
<i :class="v.icon" :style="{ color: v.iconColor }"></i> </el-card>
</div>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" class="mt20 resource-info">
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('machine')">
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.Machine.extra.icon"
:color="TagResourceTypeEnum.Machine.extra.iconColor"
/>
<span class="">{{ state.machine.num }}</span>
</div>
</el-row>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.machine.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="400" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('machine', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('db')">
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon" :color="TagResourceTypeEnum.Db.extra.iconColor" />
<span class="">{{ state.db.num }}</span>
</div>
</el-row>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('db', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt20 resource-info">
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('redis')">
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.Redis.extra.icon"
:color="TagResourceTypeEnum.Redis.extra.iconColor"
/>
<span class="">{{ state.redis.num }}</span>
</div>
</el-row>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.redis.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('redis', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('mongo')">
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.Mongo.extra.icon"
:color="TagResourceTypeEnum.Mongo.extra.iconColor"
/>
<span class="">{{ state.mongo.num }}</span>
</div>
</el-row>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.mongo.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('mongo', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<el-dialog width="900px" title="消息" v-model="msgDialog.visible">
<el-table border :data="msgDialog.msgs.list" size="small">
<el-table-column property="type" label="类型" width="60">
<template #default="scope">
{{ getMsgTypeDesc(scope.row.type) }}
</template>
</el-table-column>
<el-table-column property="msg" label="消息"></el-table-column>
<el-table-column property="createTime" label="时间" width="150">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-row type="flex" class="mt5" justify="center">
<el-pagination
small
@current-change="searchMsg"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="msgDialog.msgs.total"
v-model:current-page="msgDialog.query.pageNum"
:page-size="msgDialog.query.pageSize"
/>
</el-row>
</el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, onMounted, nextTick, computed } from 'vue'; import { toRefs, reactive, onMounted, computed } from 'vue';
// import * as echarts from 'echarts'; // import * as echarts from 'echarts';
import { CountUp } from 'countup.js';
import { formatAxis } from '@/common/utils/format'; import { formatAxis } from '@/common/utils/format';
import { indexApi } from './api'; import { indexApi } from './api';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { personApi } from '../personal/api';
import { formatDate } from '@/common/utils/format';
import SvgIcon from '@/components/svgIcon/index.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { resourceOpLogApi } from '../ops/tag/api';
import TagCodePath from '../ops/component/TagCodePath.vue';
import { useAutoOpenResource } from '@/store/autoOpenResource';
const router = useRouter(); const router = useRouter();
const { userInfo } = storeToRefs(useUserInfo()); const { userInfo } = storeToRefs(useUserInfo());
const state = reactive({ const state = reactive({
topCardItemList: [ accountInfo: {
{ roles: [],
title: 'Linux机器', },
id: 'machineNum', msgs: [],
color: '#F95959', msgDialog: {
visible: false,
query: {
pageSize: 10,
pageNum: 1,
}, },
{ msgs: {
title: '数据库', list: [],
id: 'dbNum', total: null,
color: '#8595F4',
}, },
{ },
title: 'redis', resourceOpTableHeight: 180,
id: 'redisNum', defaultLogSize: 5,
color: '#1abc9c', machine: {
}, num: 0,
{ opLogs: [],
title: 'Mongo', },
id: 'mongoNum', db: {
color: '#FEBB50', num: 0,
}, opLogs: [],
], },
redis: {
num: 0,
opLogs: [],
},
mongo: {
num: 0,
opLogs: [],
},
}); });
const { topCardItemList } = toRefs(state); const { msgDialog } = toRefs(state);
const roleInfo = computed(() => {
if (state.accountInfo.roles.length == 0) {
return '';
}
return state.accountInfo.roles.map((val: any) => val.roleName).join('、');
});
// 当前时间提示语 // 当前时间提示语
const currentTime = computed(() => { const currentTime = computed(() => {
return formatAxis(new Date()); return formatAxis(new Date());
}); });
// 页面加载时
onMounted(() => {
initData();
getAccountInfo();
getMsgs().then((res) => {
state.msgs = res.list;
});
});
const showMsgs = async () => {
state.msgDialog.query.pageNum = 1;
searchMsg();
state.msgDialog.visible = true;
};
const searchMsg = async () => {
state.msgDialog.msgs = await getMsgs();
};
const getMsgTypeDesc = (type: number) => {
if (type == 1) {
return '登录';
}
if (type == 2) {
return '通知';
}
};
const getAccountInfo = async () => {
state.accountInfo = await personApi.accountInfo.request();
};
const getMsgs = async () => {
return await personApi.getMsgs.request(state.msgDialog.query);
};
// 初始化数字滚动 // 初始化数字滚动
const initNumCountUp = async () => { const initData = async () => {
indexApi.machineDashbord.request().then((res: any) => { resourceOpLogApi.getAccountResourceOpLogs
nextTick(() => { .request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
new CountUp('machineNum', res.machineNum).start(); .then((res: any) => {
state.machine.opLogs = res.list;
}); });
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.db.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.redis.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.mongo.opLogs = res.list;
});
indexApi.machineDashbord.request().then((res: any) => {
state.machine.num = res.machineNum;
}); });
indexApi.dbDashbord.request().then((res: any) => { indexApi.dbDashbord.request().then((res: any) => {
nextTick(() => { state.db.num = res.dbNum;
new CountUp('dbNum', res.dbNum).start();
});
}); });
indexApi.redisDashbord.request().then((res: any) => { indexApi.redisDashbord.request().then((res: any) => {
nextTick(() => { state.redis.num = res.redisNum;
new CountUp('redisNum', res.redisNum).start();
});
}); });
indexApi.mongoDashbord.request().then((res: any) => { indexApi.mongoDashbord.request().then((res: any) => {
nextTick(() => { state.mongo.num = res.mongoNum;
new CountUp('mongoNum', res.mongoNum).start();
});
}); });
}; };
const toPage = (item: any) => { const toPage = (item: any, codePath = '') => {
switch (item.id) { let path;
switch (item) {
case 'personal': { case 'personal': {
router.push('/personal'); router.push('/personal');
break; break;
} }
case 'mongoNum': { case 'mongo': {
router.push('/mongo/mongo-data-operation'); useAutoOpenResource().setMongoCodePath(codePath);
path = '/mongo/mongo-data-operation';
break; break;
} }
case 'machineNum': { case 'machine': {
router.push('/machine/machines-op'); useAutoOpenResource().setMachineCodePath(codePath);
path = '/machine/machines-op';
break; break;
} }
case 'dbNum': { case 'db': {
router.push('/dbms/sql-exec'); useAutoOpenResource().setDbCodePath(codePath);
path = '/dbms/sql-exec';
break; break;
} }
case 'redisNum': { case 'redis': {
router.push('/redis/data-operation'); useAutoOpenResource().setRedisCodePath(codePath);
path = '/redis/data-operation';
break; break;
} }
} }
};
// 页面加载时 router.push({ path });
onMounted(() => { };
initNumCountUp();
// initHomeLaboratory();
// initHomeOvertime();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/theme/mixins/index.scss';
.personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 100px;
height: 130px;
border-radius: 3px;
::v-deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: gray;
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: gray;
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: grey;
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
}
.resource-info {
text-align: center;
::v-deep(.el-card__header) {
padding: 2px 20px;
}
.resource-num {
font-weight: 700;
font-size: 2vw;
}
}
.home-container { .home-container {
overflow-x: hidden; overflow-x: hidden;
@@ -182,7 +586,7 @@ onMounted(() => {
} }
.home-card-item-title-num { .home-card-item-title-num {
font-size: 18px; font-size: 2vw;
} }
.home-card-item-tip-num { .home-card-item-tip-num {
@@ -190,124 +594,5 @@ onMounted(() => {
} }
} }
} }
.home-card-first {
background: var(--bg-main-color);
border: 1px solid var(--el-border-color-light, #ebeef5);
display: flex;
align-items: center;
img {
width: 60px;
height: 60px;
border-radius: 100%;
border: 2px solid var(--el-color-primary-light-5);
}
.home-card-first-right {
flex: 1;
display: flex;
flex-direction: column;
.home-card-first-right-msg {
font-size: 13px;
color: gray;
}
}
}
.home-monitor {
height: 200px;
.flex-warp-item {
width: 50%;
height: 100px;
display: flex;
.flex-warp-item-box {
margin: auto;
height: auto;
text-align: center;
}
}
}
.home-warning-card {
height: 292px;
::v-deep(.el-card) {
height: 100%;
}
}
.home-dynamic {
height: 200px;
.home-dynamic-item {
display: flex;
width: 100%;
height: 60px;
overflow: hidden;
&:first-of-type {
.home-dynamic-item-line {
i {
color: orange !important;
}
}
}
.home-dynamic-item-left {
text-align: right;
.home-dynamic-item-left-time1 {
}
.home-dynamic-item-left-time2 {
font-size: 13px;
color: gray;
}
}
.home-dynamic-item-line {
height: 60px;
border-right: 2px dashed #dfdfdf;
margin: 0 20px;
position: relative;
i {
color: var(--el-color-primary);
font-size: 12px;
position: absolute;
top: 1px;
left: -6px;
transform: rotate(46deg);
background: white;
}
}
.home-dynamic-item-right {
flex: 1;
.home-dynamic-item-right-title {
i {
margin-right: 5px;
border: 1px solid #dfdfdf;
width: 20px;
height: 20px;
border-radius: 100%;
padding: 3px 2px 2px;
text-align: center;
color: var(--el-color-primary);
}
}
.home-dynamic-item-right-label {
font-size: 13px;
color: gray;
}
}
}
}
} }
</style> </style>

View File

@@ -132,7 +132,7 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { initRouter } from '@/router/index'; import { initRouter } from '@/router/index';
import { saveToken, saveUser } from '@/common/utils/storage'; import { getRefreshToken, saveRefreshToken, saveToken, saveUser } from '@/common/utils/storage';
import { formatAxis } from '@/common/utils/format'; import { formatAxis } from '@/common/utils/format';
import openApi from '@/common/openApi'; import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa'; import { RsaEncrypt } from '@/common/rsa';
@@ -279,19 +279,20 @@ const login = () => {
}; };
const otpVerify = async () => { const otpVerify = async () => {
otpFormRef.value.validate(async (valid: boolean) => { try {
if (!valid) { await otpFormRef.value.validate();
return false; } catch (e: any) {
} return false;
try { }
state.loading.otpConfirm = true;
const accessToken = await openApi.otpVerify(state.otpDialog.form); try {
await signInSuccess(accessToken); state.loading.otpConfirm = true;
state.otpDialog.visible = false; const res = await openApi.otpVerify(state.otpDialog.form);
} finally { await signInSuccess(res.token, res.refresh_token);
state.loading.otpConfirm = false; state.otpDialog.visible = false;
} } finally {
}); state.loading.otpConfirm = false;
}
}; };
// 登录 // 登录
@@ -327,22 +328,23 @@ const onSignIn = async () => {
}; };
const updateUserInfo = async () => { const updateUserInfo = async () => {
baseInfoFormRef.value.validate(async (valid: boolean) => { try {
if (!valid) { await baseInfoFormRef.value.validate();
return false; } catch (e: any) {
} return false;
try { }
state.loading.updateUserConfirm = true;
const form = state.baseInfoDialog.form; try {
await personApi.updateAccount.request(state.baseInfoDialog.form); state.loading.updateUserConfirm = true;
state.baseInfoDialog.visible = false; const form = state.baseInfoDialog.form;
useUserInfo().userInfo.username = form.username; await personApi.updateAccount.request(state.baseInfoDialog.form);
useUserInfo().userInfo.name = form.name; state.baseInfoDialog.visible = false;
await toIndex(); useUserInfo().userInfo.username = form.username;
} finally { useUserInfo().userInfo.name = form.name;
state.loading.updateUserConfirm = false; await toIndex();
} } finally {
}); state.loading.updateUserConfirm = false;
}
}; };
const loginResDeal = (loginRes: any) => { const loginResDeal = (loginRes: any) => {
@@ -366,7 +368,7 @@ const loginResDeal = (loginRes: any) => {
const token = loginRes.token; const token = loginRes.token;
// 如果不需要 otp校验则该token即为accessToken否则为otp校验token // 如果不需要 otp校验则该token即为accessToken否则为otp校验token
if (loginRes.otp == -1) { if (loginRes.otp == -1) {
signInSuccess(token); signInSuccess(token, loginRes.refresh_token);
return; return;
} }
@@ -379,12 +381,16 @@ const loginResDeal = (loginRes: any) => {
}; };
// 登录成功后的跳转 // 登录成功后的跳转
const signInSuccess = async (accessToken: string = '') => { const signInSuccess = async (accessToken: string = '', refreshToken = '') => {
if (!accessToken) { if (!accessToken) {
accessToken = getToken(); accessToken = getToken();
} }
if (!refreshToken) {
refreshToken = getRefreshToken();
}
// 存储 token 到浏览器缓存 // 存储 token 到浏览器缓存
saveToken(accessToken); saveToken(accessToken);
saveRefreshToken(refreshToken);
// 初始化路由 // 初始化路由
await initRouter(); await initRouter();
@@ -415,26 +421,27 @@ const toIndex = async () => {
}, 300); }, 300);
}; };
const changePwd = () => { const changePwd = async () => {
changePwdFormRef.value.validate(async (valid: boolean) => { try {
if (!valid) { await changePwdFormRef.value.validate();
return false; } catch (e: any) {
} return false;
try { }
state.loading.changePwd = true;
const form = state.changePwdDialog.form; try {
const changePwdReq: any = { ...form }; state.loading.changePwd = true;
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword); const form = state.changePwdDialog.form;
changePwdReq.newPassword = await RsaEncrypt(form.newPassword); const changePwdReq: any = { ...form };
await openApi.changePwd(changePwdReq); changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
ElMessage.success('密码修改成功, 新密码已填充至登录密码框'); changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
state.loginForm.password = state.changePwdDialog.form.newPassword; await openApi.changePwd(changePwdReq);
state.changePwdDialog.visible = false; ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
getCaptcha(); state.loginForm.password = state.changePwdDialog.form.newPassword;
} finally { state.changePwdDialog.visible = false;
state.loading.changePwd = false; getCaptcha();
} } finally {
}); state.loading.changePwd = false;
}
}; };
const cancelChangePwd = () => { const cancelChangePwd = () => {

View File

@@ -39,6 +39,11 @@
/> />
<el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" /> <el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" />
<el-option
:key="TagResourceTypeEnum.Redis.value"
:label="TagResourceTypeEnum.Redis.label"
:value="TagResourceTypeEnum.Redis.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item prop="resourceCode" label="资源编号" required> <el-form-item prop="resourceCode" label="资源编号" required>
@@ -191,12 +196,13 @@ const showResourceEdit = computed(() => {
return state.form.type != AuthCertTypeEnum.Public.value && !props.resourceEdit; return state.form.type != AuthCertTypeEnum.Public.value && !props.resourceEdit;
}); });
watch( watch(dialogVisible, (val: any) => {
() => props.authCert, if (val) {
(val: any) => { setForm(props.authCert);
setForm(val); } else {
cancelEdit();
} }
); });
const setForm = (val: any) => { const setForm = (val: any) => {
val = { ...val }; val = { ...val };
@@ -246,17 +252,18 @@ const getCiphertext = async () => {
const cancelEdit = () => { const cancelEdit = () => {
dialogVisible.value = false; dialogVisible.value = false;
emit('cancel');
setTimeout(() => { setTimeout(() => {
acForm.value?.resetFields();
state.form = { ...DefaultForm }; state.form = { ...DefaultForm };
acForm.value?.resetFields();
emit('cancel');
}, 300); }, 300);
}; };
const btnOk = async () => { const btnOk = async () => {
acForm.value.validate(async (valid: boolean) => { acForm.value.validate(async (valid: boolean) => {
if (valid) { if (valid) {
emit('confirm', state.form); emit('confirm', { ...state.form });
} }
}); });
}; };

View File

@@ -113,9 +113,6 @@ const deleteRow = (idx: any) => {
const cancelEdit = () => { const cancelEdit = () => {
state.dvisible = false; state.dvisible = false;
setTimeout(() => {
state.form = {};
}, 300);
}; };
const btnOk = async (authCert: any) => { const btnOk = async (authCert: any) => {

View File

@@ -17,6 +17,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue'; import { toRefs, reactive, onMounted } from 'vue';
import { machineApi } from '../machine/api'; import { machineApi } from '../machine/api';
import { MachineProtocolEnum } from '../machine/enums';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@@ -46,7 +47,7 @@ onMounted(async () => {
const getSshTunnelMachines = async () => { const getSshTunnelMachines = async () => {
if (state.sshTunnelMachineList.length == 0) { if (state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100, ssh: 1 }); const res = await machineApi.list.request({ pageNum: 1, pageSize: 100, protocol: MachineProtocolEnum.Ssh.value });
state.sshTunnelMachineList = res.list; state.sshTunnelMachineList = res.list;
} }
}; };

View File

@@ -0,0 +1,87 @@
<template>
<div v-if="paths">
<el-row v-for="(path, idx) in paths?.slice(0, 1)" :key="idx">
<span v-for="item in parseTagPath(path)" :key="item.code">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
class="mr2"
/>
<span> {{ item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
<!-- 展示剩余的标签信息 -->
<el-popover :show-after="300" v-if="paths.length > 1 && idx == 0" placement="bottom" width="500" trigger="hover">
<template #reference>
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
</template>
<el-row v-for="i in paths.slice(1)" :key="i">
<span v-for="item in parseTagPath(i)" :key="item.code">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
class="mr2"
/>
<span> {{ item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
</el-row>
</el-popover>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import { computed } from 'vue';
const props = defineProps({
path: {
type: [String, Array<string>],
},
});
const paths = computed(() => {
if (Array.isArray(props.path)) {
return props.path;
}
return [props.path];
});
const parseTagPath = (tagPath: string = '') => {
if (!tagPath) {
return [];
}
const res = [] as any;
const codes = tagPath.split('/');
for (let code of codes) {
const typeAndCode = code.split('|');
if (typeAndCode.length == 1) {
const tagCode = typeAndCode[0];
if (!tagCode) {
continue;
}
res.push({
type: TagResourceTypeEnum.Tag.value,
code: typeAndCode[0],
});
continue;
}
res.push({
type: typeAndCode[0],
code: typeAndCode[1],
});
}
res[res.length - 1].isEnd = true;
return res;
};
</script>
<style lang="scss"></style>

View File

@@ -18,7 +18,7 @@
:default-expanded-keys="props.defaultExpandedKeys" :default-expanded-keys="props.defaultExpandedKeys"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''"> <span :id="node.key" @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
<span v-if="data.type.value == TagTreeNode.TagPath"> <span v-if="data.type.value == TagTreeNode.TagPath">
<tag-info :tag-path="data.label" /> <tag-info :tag-path="data.label" />
</span> </span>
@@ -35,7 +35,9 @@
</slot> </slot>
</span> </span>
<slot :node="node" :data="data" name="suffix"></slot> <span class="label-suffix">
<slot :node="node" :data="data" name="suffix"></slot>
</span>
</span> </span>
</template> </template>
</el-tree> </el-tree>
@@ -46,11 +48,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs } from 'vue'; import { onMounted, reactive, ref, watch, toRefs, nextTick } from 'vue';
import { NodeType, TagTreeNode } from './tag'; import { NodeType, TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue'; import TagInfo from './TagInfo.vue';
import { Contextmenu } from '@/components/contextmenu'; import { Contextmenu } from '@/components/contextmenu';
import { tagApi } from '../tag/api'; import { tagApi } from '../tag/api';
import { isPrefixSubsequence } from '@/common/utils/string';
const props = defineProps({ const props = defineProps({
resourceType: { resourceType: {
@@ -103,8 +106,7 @@ watch(filterText, (val) => {
}); });
const filterNode = (value: string, data: any) => { const filterNode = (value: string, data: any) => {
if (!value) return true; return !value || isPrefixSubsequence(value, data.label);
return data.label.includes(value);
}; };
/** /**
@@ -124,7 +126,7 @@ const loadTags = async () => {
* @param { Object } node * @param { Object } node
* @param { Object } resolve * @param { Object } resolve
*/ */
const loadNode = async (node: any, resolve: any) => { const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
if (typeof resolve !== 'function') { if (typeof resolve !== 'function') {
return; return;
} }
@@ -139,6 +141,8 @@ const loadNode = async (node: any, resolve: any) => {
} }
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
// 调用 reject 以保持节点状态,并允许远程加载继续。
return reject();
} }
return resolve(nodes); return resolve(nodes);
}; };
@@ -203,8 +207,25 @@ const getNode = (nodeKey: any) => {
return node; return node;
}; };
const setCurrentKey = (nodeKey: any) => {
treeRef.value.setCurrentKey(nodeKey);
// 通过Id获取到对应的dom元素
const node = document.getElementById(nodeKey);
if (node) {
setTimeout(() => {
nextTick(() => {
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
node.scrollIntoView({ block: 'center' });
});
}, 100);
}
};
defineExpose({ defineExpose({
reloadNode, reloadNode,
getNode,
setCurrentKey,
}); });
</script> </script>
@@ -216,5 +237,13 @@ defineExpose({
display: inline-block; display: inline-block;
min-width: 100%; min-width: 100%;
} }
.label-suffix {
position: absolute;
right: 10px;
color: #c4c9c4;
font-size: 10px;
margin-top: 2px;
}
} }
</style> </style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="w100 tag-tree-check">
<el-input v-model="filterTag" @input="onFilterValChanged" clearable placeholder="输入关键字过滤" size="small" />
<div class="mt3" style="border: 1px solid var(--el-border-color)">
<el-scrollbar :style="{ height: props.height }">
<el-tree
v-bind="$attrs"
ref="tagTreeRef"
:data="state.tags"
:default-expanded-keys="state.defaultExpandedKeys"
:default-checked-keys="checkedTags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
:node-key="$props.nodeKey"
:props="{
value: $props.nodeKey,
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span>
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.iconColor"
/>
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
</span>
</span>
</template>
</el-tree>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import { isPrefixSubsequence } from '@/common/utils/string';
const props = defineProps({
height: {
type: [String, Number],
default: 'calc(100vh - 330px)',
},
tagType: {
type: [Number, Array<Number>],
default: TagResourceTypeEnum.Tag.value,
},
nodeKey: {
type: String,
default: 'codePath',
},
});
const checkedTags = defineModel<Array<any>>('modelValue', {
default: () => [],
});
const tagTreeRef: any = ref(null);
const filterTag = ref('');
const state = reactive({
defaultExpandedKeys: [] as any,
tags: [],
});
onMounted(() => {
state.defaultExpandedKeys = checkedTags.value;
search();
});
const search = async () => {
let tagType: any = props.tagType;
if (Array.isArray(props.tagType)) {
tagType = props.tagType.join(',');
}
state.tags = await tagApi.getTagTrees.request({ type: tagType });
setTimeout(() => {
const checkedNodes = tagTreeRef.value.getCheckedNodes();
console.log('check nodes: ', checkedNodes);
// 禁用选中节点的所有父节点,不可选中
for (let checkNodeData of checkedNodes) {
disableParentNodes(tagTreeRef.value.getNode(checkNodeData.codePath).parent);
}
}, 200);
};
const filterNode = (value: string, data: any) => {
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
};
const onFilterValChanged = (val: string) => {
tagTreeRef.value!.filter(val);
};
const tagTreeNodeCheck = (data: any) => {
const node = tagTreeRef.value.getNode(data.codePath);
console.log('check node: ', node);
if (node.checked) {
// 如果选中了子节点,则需要将父节点全部取消选中,并禁用父节点
unCheckParentNodes(node.parent);
disableParentNodes(node.parent);
} else {
// 如果取消了选中,则需要根据条件恢复父节点的选中状态
disableParentNodes(node.parent, false);
}
// 更新绑定的值
checkedTags.value = tagTreeRef.value.getCheckedKeys(false);
};
const unCheckParentNodes = (node: any) => {
if (!node) {
return;
}
tagTreeRef.value.setChecked(node, false, false);
unCheckParentNodes(node.parent);
};
/**
* 禁用该节点以及所有父节点
* @param node 节点
* @param disable 是否禁用
*/
const disableParentNodes = (node: any, disable = true) => {
if (!node) {
return;
}
if (!disable) {
// 恢复为非禁用状态时,若同层级存在一个选中状态或者禁用状态,则继续禁用 不恢复非禁用状态。
for (let oneLevelNodes of node.childNodes) {
if (oneLevelNodes.checked || oneLevelNodes.data.disabled) {
return;
}
}
}
node.data.disabled = disable;
disableParentNodes(node.parent, disable);
};
</script>
<style lang="scss" scoped>
.tag-tree-check {
.el-tree {
min-width: 100%;
// 横向滚动生效
display: inline-block;
}
}
</style>

View File

@@ -6,8 +6,7 @@
@change="changeTag" @change="changeTag"
:data="tags" :data="tags"
placeholder="请选择关联标签" placeholder="请选择关联标签"
:render-after-expand="true" :default-expanded-keys="defaultExpandedKeys"
:default-expanded-keys="[state.selectTags]"
show-checkbox show-checkbox
node-key="codePath" node-key="codePath"
:props="{ :props="{
@@ -18,6 +17,7 @@
> >
<template #default="{ data }"> <template #default="{ data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" class="mr2" />
<span style="font-size: 13px"> <span style="font-size: 13px">
{{ data.code }} {{ data.code }}
<span style="color: #3c8dbc"></span> <span style="color: #3c8dbc"></span>
@@ -32,16 +32,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue'; import { toRefs, reactive, onMounted, computed } from 'vue';
import { tagApi } from '../tag/api'; import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
//定义事件 //定义事件
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']); const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
const props = defineProps({ const props = defineProps({
selectTags: { selectTags: {
type: [Array<any>], type: [Array<any>, Object],
}, },
tagType: { tagType: {
type: Number, type: Number,
@@ -57,6 +58,16 @@ const state = reactive({
const { tags } = toRefs(state); const { tags } = toRefs(state);
const defaultExpandedKeys = computed(() => {
if (Array.isArray(state.selectTags)) {
// 如果 state.selectTags 是数组,直接返回
return state.selectTags;
}
// 如果 state.selectTags 不是数组,转换为包含 state.selectTags 的数组
return [state.selectTags];
});
onMounted(async () => { onMounted(async () => {
state.selectTags = props.selectTags; state.selectTags = props.selectTags;
state.tags = await tagApi.getTagTrees.request({ type: props.tagType }); state.tags = await tagApi.getTagTrees.request({ type: props.tagType });

View File

@@ -171,3 +171,44 @@ export function getTagPathSearchItem(resourceType: number) {
}) })
); );
} }
/**
* 根据标签路径获取对应的类型与编号数组
* @param codePath 编号路径 tag1/tag2/1|xxx/11|yyy/
* @returns {1: ['xxx'], 11: ['yyy']}
*/
export function getTagTypeCodeByPath(codePath: string) {
const result = {};
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
for (let part of parts) {
if (!part) {
continue;
}
let [key, value] = part.split('|'); // 分割数字和值部分
// 如果不存在第二个参数,则说明为标签类型
if (!value) {
value = key;
key = '-1';
}
if (!result[key]) {
result[key] = [];
}
result[key].push(value);
}
return result;
}
export function expandCodePath(codePath: string) {
const parts = codePath.split('/');
const result = [];
let currentPath = '';
for (let i = 0; i < parts.length - 1; i++) {
currentPath += parts[i] + '/';
result.push(currentPath);
}
return result;
}

View File

@@ -23,7 +23,7 @@
</el-form-item> </el-form-item>
<el-form-item prop="authCertName" label="授权凭证" required> <el-form-item prop="authCertName" label="授权凭证" required>
<el-select @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable> <el-select v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
<el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name"> <el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
{{ item.name }} {{ item.name }}
@@ -39,8 +39,15 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item prop="getDatabaseMode" label="获库方式" required>
<el-select v-model="form.getDatabaseMode" @change="onChangeGetDatabaseMode" placeholder="请选择库名获取方式">
<el-option v-for="item in DbGetDbNamesMode" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="database" label="数据库名"> <el-form-item prop="database" label="数据库名">
<el-select <el-select
:disabled="form.getDatabaseMode == DbGetDbNamesMode.Auto.value || !form.authCertName"
v-model="dbNamesSelected" v-model="dbNamesSelected"
multiple multiple
clearable clearable
@@ -49,8 +56,9 @@
filterable filterable
:filter-method="filterDbNames" :filter-method="filterDbNames"
allow-create allow-create
placeholder="请确保数据库实例信息填写完整后获取库名" placeholder="获库方式为‘指定库名’时,可选择"
style="width: 100%" @focus="getAllDatabase(form.authCertName)"
:loading="state.loadingDbNames"
> >
<template #header> <template #header>
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox> <el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
@@ -62,8 +70,6 @@
<el-form-item prop="remark" label="备注"> <el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input> <el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item> </el-form-item>
<procdef-select-form-item v-model="form.flowProcdefKey" />
</el-form> </el-form>
<template #footer> <template #footer>
@@ -77,12 +83,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, watch, ref, watchEffect } from 'vue'; import { toRefs, reactive, watch, ref } from 'vue';
import { dbApi } from './api'; import { dbApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
// import TagTreeSelect from '../component/TagTreeSelect.vue';
import type { CheckboxValueType } from 'element-plus'; import type { CheckboxValueType } from 'element-plus';
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
import { DbType } from '@/views/ops/db/dialect'; import { DbType } from '@/views/ops/db/dialect';
import { ResourceCodePattern } from '@/common/pattern'; import { ResourceCodePattern } from '@/common/pattern';
@@ -90,6 +94,7 @@ import EnumTag from '@/components/enumtag/EnumTag.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums'; import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import { resourceAuthCertApi } from '../tag/api'; import { resourceAuthCertApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { DbGetDbNamesMode } from './enums';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -144,10 +149,17 @@ const rules = {
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
database: [ authCertName: [
{ {
required: true, required: true,
message: '请添加数据库', message: '请选择授权凭证',
trigger: ['change', 'blur'],
},
],
getDatabaseMode: [
{
required: true,
message: '请选择库名获取方式',
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
@@ -169,39 +181,45 @@ const state = reactive({
authCerts: [] as any, authCerts: [] as any,
form: { form: {
id: null, id: null,
// tagId: [],
name: null, name: null,
code: '', code: '',
getDatabaseMode: DbGetDbNamesMode.Auto.value,
database: '', database: '',
remark: '', remark: '',
instanceId: null as any, instanceId: null as any,
authCertName: '', authCertName: '',
flowProcdefKey: '',
}, },
instances: [] as any, instances: [] as any,
loadingDbNames: false,
}); });
const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state); const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state);
watchEffect(() => { watch(
state.dialogVisible = props.visible; () => props.visible,
if (!state.dialogVisible) { () => {
return; state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
const db: any = props.db;
if (db.code) {
state.form = { ...db };
if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
// 将数据库名使用空格切割,获取所有数据库列表
state.dbNamesSelected = db.database.split(' ');
}
} else {
state.form = { getDatabaseMode: DbGetDbNamesMode.Auto.value } as any;
state.dbNamesSelected = [];
}
} }
const db: any = props.db; );
if (db.code) {
state.form = { ...db }; const onChangeGetDatabaseMode = (val: any) => {
// state.form.tagId = newValue.db.tags.map((t: any) => t.tagId); if (val == DbGetDbNamesMode.Auto.value) {
// 将数据库名使用空格切割,获取所有数据库列表
state.dbNamesSelected = db.database.split(' ');
} else {
state.form = {} as any;
state.dbNamesSelected = []; state.dbNamesSelected = [];
} }
});
const changeAuthCert = (val: string) => {
getAllDatabase(val);
}; };
const getAuthCerts = async () => { const getAuthCerts = async () => {
@@ -215,15 +233,20 @@ const getAuthCerts = async () => {
}; };
const getAllDatabase = async (authCertName: string) => { const getAllDatabase = async (authCertName: string) => {
const req = { ...(props.instance as any) }; try {
req.authCert = state.authCerts?.find((x: any) => x.name == authCertName); state.loadingDbNames = true;
let dbs = await dbApi.getAllDatabase.request(req); const req = { ...(props.instance as any) };
state.allDatabases = dbs; req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
let dbs = await dbApi.getAllDatabase.request(req);
state.allDatabases = dbs;
// 如果是oracle且没查出数据库列表则取实例sid // 如果是oracle且没查出数据库列表则取实例sid
let instance = state.instances.find((item: any) => item.id === state.form.instanceId); let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
if (instance && instance.type === DbType.oracle && dbs.length === 0) { if (instance && instance.type === DbType.oracle && dbs.length === 0) {
state.allDatabases = [instance.sid]; state.allDatabases = [instance.sid];
}
} finally {
state.loadingDbNames = false;
} }
}; };
@@ -235,18 +258,14 @@ const open = async () => {
}; };
const btnOk = async () => { const btnOk = async () => {
dbForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await dbForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
emit('confirm', state.form); emit('confirm', state.form);
// await saveDbExec();
// ElMessage.success('保存成功');
// emit('val-change', state.form);
// cancel();
});
}; };
const resetInputDb = () => { const resetInputDb = () => {

View File

@@ -1,93 +1,112 @@
<template> <template>
<div class="db-list"> <div class="db-list">
<page-table <el-drawer
ref="pageTableRef" :title="title"
:page-api="dbApi.dbs" v-model="dialogVisible"
:before-query-fn="checkRouteTagPath" @open="search"
:search-items="searchItems" :before-close="cancel"
v-model:query-form="query" :destroy-on-close="true"
:columns="columns" :close-on-click-modal="true"
lazy size="60%"
> >
<template #instanceSelect> <template #header>
<el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable> <DrawerHeader :header="title" :back="cancel">
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id"> <template #extra>
{{ item.name }} <div class="mr20">
<el-divider direction="vertical" border-style="dashed" /> <span>{{ $props.instance?.tags?.[0]?.codePath }}</span>
<el-divider direction="vertical" border-style="dashed" />
{{ item.type }} / {{ item.host }}:{{ item.port }} <SvgIcon :name="getDbDialect($props.instance?.type).getInfo()?.icon" :size="20" />
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
{{ item.username }} <span>{{ $props.instance?.host }}:{{ $props.instance?.port }}</span>
</el-option> </div>
</el-select>
</template>
<template #type="{ data }">
<el-tooltip :content="data.type" placement="top">
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
</el-tooltip>
</template>
<template #host="{ data }">
{{ `${data.host}:${data.port}` }}
</template>
<template #database="{ data }">
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button @click="state.currentDbs = data.database" type="primary" link>查看库</el-button>
</template> </template>
<el-table :data="filterDbs" size="small"> </DrawerHeader>
<el-table-column prop="dbName" label="数据库">
<template #header>
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
</template>
</el-table-column>
</el-table>
</el-popover>
</template> </template>
<template #tagPath="{ data }"> <page-table
<ResourceTags :tags="data.tags" /> ref="pageTableRef"
</template> :page-api="dbApi.dbs"
v-model:query-form="query"
:columns="columns"
lazy
show-selection
v-model:selection-data="state.selectionData"
>
<template #tableHeader>
<el-button v-auth="perms.saveDb" type="primary" circle icon="Plus" @click="editDb(null)"> </el-button>
<el-button v-auth="perms.delDb" :disabled="state.selectionData.length < 1" @click="deleteDb" type="danger" circle icon="delete"></el-button>
</template>
<template #action="{ data }"> <template #type="{ data }">
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button> <el-tooltip :content="data.type" placement="top">
<el-divider direction="vertical" border-style="dashed" /> <SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
</el-tooltip>
</template>
<el-dropdown @command="handleMoreActionCommand"> <template #database="{ data }">
<span class="el-dropdown-link-more"> <el-popover placement="bottom" :width="200" trigger="click">
更多 <template #reference>
<el-icon class="el-icon--right"> <el-button @click="getDbNames(data)" type="primary" link>查看库</el-button>
<arrow-down /> </template>
</el-icon> <el-table :data="filterDbs" v-loading="state.loadingDbNames" size="small">
</span> <el-table-column prop="dbName" label="数据库">
<template #dropdown> <template #header>
<el-dropdown-menu> <el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item> </template>
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item> </el-table-column>
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"> </el-table>
备份任务 </el-popover>
</el-dropdown-item> </template>
<el-dropdown-item
:command="{ type: 'backupHistory', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份历史
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'restoreDb', data }"
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复任务
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</page-table>
<el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible"> <template #tagPath="{ data }">
<ResourceTags :tags="data.tags" />
</template>
<template #action="{ data }">
<el-button v-auth="perms.saveDb" @click="editDb(data)" type="primary" link>编辑</el-button>
<el-divider direction="vertical" border-style="dashed" />
<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: 'dumpDb', data }"> 导出 </el-dropdown-item>
<el-dropdown-item
:command="{ type: 'backupDb', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份任务
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'backupHistory', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份历史
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'restoreDb', data }"
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复任务
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</page-table>
</el-drawer>
<el-dialog width="750px" :title="`${exportDialog.db} 数据库导出`" v-model="exportDialog.visible">
<el-row justify="space-between"> <el-row justify="space-between">
<el-col :span="9"> <el-col :span="9">
<el-form-item label="导出内容: "> <el-form-item label="导出内容: ">
@@ -168,128 +187,98 @@
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" /> <db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
</el-dialog> </el-dialog>
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog"> <db-edit
<el-descriptions title="详情" :column="3" border> @confirm="confirmEditDb"
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item> @cancel="cancelEditDb"
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item> :title="dbEditDialog.title"
v-model:visible="dbEditDialog.visible"
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item> :instance="props.instance"
<el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item> v-model:db="dbEditDialog.data"
></db-edit>
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="授权凭证">{{ infoDialog.instance.authCertName }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
<el-descriptions-item :span="3" label="工单流程key">{{ infoDialog.data?.flowProcdefKey }}</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue'; import { computed, defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
import { dbApi } from './api'; import { dbApi } from './api';
import config from '@/common/config'; import config from '@/common/config';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
import { isTrue } from '@/common/assert'; import { isTrue } from '@/common/assert';
import { dateFormat } from '@/common/utils/date';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import DbSqlExecLog from './DbSqlExecLog.vue'; import DbSqlExecLog from './DbSqlExecLog.vue';
import { DbType } from './dialect'; import { DbType } from './dialect';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { useRoute } from 'vue-router';
import { getDbDialect } from './dialect/index'; import { getDbDialect } from './dialect/index';
import { getTagPathSearchItem } from '../component/tag';
import { SearchItem } from '@/components/SearchForm';
import DbBackupList from './DbBackupList.vue'; import DbBackupList from './DbBackupList.vue';
import DbBackupHistoryList from './DbBackupHistoryList.vue'; import DbBackupHistoryList from './DbBackupHistoryList.vue';
import DbRestoreList from './DbRestoreList.vue'; import DbRestoreList from './DbRestoreList.vue';
import ResourceTags from '../component/ResourceTags.vue'; import ResourceTags from '../component/ResourceTags.vue';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import { DbGetDbNamesMode } from './enums';
import { DbInst } from './db';
import { ElMessage, ElMessageBox } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue')); const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
const searchItems = [ const props = defineProps({
getTagPathSearchItem(TagResourceTypeEnum.DbName.value), instance: {
SearchItem.slot('instanceId', '实例', 'instanceSelect'), type: [Object],
SearchItem.input('code', '编号'), required: true,
]; },
title: {
type: String,
},
});
const dialogVisible = defineModel<boolean>('visible');
const emit = defineEmits(['cancel']);
const columns = ref([ const columns = ref([
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
TableColumn.new('name', '名称'), TableColumn.new('name', '名称'),
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
TableColumn.new('instanceName', '实例名'),
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
TableColumn.new('authCertName', '授权凭证'), TableColumn.new('authCertName', '授权凭证'),
TableColumn.new('getDatabaseMode', '获库方式').typeTag(DbGetDbNamesMode),
TableColumn.new('database', '库').isSlot().setMinWidth(80), TableColumn.new('database', '库').isSlot().setMinWidth(80),
TableColumn.new('flowProcdefKey', '关联流程'),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('code', '编号'), TableColumn.new('code', '编号'),
TableColumn.new('action', '操作').isSlot().setMinWidth(210).fixedRight().alignCenter(),
]); ]);
const perms = { const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
backupDb: 'db:backup', backupDb: 'db:backup',
restoreDb: 'db:restore', restoreDb: 'db:restore',
}; };
// 该用户拥有的的操作列按钮权限
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
const actionBtns = hasPerms(Object.values(perms)); const actionBtns = hasPerms(Object.values(perms));
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
const route = useRoute();
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);
const state = reactive({ const state = reactive({
row: {} as any, loadingDbNames: false,
dbId: 0, currentDbNames: [],
db: '',
currentDbs: '',
dbNameSearch: '', dbNameSearch: '',
instances: [] as any, instances: [] as any,
/** /**
* 选中的数据 * 选中的数据
*/ */
selectionData: [], selectionData: [] as any,
/** /**
* 查询条件 * 查询条件
*/ */
query: { query: {
tagPath: '', instanceId: 0,
instanceId: null,
pageNum: 1, pageNum: 1,
pageSize: 0, pageSize: 0,
}, },
infoDialog: {
visible: false,
data: null as any,
instance: null as any,
query: {
instanceId: 0,
},
},
// sql执行记录弹框 // sql执行记录弹框
sqlExecLogDialog: { sqlExecLogDialog: {
title: '', title: '',
visible: false, visible: false,
dbs: [], dbs: [] as any,
dbId: 0, dbId: 0,
}, },
// 数据库备份弹框 // 数据库备份弹框
@@ -320,6 +309,7 @@ const state = reactive({
exportDialog: { exportDialog: {
visible: false, visible: false,
dbId: 0, dbId: 0,
db: '',
type: 3, type: 3,
data: [] as any, data: [] as any,
value: [], value: [],
@@ -338,64 +328,77 @@ const state = reactive({
}, },
}); });
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state); const { query, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
onMounted(async () => { const search = async () => {
if (Object.keys(actionBtns).length > 0) { state.query.instanceId = props.instance?.id;
columns.value.push(actionColumn); pageTableRef.value.search();
};
const getDbNames = async (db: any) => {
try {
state.loadingDbNames = true;
state.currentDbNames = await DbInst.getDbNames(db);
} finally {
state.loadingDbNames = false;
} }
search(); };
});
const filterDbs = computed(() => { const filterDbs = computed(() => {
const dbsStr = state.currentDbs; const dbNames = state.currentDbNames;
if (!dbsStr) { if (!dbNames) {
return []; return [];
} }
const dbs = dbsStr.split(' ').map((db: any) => { const dbNameObjs = dbNames.map((x) => {
return { dbName: db }; return {
dbName: x,
};
}); });
return dbs.filter((db: any) => { return dbNameObjs.filter((db: any) => {
return db.dbName.includes(state.dbNameSearch); return db.dbName.includes(state.dbNameSearch);
}); });
}); });
const checkRouteTagPath = (query: any) => { const editDb = (data: any) => {
if (route.query.tagPath) {
query.tagPath = route.query.tagPath as string;
}
return query;
};
const search = async (tagPath: string = '') => {
if (tagPath) {
state.query.tagPath = tagPath;
}
pageTableRef.value.search();
};
const showInfo = async (info: any) => {
state.infoDialog.data = info;
state.infoDialog.query.instanceId = info.instanceId;
const res = await dbApi.getInstance.request(state.infoDialog.query);
state.infoDialog.instance = res;
state.infoDialog.visible = true;
};
const onBeforeCloseInfoDialog = () => {
state.infoDialog.visible = false;
state.infoDialog.data = null;
state.infoDialog.instance = null;
};
const getInstances = async (instanceName = '') => {
if (!instanceName) {
state.instances = [];
return;
}
const data = await dbApi.instances.request({ name: instanceName });
if (data) { if (data) {
state.instances = data.list; state.dbEditDialog.data = { ...data };
} else {
state.dbEditDialog.data = {
instanceId: props.instance.id,
};
}
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
state.dbEditDialog.visible = true;
};
const confirmEditDb = async (db: any) => {
db.instanceId = props.instance.id;
await dbApi.saveDb.request(db);
ElMessage.success('保存成功');
search();
cancelEditDb();
};
const cancelEditDb = () => {
state.dbEditDialog.visible = false;
state.dbEditDialog.data = {};
};
const deleteDb = async () => {
try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
for (let db of state.selectionData) {
await dbApi.deleteDb.request({ id: db.id });
}
ElMessage.success('删除成功');
} catch (err) {
//
} finally {
search();
} }
}; };
@@ -403,10 +406,6 @@ const handleMoreActionCommand = (commond: any) => {
const data = commond.data; const data = commond.data;
const type = commond.type; const type = commond.type;
switch (type) { switch (type) {
case 'detail': {
showInfo(data);
return;
}
case 'dumpDb': { case 'dumpDb': {
onDumpDbs(data); onDumpDbs(data);
return; return;
@@ -429,7 +428,9 @@ const handleMoreActionCommand = (commond: any) => {
const onShowSqlExec = async (row: any) => { const onShowSqlExec = async (row: any) => {
state.sqlExecLogDialog.title = `${row.name}`; state.sqlExecLogDialog.title = `${row.name}`;
state.sqlExecLogDialog.dbId = row.id; state.sqlExecLogDialog.dbId = row.id;
state.sqlExecLogDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.sqlExecLogDialog.visible = true; state.sqlExecLogDialog.visible = true;
}; };
@@ -442,26 +443,32 @@ const onBeforeCloseSqlExecDialog = () => {
const onShowDbBackupDialog = async (row: any) => { const onShowDbBackupDialog = async (row: any) => {
state.dbBackupDialog.title = `${row.name}`; state.dbBackupDialog.title = `${row.name}`;
state.dbBackupDialog.dbId = row.id; state.dbBackupDialog.dbId = row.id;
state.dbBackupDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.dbBackupDialog.visible = true; state.dbBackupDialog.visible = true;
}; };
const onShowDbBackupHistoryDialog = async (row: any) => { const onShowDbBackupHistoryDialog = async (row: any) => {
state.dbBackupHistoryDialog.title = `${row.name}`; state.dbBackupHistoryDialog.title = `${row.name}`;
state.dbBackupHistoryDialog.dbId = row.id; state.dbBackupHistoryDialog.dbId = row.id;
state.dbBackupHistoryDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.dbBackupHistoryDialog.visible = true; state.dbBackupHistoryDialog.visible = true;
}; };
const onShowDbRestoreDialog = async (row: any) => { const onShowDbRestoreDialog = async (row: any) => {
state.dbRestoreDialog.title = `${row.name}`; state.dbRestoreDialog.title = `${row.name}`;
state.dbRestoreDialog.dbId = row.id; state.dbRestoreDialog.dbId = row.id;
state.dbRestoreDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.dbRestoreDialog.visible = true; state.dbRestoreDialog.visible = true;
}; };
const onDumpDbs = async (row: any) => { const onDumpDbs = async (row: any) => {
const dbs = row.database.split(' '); const dbs = await DbInst.getDbNames(row);
const data = []; const data = [];
for (let name of dbs) { for (let name of dbs) {
data.push({ data.push({
@@ -469,6 +476,7 @@ const onDumpDbs = async (row: any) => {
label: name, label: name,
}); });
} }
state.exportDialog.db = row.name;
state.exportDialog.value = []; state.exportDialog.value = [];
state.exportDialog.data = data; state.exportDialog.data = data;
state.exportDialog.dbId = row.id; state.exportDialog.dbId = row.id;
@@ -512,7 +520,10 @@ const supportAction = (action: string, dbType: string): boolean => {
return actions.includes(action); return actions.includes(action);
}; };
defineExpose({ search }); const cancel = () => {
dialogVisible.value = false;
emit('cancel');
};
</script> </script>
<style lang="scss"> <style lang="scss">
.db-list { .db-list {

View File

@@ -45,14 +45,14 @@
<el-descriptions :column="1" border> <el-descriptions :column="1" border>
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item> <el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{ <el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
dateFormat(infoDialog.data.pointInTime) formatDate(infoDialog.data.pointInTime)
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{ <el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
infoDialog.data.dbBackupHistoryName infoDialog.data.dbBackupHistoryName
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item> <el-descriptions-item :span="1" label="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item> <el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item> <el-descriptions-item :span="1" label="执行时间">{{ formatDate(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item> <el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
@@ -66,7 +66,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue')); const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);

View File

@@ -259,6 +259,7 @@ const handleSrcTableCheckChange = (data: { id: string; name: string }, checked:
} }
} }
if (data.id && (data.id + '').startsWith('list-item')) { if (data.id && (data.id + '').startsWith('list-item')) {
//
} }
}; };

View File

@@ -1,171 +0,0 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-table :data="state.dbs" stripe>
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100"> </el-table-column>
<el-table-column prop="authCertName" label="授权凭证" min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="database" label="库" min-width="80">
<template #default="scope">
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button @click="state.currentDbs = scope.row.database" type="primary" link>查看库</el-button>
</template>
<el-table :data="filterDbs" size="small">
<el-table-column prop="dbName" label="数据库">
<template #header>
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
<el-table-column prop="flowProcdefKey" label="关联流程" min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="code" label="编号" show-overflow-tooltip min-width="120"> </el-table-column>
<el-table-column min-wdith="120px">
<template #header>
操作
<el-button v-auth="perms.saveDb" type="primary" circle size="small" icon="Plus" @click="editDb(null)"> </el-button>
</template>
<template #default="scope">
<el-button v-auth="perms.saveDb" @click="editDb(scope.row)" type="primary" icon="edit" link></el-button>
<el-button class="ml1" v-auth="perms.delDb" type="danger" @click="deleteDb(scope.row)" icon="delete" link></el-button>
</template>
</el-table-column>
</el-table>
<db-edit
@confirm="confirmEditDb"
@cancel="cancelEditDb"
:title="dbEditDialog.title"
v-model:visible="dbEditDialog.visible"
:instance="props.instance"
v-model:db="dbEditDialog.data"
></db-edit>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, toRefs, watchEffect } from 'vue';
import { dbApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import DbEdit from './DbEdit.vue';
const props = defineProps({
visible: {
type: Boolean,
},
instance: {
type: [Object],
required: true,
},
title: {
type: String,
},
});
const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
};
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const state = reactive({
dialogVisible: false,
dbs: [] as any,
currentDbs: '', // 当前数据库名,空格分割库名
dbNameSearch: '',
dbEditDialog: {
visible: false,
data: null as any,
title: '新增数据库',
},
});
const { dialogVisible, dbEditDialog } = toRefs(state);
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
getDbs();
});
const filterDbs = computed(() => {
const dbsStr = state.currentDbs;
if (!dbsStr) {
return [];
}
const dbs = dbsStr.split(' ').map((db: any) => {
return { dbName: db };
});
return dbs.filter((db: any) => {
return db.dbName.includes(state.dbNameSearch);
});
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
const getDbs = () => {
dbApi.dbs.request({ pageSize: 200, instanceId: props.instance.id }).then((res: any) => {
state.dbs = res.list || [];
});
};
const editDb = (data: any) => {
if (data) {
state.dbEditDialog.data = { ...data };
} else {
state.dbEditDialog.data = {
instanceId: props.instance.id,
};
}
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
state.dbEditDialog.visible = true;
};
const deleteDb = async (db: any) => {
try {
await ElMessageBox.confirm(`确定删除【${db.name}】库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDb.request({ id: db.id });
ElMessage.success('删除成功');
getDbs();
} catch (err) {
//
}
};
const confirmEditDb = async (db: any) => {
db.instanceId = props.instance.id;
await dbApi.saveDb.request(db);
ElMessage.success('保存成功');
getDbs();
cancelEditDb();
};
const cancelEditDb = () => {
state.dbEditDialog.visible = false;
state.dbEditDialog.data = {};
};
</script>
<style lang="scss"></style>

View File

@@ -267,33 +267,33 @@ const getReqForm = async () => {
}; };
const testConn = async (authCert: any) => { const testConn = async (authCert: any) => {
dbForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await dbForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
state.submitForm = await getReqForm(); state.submitForm = await getReqForm();
state.submitForm.authCerts = [authCert]; state.submitForm.authCerts = [authCert];
await testConnExec(); await testConnExec();
ElMessage.success('连接成功'); ElMessage.success('连接成功');
});
}; };
const btnOk = async () => { const btnOk = async () => {
dbForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await dbForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
state.submitForm = await getReqForm(); state.submitForm = await getReqForm();
await saveInstanceExec(); await saveInstanceExec();
ElMessage.success('保存成功'); ElMessage.success('保存成功');
state.form.id = saveInstanceRes as any; state.form.id = saveInstanceRes as any;
emit('val-change', state.form); emit('val-change', state.form);
cancel(); cancel();
});
}; };
const cancel = () => { const cancel = () => {

View File

@@ -35,7 +35,7 @@
<template #action="{ data }"> <template #action="{ data }">
<el-button @click="showInfo(data)" link>详情</el-button> <el-button @click="showInfo(data)" link>详情</el-button>
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button> <el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>配置</el-button> <el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>管理</el-button>
</template> </template>
</page-table> </page-table>
@@ -53,10 +53,10 @@
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item> <el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item> <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="更新时间">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item> <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
@@ -68,7 +68,7 @@
v-model:data="instanceEditDialog.data" v-model:data="instanceEditDialog.data"
></instance-edit> ></instance-edit>
<instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" /> <DbList :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
</div> </div>
</template> </template>
@@ -76,7 +76,7 @@
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue'; import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api'; import { dbApi } from './api';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
@@ -89,7 +89,7 @@ import { getTagPathSearchItem } from '../component/tag';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue')); const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue')); const DbList = defineAsyncComponent(() => import('./DbList.vue'));
const props = defineProps({ const props = defineProps({
lazy: { lazy: {
@@ -215,7 +215,7 @@ const deleteInstance = async () => {
const editDb = (data: any) => { const editDb = (data: any) => {
state.dbEditDialog.instance = data; state.dbEditDialog.instance = data;
state.dbEditDialog.title = `配置 "${data.name}" 数据库`; state.dbEditDialog.title = `管理 "${data.name}" 数据库`;
state.dbEditDialog.visible = true; state.dbEditDialog.visible = true;
}; };

View File

@@ -2,7 +2,12 @@
<div class="db-sql-exec"> <div class="db-sql-exec">
<Splitpanes class="default-theme"> <Splitpanes class="default-theme">
<Pane size="20" max-size="30"> <Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.DbName.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef"> <tag-tree
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.DbName.value"
:tag-path-node-type="NodeTypeTagPath"
ref="tagTreeRef"
>
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type.value == SqlExecNodeType.DbInst"> <span v-if="data.type.value == SqlExecNodeType.DbInst">
<el-popover <el-popover
@@ -42,10 +47,8 @@
</template> </template>
<template #suffix="{ data }"> <template #suffix="{ data }">
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span> <span 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">{{ <span v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{ ` ${data.params.dbTableSize}` }}</span>
` ${data.params.dbTableSize}`
}}</span>
</template> </template>
</tag-tree> </tag-tree>
</Pane> </Pane>
@@ -55,16 +58,61 @@
<el-row> <el-row>
<el-col :span="24" v-if="state.db"> <el-col :span="24" v-if="state.db">
<el-descriptions :column="4" size="small" border> <el-descriptions :column="4" size="small" border>
<el-descriptions-item label-align="right" label="操作" <el-descriptions-item label-align="right" label="操作">
><el-button <el-button
:disabled="!state.db || !nowDbInst.id" :disabled="!state.db || !nowDbInst.id"
type="primary" type="primary"
icon="Search" icon="Search"
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)" link
size="small" @click="
>新建查询</el-button addQueryTab(
></el-descriptions-item { id: nowDbInst.id, dbs: nowDbInst.databases, nodeKey: getSqlMenuNodeKey(nowDbInst.id, state.db) },
> state.db
)
"
title="新建查询"
>
</el-button>
<template v-if="!dbConfig.locationTreeNode">
<el-divider direction="vertical" border-style="dashed" />
<el-button @click="locationNowTreeNode(null)" title="定位至左侧树的指定位置" icon="Location" link></el-button>
</template>
<el-divider direction="vertical" border-style="dashed" />
<!-- 数据库展示配置 -->
<el-popover
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
placement="bottom"
width="auto"
title="数据库展示配置"
trigger="click"
>
<el-row>
<el-checkbox
v-model="dbConfig.showColumnComment"
label="显示字段备注"
:true-value="1"
:false-value="0"
size="small"
/>
</el-row>
<el-row>
<el-checkbox
v-model="dbConfig.locationTreeNode"
label="自动定位树节点"
:true-value="1"
:false-value="0"
size="small"
/>
</el-row>
<template #reference>
<el-link type="primary" icon="setting" :underline="false"></el-link>
</template>
</el-popover>
</el-descriptions-item>
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item> <el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
@@ -100,7 +148,9 @@
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key"> <el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label> <template #label>
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250"> <el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<template #reference> {{ dt.label }} </template> <template #reference>
<span class="font12">{{ dt.label }}</span>
</template>
<template #default> <template #default>
<el-descriptions :column="1" size="small"> <el-descriptions :column="1" size="small">
<el-descriptions-item label="tagPath"> <el-descriptions-item label="tagPath">
@@ -127,6 +177,7 @@
:db-name="dt.db" :db-name="dt.db"
:table-name="dt.params.table" :table-name="dt.params.table"
:table-height="state.dataTabsTableHeight" :table-height="state.dataTabsTableHeight"
:ref="(el: any) => (dt.componentRef = el)"
></db-table-data-op> ></db-table-data-op>
<db-sql-editor <db-sql-editor
@@ -135,6 +186,7 @@
:db-name="dt.db" :db-name="dt.db"
:sql-name="dt.params.sqlName" :sql-name="dt.params.sqlName"
@save-sql-success="reloadSqls" @save-sql-success="reloadSqls"
:ref="(el: any) => (dt.componentRef = el)"
> >
</db-sql-editor> </db-sql-editor>
@@ -143,7 +195,7 @@
:db-id="dt.params.id" :db-id="dt.params.id"
:db="dt.params.db" :db="dt.params.db"
:db-type="dt.params.type" :db-type="dt.params.type"
:flow-procdef-key="dt.params.flowProcdefKey" :flow-procdef="dt.params.flowProcdef"
:height="state.tablesOpHeight" :height="state.tablesOpHeight"
/> />
</el-tab-pane> </el-tab-pane>
@@ -158,7 +210,7 @@
:dbId="tableCreateDialog.dbId" :dbId="tableCreateDialog.dbId"
:db="tableCreateDialog.db" :db="tableCreateDialog.db"
:dbType="tableCreateDialog.dbType" :dbType="tableCreateDialog.dbType"
:flow-procdef-key="tableCreateDialog.flowProcdefKey" :flow-procdef="tableCreateDialog.flowProcdef"
:data="tableCreateDialog.data" :data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible" v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitEditTableSql" @submit-sql="onSubmitEditTableSql"
@@ -167,11 +219,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue'; import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus'; import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db'; import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { NodeType, TagTreeNode } from '../component/tag'; import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { dbApi } from './api'; import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider'; import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
@@ -181,8 +233,11 @@ import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes'; import { Pane, Splitpanes } from 'splitpanes';
import { useEventListener } from '@vueuse/core'; import { useEventListener, useStorage } from '@vueuse/core';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox'; import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
import { procdefApi } from '@/views/flow/api';
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue')); const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue')); const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
@@ -236,7 +291,7 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
type: params.type, type: params.type,
tagPath: params.tagPath, tagPath: params.tagPath,
databases: params.dbs, databases: params.dbs,
flowProcdefKey: params.flowProcdefKey, flowProcdef: params.flowProcdef,
}, },
params.db params.db
); );
@@ -258,15 +313,17 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
await sleep(100); await sleep(100);
return dbInfos?.map((x: any) => { return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key; x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x); return new TagTreeNode(`${x.code}`, x.name, NodeTypeDbInst).withParams(x);
}); });
}) })
.withContextMenuItems([ContextmenuItemRefresh]); .withContextMenuItems([ContextmenuItemRefresh]);
// 数据库实例节点类型 // 数据库实例节点类型
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => { const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params; const params = parentNode.params;
const dbs = params.database.split(' ')?.sort(); const dbs = (await DbInst.getDbNames(params))?.sort();
const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.DbName.value, resourceCode: params.code });
return dbs.map((x: any) => { return dbs.map((x: any) => {
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb) return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
.withParams({ .withParams({
@@ -277,7 +334,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
host: `${params.host}:${params.port}`, host: `${params.host}:${params.port}`,
dbs: dbs, dbs: dbs,
db: x, db: x,
flowProcdefKey: params.flowProcdefKey, flowProcdef: flowProcdef,
}) })
.withIcon(DbIcon); .withIcon(DbIcon);
}); });
@@ -338,7 +395,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
]) ])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => { .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params; const params = parentNode.params;
let { id, db, type, flowProcdefKey, schema } = params; let { id, db, type, flowProcdef, schema } = params;
// 获取当前库的所有表信息 // 获取当前库的所有表信息
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus); let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false; state.reloadStatus = false;
@@ -354,7 +411,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
db, db,
type, type,
schema, schema,
flowProcdefKey: flowProcdefKey, flowProcdef: flowProcdef,
key: key, key: key,
parentKey: parentNode.key, parentKey: parentNode.key,
tableName: x.tableName, tableName: x.tableName,
@@ -418,6 +475,7 @@ const tagTreeRef: any = ref(null);
const tabs: Map<string, TabInfo> = new Map(); const tabs: Map<string, TabInfo> = new Map();
const state = reactive({ const state = reactive({
defaultExpendKey: [] as any,
/** /**
* 当前操作的数据库实例 * 当前操作的数据库实例
*/ */
@@ -439,7 +497,7 @@ const state = reactive({
dbId: 0, dbId: 0,
db: '', db: '',
dbType: '', dbType: '',
flowProcdefKey: '', flowProcdef: null as any,
data: {}, data: {},
parentKey: '', parentKey: '',
}, },
@@ -447,12 +505,18 @@ const state = reactive({
const { nowDbInst, tableCreateDialog } = toRefs(state); const { nowDbInst, tableCreateDialog } = toRefs(state);
const dbConfig = useStorage('dbConfig', DbThemeConfig);
const serverInfoReqParam = ref({ const serverInfoReqParam = ref({
instanceId: 0, instanceId: 0,
}); });
const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam); const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam);
const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
onMounted(() => { onMounted(() => {
autoOpenDb(autoOpenResource.value.dbCodePath);
setHeight(); setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度 // 监听浏览器窗口大小变化,更新对应组件高度
useEventListener(window, 'resize', setHeight); useEventListener(window, 'resize', setHeight);
@@ -462,6 +526,31 @@ onBeforeUnmount(() => {
dispposeCompletionItemProvider('sql'); dispposeCompletionItemProvider('sql');
}); });
watch(
() => autoOpenResource.value.dbCodePath,
(codePath: any) => {
autoOpenDb(codePath);
}
);
const autoOpenDb = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
state.defaultExpendKey = [tagPath, dbCode];
setTimeout(() => {
// 置空
autoOpenResourceStore.setDbCodePath('');
tagTreeRef.value.setCurrentKey(dbCode);
}, 1000);
};
/** /**
* 设置editor高度和数据表高度 * 设置editor高度和数据表高度
*/ */
@@ -492,7 +581,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
} }
changeDb(db, dbName); changeDb(db, dbName);
const key = `${db.id}:\`${dbName}\`.${tableName}`; const key = `tableData:${db.id}.${dbName}.${tableName}`;
let tab = state.tabs.get(key); let tab = state.tabs.get(key);
state.activeName = key; state.activeName = key;
// 如果存在该表tab则直接返回 // 如果存在该表tab则直接返回
@@ -527,7 +616,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
// 存在sql模板名则该模板名只允许一个tab // 存在sql模板名则该模板名只允许一个tab
if (sqlName) { if (sqlName) {
label = `查询-${sqlName}`; label = `查询-${sqlName}`;
key = `查询:${dbId}:${dbName}.${sqlName}`; key = `query:${dbId}.${dbName}.${sqlName}`;
} else { } else {
let count = 1; let count = 1;
state.tabs.forEach((v) => { state.tabs.forEach((v) => {
@@ -536,7 +625,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
} }
}); });
label = `新查询-${count}`; label = `新查询-${count}`;
key = `新查询${count}:${dbId}:${dbName}`; key = `query:${count}.${dbId}.${dbName}`;
} }
state.activeName = key; state.activeName = key;
let tab = state.tabs.get(key); let tab = state.tabs.get(key);
@@ -573,7 +662,7 @@ const addTablesOpTab = async (db: any) => {
changeDb(db, dbName); changeDb(db, dbName);
const dbId = db.id; const dbId = db.id;
let key = `表操作:${dbId}:${dbName}.tablesOp`; let key = `tablesOp:${dbId}.${dbName}`;
state.activeName = key; state.activeName = key;
let tab = state.tabs.get(key); let tab = state.tabs.get(key);
@@ -604,15 +693,22 @@ const onRemoveTab = (targetName: string) => {
if (tabName !== targetName) { if (tabName !== targetName) {
continue; continue;
} }
state.tabs.delete(targetName);
if (activeName != targetName) {
break;
}
// 如果删除的tab是当前激活的tab则切换到前一个或后一个tab
const nextTab = tabNames[i + 1] || tabNames[i - 1]; const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) { if (nextTab) {
activeName = nextTab; activeName = nextTab;
} else { } else {
activeName = ''; activeName = '';
} }
state.tabs.delete(targetName);
state.activeName = activeName; state.activeName = activeName;
onTabChange(); onTabChange();
break;
} }
}; };
@@ -631,6 +727,23 @@ const onTabChange = () => {
// 注册sql提示 // 注册sql提示
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type); registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
} }
// 激活当前tab需要调用DbTableData组件的active否则表头与数据会出现错位暂不知为啥先这样处理
nowTab?.componentRef?.active();
if (dbConfig.value.locationTreeNode) {
locationNowTreeNode(nowTab);
}
};
/**
* 定位至当前树节点
*/
const locationNowTreeNode = (nowTab: any = null) => {
if (!nowTab) {
nowTab = state.tabs.get(state.activeName);
}
tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey);
}; };
const reloadSqls = (dbId: number, db: string) => { const reloadSqls = (dbId: number, db: string) => {
@@ -662,7 +775,7 @@ const reloadNode = (nodeKey: string) => {
}; };
const onEditTable = async (data: any) => { const onEditTable = async (data: any) => {
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdefKey } = data.params; let { db, id, tableName, tableComment, type, parentKey, key, flowProcdef } = data.params;
// data.label就是表名 // data.label就是表名
if (tableName) { if (tableName) {
state.tableCreateDialog.title = '修改表'; state.tableCreateDialog.title = '修改表';
@@ -681,12 +794,12 @@ const onEditTable = async (data: any) => {
state.tableCreateDialog.dbId = id; state.tableCreateDialog.dbId = id;
state.tableCreateDialog.db = db; state.tableCreateDialog.db = db;
state.tableCreateDialog.dbType = type; state.tableCreateDialog.dbType = type;
state.tableCreateDialog.flowProcdefKey = flowProcdefKey; state.tableCreateDialog.flowProcdef = flowProcdef;
state.tableCreateDialog.visible = true; state.tableCreateDialog.visible = true;
}; };
const onDeleteTable = async (data: any) => { const onDeleteTable = async (data: any) => {
let { db, id, tableName, parentKey, flowProcdefKey, schema } = data.params; let { db, id, tableName, parentKey, flowProcdef, schema } = data.params;
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', { await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@@ -698,7 +811,7 @@ const onDeleteTable = async (data: any) => {
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : ''; let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => { dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
if (flowProcdefKey) { if (flowProcdef) {
ElMessage.success('工单提交成功'); ElMessage.success('工单提交成功');
return; return;
} }
@@ -710,7 +823,7 @@ const onDeleteTable = async (data: any) => {
}; };
const onRenameTable = async (data: any) => { const onRenameTable = async (data: any) => {
let { db, id, tableName, parentKey, flowProcdefKey } = data.params; let { db, id, tableName, parentKey, flowProcdef } = data.params;
let tableData = { db, oldTableName: tableName, tableName }; let tableData = { db, oldTableName: tableName, tableName };
let value = ref(tableName); let value = ref(tableName);
@@ -733,7 +846,7 @@ const onRenameTable = async (data: any) => {
dbId: id as any, dbId: id as any,
db: db as any, db: db as any,
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect, dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
flowProcdefKey: flowProcdefKey, flowProcdef: flowProcdef,
runSuccessCallback: () => { runSuccessCallback: () => {
setTimeout(() => { setTimeout(() => {
parentKey && reloadNode(parentKey); parentKey && reloadNode(parentKey);
@@ -793,7 +906,7 @@ const getNowDbInfo = () => {
name: di.name, name: di.name,
type: di.type, type: di.type,
host: di.host, host: di.host,
flowProcdefKey: di.flowProcdefKey, flowProcdef: di.flowProcdef,
dbName: state.db, dbName: state.db,
}; };
}; };
@@ -801,13 +914,8 @@ const getNowDbInfo = () => {
<style lang="scss"> <style lang="scss">
.db-sql-exec { .db-sql-exec {
.db-table-size {
color: #c4c9c4;
font-size: 9px;
}
.db-op { .db-op {
height: calc(100vh - 108px); height: calc(100vh - 106px);
} }
#data-exec { #data-exec {
@@ -819,7 +927,7 @@ const getNowDbInfo = () => {
margin: 0 0 5px; margin: 0 0 5px;
.el-tabs__item { .el-tabs__item {
padding: 0 10px; padding: 0 5px;
} }
} }

View File

@@ -115,7 +115,7 @@
<el-option <el-option
v-for="item in state.targetColumnList" v-for="item in state.targetColumnList"
:key="item.columnName" :key="item.columnName"
:label="item.columnName + ` ${item.showDataType}` + (item.columnComment && ' - ' + item.columnComment)" :label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
:value="item.columnName" :value="item.columnName"
/> />
</el-select> </el-select>

View File

@@ -23,7 +23,10 @@ export const dbApi = {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log(param.sql); console.log(param.sql);
} }
param.sql = Base64.encode(param.sql); // 非base64编码sql则进行base64编码refreshToken时会重复调用该方法故简单判断下
if (!Base64.isValid(param.sql)) {
param.sql = Base64.encode(param.sql);
}
} }
return param; return param;
}), }),
@@ -40,6 +43,7 @@ export const dbApi = {
instances: Api.newGet('/instances'), instances: Api.newGet('/instances'),
getInstance: Api.newGet('/instances/{instanceId}'), getInstance: Api.newGet('/instances/{instanceId}'),
getAllDatabase: Api.newPost('/instances/databases'), getAllDatabase: Api.newPost('/instances/databases'),
getDbNamesByAc: Api.newGet('/instances/databases/{authCertName}'),
getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'), getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
testConn: Api.newPost('/instances/test-conn'), testConn: Api.newPost('/instances/test-conn'),
saveInstance: Api.newPost('/instances'), saveInstance: Api.newPost('/instances'),

View File

@@ -25,6 +25,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect'; import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue'; import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { DbInst } from '../db';
const props = defineProps({ const props = defineProps({
dbId: { dbId: {
@@ -101,9 +102,9 @@ const noSchemaType = (type: string) => {
}; };
// 数据库实例节点类型 // 数据库实例节点类型
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => { const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params; const params = parentNode.params;
const dbs = params.database.split(' ')?.sort(); const dbs = (await DbInst.getDbNames(params))?.sort();
let fn: NodeType; let fn: NodeType;
if (noSchemaType(params.type)) { if (noSchemaType(params.type)) {
fn = MysqlNodeTypes; fn = MysqlNodeTypes;

View File

@@ -52,7 +52,7 @@
<Pane :size="100 - state.editorSize"> <Pane :size="100 - state.editorSize">
<div class="mt5 sql-exec-res h100"> <div class="mt5 sql-exec-res h100">
<el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" v-model="state.activeTab"> <el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" @tab-change="active" v-model="state.activeTab">
<el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id"> <el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
<template #label> <template #label>
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300"> <el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
@@ -296,44 +296,37 @@ const onRunSql = async (newTab = false) => {
notBlank(sql && sql.trim(), '请选中需要执行的sql'); notBlank(sql && sql.trim(), '请选中需要执行的sql');
// 去除字符串前的空格、换行等 // 去除字符串前的空格、换行等
sql = sql.replace(/(^\s*)/g, ''); sql = sql.replace(/(^\s*)/g, '');
let execRemark = '';
let canRun = true;
// 简单截取前十个字符 // 简单截取前十个字符
const sqlPrefix = sql.slice(0, 10).toLowerCase(); const sqlPrefix = sql.slice(0, 10).toLowerCase();
if ( const nonQuery =
sqlPrefix.startsWith('update') || sqlPrefix.startsWith('update') ||
sqlPrefix.startsWith('insert') || sqlPrefix.startsWith('insert') ||
sqlPrefix.startsWith('delete') || sqlPrefix.startsWith('delete') ||
sqlPrefix.startsWith('alert') || sqlPrefix.startsWith('alert') ||
sqlPrefix.startsWith('drop') || sqlPrefix.startsWith('drop') ||
sqlPrefix.startsWith('create') sqlPrefix.startsWith('create');
) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', { // 启用工单审批
confirmButtonText: '确定', if (nonQuery && getNowDbInst().flowProcdef) {
cancelButtonText: '取消', try {
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/, getNowDbInst().promptExeSql(props.dbName, sql, null, () => {
inputErrorMessage: '请输入执行该sql的备注信息', ElMessage.success('工单提交成功');
}); });
execRemark = res.value; } catch (e) {
if (!execRemark) { ElMessage.success('工单提交失败');
canRun = false;
} }
}
if (!canRun) {
return; return;
} }
// 启用工单审批 let execRemark;
if (execRemark && getNowDbInst().flowProcdefKey) { if (nonQuery) {
try { const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
await getNowDbInst().runSql(props.dbName, sql, execRemark); confirmButtonText: '确定',
ElMessage.success('工单提交成功'); cancelButtonText: '取消',
return; inputErrorMessage: '输入执行该sql的备注信息',
} catch (e) { });
ElMessage.success('工单提交失败'); execRemark = res.value;
return;
}
} }
let execRes: ExecResTab; let execRes: ExecResTab;
@@ -707,6 +700,19 @@ const initMonacoEditor = () => {
}, },
}); });
}; };
const active = () => {
const resTab = state.execResTabs[state.activeTab - 1];
if (!resTab || !resTab.dbTableRef) {
return;
}
resTab.dbTableRef?.active();
};
defineExpose({
active,
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -6,7 +6,7 @@ export type SqlExecProps = {
dbId: number; dbId: number;
db: string; db: string;
dbType?: string; dbType?: string;
flowProcdefKey?: string; flowProcdef?: any;
runSuccessCallback?: Function; runSuccessCallback?: Function;
cancelCallback?: Function; cancelCallback?: Function;
}; };

View File

@@ -1,18 +1,18 @@
<template> <template>
<div> <div>
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px"> <el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" :close-on-click-modal="false">
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" /> <monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
<el-input <el-input
@keyup.enter="runSql" @keyup.enter="runSql"
ref="remarkInputRef" ref="remarkInputRef"
v-model="remark" v-model="remark"
:placeholder="props.flowProcdefKey ? '执行备注(必填)' : '执行备注(选填)'" :placeholder="props.flowProcdef ? '执行备注(必填)' : '执行备注(选填)'"
class="mt5" class="mt5"
/> />
<div v-if="props.flowProcdefKey"> <div v-if="props.flowProcdef">
<el-divider content-position="left">审批节点</el-divider> <el-divider content-position="left">审批节点</el-divider>
<procdef-tasks :procdef-key="props.flowProcdefKey" /> <procdef-tasks :procdef="props.flowProcdef" />
</div> </div>
<template #footer> <template #footer>
@@ -59,13 +59,15 @@ onMounted(() => {
*/ */
const runSql = async () => { const runSql = async () => {
// 存在流程审批,则备注为必填 // 存在流程审批,则备注为必填
if (!state.remark && props.flowProcdefKey) { if (!state.remark && props.flowProcdef) {
ElMessage.error('请输入执行的备注信息'); ElMessage.error('请输入执行的备注信息');
return; return;
} }
try { try {
state.btnLoading = true; state.btnLoading = true;
runSuccess = true;
const res = await dbApi.sqlExec.request({ const res = await dbApi.sqlExec.request({
id: props.dbId, id: props.dbId,
db: props.db, db: props.db,
@@ -74,8 +76,7 @@ const runSql = async () => {
}); });
// 存在流程审批 // 存在流程审批
if (props.flowProcdefKey) { if (props.flowProcdef) {
runSuccess = false;
ElMessage.success('工单提交成功'); ElMessage.success('工单提交成功');
return; return;
} }
@@ -87,7 +88,6 @@ const runSql = async () => {
} }
} }
runSuccess = true;
ElMessage.success('执行成功'); ElMessage.success('执行成功');
} catch (e) { } catch (e) {
runSuccess = false; runSuccess = false;
@@ -96,9 +96,9 @@ const runSql = async () => {
if (props.runSuccessCallback) { if (props.runSuccessCallback) {
props.runSuccessCallback(); props.runSuccessCallback();
} }
cancel();
} }
state.btnLoading = false; state.btnLoading = false;
cancel();
} }
}; };
@@ -113,7 +113,7 @@ const cancel = () => {
}; };
const open = () => { const open = () => {
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType || 'mysql' }); state.sqlValue = sqlFormatter(props.sql, { language: (props.dbType || 'mysql') as any });
state.dialogVisible = true; state.dialogVisible = true;
setTimeout(() => { setTimeout(() => {
remarkInputRef.value?.focus(); remarkInputRef.value?.focus();

View File

@@ -1,7 +1,6 @@
<template> <template>
<div class="string-input-container w100" v-if="dataType == DataType.String"> <div class="string-input-container w100" v-if="dataType == DataType.String || dataType == DataType.Number">
<el-input <el-input
v-if="dataType == DataType.String"
:ref="(el: any) => focus && el?.focus()" :ref="(el: any) => focus && el?.focus()"
:disabled="disabled" :disabled="disabled"
@blur="handleBlur" @blur="handleBlur"
@@ -13,18 +12,6 @@
<SvgIcon v-if="showEditorIcon" @mousedown="openEditor" class="string-input-container-icon" name="FullScreen" :size="10" /> <SvgIcon v-if="showEditorIcon" @mousedown="openEditor" class="string-input-container-icon" name="FullScreen" :size="10" />
</div> </div>
<el-input
v-else-if="dataType == DataType.Number"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@blur="handleBlur"
class="w100 mb4"
size="small"
v-model.number="itemValue"
:placeholder="placeholder"
type="number"
/>
<el-date-picker <el-date-picker
v-else-if="dataType == DataType.Date" v-else-if="dataType == DataType.Date"
:ref="(el: any) => focus && el?.focus()" :ref="(el: any) => focus && el?.focus()"
@@ -38,7 +25,7 @@
:clearable="false" :clearable="false"
type="Date" type="Date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="选择日期" :placeholder="`选择日期-${placeholder}`"
/> />
<el-date-picker <el-date-picker
@@ -54,7 +41,7 @@
:clearable="false" :clearable="false"
type="datetime" type="datetime"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期时间" :placeholder="`选择日期时间-${placeholder}`"
/> />
<el-time-picker <el-time-picker
@@ -69,13 +56,13 @@
v-model="itemValue" v-model="itemValue"
:clearable="false" :clearable="false"
value-format="HH:mm:ss" value-format="HH:mm:ss"
placeholder="选择时间" :placeholder="`选择时间-${placeholder}`"
/> />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, Ref } from 'vue'; import { computed, ref, Ref } from 'vue';
import { ElInput } from 'element-plus'; import { ElInput, ElMessage } from 'element-plus';
import { DataType } from '../../dialect/index'; import { DataType } from '../../dialect/index';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import MonacoEditorDialog from '@/components/monaco/MonacoEditorDialog'; import MonacoEditorDialog from '@/components/monaco/MonacoEditorDialog';
@@ -130,6 +117,10 @@ const handleBlur = () => {
if (editorOpening.value) { if (editorOpening.value) {
return; return;
} }
if (props.dataType == DataType.Number && itemValue.value && !/^-?\d*\.?\d+$/.test(itemValue.value)) {
ElMessage.error('输入内容与类型不匹配');
return;
}
emit('update:modelValue', itemValue.value); emit('update:modelValue', itemValue.value);
emit('blur'); emit('blur');
}; };
@@ -161,6 +152,10 @@ const getEditorLangByValue = (value: any) => {
<style lang="scss"> <style lang="scss">
.string-input-container { .string-input-container {
position: relative; position: relative;
.el-input__wrapper {
padding: 1px 3px;
}
} }
.string-input-container-show-icon { .string-input-container-show-icon {
.el-input__inner { .el-input__inner {
@@ -183,6 +178,10 @@ const getEditorLangByValue = (value: any) => {
.el-input__prefix { .el-input__prefix {
display: none; display: none;
} }
.el-input__wrapper {
padding: 1px 3px;
}
} }
.edit-time-picker-popper { .edit-time-picker-popper {

View File

@@ -15,6 +15,7 @@
fixed fixed
class="table" class="table"
:row-event-handlers="rowEventHandlers" :row-event-handlers="rowEventHandlers"
@scroll="onTableScroll"
> >
<template #header="{ columns }"> <template #header="{ columns }">
<div v-for="(column, i) in columns" :key="i"> <div v-for="(column, i) in columns" :key="i">
@@ -36,7 +37,7 @@
<!-- 字段列的数据类型 --> <!-- 字段列的数据类型 -->
<div class="column-type"> <div class="column-type">
<span v-if="column.dataTypeSubscript === 'icon-clock'"> <span v-if="column.dataTypeSubscript === 'icon-clock'">
<SvgIcon :size="10" name="Clock" style="cursor: unset" /> <SvgIcon :size="9" name="Clock" style="cursor: unset" />
</span> </span>
<span class="font8" v-else>{{ column.dataTypeSubscript }}</span> <span class="font8" v-else>{{ column.dataTypeSubscript }}</span>
</div> </div>
@@ -59,9 +60,7 @@
</div> </div>
<div v-else class="header-column-title"> <div v-else class="header-column-title">
<b class="el-text"> <b class="el-text"> {{ column.title }} </b>
{{ column.title }}
</b>
</div> </div>
<!-- 字段列右部分内容 --> <!-- 字段列右部分内容 -->
@@ -96,7 +95,7 @@
/> />
</div> </div>
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''"> <div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml2 mr2' : 'ml2 mr2'">
<span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span> <span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated"> <span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
@@ -121,7 +120,7 @@
<template #empty> <template #empty>
<div style="text-align: center"> <div style="text-align: center">
<el-empty class="h100" :description="props.emptyText" :image-size="100" /> <el-empty :description="props.emptyText" :image-size="100" />
</div> </div>
</template> </template>
</el-table-v2> </el-table-v2>
@@ -157,11 +156,11 @@
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue'; import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElInput, ElMessage } from 'element-plus'; import { ElInput, ElMessage } from 'element-plus';
import { copyToClipboard } from '@/common/utils/string'; import { copyToClipboard } from '@/common/utils/string';
import { DbInst } from '@/views/ops/db/db'; import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { exportCsv, exportFile } from '@/common/utils/export'; import { exportCsv, exportFile } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { useIntervalFn, useStorage } from '@vueuse/core'; import { useIntervalFn, useStorage } from '@vueuse/core';
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index'; import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue'; import ColumnFormItem from './ColumnFormItem.vue';
@@ -259,12 +258,10 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
return state.table == ''; return state.table == '';
}); });
const cmDataEdit = new ContextmenuItem('editData', '编辑行') const cmFormView = new ContextmenuItem('formView', '表单视图').withIcon('Document').withOnClick(() => onEditRowData());
.withIcon('edit') // .withHideFunc(() => {
.withOnClick(() => onEditRowData()) // return state.table == '';
.withHideFunc(() => { // });
return state.table == '';
});
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL') const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
.withIcon('tickets') .withIcon('tickets')
@@ -364,7 +361,7 @@ const state = reactive({
const { tableHeight, datas } = toRefs(state); const { tableHeight, datas } = toRefs(state);
const dbConfig = useStorage('dbConfig', { showColumnComment: false }); const dbConfig = useStorage('dbConfig', DbThemeConfig);
/** /**
* 行号字段列 * 行号字段列
@@ -476,9 +473,9 @@ const setTableColumns = (columns: any) => {
state.columns = columns.map((x: any) => { state.columns = columns.map((x: any) => {
const columnName = x.columnName; const columnName = x.columnName;
// 数据类型 // 数据类型
x.dataType = dbDialect.getDataType(x.dataType); x.dataType = dbDialect.getDataType(x.columnType);
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType]; x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
x.remark = `${x.showDataType} ${x.columnComment ? ' | ' + x.columnComment : ''}`; x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
return { return {
...x, ...x,
@@ -486,7 +483,7 @@ const setTableColumns = (columns: any) => {
dataKey: columnName, dataKey: columnName,
width: DbInst.flexColumnWidth(columnName, state.datas), width: DbInst.flexColumnWidth(columnName, state.datas),
title: columnName, title: columnName,
align: 'center', align: x.dataType == DataType.Number ? 'right' : 'left',
headerClass: 'table-column', headerClass: 'table-column',
class: 'table-column', class: 'table-column',
sortable: true, sortable: true,
@@ -596,7 +593,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
const { clientX, clientY } = event; const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX; state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY; state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql]; state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
contextmenuRef.value.openContextmenu({ column, rowData: data }); contextmenuRef.value.openContextmenu({ column, rowData: data });
}; };
@@ -617,6 +614,10 @@ const onDeleteData = async () => {
const db = state.db; const db = state.db;
const dbInst = getNowDbInst(); const dbInst = getNowDbInst();
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => { dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
// 存在流程则恢复原值,需工单流程审批完后自动执行
if (dbInst.flowProcdef) {
return;
}
emits('dataDelete', deleteDatas); emits('dataDelete', deleteDatas);
}); });
}; };
@@ -624,12 +625,12 @@ const onDeleteData = async () => {
const onEditRowData = () => { const onEditRowData = () => {
const selectionDatas = Array.from(selectionRowsMap.values()); const selectionDatas = Array.from(selectionRowsMap.values());
if (selectionDatas.length > 1) { if (selectionDatas.length > 1) {
ElMessage.warning('只能编辑一行数据'); ElMessage.warning('只能选择一行数据');
return; return;
} }
const data = selectionDatas[0]; const data = selectionDatas[0];
state.tableDataFormDialog.data = data; state.tableDataFormDialog.data = { ...data };
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`; state.tableDataFormDialog.title = state.table ? `'${props.table}'表单数据` : '表单视图';
state.tableDataFormDialog.visible = true; state.tableDataFormDialog.visible = true;
}; };
@@ -645,7 +646,7 @@ const onGenerateJson = async () => {
// 按列字段重新排序对象key // 按列字段重新排序对象key
const jsonObj = []; const jsonObj = [];
for (let selectionData of selectionDatas) { for (let selectionData of selectionDatas) {
let obj = {}; let obj: any = {};
for (let column of state.columns) { for (let column of state.columns) {
if (column.show) { if (column.show) {
obj[column.title] = selectionData[column.dataKey]; obj[column.title] = selectionData[column.dataKey];
@@ -674,13 +675,13 @@ const onExportCsv = () => {
columnNames.push(column.columnName); columnNames.push(column.columnName);
} }
} }
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList); exportCsv(`数据导出-${state.table}-${formatDate(new Date(), 'yyyyMMddHHmm')}`, columnNames, dataList);
}; };
const onExportSql = async () => { const onExportSql = async () => {
const selectionDatas = state.datas; const selectionDatas = state.datas;
exportFile( exportFile(
`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}.sql`, `数据导出-${state.table}-${formatDate(new Date(), 'yyyyMMddHHmm')}.sql`,
await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas) await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas)
); );
}; };
@@ -749,7 +750,7 @@ const submitUpdateFields = async () => {
for (let updateRow of cellUpdateMap.values()) { for (let updateRow of cellUpdateMap.values()) {
const rowData = { ...updateRow.rowData }; const rowData = { ...updateRow.rowData };
let updateColumnValue = {}; let updateColumnValue: any = {};
for (let k of updateRow.columnsMap.keys()) { for (let k of updateRow.columnsMap.keys()) {
const v = updateRow.columnsMap.get(k); const v = updateRow.columnsMap.get(k);
@@ -763,7 +764,12 @@ const submitUpdateFields = async () => {
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData); res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
} }
dbInst.promptExeSql(db, res, cancelUpdateFields, () => { dbInst.promptExeSql(db, res, null, () => {
// 存在流程则恢复原值,需工单流程审批完后自动执行
if (dbInst.flowProcdef) {
cancelUpdateFields();
return;
}
triggerRefresh(); triggerRefresh();
cellUpdateMap.clear(); cellUpdateMap.clear();
changeUpdatedField(); changeUpdatedField();
@@ -810,11 +816,11 @@ const getFormatTimeValue = (dataType: DataType, originValue: string): string =>
switch (dataType) { switch (dataType) {
case DataType.Time: case DataType.Time:
return dateStrFormat('HH:mm:ss', originValue); return formatDate(originValue, 'HH:mm:ss');
case DataType.Date: case DataType.Date:
return dateStrFormat('yyyy-MM-dd', originValue); return formatDate(originValue, 'YYYY-MM-DD');
case DataType.DateTime: case DataType.DateTime:
return dateStrFormat('yyyy-MM-dd HH:mm:ss', originValue); return formatDate(originValue, 'YYYY-MM-DD HH:mm:ss');
default: default:
return originValue; return originValue;
} }
@@ -832,11 +838,23 @@ const triggerRefresh = () => {
} }
}; };
const scrollLeftValue = ref(0);
const onTableScroll = (param: any) => {
scrollLeftValue.value = param.scrollLeft;
};
/**
* 激活表格,恢复滚动位置,否则会造成表头与数据单元格错位(暂不知为啥,先这样解决)
*/
const active = () => {
setTimeout(() => tableRef.value.scrollToLeft(scrollLeftValue.value));
};
const getNowDbInst = () => { const getNowDbInst = () => {
return DbInst.getInst(state.dbId); return DbInst.getInst(state.dbId);
}; };
defineExpose({ defineExpose({
active,
submitUpdateFields, submitUpdateFields,
cancelUpdateFields, cancelUpdateFields,
}); });
@@ -880,8 +898,8 @@ defineExpose({
color: var(--el-color-info-light-3); color: var(--el-color-info-light-3);
font-weight: bold; font-weight: bold;
position: absolute; position: absolute;
top: -5px; top: -7px;
padding: 2px; padding: 1px;
} }
.column-right { .column-right {

View File

@@ -6,10 +6,10 @@
:key="column.columnName" :key="column.columnName"
class="w100 mb5" class="w100 mb5"
:prop="column.columnName" :prop="column.columnName"
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity" :required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.isIdentity"
> >
<template #label> <template #label>
<span class="pointer" :title="`${column.showDataType} | ${column.columnComment}`"> <span class="pointer" :title="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType">
{{ column.columnName }} {{ column.columnName }}
</span> </span>
</template> </template>
@@ -17,13 +17,13 @@
<ColumnFormItem <ColumnFormItem
v-model="modelValue[`${column.columnName}`]" v-model="modelValue[`${column.columnName}`]"
:data-type="dbInst.getDialect().getDataType(column.dataType)" :data-type="dbInst.getDialect().getDataType(column.dataType)"
:placeholder="`${column.showDataType} ${column.columnComment}`" :placeholder="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType"
:column-name="column.columnName" :column-name="column.columnName"
:disabled="column.isIdentity" :disabled="column.isIdentity"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer v-if="props.tableName">
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="closeDialog">取消</el-button> <el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirm">确定</el-button> <el-button type="primary" @click="confirm">确定</el-button>
@@ -37,7 +37,6 @@ import { ref, watch, onMounted } from 'vue';
import ColumnFormItem from './ColumnFormItem.vue'; import ColumnFormItem from './ColumnFormItem.vue';
import { DbInst } from '../../db'; import { DbInst } from '../../db';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { getDbDialect } from '@/views/ops/db/dialect';
export interface ColumnFormItemProps { export interface ColumnFormItemProps {
dbInst: DbInst; dbInst: DbInst;
@@ -86,35 +85,35 @@ const closeDialog = () => {
}; };
const confirm = async () => { const confirm = async () => {
dataForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await dataForm.value.validate();
ElMessage.error('请正确填写数据信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写数据信息');
} return false;
}
const dbInst = props.dbInst; const dbInst = props.dbInst;
const data = modelValue.value; const data = modelValue.value;
const db = props.dbName; const db = props.dbName;
const tableName = props.tableName; const tableName = props.tableName;
let sql = ''; let sql = '';
if (oldValue) { if (oldValue) {
const updateColumnValue = {}; const updateColumnValue: any = {};
Object.keys(oldValue).forEach((key) => { Object.keys(oldValue).forEach((key) => {
// 如果新旧值不相等,则为需要更新的字段 // 如果新旧值不相等,则为需要更新的字段
if (oldValue[key] !== modelValue.value[key]) { if (oldValue[key] !== modelValue.value[key]) {
updateColumnValue[key] = modelValue.value[key]; updateColumnValue[key] = modelValue.value[key];
} }
});
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
} else {
sql = await dbInst.genInsertSql(db, tableName, [data], true);
}
dbInst.promptExeSql(db, sql, null, () => {
closeDialog();
emit('submitSuccess');
}); });
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
} else {
sql = await dbInst.genInsertSql(db, tableName, [data], true);
}
dbInst.promptExeSql(db, sql, null, () => {
closeDialog();
emit('submitSuccess');
}); });
}; };
</script> </script>

View File

@@ -12,15 +12,29 @@
width="auto" width="auto"
title="表格字段配置" title="表格字段配置"
trigger="click" trigger="click"
@hide="triggerCheckedColumns"
> >
<div v-for="(item, index) in columns" :key="index"> <div><el-input v-model="checkedShowColumns.searchKey" size="small" placeholder="输入列名或备注过滤" /></div>
<div>
<el-checkbox <el-checkbox
v-model="item.show" v-model="checkedShowColumns.checkedAllColumn"
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`" :indeterminate="checkedShowColumns.isIndeterminate"
:true-value="true" @change="handleCheckAllColumnChange"
:false-value="false"
size="small" size="small"
/> >
选择所有
</el-checkbox>
<el-checkbox-group v-model="checkedShowColumns.columnNames" @change="handleCheckedColumnChange">
<div v-for="(item, index) in filterCheckedColumns" :key="index">
<el-checkbox
:key="index"
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
:value="item.columnName"
size="small"
/>
</div>
</el-checkbox-group>
</div> </div>
<template #reference> <template #reference>
<el-link icon="Operation" size="small" :underline="false"></el-link> <el-link icon="Operation" size="small" :underline="false"></el-link>
@@ -36,33 +50,6 @@
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" border-style="dashed" /> <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. 双击单元格可编辑数据 <br />
4. 鼠标悬停字段名或标签树的表名可提示相关备注
</template>
<el-link icon="QuestionFilled" :underline="false"> </el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<!-- 表数据展示配置 -->
<el-popover
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
placement="bottom"
width="auto"
title="展示配置"
trigger="click"
>
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
<template #reference>
<el-link type="primary" icon="setting" :underline="false"></el-link>
</template>
</el-popover>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top"> <el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="font12">提交</el-link> <el-link @click="submitUpdateFields()" type="success" :underline="false" class="font12">提交</el-link>
</el-tooltip> </el-tooltip>
@@ -98,7 +85,7 @@
<el-divider direction="vertical" /> <el-divider direction="vertical" />
<span style="color: var(--el-color-info-light-3)"> <span style="color: var(--el-color-info-light-3)">
{{ item.showDataType }} {{ item.columnType }}
<template v-if="item.columnComment"> <template v-if="item.columnComment">
<el-divider direction="vertical" /> <el-divider direction="vertical" />
@@ -255,8 +242,8 @@ import { DbInst } from '@/views/ops/db/db';
import DbTableData from './DbTableData.vue'; import DbTableData from './DbTableData.vue';
import { DbDialect } from '@/views/ops/db/dialect'; import { DbDialect } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { useEventListener, useStorage } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import { copyToClipboard } from '@/common/utils/string'; import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
import DbTableDataForm from './DbTableDataForm.vue'; import DbTableDataForm from './DbTableDataForm.vue';
const props = defineProps({ const props = defineProps({
@@ -285,8 +272,6 @@ const condDialogInputRef: Ref = ref(null);
const defaultPageSize = DbInst.DefaultLimit; const defaultPageSize = DbInst.DefaultLimit;
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
const state = reactive({ const state = reactive({
datas: [], datas: [],
sql: '', // 当前数据tab执行的sql sql: '', // 当前数据tab执行的sql
@@ -329,9 +314,17 @@ const state = reactive({
tableHeight: '600px', tableHeight: '600px',
hasUpdatedFileds: false, hasUpdatedFileds: false,
dbDialect: {} as DbDialect, dbDialect: {} as DbDialect,
checkedShowColumns: {
searchKey: '',
checkedAllColumn: true,
isIndeterminate: false,
columnNames: [] as any,
},
}); });
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state); const { datas, condition, loading, columns, checkedShowColumns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } =
toRefs(state);
watch( watch(
() => props.tableHeight, () => props.tableHeight,
@@ -351,6 +344,8 @@ onMounted(async () => {
state.dbDialect = getNowDbInst().getDialect(); state.dbDialect = getNowDbInst().getDialect();
useEventListener('click', handlerWindowClick); useEventListener('click', handlerWindowClick);
state.checkedShowColumns.columnNames = state.columns.map((item: any) => item.columnName);
}); });
const handlerWindowClick = () => { const handlerWindowClick = () => {
@@ -414,6 +409,7 @@ const handleSetPageNum = async () => {
state.pageNum = state.setPageNum; state.pageNum = state.setPageNum;
await selectData(); await selectData();
}; };
const handleCount = async () => { const handleCount = async () => {
state.counting = true; state.counting = true;
@@ -431,6 +427,24 @@ const handleCount = async () => {
state.counting = false; state.counting = false;
}; };
const handleCheckAllColumnChange = (val: boolean) => {
state.checkedShowColumns.columnNames = val ? state.columns.map((x: any) => x.columnName) : [];
state.checkedShowColumns.isIndeterminate = false;
};
const handleCheckedColumnChange = (value: string[]) => {
const checkedCount = value.length;
state.checkedShowColumns.checkedAllColumn = checkedCount === state.columns.length;
state.checkedShowColumns.isIndeterminate = checkedCount > 0 && checkedCount < state.columns.length;
};
const triggerCheckedColumns = () => {
const checkedColumnNames = state.checkedShowColumns.columnNames;
for (let column of state.columns) {
column.show = checkedColumnNames.includes(column.columnName);
}
};
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存 // 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
let completeCond = ''; let completeCond = '';
// 是否存在列建议 // 是否存在列建议
@@ -444,10 +458,7 @@ const getColumnTips = (queryString: string, callback: any) => {
let res = []; let res = [];
if (columnNameSearch) { if (columnNameSearch) {
columnNameSearch = columnNameSearch.toLowerCase(); res = fuzzyMatchField(columnNameSearch, columns, (x: any) => x.columnName);
res = columns.filter((data: any) => {
return data.columnName.toLowerCase().includes(columnNameSearch);
});
} }
completeCond = condition.value; completeCond = condition.value;
@@ -490,16 +501,25 @@ const chooseCondColumnName = () => {
* 过滤条件列名 * 过滤条件列名
*/ */
const filterCondColumns = computed(() => { const filterCondColumns = computed(() => {
return filterColumns(state.columnNameSearch);
});
const filterCheckedColumns = computed(() => {
return filterColumns(state.checkedShowColumns.searchKey);
});
const filterColumns = (searchKey: string) => {
const columns = state.columns; const columns = state.columns;
let columnNameSearch = state.columnNameSearch; if (!searchKey) {
if (!columnNameSearch) {
return columns; return columns;
} }
columnNameSearch = columnNameSearch.toLowerCase(); return fuzzyMatchField(
return columns.filter((data: any) => { searchKey,
return data.columnName.toLowerCase().includes(columnNameSearch) || data.columnComment.toLowerCase().includes(columnNameSearch); columns,
}); (x: any) => x.columnName,
}); (x: any) => x.columnComment
);
};
/** /**
* 条件查询,点击列信息后显示输入对应的值 * 条件查询,点击列信息后显示输入对应的值
@@ -507,7 +527,7 @@ const filterCondColumns = computed(() => {
const onConditionRowClick = (event: any) => { const onConditionRowClick = (event: any) => {
const row = event[0]; const row = event[0];
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`; state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
state.conditionDialog.placeholder = `${row.showDataType} ${row.columnComment}`; state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
state.conditionDialog.columnRow = row; state.conditionDialog.columnRow = row;
state.conditionDialog.visible = true; state.conditionDialog.visible = true;
setTimeout(() => { setTimeout(() => {
@@ -583,6 +603,10 @@ const onShowAddDataDialog = async () => {
state.addDataDialog.title = `添加'${props.tableName}'表数据`; state.addDataDialog.title = `添加'${props.tableName}'表数据`;
state.addDataDialog.visible = true; state.addDataDialog.visible = true;
}; };
defineExpose({
active: () => dbTableRef.value.active(),
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -152,8 +152,8 @@ const props = defineProps({
dbType: { dbType: {
type: String, type: String,
}, },
flowProcdefKey: { flowProcdef: {
type: String, type: Object,
}, },
}); });
@@ -335,7 +335,7 @@ const submit = async () => {
dbId: props.dbId as any, dbId: props.dbId as any,
db: props.db as any, db: props.db as any,
dbType: dbDialect.getInfo().formatSqlDialect, dbType: dbDialect.getInfo().formatSqlDialect,
flowProcdefKey: props.flowProcdefKey, flowProcdef: props.flowProcdef,
runSuccessCallback: () => { runSuccessCallback: () => {
emit('submit-sql', { tableName: state.tableData.tableName }); emit('submit-sql', { tableName: state.tableData.tableName });
// cancel(); // cancel();

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="db-table"> <div class="db-table">
<el-row class="mb5"> <el-row class="mb5">
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right" trigger="click"> <el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
<template #reference> <template #reference>
<el-button class="ml5" type="success" size="small">导出</el-button> <el-button :disabled="state.dumpInfo.tables?.length == 0" class="ml5" type="success" size="small">导出</el-button>
</template> </template>
<el-form-item label="导出内容: "> <el-form-item label="导出内容: ">
<el-radio-group v-model="dumpInfo.type"> <el-radio-group v-model="dumpInfo.type">
@@ -13,16 +13,15 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="导出表: "> <el-form-item>
<el-table @selection-change="handleDumpTableSelectionChange" max-height="300" size="small" :data="tables"> <el-table :data="state.dumpInfo.tables" empty-text="请先选择要导出的表" max-height="300" size="small">
<el-table-column type="selection" width="45" />
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column> <el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip> </el-table-column> <el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip> </el-table-column>
</el-table> </el-table>
</el-form-item> </el-form-item>
<div style="text-align: right"> <div style="text-align: right">
<el-button @click="showDumpInfo = false" size="small">取消</el-button> <el-button @click="state.dumpInfo.visible = false" size="small">取消</el-button>
<el-button @click="dump(db)" type="success" size="small">确定</el-button> <el-button @click="dump(db)" type="success" size="small">确定</el-button>
</div> </div>
</el-popover> </el-popover>
@@ -30,7 +29,9 @@
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button> <el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
</el-row> </el-row>
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" :height="height"> <el-table v-loading="loading" @selection-change="handleDumpTableSelectionChange" border stripe :data="filterTableInfos" size="small" :height="height">
<el-table-column type="selection" width="30" />
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> <el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
<template #header> <template #header>
<el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable /> <el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
@@ -82,7 +83,7 @@
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible"> <el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
<el-table border stripe :data="columnDialog.columns" size="small"> <el-table border stripe :data="columnDialog.columns" size="small">
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column> <el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column width="120" prop="showDataType" label="类型" show-overflow-tooltip> </el-table-column> <el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
<el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column> <el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column> <el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
</el-table> </el-table>
@@ -108,7 +109,7 @@
:dbId="dbId" :dbId="dbId"
:db="db" :db="db"
:dbType="dbType" :dbType="dbType"
:flow-procdef-key="props.flowProcdefKey" :flow-procdef="props.flowProcdef"
:data="tableCreateDialog.data" :data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible" v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitSql" @submit-sql="onSubmitSql"
@@ -130,6 +131,7 @@ import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index'
import { DbInst } from '../../db'; import { DbInst } from '../../db';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { format as sqlFormatter } from 'sql-formatter'; import { format as sqlFormatter } from 'sql-formatter';
import { fuzzyMatchField } from '@/common/utils/string';
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue')); const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
@@ -150,8 +152,8 @@ const props = defineProps({
type: [String], type: [String],
required: true, required: true,
}, },
flowProcdefKey: { flowProcdef: {
type: [String], type: [Object],
}, },
}); });
@@ -161,8 +163,8 @@ const state = reactive({
tables: [], tables: [],
tableNameSearch: '', tableNameSearch: '',
tableCommentSearch: '', tableCommentSearch: '',
showDumpInfo: false,
dumpInfo: { dumpInfo: {
visible: false,
id: 0, id: 0,
db: '', db: '',
type: 3, type: 3,
@@ -201,19 +203,7 @@ const state = reactive({
}, },
}); });
const { const { loading, tableNameSearch, tableCommentSearch, dumpInfo, chooseTableName, columnDialog, indexDialog, ddlDialog, tableCreateDialog } = toRefs(state);
loading,
tables,
tableNameSearch,
tableCommentSearch,
showDumpInfo,
dumpInfo,
chooseTableName,
columnDialog,
indexDialog,
ddlDialog,
tableCreateDialog,
} = toRefs(state);
onMounted(async () => { onMounted(async () => {
getTables(); getTables();
@@ -230,17 +220,11 @@ const filterTableInfos = computed(() => {
if (!tableNameSearch && !tableCommentSearch) { if (!tableNameSearch && !tableCommentSearch) {
return tables; return tables;
} }
return tables.filter((data: any) => {
let tnMatch = true; if (tableNameSearch) {
let tcMatch = true; return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName);
if (tableNameSearch) { }
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase()); return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment);
}
if (tableCommentSearch) {
tcMatch = data.tableComment.includes(tableCommentSearch);
}
return tnMatch && tcMatch;
});
}); });
const getTables = async () => { const getTables = async () => {
@@ -259,21 +243,22 @@ const getTables = async () => {
* 选择导出数据库表 * 选择导出数据库表
*/ */
const handleDumpTableSelectionChange = (vals: any) => { const handleDumpTableSelectionChange = (vals: any) => {
state.dumpInfo.tables = vals.map((x: any) => x.tableName); state.dumpInfo.tables = vals;
}; };
/** /**
* 数据库信息导出 * 数据库信息导出
*/ */
const dump = (db: string) => { const dump = (db: string) => {
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表'); isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
const tableNames = state.dumpInfo.tables.map((x: any) => x.tableName);
const a = document.createElement('a'); const a = document.createElement('a');
a.setAttribute( a.setAttribute(
'href', 'href',
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&${joinClientParams()}` `${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${tableNames.join(',')}&${joinClientParams()}`
); );
a.click(); a.click();
state.showDumpInfo = false; state.dumpInfo.visible = false;
}; };
const showColumns = async (row: any) => { const showColumns = async (row: any) => {
@@ -327,7 +312,7 @@ const dropTable = async (row: any) => {
sql: `DROP TABLE ${tableName}`, sql: `DROP TABLE ${tableName}`,
dbId: props.dbId as any, dbId: props.dbId as any,
db: props.db as any, db: props.db as any,
flowProcdefKey: props.flowProcdefKey, flowProcdef: props.flowProcdef,
runSuccessCallback: async () => { runSuccessCallback: async () => {
await getTables(); await getTables();
}, },

View File

@@ -8,6 +8,7 @@ import { editor, languages, Position } from 'monaco-editor';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider'; import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect'; import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
import { type RemovableRef, useLocalStorage } from '@vueuse/core'; import { type RemovableRef, useLocalStorage } from '@vueuse/core';
import { DbGetDbNamesMode } from './enums';
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map()); const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map()); const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
@@ -41,9 +42,9 @@ export class DbInst {
type: string; type: string;
/** /**
* 流程定义key,若存在则需要审批执行 * 流程定义,若存在则需要审批执行
*/ */
flowProcdefKey: string; flowProcdef: any;
/** /**
* dbName -> db * dbName -> db
@@ -359,7 +360,7 @@ export class DbInst {
dbType: this.getDialect().getInfo().formatSqlDialect, dbType: this.getDialect().getInfo().formatSqlDialect,
runSuccessCallback: successFunc, runSuccessCallback: successFunc,
cancelCallback: cancelFunc, cancelCallback: cancelFunc,
flowProcdefKey: this.flowProcdefKey, flowProcdef: this.flowProcdef,
}); });
}; };
@@ -383,6 +384,11 @@ export class DbInst {
} }
let dbInst = dbInstCache.get(inst.id); let dbInst = dbInstCache.get(inst.id);
if (dbInst) { if (dbInst) {
// 更新可能更改的流程定义
if (inst.flowProcdef !== undefined) {
dbInst.flowProcdef = inst.flowProcdef;
dbInstCache.set(dbInst.id, dbInst);
}
return dbInst; return dbInst;
} }
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`); console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
@@ -393,7 +399,7 @@ export class DbInst {
dbInst.name = inst.name; dbInst.name = inst.name;
dbInst.type = inst.type; dbInst.type = inst.type;
dbInst.databases = inst.databases; dbInst.databases = inst.databases;
dbInst.flowProcdefKey = inst.flowProcdefKey; dbInst.flowProcdef = inst.flowProcdef;
dbInstCache.set(dbInst.id, dbInst); dbInstCache.set(dbInst.id, dbInst);
return dbInst; return dbInst;
@@ -444,8 +450,8 @@ export class DbInst {
return; return;
} }
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符 // 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符、排序图标等
const columnWidth: number = getTextWidth(prop + 'abc') + 23; const columnWidth: number = getTextWidth(prop + 'abc') + 10;
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量); // prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) { if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return columnWidth; return columnWidth;
@@ -465,7 +471,7 @@ export class DbInst {
maxWidthText = nowText; maxWidthText = nowText;
} }
} }
const contentWidth: number = getTextWidth(maxWidthText) + 15; const contentWidth: number = getTextWidth(maxWidthText) + 3;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth; const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
return flexWidth > 500 ? 500 : flexWidth; return flexWidth > 500 ? 500 : flexWidth;
}; };
@@ -477,17 +483,17 @@ export class DbInst {
} }
for (let col of columns) { for (let col of columns) {
if (col.charMaxLength > 0) { if (col.charMaxLength > 0) {
col.showDataType = `${col.dataType}(${col.charMaxLength})`; col.columnType = `${col.dataType}(${col.charMaxLength})`;
col.showLength = col.charMaxLength; col.showLength = col.charMaxLength;
col.showScale = null; col.showScale = null;
continue; continue;
} }
if (col.numPrecision > 0) { if (col.numPrecision > 0) {
if (col.numScale > 0) { if (col.numScale > 0) {
col.showDataType = `${col.dataType}(${col.numPrecision},${col.numScale})`; col.columnType = `${col.dataType}(${col.numPrecision},${col.numScale})`;
col.showScale = col.numScale; col.showScale = col.numScale;
} else { } else {
col.showDataType = `${col.dataType}(${col.numPrecision})`; col.columnType = `${col.dataType}(${col.numPrecision})`;
col.showScale = null; col.showScale = null;
} }
@@ -495,9 +501,22 @@ export class DbInst {
continue; continue;
} }
col.showDataType = col.dataType; col.columnType = col.dataType;
} }
} }
/**
* 根据数据库配置信息获取对应的库名列表
* @param db db配置信息
* @returns 库名列表
*/
static async getDbNames(db: any) {
if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
return db.database.split(' ');
}
return await dbApi.getDbNamesByAc.request({ authCertName: db.authCertName });
}
} }
/** /**
@@ -582,6 +601,11 @@ export class TabInfo {
*/ */
params: any; params: any;
/**
* 组件ref
*/
componentRef: any;
getNowDbInst() { getNowDbInst() {
return DbInst.getInst(this.dbId); return DbInst.getInst(this.dbId);
} }
@@ -818,3 +842,18 @@ function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string)
return tables.length > 0 ? tables[0] : undefined; return tables.length > 0 ? tables[0] : undefined;
} }
} }
/**
* 数据库主题配置
*/
export const DbThemeConfig = {
/**
* 表数据表头是否显示备注
*/
showColumnComment: true,
/**
* 是否自动定位至树节点
*/
locationTreeNode: true,
};

View File

@@ -69,11 +69,11 @@ export enum DataType {
} }
/** 列数据类型角标 */ /** 列数据类型角标 */
export const ColumnTypeSubscript = { export const ColumnTypeSubscript: any = {
/** 字符串 */ /** 字符串 */
string: 'abc', string: 'ab',
/** 数字 */ /** 数字 */
number: '123', number: '12',
/** 日期 */ /** 日期 */
date: 'icon-clock', date: 'icon-clock',
/** 时间 */ /** 时间 */

View File

@@ -1,5 +1,10 @@
import { EnumValue } from '@/common/Enum'; import { EnumValue } from '@/common/Enum';
export const DbGetDbNamesMode = {
Auto: EnumValue.of(-1, '实时获取').setTagType('warning'),
Assign: EnumValue.of(1, '指定库名').setTagType('primary'),
};
// 数据库sql执行类型 // 数据库sql执行类型
export const DbSqlExecTypeEnum = { export const DbSqlExecTypeEnum = {
Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'), Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'),

View File

@@ -106,7 +106,9 @@ const props = defineProps({
}); });
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']); const emit = defineEmits(['cancel', 'val-change']);
const dialogVisible = defineModel<boolean>('visible', { default: false });
const rules = { const rules = {
tagCodePaths: [ tagCodePaths: [
@@ -170,23 +172,19 @@ const defaultForm = {
}; };
const state = reactive({ const state = reactive({
dialogVisible: false,
sshTunnelMachineList: [] as any, sshTunnelMachineList: [] as any,
form: defaultForm, form: defaultForm,
submitForm: {} as any, submitForm: {} as any,
pwd: '', pwd: '',
}); });
const { dialogVisible, form, submitForm } = toRefs(state); const { form, submitForm } = toRefs(state);
const { isFetching: testConnBtnLoading, execute: testConnExec } = machineApi.testConn.useApi(submitForm); const { isFetching: testConnBtnLoading, execute: testConnExec } = machineApi.testConn.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveMachineExec } = machineApi.saveMachine.useApi(submitForm); const { isFetching: saveBtnLoading, execute: saveMachineExec } = machineApi.saveMachine.useApi(submitForm);
watchEffect(() => { watchEffect(() => {
state.dialogVisible = props.visible; if (!dialogVisible.value) {
if (!state.dialogVisible) {
state.form = { ...defaultForm };
state.form.authCerts = [];
return; return;
} }
const machine: any = props.machine; const machine: any = props.machine;
@@ -194,41 +192,44 @@ watchEffect(() => {
state.form = { ...machine }; state.form = { ...machine };
state.form.tagCodePaths = machine.tags.map((t: any) => t.codePath); state.form.tagCodePaths = machine.tags.map((t: any) => t.codePath);
state.form.authCerts = machine.authCerts || []; state.form.authCerts = machine.authCerts || [];
} else {
state.form = { ...defaultForm };
state.form.authCerts = [];
} }
}); });
const testConn = async (authCert: any) => { const testConn = async (authCert: any) => {
machineForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await machineForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
state.submitForm = getReqForm(); state.submitForm = getReqForm();
state.submitForm.authCerts = [authCert]; state.submitForm.authCerts = [authCert];
await testConnExec(); await testConnExec();
ElMessage.success('连接成功'); ElMessage.success('连接成功');
});
}; };
const btnOk = async () => { const btnOk = async () => {
machineForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await machineForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
if (state.form.authCerts.length == 0) { if (state.form.authCerts.length == 0) {
ElMessage.error('请完善授权凭证账号信息'); ElMessage.error('请完善授权凭证账号信息');
return false; return false;
} }
state.submitForm = getReqForm(); state.submitForm = getReqForm();
await saveMachineExec(); await saveMachineExec();
ElMessage.success('保存成功'); ElMessage.success('保存成功');
emit('val-change', submitForm); emit('val-change', submitForm);
cancel(); cancel();
});
}; };
const getReqForm = () => { const getReqForm = () => {
@@ -250,7 +251,7 @@ const handleChangeProtocol = (val: any) => {
}; };
const cancel = () => { const cancel = () => {
emit('update:visible', false); dialogVisible.value = false;
emit('cancel'); emit('cancel');
}; };
</script> </script>

View File

@@ -96,7 +96,7 @@
</el-tooltip> </el-tooltip>
<el-button v-if="data.protocol == MachineProtocolEnum.Rdp.value" type="primary" @click="showRDP(data)" link>RDP</el-button> <el-button v-if="data.protocol == MachineProtocolEnum.Rdp.value" type="primary" @click="showRDP(data)" link>RDP</el-button>
<el-button v-if="data.protocol == 3" type="primary" @click="showRDP(data)" link>VNC</el-button> <el-button v-if="data.protocol == MachineProtocolEnum.Vnc.value" type="primary" @click="showRDP(data)" link>VNC</el-button>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
</span> </span>
@@ -160,10 +160,10 @@
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item> <el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item> <el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item> <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="更新时间">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item> <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
@@ -236,12 +236,11 @@ import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { getMachineTerminalSocketUrl, machineApi } from './api'; import { getMachineTerminalSocketUrl, machineApi } from './api';
import { dateFormat } from '@/common/utils/date';
import ResourceTags from '../component/ResourceTags.vue'; import ResourceTags from '../component/ResourceTags.vue';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize, formatDate } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { getTagPathSearchItem } from '../component/tag'; import { getTagPathSearchItem } from '../component/tag';

View File

@@ -8,6 +8,7 @@
ref="tagTreeRef" ref="tagTreeRef"
:resource-type="TagResourceTypeEnum.MachineAuthCert.value" :resource-type="TagResourceTypeEnum.MachineAuthCert.value"
:tag-path-node-type="NodeTypeTagPath" :tag-path-node-type="NodeTypeTagPath"
:default-expanded-keys="state.defaultExpendKey"
> >
<template #prefix="{ data }"> <template #prefix="{ data }">
<SvgIcon <SvgIcon
@@ -24,7 +25,7 @@
</template> </template>
<template #suffix="{ data }"> <template #suffix="{ data }">
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.AuthCert">{{ <span v-if="data.type.value == MachineNodeType.AuthCert">{{
` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}` ` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}`
}}</span> }}</span>
</template> </template>
@@ -33,20 +34,15 @@
<Pane> <Pane>
<div class="machine-terminal-tabs card pd5"> <div class="machine-terminal-tabs card pd5">
<el-tabs <el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTermName" class="h100">
v-if="state.tabs.size > 0"
type="card"
@tab-remove="onRemoveTab"
@tab-change="onTabChange"
style="width: 100%"
v-model="state.activeTermName"
class="h100"
>
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key"> <el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label> <template #label>
<el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?"> <el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?">
<template #reference> <template #reference>
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'" <el-icon
class="mr5"
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : '点击重连'"
><Connection /> ><Connection />
</el-icon> </el-icon>
</template> </template>
@@ -61,7 +57,7 @@
<el-descriptions :column="1" size="small"> <el-descriptions :column="1" size="small">
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item> <el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item> <el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item> <el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item> <el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
</el-descriptions> </el-descriptions>
</template> </template>
@@ -102,10 +98,10 @@
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item> <el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item> <el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item> <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="更新时间">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item> <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
@@ -153,22 +149,25 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, nextTick, reactive, ref, toRefs, watch } from 'vue'; import { defineAsyncComponent, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getMachineTerminalSocketUrl, machineApi } from './api'; import { getMachineTerminalSocketUrl, machineApi } from './api';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { NodeType, TagTreeNode } from '../component/tag'; import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { Pane, Splitpanes } from 'splitpanes'; import { Pane, Splitpanes } from 'splitpanes';
import { ContextmenuItem } from '@/components/contextmenu/index'; import { ContextmenuItem } from '@/components/contextmenu/index';
import TerminalBody from '@/components/terminal/TerminalBody.vue'; import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { TerminalStatus } from '@/components/terminal/common'; import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue'; import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue'; import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
import ResourceTags from '../component/ResourceTags.vue'; import ResourceTags from '../component/ResourceTags.vue';
import { MachineProtocolEnum } from './enums'; import { MachineProtocolEnum } from './enums';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
import EnumValue from '@/common/Enum';
// 组件 // 组件
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue')); const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
@@ -196,6 +195,7 @@ class MachineNodeType {
} }
const state = reactive({ const state = reactive({
defaultExpendKey: [] as any,
params: { params: {
pageNum: 1, pageNum: 1,
pageSize: 0, pageSize: 0,
@@ -252,6 +252,9 @@ const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog
const tagTreeRef: any = ref(null); const tagTreeRef: any = ref(null);
const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
let openIds = {}; let openIds = {};
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => { const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
@@ -263,7 +266,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
// 把list 根据name字段排序 // 把list 根据name字段排序
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name)); res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
return res.list.map((x: any) => return res.list.map((x: any) =>
new TagTreeNode(x.id, x.name, NodeTypeMachine) new TagTreeNode(x.code, x.name, NodeTypeMachine)
.withParams(x) .withParams(x)
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value) .withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
.withIcon({ .withIcon({
@@ -279,7 +282,7 @@ const NodeTypeMachine = new NodeType(MachineNodeType.Machine)
// 获取授权凭证列表 // 获取授权凭证列表
const authCerts = machine.authCerts; const authCerts = machine.authCerts;
return authCerts.map((x: any) => return authCerts.map((x: any) =>
new TagTreeNode(x.id, x.username, NodeTypeAuthCert) new TagTreeNode(x.name, x.username, NodeTypeAuthCert)
.withParams({ ...machine, selectAuthCert: x }) .withParams({ ...machine, selectAuthCert: x })
.withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value) .withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
.withIcon({ .withIcon({
@@ -323,6 +326,52 @@ const NodeTypeAuthCert = new NodeType(MachineNodeType.AuthCert)
.withOnClick((node: any) => serviceManager(node.params)), .withOnClick((node: any) => serviceManager(node.params)),
]); ]);
watch(
() => autoOpenResource.value.machineCodePath,
(codePath: any) => {
autoOpenTerminal(codePath);
}
);
watch(
() => state.activeTermName,
(newValue, oldValue) => {
fitTerminal();
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
const nowTab = state.tabs.get(state.activeTermName);
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
}
);
onMounted(() => {
autoOpenTerminal(autoOpenResource.value.machineCodePath);
});
const autoOpenTerminal = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const machineCode = typeAndCodes[TagResourceTypeEnum.Machine.value][0];
state.defaultExpendKey = [tagPath, machineCode];
const authCertName = typeAndCodes[TagResourceTypeEnum.MachineAuthCert.value][0];
setTimeout(() => {
// 置空
autoOpenResourceStore.setMachineCodePath('');
tagTreeRef.value.setCurrentKey(authCertName);
const acNode = tagTreeRef.value.getNode(authCertName);
openTerminal(acNode.data.params);
}, 1000);
};
const openTerminal = (machine: any, ex?: boolean) => { const openTerminal = (machine: any, ex?: boolean) => {
// 授权凭证名 // 授权凭证名
const ac = machine.selectAuthCert.name; const ac = machine.selectAuthCert.name;
@@ -354,14 +403,14 @@ const openTerminal = (machine: any, ex?: boolean) => {
} }
} }
let { name, username } = machine; let { name } = machine;
const labelName = `${machine.selectAuthCert.username}@${name}`; const labelName = `${machine.selectAuthCert.username}@${name}`;
// 同一个机器的终端打开多次key后添加下划线和数字区分 // 同一个机器的终端打开多次key后添加下划线和数字区分
openIds[ac] = openIds[ac] ? ++openIds[ac] : 1; openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
let sameIndex = openIds[ac]; let sameIndex = openIds[ac];
let key = `${ac}_${username}_${sameIndex}`; let key = `${ac}_${sameIndex}`;
// 只保留name的15个字超出部分只保留前后10个字符中间用省略号代替 // 只保留name的15个字超出部分只保留前后10个字符中间用省略号代替
const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName; const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
@@ -448,32 +497,30 @@ const onRemoveTab = (targetName: string) => {
if (tabName !== targetName) { if (tabName !== targetName) {
continue; continue;
} }
state.tabs.delete(targetName);
let info = state.tabs.get(targetName);
if (info) {
terminalRefs[info.key]?.close();
}
if (activeTermName != targetName) {
break;
}
// 如果删除的tab是当前激活的tab则切换到前一个或后一个tab
const nextTab = tabNames[i + 1] || tabNames[i - 1]; const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) { if (nextTab) {
activeTermName = nextTab; activeTermName = nextTab;
} else { } else {
activeTermName = ''; activeTermName = '';
} }
let info = state.tabs.get(targetName);
if (info) {
terminalRefs[info.key]?.close();
}
state.tabs.delete(targetName);
state.activeTermName = activeTermName; state.activeTermName = activeTermName;
onTabChange(); break;
} }
}; };
watch(
() => state.activeTermName,
(newValue, oldValue) => {
console.log('oldValue', oldValue);
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
}
);
const terminalStatusChange = (key: string, status: TerminalStatus) => { const terminalStatusChange = (key: string, status: TerminalStatus) => {
state.tabs.get(key).status = status; state.tabs.get(key).status = status;
}; };
@@ -496,18 +543,13 @@ const onResizeTagTree = () => {
fitTerminal(); fitTerminal();
}; };
const onTabChange = () => {
fitTerminal();
};
const fitTerminal = () => { const fitTerminal = () => {
setTimeout(() => { setTimeout(() => {
let info = state.tabs.get(state.activeTermName); let info = state.tabs.get(state.activeTermName);
if (info) { if (info) {
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal(); terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
terminalRefs[info.key]?.focus && terminalRefs[info.key]?.focus();
} }
}, 100); });
}; };
const handleReconnect = (tab: any, force = false) => { const handleReconnect = (tab: any, force = false) => {

View File

@@ -28,12 +28,12 @@
<div ref="playerRef" id="rc-player"></div> <div ref="playerRef" id="rc-player"></div>
</el-dialog> </el-dialog>
<el-dialog :title="title" v-model="execCmdsDialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="500"> <el-dialog title="执行命令记录" v-model="execCmdsDialogVisible" :destroy-on-close="true" width="500">
<el-table :data="state.execCmds" max-height="480" stripe size="small"> <el-table :data="state.execCmds" max-height="480" stripe size="small">
<el-table-column prop="cmd" label="命令" show-overflow-tooltip min-width="150px"> </el-table-column> <el-table-column prop="cmd" label="命令" show-overflow-tooltip min-width="150px"> </el-table-column>
<el-table-column prop="time" label="执行时间" min-width="80" show-overflow-tooltip> <el-table-column prop="time" label="执行时间" min-width="80" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
{{ dateFormat(new Date(scope.row.time * 1000).toString()) }} {{ formatDate(new Date(scope.row.time * 1000).toString()) }}
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -48,7 +48,7 @@ import * as AsciinemaPlayer from 'asciinema-player';
import 'asciinema-player/dist/bundle/asciinema-player.css'; import 'asciinema-player/dist/bundle/asciinema-player.css';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
const props = defineProps({ const props = defineProps({
visible: { type: Boolean }, visible: { type: Boolean },
@@ -122,8 +122,8 @@ const playRec = async (rec: any) => {
idleTimeLimit: 2, idleTimeLimit: 2,
// fit: false, // fit: false,
// terminalFontSize: 'small', // terminalFontSize: 'small',
cols: 144, // cols: 144,
rows: 32, // rows: 32,
}); });
}); });
} finally { } finally {

View File

@@ -12,6 +12,8 @@ export const machineApi = {
process: Api.newGet('/machines/{id}/process'), process: Api.newGet('/machines/{id}/process'),
// 终止进程 // 终止进程
killProcess: Api.newDelete('/machines/{id}/process'), killProcess: Api.newDelete('/machines/{id}/process'),
users: Api.newGet('/machines/{id}/users'),
groups: Api.newGet('/machines/{id}/groups'),
testConn: Api.newPost('/machines/test-conn'), testConn: Api.newPost('/machines/test-conn'),
// 保存按钮 // 保存按钮
saveMachine: Api.newPost('/machines'), saveMachine: Api.newPost('/machines'),
@@ -58,6 +60,12 @@ export const cronJobApi = {
execList: Api.newGet('/machine-cronjobs/execs'), execList: Api.newGet('/machine-cronjobs/execs'),
}; };
export const cmdConfApi = {
list: Api.newGet('/machine/security/cmd-confs'),
save: Api.newPost('/machine/security/cmd-confs'),
delete: Api.newDelete('/machine/security/cmd-confs/{id}'),
};
export function getMachineTerminalSocketUrl(authCertName: any) { export function getMachineTerminalSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/terminal/${authCertName}?${joinClientParams()}`; return `${config.baseWsUrl}/machines/terminal/${authCertName}?${joinClientParams()}`;
} }

View File

@@ -0,0 +1,69 @@
<template>
<div>
<el-popover placement="right" width="auto" title="机器详情" trigger="click">
<template #reference>
<el-link @click="getMachineDetail" type="primary">{{ props.code }}</el-link>
</template>
<el-descriptions v-loading="state.loading" :column="3" border>
<el-descriptions-item :span="1" label="机器id">{{ state.machineDetail.id }}</el-descriptions-item>
<el-descriptions-item :span="1" label="编号">{{ state.machineDetail.code }}</el-descriptions-item>
<el-descriptions-item :span="1" label="名称">{{ state.machineDetail.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="state.machineDetail.tags" /></el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ state.machineDetail.ip }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ state.machineDetail.port }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ state.machineDetail.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ state.machineDetail.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ state.machineDetail.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(state.machineDetail.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ state.machineDetail.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ formatDate(state.machineDetail.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ state.machineDetail.modifier }}</el-descriptions-item>
</el-descriptions>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { machineApi } from '../api';
import { formatDate } from '@/common/utils/format';
import ResourceTags from '../../component/ResourceTags.vue';
const props = defineProps({
code: {
type: [String],
requierd: true,
},
});
const state = reactive({
loading: false,
machineDetail: {} as any,
});
const getMachineDetail = async () => {
try {
state.machineDetail = {};
state.loading = true;
const res = await machineApi.list.request({
code: props.code,
});
if (res.total == 0) {
return;
}
state.machineDetail = res.list?.[0];
} finally {
state.loading = false;
}
};
</script>
<style></style>

View File

@@ -1,14 +1,18 @@
<template> <template>
<div class="mock-data-dialog"> <div class="mock-data-dialog">
<el-dialog <el-drawer
:title="title" :title="title"
v-model="dialogVisible" v-model="dialogVisible"
:close-on-click-modal="false" :close-on-click-modal="false"
:before-close="cancel" :before-close="cancel"
:show-close="true" :show-close="true"
:destroy-on-close="true" :destroy-on-close="true"
width="900px" size="40%"
> >
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto"> <el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
<el-form-item prop="name" label="名称"> <el-form-item prop="name" label="名称">
<el-input v-model="form.name" placeholder="请输入名称"></el-input> <el-input v-model="form.name" placeholder="请输入名称"></el-input>
@@ -34,19 +38,13 @@
<el-input v-model="form.remark" placeholder="请输入备注"></el-input> <el-input v-model="form.remark" placeholder="请输入备注"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="machineIds" label="关联机器">
<el-select multiple v-model="form.machineIds" filterable placeholder="请选关联机器" style="width: 100%">
<el-option v-for="ac in state.machines" :key="ac.id" :value="ac.id" :label="ac.ip">
{{ ac.ip }}
<el-divider direction="vertical" border-style="dashed" />
{{ ac.tagPath }}{{ ac.name }}
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="script" label="执行脚本" required> <el-form-item prop="script" label="执行脚本" required>
<monaco-editor style="width: 100%" v-model="form.script" language="shell" height="300px" <monaco-editor style="width: 100%" v-model="form.script" language="shell" height="200px"
/></el-form-item> /></el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联机器">
<tag-tree-check height="200px" :tag-type="TagResourceTypeEnum.Machine.value" v-model="form.codePaths" />
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -55,7 +53,7 @@
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" :disabled="submitDisabled"> </el-button> <el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" :disabled="submitDisabled"> </el-button>
</div> </div>
</template> </template>
</el-dialog> </el-drawer>
</div> </div>
</template> </template>
@@ -67,6 +65,9 @@ import { CronJobStatusEnum, CronJobSaveExecResTypeEnum } from '../enums';
import { notEmpty } from '@/common/assert'; import { notEmpty } from '@/common/assert';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import CrontabInput from '@/components/crontab/CrontabInput.vue'; import CrontabInput from '@/components/crontab/CrontabInput.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import TagTreeCheck from '../../component/TagTreeCheck.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -130,11 +131,11 @@ const state = reactive({
id: null, id: null,
name: '', name: '',
cron: '', cron: '',
machineIds: [],
remark: '', remark: '',
script: '', script: '',
status: 1, status: 1,
saveExecResType: -1, saveExecResType: -1,
codePaths: [],
}, },
machines: [] as any, machines: [] as any,
btnLoading: false, btnLoading: false,
@@ -154,7 +155,7 @@ watch(props, async (newValue: any) => {
} }
if (newValue.data) { if (newValue.data) {
state.form = { ...newValue.data }; state.form = { ...newValue.data };
state.form.machineIds = await cronJobApi.relateMachineIds.request({ cronJobId: state.form.id }); state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
} else { } else {
state.form = { script: '', status: 1 } as any; state.form = { script: '', status: 1 } as any;
state.chooseMachines = []; state.chooseMachines = [];

View File

@@ -3,6 +3,7 @@
<el-dialog <el-dialog
:title="title" :title="title"
v-model="dialogVisible" v-model="dialogVisible"
@open="search()"
:close-on-click-modal="false" :close-on-click-modal="false"
:before-close="cancel" :before-close="cancel"
:show-close="true" :show-close="true"
@@ -13,20 +14,13 @@
ref="pageTableRef" ref="pageTableRef"
:page-api="cronJobApi.execList" :page-api="cronJobApi.execList"
:lazy="true" :lazy="true"
:data-handler-fn="parseData"
:search-items="searchItems" :search-items="searchItems"
v-model:query-form="params" v-model:query-form="params"
:data="state.data.list" :data="state.data.list"
:columns="columns" :columns="columns"
> >
<template #machineSelect> <template #machineCode="{ data }">
<el-select v-model="params.machineId" filterable placeholder="选择机器查询" clearable> <MachineDetail :code="data.machineCode" />
<el-option v-for="ac in machineMap.values()" :key="ac.id" :value="ac.id" :label="ac.ip">
{{ ac.ip }}
<el-divider direction="vertical" border-style="dashed" />
{{ ac.tagPath }}{{ ac.name }}
</el-option>
</el-select>
</template> </template>
</page-table> </page-table>
</el-dialog> </el-dialog>
@@ -34,12 +28,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, ref, toRefs, reactive, Ref } from 'vue'; import { ref, toRefs, reactive, Ref } from 'vue';
import { cronJobApi, machineApi } from '../api'; import { cronJobApi } from '../api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { CronJobExecStatusEnum } from '../enums'; import { CronJobExecStatusEnum } from '../enums';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import MachineDetail from '../component/MachineDetail.vue';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -53,13 +48,10 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['update:visible', 'update:data', 'cancel']); const searchItems = [SearchItem.input('machineCode', '机器编号'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
const searchItems = [SearchItem.slot('machineId', '机器', 'machineSelect'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
const columns = ref([ const columns = ref([
TableColumn.new('machineIp', '机器IP').setMinWidth(120), TableColumn.new('machineCode', '机器编号').isSlot(),
TableColumn.new('machineName', '机器名称').setMinWidth(100),
TableColumn.new('status', '状态').typeTag(CronJobExecStatusEnum).setMinWidth(70), TableColumn.new('status', '状态').typeTag(CronJobExecStatusEnum).setMinWidth(70),
TableColumn.new('res', '执行结果').setMinWidth(250).canBeautify(), TableColumn.new('res', '执行结果').setMinWidth(250).canBeautify(),
TableColumn.new('execTime', '执行时间').isTime().setMinWidth(150), TableColumn.new('execTime', '执行时间').isTime().setMinWidth(150),
@@ -72,10 +64,10 @@ const state = reactive({
tags: [] as any, tags: [] as any,
params: { params: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 8,
cronJobId: 0, cronJobId: 0,
status: null, status: null,
machineId: null, machineCode: '',
}, },
// 列表数据 // 列表数据
data: { data: {
@@ -85,64 +77,17 @@ const state = reactive({
machines: [], machines: [],
}); });
const machineMap: Map<number, any> = new Map(); const { params } = toRefs(state);
const { dialogVisible, params } = toRefs(state); const dialogVisible = defineModel<boolean>('visible');
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
if (!newValue.visible) {
return;
}
const machineIds = await cronJobApi.relateMachineIds.request({
cronJobId: props.data?.id,
});
const res = await machineApi.list.request({
ids: machineIds?.join(','),
});
res.list?.forEach((x: any) => {
machineMap.set(x.id, x);
});
state.params.cronJobId = props.data?.id;
search();
});
const search = async () => { const search = async () => {
state.params.cronJobId = props.data?.id;
pageTableRef.value.search(); pageTableRef.value.search();
}; };
const parseData = async (res: any) => {
const dataList = res.list;
// 填充机器信息
for (let x of dataList) {
const machineId = x.machineId;
let machine = machineMap.get(machineId);
// 如果未找到,则可能被移除,则调接口查询机器信息
if (!machine) {
const machineRes = await machineApi.list.request({ ids: machineId });
if (!machineRes.list) {
machine = {
id: machineId,
ip: machineId,
name: '该机器已被删除',
};
} else {
machine = machineRes.list[0];
}
machineMap.set(machineId, machine);
}
x.machineIp = machine?.ip;
x.machineName = machine?.name;
}
return res;
};
const cancel = () => { const cancel = () => {
emit('update:visible', false); dialogVisible.value = false;
setTimeout(() => { setTimeout(() => {
initData(); initData();
}, 500); }, 500);
@@ -152,7 +97,7 @@ const initData = () => {
state.data.list = []; state.data.list = [];
state.data.total = 0; state.data.total = 0;
state.params.pageNum = 1; state.params.pageNum = 1;
state.params.machineId = null; state.params.machineCode = '';
state.params.status = null; state.params.status = null;
}; };
</script> </script>

View File

@@ -19,6 +19,10 @@
<el-tag v-else type="danger" effect="plain">未运行</el-tag> <el-tag v-else type="danger" effect="plain">未运行</el-tag>
</template> </template>
<template #codePaths="{ data }">
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
</template>
<template #action="{ data }"> <template #action="{ data }">
<el-button :disabled="data.status == CronJobStatusEnum.Disable.value" v-auth="perms.saveCronJob" type="primary" @click="runCronJob(data)" link <el-button :disabled="data.status == CronJobStatusEnum.Disable.value" v-auth="perms.saveCronJob" type="primary" @click="runCronJob(data)" link
>执行</el-button >执行</el-button
@@ -41,6 +45,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { CronJobStatusEnum, CronJobSaveExecResTypeEnum } from '../enums'; import { CronJobStatusEnum, CronJobSaveExecResTypeEnum } from '../enums';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import TagCodePath from '../../component/TagCodePath.vue';
const CronJobEdit = defineAsyncComponent(() => import('./CronJobEdit.vue')); const CronJobEdit = defineAsyncComponent(() => import('./CronJobEdit.vue'));
const CronJobExecList = defineAsyncComponent(() => import('./CronJobExecList.vue')); const CronJobExecList = defineAsyncComponent(() => import('./CronJobExecList.vue'));
@@ -61,6 +66,7 @@ const columns = ref([
TableColumn.new('running', '运行状态').isSlot(), TableColumn.new('running', '运行状态').isSlot(),
TableColumn.new('saveExecResType', '记录类型').typeTag(CronJobSaveExecResTypeEnum), TableColumn.new('saveExecResType', '记录类型').typeTag(CronJobSaveExecResTypeEnum),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('codePaths', '关联机器').isSlot().setMinWidth('250px'),
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter(),
]); ]);

View File

@@ -3,6 +3,7 @@ import { EnumValue } from '@/common/Enum';
export const MachineProtocolEnum = { export const MachineProtocolEnum = {
Ssh: EnumValue.of(1, 'SSH'), Ssh: EnumValue.of(1, 'SSH'),
Rdp: EnumValue.of(2, 'RDP'), Rdp: EnumValue.of(2, 'RDP'),
Vnc: EnumValue.of(3, 'VNC'),
}; };
// 脚本执行结果类型 // 脚本执行结果类型

View File

@@ -18,12 +18,12 @@
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip> <el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input> <el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-wdith="120px"> <el-table-column label="操作" min-width="130">
<template #default="scope"> <template #default="scope">
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button> <el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button> <el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>

View File

@@ -22,7 +22,7 @@
> >
<el-table-column type="selection" width="30" /> <el-table-column type="selection" width="30" />
<el-table-column prop="name" label="名称"> <el-table-column prop="name" label="名称" min-width="380">
<template #header> <template #header>
<div class="machine-file-table-header"> <div class="machine-file-table-header">
<div> <div>
@@ -171,7 +171,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="size" label="大小" width="100" sortable> <el-table-column prop="size" label="大小" min-width="90" sortable>
<template #default="scope"> <template #default="scope">
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span> <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span>
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span> <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
@@ -182,7 +182,11 @@
</el-table-column> </el-table-column>
<el-table-column prop="mode" label="属性" width="110"> </el-table-column> <el-table-column prop="mode" label="属性" width="110"> </el-table-column>
<el-table-column prop="modTime" label="修改时间" width="165" sortable> </el-table-column> <el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="username" label="用户" min-width="55" show-overflow-tooltip>
</el-table-column>
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="groupname" label="组" min-width="55" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="modTime" label="修改时间" width="160" sortable> </el-table-column>
<el-table-column width="100"> <el-table-column width="100">
<template #header> <template #header>
@@ -288,6 +292,8 @@ import MachineFileContent from './MachineFileContent.vue';
import { getToken } from '@/common/utils/storage'; import { getToken } from '@/common/utils/storage';
import { convertToBytes, formatByteSize } from '@/common/utils/format'; import { convertToBytes, formatByteSize } from '@/common/utils/format';
import { getMachineConfig } from '@/common/sysconfig'; import { getMachineConfig } from '@/common/sysconfig';
import { MachineProtocolEnum } from '../enums';
import { fuzzyMatchField } from '@/common/utils/string';
const props = defineProps({ const props = defineProps({
machineId: { type: Number }, machineId: { type: Number },
@@ -303,6 +309,9 @@ const folderUploadRef: any = ref();
const folderType = 'd'; const folderType = 'd';
const userMap = new Map<number, any>();
const groupMap = new Map<number, any>();
// 路径分隔符 // 路径分隔符
const pathSep = '/'; const pathSep = '/';
@@ -343,13 +352,27 @@ const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressS
onMounted(async () => { onMounted(async () => {
state.basePath = props.path; state.basePath = props.path;
const machineId = props.machineId;
if (props.protocol == MachineProtocolEnum.Ssh.value) {
machineApi.users.request({ id: machineId }).then((res: any) => {
for (let user of res) {
userMap.set(user.uid, user);
}
});
machineApi.groups.request({ id: machineId }).then((res: any) => {
for (let group of res) {
groupMap.set(group.gid, group);
}
});
}
setFiles(props.path); setFiles(props.path);
state.machineConfig = await getMachineConfig(); state.machineConfig = await getMachineConfig();
}); });
const filterFiles = computed(() => const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name));
state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
);
const filePathNav = computed(() => { const filePathNav = computed(() => {
let basePath = state.basePath; let basePath = state.basePath;
@@ -517,6 +540,11 @@ const lsFile = async (path: string) => {
path, path,
}); });
for (const file of res) { for (const file of res) {
if (props.protocol == MachineProtocolEnum.Ssh.value) {
file.username = userMap.get(file.uid)?.uname || file.uid;
file.groupname = groupMap.get(file.gid)?.gname || file.gid;
}
const type = file.type; const type = file.type;
if (type == folderType) { if (type == folderType) {
file.isFolder = true; file.isFolder = true;

View File

@@ -0,0 +1,222 @@
<template>
<div>
<el-table :data="cmdConfs" stripe>
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100px"> </el-table-column>
<el-table-column prop="cmds" label="过滤命令" min-width="320px" show-overflow-tooltip>
<template #default="scope">
<el-tag class="ml2 mt2" v-for="cmd in scope.row.cmds" :key="cmd" type="danger">
{{ cmd }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="codePaths" label="关联机器" min-width="250px" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.tags?.map((tag: any) => tag.codePath)" />
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
<el-table-column prop="creator" label="创建者" show-overflow-tooltip width="100px"> </el-table-column>
<el-table-column label="操作" min-wdith="100px">
<template #header>
<el-text tag="b">操作</el-text>
<el-button v-auth="'cmdconf:save'" class="ml5" type="primary" circle size="small" icon="Plus" @click="openFormDialog(false)"> </el-button>
</template>
<template #default="scope">
<el-button v-auth="'cmdconf:save'" @click="openFormDialog(scope.row)" type="primary" link>编辑</el-button>
<el-button v-auth="'cmdconf:del'" @click="deleteCmdConf(scope.row)" type="danger" link>删除</el-button>
</template>
</el-table-column>
</el-table>
<el-drawer title="命令配置" v-model="dialogVisible" :show-close="false" width="600px" :destroy-on-close="true" :close-on-click-modal="false">
<template #header>
<DrawerHeader header="命令配置" :back="cancelEdit" />
</template>
<el-form ref="formRef" :model="state.form" :rules="rules" label-width="auto">
<el-form-item prop="name" label="名称" required>
<el-input v-model="form.name" placeholder="名称"></el-input>
</el-form-item>
<el-form-item prop="cmds" label="过滤命令" required>
<el-row>
<el-tag
class="ml2 mt2"
v-for="tag in form.cmds"
:key="tag"
closable
:disable-transitions="false"
@close="handleCmdClose(tag)"
type="danger"
>
{{ tag }}
</el-tag>
<el-input
v-if="state.inputCmdVisible"
ref="cmdInputRef"
v-model="state.cmdInputValue"
class="mt3"
size="small"
@keyup.enter="handleCmdInputConfirm"
@blur="handleCmdInputConfirm"
placeholder="请输入命令正则表达式"
/>
<el-button v-else class="ml2 mt2" size="small" @click="showCmdInput"> + 新建命令 </el-button>
</el-row>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联机器">
<tag-tree-check height="calc(100vh - 430px)" :tag-type="TagResourceTypeEnum.MachineAuthCert.value" v-model="form.codePaths" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="submiting" @click="cancelEdit"> </el-button>
<el-button v-auth="'cmdconf:save'" type="primary" :loading="submiting" @click="submitForm"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, nextTick } from 'vue';
import TagTreeCheck from '../../component/TagTreeCheck.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { ElMessage, ElMessageBox } from 'element-plus';
import { cmdConfApi } from '../api';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import TagCodePath from '../../component/TagCodePath.vue';
import _ from 'lodash';
const rules = {
tags: [
{
required: true,
message: '请选择关联的机器',
trigger: ['change'],
},
],
cmds: [
{
required: true,
message: '请创建命令',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入名称',
trigger: ['change', 'blur'],
},
],
};
const tagSelectRef: any = ref(null);
const formRef: any = ref(null);
const cmdInputRef: any = ref(null);
const DefaultForm = {
id: 0,
name: '',
codePaths: [],
cmds: [] as any,
remark: '',
};
const state = reactive({
cmdConfs: [],
dialogVisible: false,
form: DefaultForm,
submiting: false,
inputCmdVisible: false,
cmdInputValue: '',
});
const { cmdConfs, dialogVisible, form, submiting } = toRefs(state);
onMounted(async () => {
getCmdConfs();
});
const getCmdConfs = async () => {
state.cmdConfs = await cmdConfApi.list.request();
};
const handleCmdClose = (tag: string) => {
state.form.cmds.splice(state.form.cmds.indexOf(tag), 1);
};
const showCmdInput = () => {
state.inputCmdVisible = true;
nextTick(() => {
cmdInputRef.value!.input!.focus();
});
};
const handleCmdInputConfirm = () => {
if (state.cmdInputValue) {
state.form.cmds.push(state.cmdInputValue);
}
state.inputCmdVisible = false;
state.cmdInputValue = '';
};
const openFormDialog = (data: any) => {
if (!data) {
state.form = { ...DefaultForm };
} else {
state.form = _.cloneDeep(data);
state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
}
state.dialogVisible = true;
};
const deleteCmdConf = async (data: any) => {
await ElMessageBox.confirm(`确定删除该[${data.name}]命令配置?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await cmdConfApi.delete.request({ id: data.id });
ElMessage.success('操作成功');
getCmdConfs();
};
const cancelEdit = () => {
state.dialogVisible = false;
// 取消表单的校验
setTimeout(() => {
state.form = { ...DefaultForm };
formRef.value.resetFields();
}, 200);
};
const submitForm = () => {
formRef.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
return false;
}
try {
state.submiting = true;
await cmdConfApi.save.request(state.form);
ElMessage.success('操作成功');
cancelEdit();
getCmdConfs();
} finally {
state.submiting = false;
}
});
};
</script>
<style></style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="card">
<el-tabs v-model="activeName" class="demo-tabs" @tab-change="handleTabChange">
<el-tab-pane label="命令配置" :name="CmdConfTab">
<CmdConfList />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
const CmdConfList = defineAsyncComponent(() => import('./CmdConfList.vue'));
const CmdConfTab = 'cmdConf';
const state = reactive({
activeName: CmdConfTab,
cmdConfs: [],
});
const { activeName } = toRefs(state);
onMounted(async () => {
state.activeName = CmdConfTab;
});
const handleTabChange = (tabName: any) => {
if (tabName == CmdConfTab) {
console.log('get cmd confs');
}
console.log(tabName);
};
</script>
<style></style>

View File

@@ -2,7 +2,12 @@
<div class="flex-all-center"> <div class="flex-all-center">
<Splitpanes class="default-theme"> <Splitpanes class="default-theme">
<Pane size="20" max-size="30"> <Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Mongo.value" :tag-path-node-type="NodeTypeTagPath"> <tag-tree
ref="tagTreeRef"
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.Mongo.value"
:tag-path-node-type="NodeTypeTagPath"
>
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type.value == MongoNodeType.Mongo"> <span v-if="data.type.value == MongoNodeType.Mongo">
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250"> <el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
@@ -31,13 +36,8 @@
/> />
</template> </template>
<template #label="{ data }"> <template #suffix="{ data }">
<span v-if="data.type.value == MongoNodeType.Dbs"> <span v-if="data.type.value == MongoNodeType.Dbs">{{ formatByteSize(data.params.size) }}</span>
{{ data.params.database }}
<span style="color: #8492a6; font-size: 13px"> [{{ formatByteSize(data.params.size) }}] </span>
</span>
<span v-else>{{ data.label }}</span>
</template> </template>
</tag-tree> </tag-tree>
</Pane> </Pane>
@@ -168,16 +168,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { mongoApi } from './api'; import { mongoApi } from './api';
import { computed, defineAsyncComponent, reactive, ref, toRefs } from 'vue'; import { computed, defineAsyncComponent, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { isTrue, notBlank } from '@/common/assert'; import { isTrue, notBlank } from '@/common/assert';
import { TagTreeNode, NodeType } from '../component/tag'; import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import { Splitpanes, Pane } from 'splitpanes'; import { Splitpanes, Pane } from 'splitpanes';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue')); const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
@@ -207,7 +209,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
await sleep(100); await sleep(100);
return mongoInfos?.map((x: any) => { return mongoInfos?.map((x: any) => {
x.tagPath = parentNode.key; x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeMongo).withParams(x); return new TagTreeNode(`${x.code}`, x.name, NodeTypeMongo).withParams(x);
}); });
}); });
@@ -250,7 +252,10 @@ const NodeTypeColl = new NodeType(MongoNodeType.Coll).withNodeClickFunc((nodeDat
}); });
const findParamInputRef: any = ref(null); const findParamInputRef: any = ref(null);
const tagTreeRef: any = ref(null);
const state = reactive({ const state = reactive({
defaultExpendKey: [] as any,
tags: [], tags: [],
mongoList: [] as any, mongoList: [] as any,
activeName: '', // 当前操作的tab activeName: '', // 当前操作的tab
@@ -282,10 +287,42 @@ const state = reactive({
const { findDialog, docEditDialog } = toRefs(state); const { findDialog, docEditDialog } = toRefs(state);
const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
const nowColl = computed(() => { const nowColl = computed(() => {
return getNowDataTab(); return getNowDataTab();
}); });
watch(
() => autoOpenResource.value.mongoCodePath,
(codePath: any) => {
autoOpenMongo(codePath);
}
);
onMounted(() => {
autoOpenMongo(autoOpenResource.value.mongoCodePath);
});
const autoOpenMongo = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const mongoCode = typeAndCodes[TagResourceTypeEnum.Mongo.value][0];
state.defaultExpendKey = [tagPath, mongoCode];
setTimeout(() => {
// 置空
autoOpenResourceStore.setMongoCodePath('');
tagTreeRef.value.setCurrentKey(mongoCode);
}, 600);
};
const changeCollection = async (id: any, schema: string, collection: string) => { const changeCollection = async (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`; const label = `${id}:\`${schema}\`.${collection}`;
let dataTab = state.dataTabs[label]; let dataTab = state.dataTabs[label];

View File

@@ -163,30 +163,30 @@ const getReqForm = () => {
}; };
const testConn = async () => { const testConn = async () => {
mongoForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await mongoForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
state.submitForm = getReqForm(); state.submitForm = getReqForm();
await testConnExec(); await testConnExec();
ElMessage.success('连接成功'); ElMessage.success('连接成功');
});
}; };
const btnOk = async () => { const btnOk = async () => {
mongoForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await mongoForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
state.submitForm = getReqForm(); }
await saveMongoExec();
ElMessage.success('保存成功'); state.submitForm = getReqForm();
emit('val-change', state.form); await saveMongoExec();
cancel(); ElMessage.success('保存成功');
}); emit('val-change', state.form);
cancel();
}; };
const cancel = () => { const cancel = () => {

View File

@@ -2,7 +2,12 @@
<div class="redis-data-op flex-all-center"> <div class="redis-data-op flex-all-center">
<Splitpanes class="default-theme"> <Splitpanes class="default-theme">
<Pane size="20" max-size="30"> <Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Redis.value" :tag-path-node-type="NodeTypeTagPath"> <tag-tree
ref="tagTreeRef"
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.Redis.value"
:tag-path-node-type="NodeTypeTagPath"
>
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type.value == RedisNodeType.Redis"> <span v-if="data.type.value == RedisNodeType.Redis">
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250"> <el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
@@ -30,6 +35,10 @@
<SvgIcon v-if="data.type.value == RedisNodeType.Db" name="Coin" color="#67c23a" /> <SvgIcon v-if="data.type.value == RedisNodeType.Db" name="Coin" color="#67c23a" />
</template> </template>
<template #suffix="{ data }">
<span v-if="data.type.value == RedisNodeType.Db">{{ data.params.keys }}</span>
</template>
</tag-tree> </tag-tree>
</Pane> </Pane>
@@ -178,11 +187,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { redisApi } from './api'; import { redisApi } from './api';
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref } from 'vue'; import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { isTrue, notBlank, notNull } from '@/common/assert'; import { isTrue, notBlank, notNull } from '@/common/assert';
import { copyToClipboard } from '@/common/utils/string'; import { copyToClipboard } from '@/common/utils/string';
import { TagTreeNode, NodeType } from '../component/tag'; import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { keysToTree, sortByTreeNodes, keysToList } from './utils'; import { keysToTree, sortByTreeNodes, keysToList } from './utils';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
@@ -190,6 +199,9 @@ import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Splitpanes, Pane } from 'splitpanes'; import { Splitpanes, Pane } from 'splitpanes';
import { RedisInst } from './redis'; import { RedisInst } from './redis';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
import { procdefApi } from '@/views/flow/api';
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue')); const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
@@ -230,18 +242,20 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
await sleep(100); await sleep(100);
return redisInfos.map((x: any) => { return redisInfos.map((x: any) => {
x.tagPath = parentNode.key; x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeRedis).withParams(x); return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
}); });
}); });
// redis实例节点类型 // redis实例节点类型
const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async (parentNode: TagTreeNode) => { const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const redisInfo = parentNode.params; const redisInfo = parentNode.params;
const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.Redis.value, resourceCode: redisInfo.code });
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => { let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
return new TagTreeNode(x, `db${x}`, NodeTypeDb).withIsLeaf(true).withParams({ return new TagTreeNode(x, `db${x}`, NodeTypeDb).withIsLeaf(true).withParams({
id: redisInfo.id, id: redisInfo.id,
db: x, db: x,
flowProcdefKey: redisInfo.flowProcdefKey, flowProcdef: flowProcdef,
name: `db${x}`, name: `db${x}`,
keys: 0, keys: 0,
}); });
@@ -261,7 +275,7 @@ const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async
} }
// 替换label // 替换label
dbs.forEach((e: any) => { dbs.forEach((e: any) => {
e.label = `${e.params.name} [${e.params.keys}]`; e.label = `${e.params.name}`;
}); });
return dbs; return dbs;
}); });
@@ -274,7 +288,7 @@ const NodeTypeDb = new NodeType(RedisNodeType.Db).withNodeClickFunc((nodeData: T
redisInst.value.id = nodeData.params.id; redisInst.value.id = nodeData.params.id;
redisInst.value.db = Number.parseInt(nodeData.params.db); redisInst.value.db = Number.parseInt(nodeData.params.db);
redisInst.value.flowProcdefKey = nodeData.params.flowProcdefKey; redisInst.value.flowProcdef = nodeData.params.flowProcdef;
scan(); scan();
}); });
@@ -288,9 +302,11 @@ const treeProps = {
const defaultCount = 250; const defaultCount = 250;
const keyTreeRef: any = ref(null); const keyTreeRef: any = ref(null);
const tagTreeRef: any = ref(null);
const redisInst: Ref<RedisInst> = ref(new RedisInst()); const redisInst: Ref<RedisInst> = ref(new RedisInst());
const state = reactive({ const state = reactive({
defaultExpendKey: [] as any,
tags: [], tags: [],
redisList: [] as any, redisList: [] as any,
dbList: [], dbList: [],
@@ -331,7 +347,37 @@ const state = reactive({
const { scanParam, keyTreeData, newKeyDialog } = toRefs(state); const { scanParam, keyTreeData, newKeyDialog } = toRefs(state);
onMounted(async () => {}); const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
onMounted(async () => {
autoOpenRedis(autoOpenResource.value.redisCodePath);
});
watch(
() => autoOpenResource.value.redisCodePath,
(codePath: any) => {
autoOpenRedis(codePath);
}
);
const autoOpenRedis = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const redisCode = typeAndCodes[TagResourceTypeEnum.Redis.value][0];
state.defaultExpendKey = [tagPath, redisCode];
setTimeout(() => {
// 置空
autoOpenResourceStore.setRedisCodePath('');
tagTreeRef.value.setCurrentKey(redisCode);
}, 600);
};
const scan = async (appendKey = false) => { const scan = async (appendKey = false) => {
isTrue(state.scanParam.id != null, '请先选择redis'); isTrue(state.scanParam.id != null, '请先选择redis');

View File

@@ -58,13 +58,6 @@
placeholder="请输入密码, 修改操作可不填" placeholder="请输入密码, 修改操作可不填"
autocomplete="new-password" autocomplete="new-password"
> >
<!-- <template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
<template #reference>
<el-link @click="getPwd" :underline="false" type="primary" class="mr5">原密码</el-link>
</template>
</el-popover>
</template> -->
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="db" label="库号" required> <el-form-item prop="db" label="库号" required>
@@ -84,8 +77,6 @@
<el-form-item prop="remark" label="备注"> <el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input> <el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item> </el-form-item>
<procdef-select-form-item v-model="form.flowProcdefKey" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="其他配置" name="other"> <el-tab-pane label="其他配置" name="other">
@@ -108,12 +99,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, ref, watchEffect } from 'vue'; import { toRefs, reactive, ref, watch } from 'vue';
import { redisApi } from './api'; import { redisApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue'; import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue'; import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
import { ResourceCodePattern } from '@/common/pattern'; import { ResourceCodePattern } from '@/common/pattern';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue'; import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
@@ -199,7 +189,6 @@ const state = reactive({
db: '', db: '',
remark: '', remark: '',
sshTunnelMachineId: -1, sshTunnelMachineId: -1,
flowProcdefKey: '',
}, },
submitForm: {} as any, submitForm: {} as any,
dbList: [0], dbList: [0],
@@ -211,22 +200,25 @@ const { dialogVisible, tabActiveName, form, submitForm, dbList } = toRefs(state)
const { isFetching: testConnBtnLoading, execute: testConnExec } = redisApi.testConn.useApi(submitForm); const { isFetching: testConnBtnLoading, execute: testConnExec } = redisApi.testConn.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveRedisExec } = redisApi.saveRedis.useApi(submitForm); const { isFetching: saveBtnLoading, execute: saveRedisExec } = redisApi.saveRedis.useApi(submitForm);
watchEffect(() => { watch(
state.dialogVisible = props.visible; () => props.visible,
if (!state.dialogVisible) { () => {
return; state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
const redis: any = props.redis;
if (redis) {
state.form = { ...redis };
state.form.tagCodePaths = redis.tags.map((t: any) => t.codePath);
convertDb(state.form.db);
} else {
state.form = { db: '0', tagCodePaths: [] } as any;
state.dbList = [0];
}
} }
state.tabActiveName = 'basic'; );
const redis: any = props.redis;
if (redis) {
state.form = { ...redis };
state.form.tagCodePaths = redis.tags.map((t: any) => t.codePath);
convertDb(state.form.db);
} else {
state.form = { db: '0' } as any;
state.dbList = [0];
}
});
const convertDb = (db: string) => { const convertDb = (db: string) => {
state.dbList = db.split(',').map((x) => Number.parseInt(x)); state.dbList = db.split(',').map((x) => Number.parseInt(x));
@@ -252,30 +244,31 @@ const getReqForm = async () => {
}; };
const testConn = async () => { const testConn = async () => {
redisForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await redisForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
state.submitForm = await getReqForm(); state.submitForm = await getReqForm();
await testConnExec(); await testConnExec();
ElMessage.success('连接成功'); ElMessage.success('连接成功');
});
}; };
const btnOk = async () => { const btnOk = async () => {
redisForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await redisForm.value.validate();
ElMessage.error('请正确填写信息'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
state.submitForm = await getReqForm(); }
await saveRedisExec();
ElMessage.success('保存成功'); state.submitForm = await getReqForm();
emit('val-change', state.form); await saveRedisExec();
cancel(); ElMessage.success('保存成功');
}); emit('val-change', state.form);
cancel();
}; };
const cancel = () => { const cancel = () => {

View File

@@ -128,13 +128,12 @@
<el-descriptions-item :span="3" label="库">{{ detailDialog.data.db }}</el-descriptions-item> <el-descriptions-item :span="3" label="库">{{ detailDialog.data.db }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ detailDialog.data.remark }}</el-descriptions-item> <el-descriptions-item :span="3" label="备注">{{ detailDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="3" label="工单流程key">{{ detailDialog.data?.flowProcdefKey }}</el-descriptions-item>
<el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item> <el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(detailDialog.data.createTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="创建时间">{{ formatDate(detailDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ detailDialog.data.creator }}</el-descriptions-item> <el-descriptions-item :span="1" label="创建者">{{ detailDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(detailDialog.data.updateTime) }} </el-descriptions-item> <el-descriptions-item :span="2" label="更新时间">{{ formatDate(detailDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ detailDialog.data.modifier }}</el-descriptions-item> <el-descriptions-item :span="1" label="修改者">{{ detailDialog.data.modifier }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
@@ -154,7 +153,7 @@ import { redisApi } from './api';
import { onMounted, reactive, ref, Ref, toRefs } from 'vue'; import { onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import RedisEdit from './RedisEdit.vue'; import RedisEdit from './RedisEdit.vue';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import ResourceTags from '../component/ResourceTags.vue'; import ResourceTags from '../component/ResourceTags.vue';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
@@ -180,7 +179,6 @@ const columns = ref([
TableColumn.new('name', '名称'), TableColumn.new('name', '名称'),
TableColumn.new('host', 'host:port'), TableColumn.new('host', 'host:port'),
TableColumn.new('mode', 'mode'), TableColumn.new('mode', 'mode'),
TableColumn.new('flowProcdefKey', '关联流程'),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('code', '编号'), TableColumn.new('code', '编号'),
TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(),

View File

@@ -5,7 +5,7 @@ export type CmdExecProps = {
id: number; id: number;
db: number | string; db: number | string;
cmd: any[]; cmd: any[];
flowProcdefKey?: string; flowProcdef?: any;
visible?: boolean; visible?: boolean;
runSuccessFn?: Function; runSuccessFn?: Function;
cancelFn?: Function; cancelFn?: Function;

View File

@@ -4,9 +4,9 @@
<el-input type="textarea" disabled v-model="state.cmdStr" class="mt5" rows="5" /> <el-input type="textarea" disabled v-model="state.cmdStr" class="mt5" rows="5" />
<el-input @keyup.enter="runCmd" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" /> <el-input @keyup.enter="runCmd" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
<div v-if="props.flowProcdefKey"> <div v-if="props.flowProcdef">
<el-divider content-position="left">审批节点</el-divider> <el-divider content-position="left">审批节点</el-divider>
<procdef-tasks :procdef-key="props.flowProcdefKey" /> <procdef-tasks :procdef="props.flowProcdef" />
</div> </div>
<template #footer> <template #footer>
@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<CmdExecProps>(), {});
const remarkInputRef = ref<InputInstance>(); const remarkInputRef = ref<InputInstance>();
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
flowProcdefKey: '' as any, flowProcdef: null as any,
cmdStr: '', cmdStr: '',
remark: '', remark: '',
btnLoading: false, btnLoading: false,

View File

@@ -13,9 +13,9 @@ export class RedisInst {
db: number; db: number;
/** /**
* 流程定义key,若存在则需要审批执行 * 流程定义,若存在则需要审批执行
*/ */
flowProcdefKey: string; flowProcdef: any;
/** /**
* 执行命令 * 执行命令
@@ -24,11 +24,11 @@ export class RedisInst {
*/ */
async runCmd(cmd: any[]) { async runCmd(cmd: any[]) {
// 工单流程定义存在,并且为写入命令时,弹窗输入工单相关信息并提交 // 工单流程定义存在,并且为写入命令时,弹窗输入工单相关信息并提交
if (this.flowProcdefKey && writeCmd[cmd[0].toUpperCase()]) { if (this.flowProcdef && writeCmd[cmd[0].toUpperCase()]) {
showCmdExecBox({ showCmdExecBox({
id: this.id, id: this.id,
db: this.db, db: this.db,
flowProcdefKey: this.flowProcdefKey, flowProcdef: this.flowProcdef,
cmd, cmd,
}); });
// 报错,阻止后续继续执行 // 报错,阻止后续继续执行

View File

@@ -11,6 +11,14 @@
<el-button v-auth="'authcert:save'" type="primary" icon="plus" @click="edit(false)">添加</el-button> <el-button v-auth="'authcert:save'" type="primary" icon="plus" @click="edit(false)">添加</el-button>
</template> </template>
<template #resourceCode="{ data }">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.resourceType)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.resourceType)?.extra.iconColor"
/>
{{ data.resourceCode }}
</template>
<template #action="{ data }"> <template #action="{ data }">
<el-button v-auth="'authcert:save'" @click="edit(data)" type="primary" link>编辑</el-button> <el-button v-auth="'authcert:save'" @click="edit(data)" type="primary" link>编辑</el-button>
@@ -41,6 +49,7 @@ import { SearchItem } from '@/components/SearchForm';
import { AuthCertCiphertextTypeEnum, AuthCertTypeEnum } from './enums'; import { AuthCertCiphertextTypeEnum, AuthCertTypeEnum } from './enums';
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum'; import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
import ResourceAuthCertEdit from '../component/ResourceAuthCertEdit.vue'; import ResourceAuthCertEdit from '../component/ResourceAuthCertEdit.vue';
import EnumValue from '@/common/Enum';
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);
const state = reactive({ const state = reactive({
@@ -50,6 +59,7 @@ const state = reactive({
name: null, name: null,
}, },
searchItems: [ searchItems: [
SearchItem.input('resourceCode', '资源编号'),
SearchItem.input('name', '凭证名称'), SearchItem.input('name', '凭证名称'),
SearchItem.select('resourceType', '资源类型').withEnum(ResourceTypeEnum), SearchItem.select('resourceType', '资源类型').withEnum(ResourceTypeEnum),
SearchItem.select('type', '凭证类型').withEnum(AuthCertTypeEnum), SearchItem.select('type', '凭证类型').withEnum(AuthCertTypeEnum),
@@ -60,8 +70,7 @@ const state = reactive({
TableColumn.new('type', '凭证类型').typeTag(AuthCertTypeEnum), TableColumn.new('type', '凭证类型').typeTag(AuthCertTypeEnum),
TableColumn.new('username', '用户名'), TableColumn.new('username', '用户名'),
TableColumn.new('ciphertextType', '密文类型').typeTag(AuthCertCiphertextTypeEnum), TableColumn.new('ciphertextType', '密文类型').typeTag(AuthCertCiphertextTypeEnum),
TableColumn.new('resourceType', '资源类型').typeTag(TagResourceTypeEnum), TableColumn.new('resourceCode', '资源编号').isSlot().setAddWidth(30),
TableColumn.new('resourceCode', '资源编号'),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('creator', '创建人'), TableColumn.new('creator', '创建人'),
TableColumn.new('createTime', '创建时间').isTime(), TableColumn.new('createTime', '创建时间').isTime(),

View File

@@ -75,20 +75,16 @@
<el-descriptions-item label="code">{{ currentTag.code }}</el-descriptions-item> <el-descriptions-item label="code">{{ currentTag.code }}</el-descriptions-item>
<el-descriptions-item label="路径" :span="2"> <el-descriptions-item label="路径" :span="2">
<span v-for="item in parseTagPath(currentTag.codePath)" :key="item.code"> <TagCodePath :path="currentTag.codePath" />
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon" class="mr2" />
<span> {{ item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="名称">{{ currentTag.name }}</el-descriptions-item> <el-descriptions-item label="名称">{{ currentTag.name }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ currentTag.remark }}</el-descriptions-item> <el-descriptions-item label="备注">{{ currentTag.remark }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ currentTag.creator }}</el-descriptions-item> <el-descriptions-item label="创建者">{{ currentTag.creator }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ dateFormat(currentTag.createTime) }}</el-descriptions-item> <el-descriptions-item label="创建时间">{{ formatDate(currentTag.createTime) }}</el-descriptions-item>
<el-descriptions-item label="修改者">{{ currentTag.modifier }}</el-descriptions-item> <el-descriptions-item label="修改者">{{ currentTag.modifier }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ dateFormat(currentTag.updateTime) }}</el-descriptions-item> <el-descriptions-item label="更新时间">{{ formatDate(currentTag.updateTime) }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-tab-pane> </el-tab-pane>
@@ -149,20 +145,23 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, ref, watch, reactive, onMounted, Ref } from 'vue'; import { toRefs, ref, watch, reactive, onMounted, Ref, defineAsyncComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { tagApi } from './api'; import { tagApi } from './api';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { Splitpanes, Pane } from 'splitpanes'; import { Splitpanes, Pane } from 'splitpanes';
import MachineList from '../machine/MachineList.vue';
import RedisList from '../redis/RedisList.vue';
import MongoList from '../mongo/MongoList.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumTag from '@/components/enumtag/EnumTag.vue';
import EnumValue from '@/common/Enum'; import EnumValue from '@/common/Enum';
import InstanceList from '../db/InstanceList.vue'; import TagCodePath from '../component/TagCodePath.vue';
import { isPrefixSubsequence } from '@/common/utils/string';
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
const RedisList = defineAsyncComponent(() => import('../redis/RedisList.vue'));
const MongoList = defineAsyncComponent(() => import('../mongo/MongoList.vue'));
interface Tree { interface Tree {
id: number; id: number;
@@ -198,6 +197,9 @@ const contextmenuAdd = new ContextmenuItem('addTag', '添加子标签')
const contextmenuEdit = new ContextmenuItem('edit', '编辑') const contextmenuEdit = new ContextmenuItem('edit', '编辑')
.withIcon('edit') .withIcon('edit')
.withPermission('tag:save') .withPermission('tag:save')
.withHideFunc((data: any) => {
return data.type != TagResourceTypeEnum.Tag.value;
})
.withOnClick((data: any) => showEditTagDialog(data)); .withOnClick((data: any) => showEditTagDialog(data));
const contextmenuDel = new ContextmenuItem('delete', '删除') const contextmenuDel = new ContextmenuItem('delete', '删除')
@@ -345,38 +347,6 @@ const handleDrop = async (draggingNode: any, dropNode: any) => {
} }
}; };
const parseTagPath = (tagPath: string) => {
if (!tagPath) {
return [];
}
const res = [] as any;
const codes = tagPath.split('/');
for (let code of codes) {
const typeAndCode = code.split('|');
if (typeAndCode.length == 1) {
const tagCode = typeAndCode[0];
if (!tagCode) {
continue;
}
res.push({
type: TagResourceTypeEnum.Tag.value,
code: typeAndCode[0],
});
continue;
}
res.push({
type: typeAndCode[0],
code: typeAndCode[1],
});
}
res[res.length - 1].isEnd = true;
return res;
};
const tabChange = () => { const tabChange = () => {
setNowTabData(); setNowTabData();
}; };
@@ -402,8 +372,7 @@ const setNowTabData = () => {
}; };
const filterNode = (value: string, data: Tree) => { const filterNode = (value: string, data: Tree) => {
if (!value) return true; return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
}; };
const search = async () => { const search = async () => {
@@ -411,6 +380,11 @@ const search = async () => {
state.data = res; state.data = res;
}; };
const getDetail = async (id: number) => {
const tags = await tagApi.listByQuery.request({ id });
return tags?.[0];
};
// 树节点右击事件 // 树节点右击事件
const nodeContextmenu = (event: any, data: any) => { const nodeContextmenu = (event: any, data: any) => {
const { clientX, clientY } = event; const { clientX, clientY } = event;
@@ -419,8 +393,8 @@ const nodeContextmenu = (event: any, data: any) => {
contextmenuRef.value.openContextmenu(data); contextmenuRef.value.openContextmenu(data);
}; };
const treeNodeClick = (data: any) => { const treeNodeClick = async (data: any) => {
state.currentTag = data; state.currentTag = await getDetail(data.id);
// 关闭可能存在的右击菜单 // 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu(); contextmenuRef.value.closeContextmenu();
}; };

View File

@@ -14,13 +14,12 @@
<el-button v-auth="'team:del'" :disabled="selectionData.length < 1" @click="deleteTeam()" type="danger" icon="delete">删除</el-button> <el-button v-auth="'team:del'" :disabled="selectionData.length < 1" @click="deleteTeam()" type="danger" icon="delete">删除</el-button>
</template> </template>
<template #tagPath="{ data }"> <template #tags="{ data }">
<tag-info :tag-path="data.tagPath" /> <TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
<span class="ml5">
{{ data.tagPath }}
</span>
</template> </template>
<template #validityDate="{ data }"> {{ data.validityStartDate }} ~ {{ data.validityEndDate }} </template>
<template #action="{ data }"> <template #action="{ data }">
<el-button @click.prevent="showMembers(data)" link type="primary">成员</el-button> <el-button @click.prevent="showMembers(data)" link type="primary">成员</el-button>
@@ -39,54 +38,30 @@
<DrawerHeader :header="addTeamDialog.form.id ? '编辑团队' : '添加团队'" :back="cancelSaveTeam" /> <DrawerHeader :header="addTeamDialog.form.id ? '编辑团队' : '添加团队'" :back="cancelSaveTeam" />
</template> </template>
<el-form ref="teamForm" :model="addTeamDialog.form" label-width="auto"> <el-form ref="teamForm" :model="addTeamDialog.form" :rules="teamFormRules" label-width="auto">
<el-form-item prop="name" label="团队名" required> <el-form-item prop="name" label="团队名" required>
<el-input :disabled="addTeamDialog.form.id" v-model="addTeamDialog.form.name" auto-complete="off"></el-input> <el-input :disabled="addTeamDialog.form.id" v-model="addTeamDialog.form.name" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="validityDate" label="生效时间" required>
<el-date-picker
v-model="addTeamDialog.form.validityDate"
type="datetimerange"
start-placeholder="生效开始时间"
end-placeholder="生效结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
date-format="YYYY-MM-DD"
time-format="HH:mm:ss"
/>
</el-form-item>
<el-form-item label="备注"> <el-form-item label="备注">
<el-input v-model="addTeamDialog.form.remark" auto-complete="off"></el-input> <el-input v-model="addTeamDialog.form.remark" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="tag" label="标签"> <el-form-item prop="tag" label="标签">
<div class="w100" style="border: 1px solid var(--el-border-color)"> <TagTreeCheck height="calc(100vh - 390px)" v-model="state.addTeamDialog.form.codePaths" :tag-type="0" />
<el-input v-model="filterTag" clearable placeholder="输入关键字过滤" size="small" />
<el-scrollbar style="height: calc(100vh - 330px)">
<el-tree
ref="tagTreeRef"
style="width: 100%"
:data="state.tags"
:default-expanded-keys="state.addTeamDialog.form.tags"
:default-checked-keys="state.addTeamDialog.form.tags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
node-key="id"
:props="{
value: 'id',
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" />
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
</span>
</span>
</template>
</el-tree>
</el-scrollbar>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -131,7 +106,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, Ref, watch } from 'vue'; import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import { tagApi } from './api'; import { tagApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { notBlank } from '@/common/assert'; import { notBlank } from '@/common/assert';
@@ -139,19 +114,37 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue'; import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue'; import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import TagTreeCheck from '../component/TagTreeCheck.vue';
import TagCodePath from '../component/TagCodePath.vue';
import { formatDate } from '@/common/utils/format';
const teamForm: any = ref(null); const teamForm: any = ref(null);
const tagTreeRef: any = ref(null);
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);
const showMemPageTableRef: Ref<any> = ref(null); const showMemPageTableRef: Ref<any> = ref(null);
const filterTag = ref('');
const teamFormRules = {
name: [
{
required: true,
message: '请输入团队名',
trigger: ['change', 'blur'],
},
],
validityDate: [
{
required: true,
message: '请选择生效时间',
trigger: ['change', 'blur'],
},
],
};
const searchItems = [SearchItem.input('name', '团队名称')]; const searchItems = [SearchItem.input('name', '团队名称')];
const columns = [ const columns = [
TableColumn.new('name', '团队名称'), TableColumn.new('name', '团队名称'),
TableColumn.new('tags', '分配标签').isSlot().setAddWidth(40),
TableColumn.new('validityDate', '有效期').isSlot('validityDate').setMinWidth(310),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('creator', '创建者'), TableColumn.new('creator', '创建者'),
TableColumn.new('createTime', '创建时间').isTime(), TableColumn.new('createTime', '创建时间').isTime(),
@@ -162,10 +155,9 @@ const columns = [
const state = reactive({ const state = reactive({
currentEditPermissions: false, currentEditPermissions: false,
tags: [],
addTeamDialog: { addTeamDialog: {
visible: false, visible: false,
form: { id: 0, name: '', remark: '', tags: [] }, form: { id: 0, name: '', validityDate: ['', ''], validityStartDate: '', validityEndDate: '', remark: '', codePaths: [] },
}, },
query: { query: {
pageNum: 1, pageNum: 1,
@@ -211,50 +203,37 @@ const search = async () => {
pageTableRef.value.search(); pageTableRef.value.search();
}; };
watch(filterTag, (val) => {
tagTreeRef.value!.filter(val);
});
const filterNode = (value: string, data: any) => {
if (!value) {
return true;
}
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
};
const showSaveTeamDialog = async (data: any) => { const showSaveTeamDialog = async (data: any) => {
state.tags = await tagApi.getTagTrees.request(null);
if (data) { if (data) {
state.addTeamDialog.form.id = data.id; state.addTeamDialog.form.id = data.id;
state.addTeamDialog.form.name = data.name; state.addTeamDialog.form.name = data.name;
state.addTeamDialog.form.validityDate = [data.validityStartDate, data.validityEndDate];
state.addTeamDialog.form.remark = data.remark; state.addTeamDialog.form.remark = data.remark;
state.addTeamDialog.form.tags = await tagApi.getTeamTagIds.request({ teamId: data.id }); state.addTeamDialog.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
} else {
setTimeout(() => { let end = new Date();
const checkedNodes = tagTreeRef.value.getCheckedNodes(); end.setFullYear(end.getFullYear() + 10);
console.log('check nodes: ', checkedNodes); state.addTeamDialog.form.validityDate = [formatDate(new Date()), formatDate(end)];
// 禁用选中节点的所有父节点,不可选中
for (let checkNodeData of checkedNodes) {
disableParentNodes(tagTreeRef.value.getNode(checkNodeData.id).parent);
}
}, 200);
} }
state.addTeamDialog.visible = true; state.addTeamDialog.visible = true;
}; };
const saveTeam = async () => { const saveTeam = async () => {
teamForm.value.validate(async (valid: any) => { try {
if (valid) { await teamForm.value.validate();
const form = state.addTeamDialog.form; } catch (e: any) {
form.tags = tagTreeRef.value.getCheckedKeys(false); ElMessage.error('请正确填写信息');
await tagApi.saveTeam.request(form); return false;
ElMessage.success('保存成功'); }
search();
cancelSaveTeam(); const form = state.addTeamDialog.form;
} form.validityStartDate = form.validityDate[0];
}); form.validityEndDate = form.validityDate[1];
await tagApi.saveTeam.request(form);
ElMessage.success('保存成功');
search();
cancelSaveTeam();
}; };
const cancelSaveTeam = () => { const cancelSaveTeam = () => {
@@ -318,48 +297,5 @@ const cancelAddMember = () => {
state.showMemDialog.memForm = {} as any; state.showMemDialog.memForm = {} as any;
state.showMemDialog.addVisible = false; state.showMemDialog.addVisible = false;
}; };
const tagTreeNodeCheck = (data: any) => {
const node = tagTreeRef.value.getNode(data.id);
console.log('check node: ', node);
if (node.checked) {
// 如果选中了子节点,则需要将父节点全部取消选中,并禁用父节点
unCheckParentNodes(node.parent);
disableParentNodes(node.parent);
} else {
// 如果取消了选中,则需要根据条件恢复父节点的选中状态
disableParentNodes(node.parent, false);
}
};
const unCheckParentNodes = (node: any) => {
if (!node) {
return;
}
tagTreeRef.value.setChecked(node, false, false);
unCheckParentNodes(node.parent);
};
/**
* 禁用该节点以及所有父节点
* @param node 节点
* @param disable 是否禁用
*/
const disableParentNodes = (node: any, disable = true) => {
if (!node) {
return;
}
if (!disable) {
// 恢复为非禁用状态时,若同层级存在一个选中状态或者禁用状态,则继续禁用 不恢复非禁用状态。
for (let oneLevelNodes of node.childNodes) {
if (oneLevelNodes.checked || oneLevelNodes.data.disabled) {
return;
}
}
}
node.data.disabled = disable;
disableParentNodes(node.parent, disable);
};
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -9,6 +9,7 @@ export const tagApi = {
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'), getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
countTagResource: Api.newGet('/tag-trees/resources/count'), countTagResource: Api.newGet('/tag-trees/resources/count'),
getRelateTagIds: Api.newGet('/tag-trees/relate/{relateType}/{relateId}'),
getTeams: Api.newGet('/teams'), getTeams: Api.newGet('/teams'),
saveTeam: Api.newPost('/teams'), saveTeam: Api.newPost('/teams'),
@@ -17,8 +18,6 @@ export const tagApi = {
getTeamMem: Api.newGet('/teams/{teamId}/members'), getTeamMem: Api.newGet('/teams/{teamId}/members'),
saveTeamMem: Api.newPost('/teams/{teamId}/members'), saveTeamMem: Api.newPost('/teams/{teamId}/members'),
delTeamMem: Api.newDelete('/teams/{teamId}/members/{accountId}'), delTeamMem: Api.newDelete('/teams/{teamId}/members/{accountId}'),
getTeamTagIds: Api.newGet('/teams/{teamId}/tags'),
}; };
export const resourceAuthCertApi = { export const resourceAuthCertApi = {
@@ -27,3 +26,7 @@ export const resourceAuthCertApi = {
save: Api.newPost('/auth-certs'), save: Api.newPost('/auth-certs'),
delete: Api.newDelete('/auth-certs/{id}'), delete: Api.newDelete('/auth-certs/{id}'),
}; };
export const resourceOpLogApi = {
getAccountResourceOpLogs: Api.newGet('/resource-op-logs/account'),
};

View File

@@ -14,3 +14,7 @@ export const AuthCertCiphertextTypeEnum = {
PrivateKey: EnumValue.of(2, '秘钥').tagTypeSuccess(), PrivateKey: EnumValue.of(2, '秘钥').tagTypeSuccess(),
Public: EnumValue.of(-1, '公共凭证').tagTypeSuccess(), Public: EnumValue.of(-1, '公共凭证').tagTypeSuccess(),
}; };
export const TagTreeRelateTypeEnum = {
Team: EnumValue.of(1, '团队'),
};

View File

@@ -1,112 +1,6 @@
<template> <template>
<div class="personal"> <div class="personal">
<el-row> <el-row>
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
<img :src="userInfo.photo" />
</el-upload>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb18"
>{{ currentTime }}{{ userInfo.name }}生活变的再糟糕也不妨碍我变得更好
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">用户名</div>
<div class="personal-item-value">{{ userInfo.username }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">角色</div>
<div class="personal-item-value">{{ roleInfo }}</div>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录IP</div>
<div class="personal-item-value">{{ userInfo.lastLoginIp }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录时间</div>
<div class="personal-item-value">{{ dateFormat(userInfo.lastLoginTime) }}</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</div>
</el-card>
</el-col>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>消息通知</span>
<span @click="showMsgs" class="personal-info-more">更多</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in msgDialog.msgs.list as any" :key="k" class="personal-info-li">
<a class="personal-info-li-title">{{ `[${getMsgTypeDesc(v.type)}] ${v.msg}` }}</a>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-dialog width="900px" title="消息" v-model="msgDialog.visible">
<el-table border :data="msgDialog.msgs.list" size="small">
<el-table-column property="type" label="类型" width="60">
<template #default="scope">
{{ getMsgTypeDesc(scope.row.type) }}
</template>
</el-table-column>
<el-table-column property="msg" label="消息"></el-table-column>
<el-table-column property="createTime" label="时间" width="150">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-row type="flex" class="mt5" justify="center">
<el-pagination
small
@current-change="getMsgs"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="msgDialog.msgs.total"
v-model:current-page="msgDialog.query.pageNum"
:page-size="msgDialog.query.pageSize"
/>
</el-row>
</el-dialog>
<!-- 营销推荐 -->
<!-- <el-col :span="24">
<el-card shadow="hover" class="mt15" header="营销推荐">
<el-row :gutter="15" class="personal-recommend-row">
<el-col :sm="6" v-for="(v, k) in recommendList" :key="k" class="personal-recommend-col">
<div class="personal-recommend" :style="{ 'background-color': v.bg }">
<i :class="v.icon" :style="{ color: v.iconColor }"></i>
<div class="personal-recommend-auto">
<div>{{ v.title }}</div>
<div class="personal-recommend-msg">{{ v.msg }}</div>
</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col> -->
<!-- 更新信息 --> <!-- 更新信息 -->
<el-col :span="24"> <el-col :span="24">
<el-card shadow="hover" class="mt15 personal-edit" header="更新信息"> <el-card shadow="hover" class="mt15 personal-edit" header="更新信息">
@@ -142,28 +36,6 @@
</div> </div>
</div> </div>
</span> </span>
<!-- <div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">密保手机</div>
<div class="personal-edit-safe-item-left-value">已绑定手机132****4108</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button type="text">立即修改</el-button>
</div>
</div>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">密保问题</div>
<div class="personal-edit-safe-item-left-value">已设置密保问题账号安全大幅度提升</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button type="text">立即设置</el-button>
</div>
</div>
</div> -->
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@@ -171,33 +43,16 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, computed, onMounted } from 'vue'; import { toRefs, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { formatAxis } from '@/common/utils/format';
import { personApi } from './api'; import { personApi } from './api';
import { dateFormat } from '@/common/utils/date';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import config from '@/common/config'; import config from '@/common/config';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
const { userInfo } = storeToRefs(useUserInfo());
const state = reactive({ const state = reactive({
accountInfo: { accountInfo: {
roles: [], roles: [],
}, },
msgs: [],
msgDialog: {
visible: false,
query: {
pageSize: 10,
pageNum: 1,
},
msgs: {
list: [],
total: null,
},
},
recommendList: [], recommendList: [],
accountForm: { accountForm: {
password: '', password: '',
@@ -208,27 +63,10 @@ const state = reactive({
}, },
}); });
const { msgDialog, accountForm, authStatus } = toRefs(state); const { accountForm, authStatus } = toRefs(state);
// 当前时间提示语
const currentTime = computed(() => {
return formatAxis(new Date());
});
const showMsgs = () => {
state.msgDialog.visible = true;
};
const roleInfo = computed(() => {
if (state.accountInfo.roles.length == 0) {
return '';
}
return state.accountInfo.roles.map((val: any) => val.name).join('、');
});
onMounted(async () => { onMounted(async () => {
getAccountInfo(); getAccountInfo();
getMsgs();
state.authStatus = await personApi.authStatus.request(); state.authStatus = await personApi.authStatus.request();
}); });
@@ -277,162 +115,11 @@ const unbindOAuth2 = async () => {
ElMessage.success('解绑成功'); ElMessage.success('解绑成功');
state.authStatus = await personApi.authStatus.request(); state.authStatus = await personApi.authStatus.request();
}; };
const getMsgs = async () => {
const res = await personApi.getMsgs.request(state.msgDialog.query);
state.msgDialog.msgs = res;
};
const getMsgTypeDesc = (type: number) => {
if (type == 1) {
return '登录';
}
if (type == 2) {
return '通知';
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '../../theme/mixins/index.scss'; @import '../../theme/mixins/index.scss';
.personal { .personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 100px;
height: 130px;
border-radius: 3px;
::v-deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: gray;
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: gray;
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: grey;
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
.personal-recommend-row {
.personal-recommend-col {
.personal-recommend {
position: relative;
height: 100px;
color: #ffffff;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
&:hover {
i {
right: 0px !important;
bottom: 0px !important;
transition: all ease 0.3s;
}
}
i {
position: absolute;
right: -10px;
bottom: -10px;
font-size: 70px;
transform: rotate(-30deg);
transition: all ease 0.3s;
}
.personal-recommend-auto {
padding: 15px;
position: absolute;
left: 0;
top: 5%;
.personal-recommend-msg {
font-size: 12px;
margin-top: 10px;
}
}
}
}
}
.personal-edit { .personal-edit {
.personal-edit-title { .personal-edit-title {
position: relative; position: relative;

View File

@@ -114,19 +114,19 @@ watchEffect(() => {
}); });
const btnOk = async () => { const btnOk = async () => {
accountForm.value.validate(async (valid: boolean) => { try {
if (!valid) { await accountForm.value.validate();
ElMessage.error('表单填写有误'); } catch (e: any) {
return false; ElMessage.error('请正确填写信息');
} return false;
}
await saveAccountExec(); await saveAccountExec();
ElMessage.success('操作成功'); ElMessage.success('操作成功');
emit('val-change', state.form); emit('val-change', state.form);
//重置表单域 //重置表单域
accountForm.value.resetFields(); accountForm.value.resetFields();
state.form = {} as any; state.form = {} as any;
});
}; };
const cancel = () => { const cancel = () => {

View File

@@ -42,7 +42,7 @@
<el-table-column property="creator" label="分配账号" width="125"></el-table-column> <el-table-column property="creator" label="分配账号" width="125"></el-table-column>
<el-table-column property="createTime" label="分配时间"> <el-table-column property="createTime" label="分配时间">
<template #default="scope"> <template #default="scope">
{{ dateFormat(scope.row.createTime) }} {{ formatDate(scope.row.createTime) }}
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -60,7 +60,7 @@ import AccountEdit from './AccountEdit.vue';
import { AccountStatusEnum } from '../enums'; import { AccountStatusEnum } from '../enums';
import { accountApi } from '../api'; import { accountApi } from '../api';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="900px" :destroy-on-close="true"> <el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="900px" :destroy-on-close="true">
<el-form ref="configForm" :model="form" label-width="auto"> <el-form ref="configForm" :model="form" :rules="rules" label-width="auto">
<el-form-item prop="name" label="配置项" required> <el-form-item prop="name" label="配置项" required>
<el-input v-model="form.name"></el-input> <el-input v-model="form.name"></el-input>
</el-form-item> </el-form-item>
@@ -44,6 +44,24 @@
import { ref, toRefs, reactive, watch, watchEffect } from 'vue'; import { ref, toRefs, reactive, watch, watchEffect } from 'vue';
import { configApi, accountApi } from '../api'; import { configApi, accountApi } from '../api';
import { DynamicFormEdit } from '@/components/dynamic-form'; import { DynamicFormEdit } from '@/components/dynamic-form';
import { ElMessage } from 'element-plus';
const rules = {
name: [
{
required: true,
message: '请输入配置项',
trigger: ['change', 'blur'],
},
],
key: [
{
required: true,
message: '请输入配置key',
trigger: ['change', 'blur'],
},
],
};
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -82,31 +100,34 @@ const { dvisible, params, form } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form); const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form);
watchEffect(() => { watch(
state.dvisible = props.visible; () => props.visible,
if (!state.dvisible) { () => {
return; state.dvisible = props.visible;
} if (!state.dvisible) {
return;
}
if (props.data) { if (props.data) {
state.form = { ...(props.data as any) }; state.form = { ...(props.data as any) };
if (state.form.params) { if (state.form.params) {
state.params = JSON.parse(state.form.params); state.params = JSON.parse(state.form.params);
} else {
state.params = [];
}
} else { } else {
state.form = { permission: 'all' } as any;
state.params = []; state.params = [];
} }
} else {
state.form = { permission: 'all' } as any;
state.params = [];
}
if (state.form.permission != 'all') { if (state.form.permission != 'all') {
const accounts = state.form.permission.split(','); const accounts = state.form.permission.split(',');
state.permissionAccount = accounts.slice(0, accounts.length - 1); state.permissionAccount = accounts.slice(0, accounts.length - 1);
} else { } else {
state.permissionAccount = []; state.permissionAccount = [];
}
} }
}); );
const cancel = () => { const cancel = () => {
// 更新父组件visible prop对应的值为false // 更新父组件visible prop对应的值为false
@@ -125,22 +146,25 @@ const getAccount = (username: any) => {
}; };
const btnOk = async () => { const btnOk = async () => {
configForm.value.validate(async (valid: boolean) => { try {
if (valid) { await configForm.value.validate();
if (state.params) { } catch (e: any) {
state.form.params = JSON.stringify(state.params); ElMessage.error('请正确填写信息');
} return false;
if (state.permissionAccount.length > 0) { }
state.form.permission = state.permissionAccount.join(',') + ',';
} else {
state.form.permission = 'all';
}
await saveConfigExec(); if (state.params) {
emit('val-change', state.form); state.form.params = JSON.stringify(state.params);
cancel(); }
} if (state.permissionAccount.length > 0) {
}); state.form.permission = state.permissionAccount.join(',') + ',';
} else {
state.form.permission = 'all';
}
await saveConfigExec();
emit('val-change', state.form);
cancel();
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -128,7 +128,7 @@
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
e
<template #footer> <template #footer>
<div> <div>
<el-button @click="cancel()"> </el-button> <el-button @click="cancel()"> </el-button>
@@ -254,7 +254,14 @@ const changeLinkType = () => {
state.form.meta.component = ''; state.form.meta.component = '';
}; };
const btnOk = () => { const btnOk = async () => {
try {
await menuForm.value.validate();
} catch (e: any) {
ElMessage.error('请正确填写信息');
return false;
}
const submitForm = { ...state.form }; const submitForm = { ...state.form };
if (submitForm.type == 1) { if (submitForm.type == 1) {
// 如果是菜单则解析meta如果值为false或者''则去除该值 // 如果是菜单则解析meta如果值为false或者''则去除该值
@@ -263,16 +270,12 @@ const btnOk = () => {
submitForm.meta = null as any; submitForm.meta = null as any;
} }
menuForm.value.validate(async (valid: any) => { state.submitForm = submitForm;
if (valid) { await saveResouceExec();
state.submitForm = submitForm;
await saveResouceExec();
emit('val-change', submitForm); emit('val-change', submitForm);
ElMessage.success('保存成功'); ElMessage.success('保存成功');
cancel(); cancel();
}
});
}; };
const parseMenuMeta = (meta: any) => { const parseMenuMeta = (meta: any) => {
@@ -314,10 +317,4 @@ const cancel = () => {
emit('cancel'); emit('cancel');
}; };
</script> </script>
<style lang="scss"> <style lang="scss"></style>
// .m-dialog {
// .el-cascader {
// width: 100%;
// }
// }
</style>

View File

@@ -89,9 +89,9 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="创建者">{{ currentResource.creator }}</el-descriptions-item> <el-descriptions-item label="创建者">{{ currentResource.creator }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ dateFormat(currentResource.createTime) }} </el-descriptions-item> <el-descriptions-item label="创建时间">{{ formatDate(currentResource.createTime) }} </el-descriptions-item>
<el-descriptions-item label="修改者">{{ currentResource.modifier }}</el-descriptions-item> <el-descriptions-item label="修改者">{{ currentResource.modifier }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ dateFormat(currentResource.updateTime) }} </el-descriptions-item> <el-descriptions-item label="更新时间">{{ formatDate(currentResource.updateTime) }} </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@@ -119,10 +119,11 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import ResourceEdit from './ResourceEdit.vue'; import ResourceEdit from './ResourceEdit.vue';
import { ResourceTypeEnum } from '../enums'; import { ResourceTypeEnum } from '../enums';
import { resourceApi } from '../api'; import { resourceApi } from '../api';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumTag from '@/components/enumtag/EnumTag.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { Splitpanes, Pane } from 'splitpanes'; import { Splitpanes, Pane } from 'splitpanes';
import { isPrefixSubsequence } from '@/common/utils/string';
const menuTypeValue = ResourceTypeEnum.Menu.value; const menuTypeValue = ResourceTypeEnum.Menu.value;
const permissionTypeValue = ResourceTypeEnum.Permission.value; const permissionTypeValue = ResourceTypeEnum.Permission.value;
@@ -209,10 +210,7 @@ watch(filterResource, (val) => {
}); });
const filterNode = (value: string, data: any) => { const filterNode = (value: string, data: any) => {
if (!value) { return !value || isPrefixSubsequence(value, data.name);
return true;
}
return data.name.includes(value);
}; };
const search = async () => { const search = async () => {

View File

@@ -19,8 +19,8 @@
</el-tree> </el-tree>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="cancel"> </el-button> <el-button :loading="state.submiting" @click="cancel"> </el-button>
<el-button type="primary" @click="btnOk"> </el-button> <el-button :loading="state.submiting" type="primary" @click="btnOk"> </el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@@ -66,6 +66,7 @@ const menuTree: any = ref(null);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
roleInfo: null as any, roleInfo: null as any,
submiting: false,
}); });
const { dialogVisible, roleInfo } = toRefs(state); const { dialogVisible, roleInfo } = toRefs(state);
@@ -82,12 +83,17 @@ const btnOk = async () => {
let menuIds = menuTree.value.getCheckedKeys(); let menuIds = menuTree.value.getCheckedKeys();
let halfMenuIds = menuTree.value.getHalfCheckedKeys(); let halfMenuIds = menuTree.value.getHalfCheckedKeys();
let resources = [].concat(menuIds, halfMenuIds).join(','); let resources = [].concat(menuIds, halfMenuIds).join(',');
await roleApi.saveResources.request({ try {
id: props.role!.id, state.submiting = true;
resourceIds: resources, await roleApi.saveResources.request({
}); id: props.role!.id,
ElMessage.success('保存成功!'); resourceIds: resources,
emit('cancel'); });
ElMessage.success('保存成功!');
emit('cancel');
} finally {
state.submiting = false;
}
}; };
const cancel = () => { const cancel = () => {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="role-dialog"> <div class="role-dialog">
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true"> <el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
<el-form ref="roleForm" :model="form" label-width="auto"> <el-form ref="roleForm" :model="form" :rules="rules" label-width="auto">
<el-form-item prop="name" label="角色名称" required> <el-form-item prop="name" label="角色名称" required>
<el-input v-model="form.name" auto-complete="off"></el-input> <el-input v-model="form.name" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
@@ -32,6 +32,30 @@ import { ref, toRefs, reactive, watchEffect } from 'vue';
import { roleApi } from '../api'; import { roleApi } from '../api';
import { RoleStatusEnum } from '../enums'; import { RoleStatusEnum } from '../enums';
const rules = {
name: [
{
required: true,
message: '请输入角色名称',
trigger: ['change', 'blur'],
},
],
code: [
{
required: true,
message: '请输入角色编号',
trigger: ['change', 'blur'],
},
],
status: [
{
required: true,
message: '请选择状态',
trigger: ['change', 'blur'],
},
],
};
const props = defineProps({ const props = defineProps({
visible: { visible: {
type: Boolean, type: Boolean,
@@ -80,13 +104,15 @@ const cancel = () => {
}; };
const btnOk = async () => { const btnOk = async () => {
roleForm.value.validate(async (valid: boolean) => { try {
if (valid) { await roleForm.value.validate();
await saveRoleExec(); } catch (e: any) {
emit('val-change', state.form); return false;
cancel(); }
}
}); await saveRoleExec();
emit('val-change', state.form);
cancel();
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -20,7 +20,7 @@
{{ data.creator }} {{ data.creator }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="分配时间"> <el-descriptions-item label="分配时间">
{{ dateFormat(data.createTime) }} {{ formatDate(data.createTime) }}
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</template> </template>
@@ -35,7 +35,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, watch } from 'vue'; import { toRefs, reactive, watch } from 'vue';
import { ResourceTypeEnum } from '../enums'; import { ResourceTypeEnum } from '../enums';
import { dateFormat } from '@/common/utils/date'; import { formatDate } from '@/common/utils/format';
const props = defineProps({ const props = defineProps({
visible: { visible: {

View File

@@ -11,9 +11,11 @@ server:
cert-file: ./default.pem cert-file: ./default.pem
jwt: jwt:
# jwt key不设置默认使用随机字符串 # jwt key不设置默认使用随机字符串
key: key: 333333000000
# 过期时间单位分钟 # accessToken过期时间单位分钟
expire-time: 1440 expire-time: 720
# refreshToken过期时间单位分钟
refresh-token-expire-time: 4320
# 资源密码aes加密key # 资源密码aes加密key
aes: aes:
key: 1111111111111111 key: 1111111111111111

View File

@@ -3,56 +3,58 @@ module mayfly-go
go 1.22 go 1.22
require ( require (
gitee.com/chunanyong/dm v1.8.14 gitee.com/chunanyong/dm v1.8.15
gitee.com/liuzongyang/libpq v1.0.9 gitee.com/liuzongyang/libpq v1.0.9
github.com/buger/jsonparser v1.1.1
github.com/emirpasic/gods v1.18.1 github.com/emirpasic/gods v1.18.1
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-gormigrate/gormigrate/v2 v2.1.0 github.com/go-gormigrate/gormigrate/v2 v2.1.0
github.com/go-ldap/ldap/v3 v3.4.6 github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-playground/locales v0.14.1 github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.14.0 github.com/go-playground/validator/v10 v10.20.0
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.3
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9 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/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
github.com/may-fly/cast v1.6.1 github.com/may-fly/cast v1.6.1
github.com/microsoft/go-mssqldb v1.7.0 github.com/microsoft/go-mssqldb v1.7.2
github.com/mojocn/base64Captcha v1.3.6 // github.com/mojocn/base64Captcha v1.3.6 //
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6 github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.5.1 github.com/redis/go-redis/v9 v9.5.3
github.com/robfig/cron/v3 v3.0.1 // github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.8.10 github.com/sijms/go-ora/v2 v2.8.19
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.17.1
github.com/veops/go-ansiterm v0.0.5 github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver v1.14.0 // mongo go.mongodb.org/mongo-driver v1.16.0 // mongo
golang.org/x/crypto v0.22.0 // ssh golang.org/x/crypto v0.25.0 // ssh
golang.org/x/oauth2 v0.18.0 golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.6.0 golang.org/x/sync v0.7.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
// gorm // gorm
gorm.io/driver/mysql v1.5.6 gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.9 gorm.io/gorm v1.25.11
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
@@ -67,37 +69,38 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/fs v0.1.0 // indirect github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.3 // indirect github.com/rivo/uniseg v0.4.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
golang.org/x/image v0.13.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.22.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.16.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
google.golang.org/grpc v1.52.3 // indirect google.golang.org/grpc v1.52.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.34.1 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect

View File

@@ -2,7 +2,6 @@ package api
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"mayfly-go/internal/auth/api/form" "mayfly-go/internal/auth/api/form"
"mayfly-go/internal/auth/config" "mayfly-go/internal/auth/config"
@@ -14,6 +13,7 @@ import (
"mayfly-go/pkg/cache" "mayfly-go/pkg/cache"
"mayfly-go/pkg/captcha" "mayfly-go/pkg/captcha"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/otp" "mayfly-go/pkg/otp"
"mayfly-go/pkg/req" "mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx" "mayfly-go/pkg/utils/collx"
@@ -50,7 +50,7 @@ func (a *AccountLogin) Login(rc *req.Ctx) {
biz.ErrIsNilAppendErr(err, "解密密码错误: %s") biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
account := &sysentity.Account{Username: username} account := &sysentity.Account{Username: username}
err = a.AccountApp.GetBy(account, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret") err = a.AccountApp.GetByCond(model.NewModelCond(account).Columns("Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret"))
failCountKey := fmt.Sprintf("account:login:failcount:%s", username) failCountKey := fmt.Sprintf("account:login:failcount:%s", username)
nowFailCount := cache.GetInt(failCountKey) nowFailCount := cache.GetInt(failCountKey)
@@ -70,11 +70,12 @@ func (a *AccountLogin) Login(rc *req.Ctx) {
} }
type OtpVerifyInfo struct { type OtpVerifyInfo struct {
AccountId uint64 AccountId uint64
Username string Username string
OptStatus int OptStatus int
AccessToken string AccessToken string
OtpSecret string RefreshToken string
OtpSecret string
} }
// OTP双因素校验 // OTP双因素校验
@@ -83,10 +84,9 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
req.BindJsonAndValid(rc, otpVerify) req.BindJsonAndValid(rc, otpVerify)
tokenKey := fmt.Sprintf("otp:token:%s", otpVerify.OtpToken) tokenKey := fmt.Sprintf("otp:token:%s", otpVerify.OtpToken)
otpInfoJson := cache.GetStr(tokenKey)
biz.NotEmpty(otpInfoJson, "otpToken错误或失效, 请重新登陆获取")
otpInfo := new(OtpVerifyInfo) otpInfo := new(OtpVerifyInfo)
json.Unmarshal([]byte(otpInfoJson), otpInfo) ok := cache.Get(tokenKey, otpInfo)
biz.IsTrue(ok, "otpToken错误或失效, 请重新登陆获取")
failCountKey := fmt.Sprintf("account:otp:failcount:%d", otpInfo.AccountId) failCountKey := fmt.Sprintf("account:otp:failcount:%d", otpInfo.AccountId)
failCount := cache.GetInt(failCountKey) failCount := cache.GetInt(failCountKey)
@@ -115,7 +115,19 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
go saveLogin(la, getIpAndRegion(rc)) go saveLogin(la, getIpAndRegion(rc))
cache.Del(tokenKey) cache.Del(tokenKey)
rc.ResData = accessToken rc.ResData = collx.Kvs("token", accessToken, "refresh_token", otpInfo.RefreshToken)
}
func (a *AccountLogin) RefreshToken(rc *req.Ctx) {
refreshToken := rc.Query("refresh_token")
biz.NotEmpty(refreshToken, "refresh_token不能为空")
accountId, username, err := req.ParseToken(refreshToken)
biz.IsTrueBy(err == nil, errorx.PermissionErr)
token, refreshToken, err := req.CreateToken(accountId, username)
biz.ErrIsNil(err)
rc.ResData = collx.Kvs("token", token, "refresh_token", refreshToken)
} }
func (a *AccountLogin) Logout(rc *req.Ctx) { func (a *AccountLogin) Logout(rc *req.Ctx) {

View File

@@ -41,18 +41,19 @@ func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.Acc
// 默认为不校验otp // 默认为不校验otp
otpStatus := OtpStatusNone otpStatus := OtpStatusNone
// 访问系统使用的token // 访问系统使用的token
accessToken, err := req.CreateToken(account.Id, username) accessToken, refreshToken, err := req.CreateToken(account.Id, username)
biz.ErrIsNilAppendErr(err, "token创建失败: %s") biz.ErrIsNilAppendErr(err, "token创建失败: %s")
// 若系统配置中设置开启otp双因素校验则进行otp校验 // 若系统配置中设置开启otp双因素校验则进行otp校验
if accountLoginSecurity.UseOtp { if accountLoginSecurity.UseOtp {
otpInfo, otpurl, otpToken := useOtp(account, accountLoginSecurity.OtpIssuer, accessToken) otpInfo, otpurl, otpToken := useOtp(account, accountLoginSecurity.OtpIssuer, accessToken, refreshToken)
otpStatus = otpInfo.OptStatus otpStatus = otpInfo.OptStatus
if otpurl != "" { if otpurl != "" {
res["otpUrl"] = otpurl res["otpUrl"] = otpurl
} }
accessToken = otpToken accessToken = otpToken
} else { } else {
res["refresh_token"] = refreshToken
// 不进行otp二次校验则直接返回accessToken // 不进行otp二次校验则直接返回accessToken
// 保存登录消息 // 保存登录消息
go saveLogin(account, loginIp) go saveLogin(account, loginIp)
@@ -64,7 +65,7 @@ func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.Acc
return res return res
} }
func useOtp(account *sysentity.Account, otpIssuer, accessToken string) (*OtpVerifyInfo, string, string) { func useOtp(account *sysentity.Account, otpIssuer, accessToken string, refreshToken string) (*OtpVerifyInfo, string, string) {
biz.ErrIsNil(account.OtpSecretDecrypt()) biz.ErrIsNil(account.OtpSecretDecrypt())
otpSecret := account.OtpSecret otpSecret := account.OtpSecret
// 修改状态为已注册 // 修改状态为已注册
@@ -83,13 +84,14 @@ func useOtp(account *sysentity.Account, otpIssuer, accessToken string) (*OtpVeri
otpUrl = key.URL() otpUrl = key.URL()
otpSecret = key.Secret() otpSecret = key.Secret()
} }
// 缓存otpInfo, 只有双因素校验通过才可返回真正的accessToken // 缓存otpInfo, 只有双因素校验通过才可返回真正的token
otpInfo := &OtpVerifyInfo{ otpInfo := &OtpVerifyInfo{
AccountId: account.Id, AccountId: account.Id,
Username: account.Username, Username: account.Username,
OptStatus: otpStatus, OptStatus: otpStatus,
OtpSecret: otpSecret, OtpSecret: otpSecret,
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken,
} }
cache.SetStr(fmt.Sprintf("otp:token:%s", token), jsonx.ToStr(otpInfo), time.Minute*time.Duration(3)) cache.SetStr(fmt.Sprintf("otp:token:%s", token), jsonx.ToStr(otpInfo), time.Minute*time.Duration(3))
return otpInfo, otpUrl, token return otpInfo, otpUrl, token

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