refactor: 消息模块重构,infra包路径简写等

This commit is contained in:
meilin.huang
2025-07-27 21:02:48 +08:00
parent e96379b6c0
commit 6ad6c69660
149 changed files with 969 additions and 1098 deletions

View File

@@ -7,4 +7,8 @@ VITE_OPEN = false
# public path 配置线上环境路径(打包)
VITE_PUBLIC_PATH = ''
VITE_EDITOR=idea
VITE_EDITOR=idea
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -4,8 +4,4 @@ ENV = 'development'
VITE_OPEN = true
# 本地环境接口地址
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash
VITE_API_URL = '/api'

View File

@@ -3,7 +3,3 @@ ENV = 'production'
# 线上环境接口地址
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -24,7 +24,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.10.3",
"element-plus": "^2.10.4",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"mitt": "^3.0.1",
@@ -39,8 +39,8 @@
"sql-formatter": "^15.6.5",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.17",
"vue-i18n": "^11.1.9",
"vue": "^v3.6.0-alpha.2",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.1",
"vuedraggable": "^4.1.0"
},
@@ -56,7 +56,6 @@
"@vue/compiler-sfc": "^3.5.17",
"autoprefixer": "^10.4.21",
"code-inspector-plugin": "^0.20.12",
"dotenv": "^16.5.0",
"eslint": "^9.29.0",
"eslint-plugin-vue": "^10.2.0",
"postcss": "^8.5.6",

View File

@@ -1,43 +0,0 @@
class SocketBuilder {
websocket: WebSocket;
constructor(url: string) {
if (typeof WebSocket === 'undefined') {
throw new Error('不支持websocket');
}
if (!url) {
throw new Error('websocket url不能为空');
}
this.websocket = new WebSocket(url);
}
static builder(url: string) {
return new SocketBuilder(url);
}
open(onopen: any) {
this.websocket.onopen = onopen;
return this;
}
error(onerror: any) {
this.websocket.onerror = onerror;
return this;
}
message(onmessage: any) {
this.websocket.onmessage = onmessage;
return this;
}
close(onclose: any) {
this.websocket.onclose = onclose;
return this;
}
build() {
return this.websocket;
}
}
export default SocketBuilder;

View File

@@ -42,3 +42,27 @@ export const TagResourceTypePath = {
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
};
// 消息子类型
export const MsgSubtypeEnum = {
UserLogin: EnumValue.of('user.login', 'login.login').setExtra({
notifyType: 'primary',
}),
MachineFileUploadSuccess: EnumValue.of('machine.file.upload.success', 'machine.fileUploadSuccess').setExtra({
notifyType: 'success',
}),
MachineFileUploadFail: EnumValue.of('machine.file.upload.fail', 'machine.fileUploadFail').setExtra({
notifyType: 'error',
}),
DbDumpFail: EnumValue.of('db.dump.fail', 'db.dbDumpFail').setExtra({
notifyType: 'error',
}),
SqlScriptRunSuccess: EnumValue.of('db.sqlscript.run.success', 'db.sqlScriptRunSuccess').setExtra({
notifyType: 'success',
}),
SqlScriptRunFail: EnumValue.of('db.sqlscript.run.fail', 'db.sqlScriptRunFail').setExtra({
notifyType: 'error',
}),
};

View File

@@ -1,5 +1,8 @@
import CryptoJS from 'crypto-js';
import { getToken } from '@/common/utils/storage';
import openApi from './openApi';
import JSEncrypt from 'jsencrypt';
import { notBlank } from './assert';
/**
* AES 加密数据
@@ -36,3 +39,36 @@ export function AesDecrypt(word: string, key?: string): string {
return decrypted.toString(CryptoJS.enc.Base64);
}
var encryptor: any = null;
export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey');
if (publicKey) {
return publicKey;
}
publicKey = (await openApi.getPublicKey()) as string;
sessionStorage.setItem('RsaPublicKey', publicKey);
return publicKey;
}
/**
* 公钥加密指定值
*
* @param value value
* @returns 加密后的值
*/
export async function RsaEncrypt(value: any) {
// 不存在则返回空值
if (!value) {
return '';
}
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
return encryptor.encrypt(value);
}
encryptor = new JSEncrypt();
const publicKey = (await getRsaPublicKey()) as string;
notBlank(publicKey, '获取公钥失败');
encryptor.setPublicKey(publicKey); //设置公钥
return encryptor.encrypt(value);
}

View File

@@ -204,6 +204,24 @@ function getApiUrl(url: string) {
return baseUrl + url + '?' + joinClientParams();
}
/**
* 创建 websocket
*/
export const createWebSocket = (url: string): Promise<WebSocket> => {
return new Promise<WebSocket>((resolve, reject) => {
const clientParam = (url.includes('?') ? '&' : '?') + joinClientParams();
const socket = new WebSocket(`${config.baseWsUrl}${url}${clientParam}`);
socket.onopen = () => {
resolve(socket);
};
socket.onerror = (e) => {
reject(e);
};
});
};
// 组装客户端参数,包括 token 和 clientId
export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;

View File

@@ -1,36 +0,0 @@
import openApi from './openApi';
import JSEncrypt from 'jsencrypt';
import { notBlank } from './assert';
var encryptor: any = null;
export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey');
if (publicKey) {
return publicKey;
}
publicKey = (await openApi.getPublicKey()) as string;
sessionStorage.setItem('RsaPublicKey', publicKey);
return publicKey;
}
/**
* 公钥加密指定值
*
* @param value value
* @returns 加密后的值
*/
export async function RsaEncrypt(value: any) {
// 不存在则返回空值
if (!value) {
return '';
}
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
return encryptor.encrypt(value);
}
encryptor = new JSEncrypt();
const publicKey = (await getRsaPublicKey()) as string;
notBlank(publicKey, '获取公钥失败');
encryptor.setPublicKey(publicKey); //设置公钥
return encryptor.encrypt(value);
}

View File

@@ -4,15 +4,15 @@ import { h, reactive } from 'vue';
import { ElNotification } from 'element-plus';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
export function initSysMsgs() {
registerDbSqlExecProgress();
export async function initSysMsgs() {
await registerDbSqlExecProgress();
}
const sqlExecNotifyMap: Map<string, any> = new Map();
function registerDbSqlExecProgress() {
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
const content = JSON.parse(message.msg);
async function registerDbSqlExecProgress() {
await syssocket.registerMsgHandler('sqlScriptRunProgress', function (message: any) {
const content = message.params;
const id = content.id;
let progress = sqlExecNotifyMap.get(id);
if (content.terminated) {
@@ -38,7 +38,7 @@ function registerDbSqlExecProgress() {
duration: 0,
title: message.title,
message: h(ProgressNotify, progress.props),
type: syssocket.getMsgType(message.type),
type: 'info',
showClose: false,
});
sqlExecNotifyMap.set(id, progress);

View File

@@ -1,34 +1,25 @@
import Config from './config';
import SocketBuilder from './SocketBuilder';
import { getToken } from '@/common/utils/storage';
import { joinClientParams } from './request';
import { createWebSocket } from './request';
import { ElNotification } from 'element-plus';
import { MsgSubtypeEnum } from './commonEnum';
import EnumValue from './Enum';
class SysSocket {
/**
* socket连接
*/
socket: any;
socket: WebSocket | null = null;
/**
* key -> 消息类别value -> 消息对应的处理器函数
*/
categoryHandlers: Map<string, any> = new Map();
/**
* 消息类型
*/
messageTypes: any = {
0: 'error',
1: 'success',
2: 'info',
};
/**
* 初始化全局系统消息websocket
*/
init() {
async init() {
// 存在则不需要重新建立连接
if (this.socket) {
return;
@@ -38,9 +29,9 @@ class SysSocket {
return null;
}
console.log('init system ws');
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
this.socket = SocketBuilder.builder(sysMsgUrl)
.message((event: { data: string }) => {
try {
this.socket = await createWebSocket('/sysmsg');
this.socket.onmessage = async (event: { data: string }) => {
let message;
try {
message = JSON.parse(event.data);
@@ -56,23 +47,32 @@ class SysSocket {
return;
}
// 默认通知处理
const type = this.getMsgType(message.type);
let msg = message.msg;
let duration = 0;
const msgSubtype = EnumValue.getEnumByValue(MsgSubtypeEnum, message.subtype);
if (!msgSubtype) {
console.log(`not found msg subtype: ${message.subtype}`);
return;
}
// 动态导入 i18n 或延迟获取 i18n 实例
let title = '';
try {
// 方式1: 动态导入
const { i18n } = await import('@/i18n');
title = i18n.global.t(msgSubtype?.label);
} catch (e) {
console.warn('i18n not ready, using default title');
}
ElNotification({
duration: duration,
title: message.title,
message: msg,
type: type,
duration: 0,
title,
message: message.msg,
type: msgSubtype?.extra.notifyType || 'info',
});
})
.open((event: any) => console.log(event))
.close(() => {
console.log('close sys socket');
this.socket = null;
})
.build();
};
} catch (e) {
console.error('open system ws error', e);
}
}
destory() {
@@ -87,8 +87,7 @@ class SysSocket {
* @param category 消息类别
* @param handlerFunc 消息处理函数
*/
registerMsgHandler(category: any, handlerFunc: any) {
this.init();
async registerMsgHandler(category: any, handlerFunc: any) {
if (this.categoryHandlers.has(category)) {
console.log(`${category}该类别消息处理器已存在...`);
return;
@@ -98,10 +97,6 @@ class SysSocket {
}
this.categoryHandlers.set(category, handlerFunc);
}
getMsgType(msgType: any) {
return this.messageTypes[msgType];
}
}
// 全局系统消息websocket;

View File

@@ -1,241 +0,0 @@
/**
* 2020.11.29 lyt 整理
* 工具类集合,适用于平时开发
*/
// 小数或整数(不可以负数)
export function verifyNumberIntegerAndFloat(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 只能是数字和小数点,不能是其他输入
v = v.replace(/[^\d.]/g, '');
// 以0开始只能输入一个
v = v.replace(/^0{2}$/g, '0');
// 保证第一位只能是数字,不能是点
v = v.replace(/^\./g, '');
// 小数只能出现1位
v = v.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
// 小数点后面保留2位
v = v.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3');
// 返回结果
return v;
}
// 正整数验证
export function verifiyNumberInteger(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 去掉 '.' , 防止贴贴的时候出现问题 如 0.1.12.12
v = v.replace(/[\.]*/g, '');
// 去掉以 0 开始后面的数, 防止贴贴的时候出现问题 如 00121323
v = v.replace(/(^0[\d]*)$/g, '0');
// 首位是0,只能出现一次
v = v.replace(/^0\d$/g, '0');
// 只匹配数字
v = v.replace(/[^\d]/g, '');
// 返回结果
return v;
}
// 去掉中文及空格
export function verifyCnAndSpace(val: string) {
// 匹配中文与空格
let v = val.replace(/[\u4e00-\u9fa5\s]+/g, '');
// 匹配空格
v = v.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 去掉英文及空格
export function verifyEnAndSpace(val: string) {
// 匹配英文与空格
let v = val.replace(/[a-zA-Z]+/g, '');
// 匹配空格
v = v.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 禁止输入空格
export function verifyAndSpace(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 金额用 `,` 区分开
export function verifyNumberComma(val: string) {
// 调用小数或整数(不可以负数)方法
let v: any = verifyNumberIntegerAndFloat(val);
// 字符串转成数组
v = v.toString().split('.');
// \B 匹配非单词边界,两边都是单词字符或者两边都是非单词字符
v[0] = v[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// 数组转字符串
v = v.join('.');
// 返回结果
return v;
}
// 匹配文字变色(搜索时)
export function verifyTextColor(val: string, text = '', color = 'red') {
// 返回内容,添加颜色
let v = text.replace(new RegExp(val, 'gi'), `<span style='color: ${color}'>${val}</span>`);
// 返回结果
return v;
}
// 数字转中文大写
export function verifyNumberCnUppercase(val: any, unit = '仟佰拾亿仟佰拾万仟佰拾元角分', v = '') {
// 当前内容字符串添加 2个0为什么??
val += '00';
// 返回某个指定的字符串值在字符串中首次出现的位置,没有出现,则该方法返回 -1
let lookup = val.indexOf('.');
// substring不包含结束下标内容substr包含结束下标内容
if (lookup >= 0) val = val.substring(0, lookup) + val.substr(lookup + 1, 2);
// 根据内容 val 的长度,截取返回对应大写
unit = unit.substr(unit.length - val.length);
// 循环截取拼接大写
for (let i = 0; i < val.length; i++) {
v += '零壹贰叁肆伍陆柒捌玖'.substr(val.substr(i, 1), 1) + unit.substr(i, 1);
}
// 正则处理
v = v
.replace(/零角零分$/, '整')
.replace(/零[仟佰拾]/g, '零')
.replace(/零{2,}/g, '零')
.replace(/零([亿|万])/g, '$1')
.replace(/零+元/, '元')
.replace(/亿零{0,3}万/, '亿')
.replace(/^元/, '零元');
// 返回结果
return v;
}
// 手机号码
export function verifyPhone(val: string) {
// false: 手机号码不正确
if (!/^((12[0-9])|(13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/.test(val)) return false;
// true: 手机号码正确
else return true;
}
// 国内电话号码
export function verifyTelPhone(val: string) {
// false: 国内电话号码不正确
if (!/\d{3}-\d{8}|\d{4}-\d{7}/.test(val)) return false;
// true: 国内电话号码正确
else return true;
}
// 登录账号 (字母开头允许5-16字节允许字母数字下划线)
export function verifyAccount(val: string) {
// false: 登录账号不正确
if (!/^[a-zA-Z][a-zA-Z0-9_]{4,15}$/.test(val)) return false;
// true: 登录账号正确
else return true;
}
// 密码 (以字母开头长度在6~16之间只能包含字母、数字和下划线)
export function verifyPassword(val: string) {
// false: 密码不正确
if (!/^[a-zA-Z]\w{5,15}$/.test(val)) return false;
// true: 密码正确
else return true;
}
// 强密码 (字母+数字+特殊字符长度在6-16之间)
export function verifyPasswordPowerful(val: string) {
// false: 强密码不正确
if (!/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val))
return false;
// true: 强密码正确
else return true;
}
// 密码强度
export function verifyPasswordStrength(val: string) {
let v = '';
// 弱:纯数字,纯字母,纯特殊字符
if (/^(?:\d+|[a-zA-Z]+|[!@#$%^&\.*]+){6,16}$/.test(val)) v = '弱';
// 中:字母+数字,字母+特殊字符,数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '中';
// 强:字母+数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '强';
// 返回结果
return v;
}
// IP地址
export function verifyIPAddress(val: string) {
// false: IP地址不正确
if (!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(val))
return false;
// true: IP地址正确
else return true;
}
// 邮箱
export function verifyEmail(val: string) {
// false: 邮箱不正确
if (
!/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
val
)
)
return false;
// true: 邮箱正确
else return true;
}
// 身份证
export function verifyIdCard(val: string) {
// false: 身份证不正确
if (!/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(val)) return false;
// true: 身份证正确
else return true;
}
// 姓名
export function verifyFullName(val: string) {
// false: 姓名不正确
if (!/^[\u4e00-\u9fa5]{1,6}(·[\u4e00-\u9fa5]{1,6}){0,2}$/.test(val)) return false;
// true: 姓名正确
else return true;
}
// 邮政编码
export function verifyPostalCode(val: string) {
// false: 邮政编码不正确
if (!/^[1-9][0-9]{5}$/.test(val)) return false;
// true: 邮政编码正确
else return true;
}
// url
export function verifyUrl(val: string) {
// false: url不正确
if (
!/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
val
)
)
return false;
// true: url正确
else return true;
}
// 车牌号
export function verifyCarNum(val: string) {
// false: 车牌号不正确
if (
!/^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/.test(
val
)
)
return false;
// true车牌号正确
else return true;
}

View File

@@ -1,13 +0,0 @@
const mode = import.meta.env.VITE_ROUTER_MODE;
/**
* @description 获取不同路由模式所对应的 url
* @returns {String}
*/
export function getNowUrl() {
const url = {
hash: location.hash.substring(1),
history: location.pathname + location.search,
};
return url[mode];
}

View File

@@ -1,27 +0,0 @@
// vite 打包相关
import dotenv from 'dotenv';
export interface ViteEnv {
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_PUBLIC_PATH: string;
VITE_EDITOR: string;
}
export function loadEnv(): ViteEnv {
const env = process.env.NODE_ENV;
const ret: any = {};
const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,];
envList.forEach((e) => {
dotenv.config({ path: e });
});
for (const envName of Object.keys(process.env)) {
console.log(envName);
let realName = (process.env as any)[envName].replace(/\\n/g, '\n');
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
if (envName === 'VITE_PORT') realName = Number(realName);
if (envName === 'VITE_OPEN') realName = Boolean(realName);
ret[envName] = realName;
process.env[envName] = realName;
}
return ret;
}

View File

@@ -21,7 +21,7 @@
<div class="flex">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="flex">
<el-dropdown v-if="searchItems?.length > 1">
<el-dropdown v-if="props.searchItems?.length > 1">
<SvgIcon :size="16" name="CaretBottom" class="!mr-1 !mt-1.5 simple-search-form-btn" />
<template #dropdown>
<el-dropdown-menu>
@@ -54,7 +54,7 @@
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button
v-if="showToolButton('search') && searchItems?.length > 1"
v-if="showToolButton('search') && props.searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
circle
@click="isShowSearch = !isShowSearch"

View File

@@ -22,6 +22,7 @@ import { useDebounceFn, useEventListener, useIntervalFn } from '@vueuse/core';
import themes from './themes';
import { TrzszFilter } from 'trzsz';
import { useI18n } from 'vue-i18n';
import { createWebSocket } from '@/common/request';
const { t } = useI18n();
@@ -124,7 +125,7 @@ const initTerm = async () => {
// 注册窗口大小监听器
useEventListener('resize', useDebounceFn(fitTerminal, 400));
initSocket();
await initSocket();
// 注册其他插件
loadAddon();
@@ -140,33 +141,31 @@ const initTerm = async () => {
});
};
const initSocket = () => {
const initSocket = async () => {
if (!props.socketUrl) {
return;
}
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
// 监听socket连接
socket.onopen = () => {
// 注册心跳
useIntervalFn(sendPing, 15000);
state.status = TerminalStatus.Connected;
focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendData(props.cmd + ' \r');
}
};
// 监听socket错误信息
socket.onerror = (e: Event) => {
try {
socket = await createWebSocket(`${props.socketUrl}?rows=${term?.rows}&cols=${term?.cols}`);
} catch (e) {
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
state.status = TerminalStatus.Error;
console.log('连接错误', e);
};
return;
}
// 注册心跳
useIntervalFn(sendPing, 15000);
state.status = TerminalStatus.Connected;
focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendData(props.cmd + ' \r');
}
socket.onclose = (e: CloseEvent) => {
console.log('terminal socket close...', e.reason);

View File

@@ -16,6 +16,7 @@ export default {
dbFilterPlaceholder: 'DB name: Input filterable',
sqlRecord: 'SQL records',
dump: 'Export',
dbDumpFail: 'DB export failed',
dumpContent: 'Export Content',
structure: 'Structure',
data: 'Data',
@@ -55,6 +56,8 @@ export default {
execSuccess: 'Successful execution',
execFail: 'Execution failure',
sqlScriptRun: 'Run SQL Script',
sqlScriptRunSuccess: 'SQL script executed successfully',
sqlScriptRunFail: 'SQL script execution failed',
saveSql: 'Save SQL',
execInfo: 'Execution info',
result: 'Result',

View File

@@ -138,5 +138,7 @@ export default {
fileTooLargeTips: 'The file is too large, please download and use it',
uploadSuccess: 'Upload successfully',
fileExceedsSysConf: 'The uploaded file exceeds the system configuration [{uploadMaxFileSize}]',
fileUploadSuccess: 'Machine file upload successful',
fileUploadFail: 'Machine file upload failed',
},
};

View File

@@ -16,6 +16,7 @@ export default {
dbFilterPlaceholder: '库名: 输入可过滤',
sqlRecord: 'SQL记录',
dump: '导出',
dbDumpFail: '数据库导出失败',
dumpContent: '导出内容',
structure: '结构',
data: '数据',
@@ -55,6 +56,8 @@ export default {
execSuccess: '执行成功',
execFail: '执行失败',
sqlScriptRun: 'SQL脚本执行',
sqlScriptRunSuccess: 'SQL脚本执行成功',
sqlScriptRunFail: 'SQL脚本执行失败',
saveSql: '保存SQL',
execInfo: '执行信息',
result: '结果',

View File

@@ -139,5 +139,7 @@ export default {
fileTooLargeTips: '文件太大, 请下载使用',
uploadSuccess: '上传成功',
fileExceedsSysConf: '上传的文件超过系统配置的【{uploadMaxFileSize}】',
fileUploadSuccess: '机器文件上传成功',
fileUploadFail: '机器文件上传失败',
},
};

View File

@@ -41,25 +41,27 @@
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<SvgIcon name="search" :title="$t('layout.user.menuSearch')" />
</div>
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<SvgIcon name="setting" :title="$t('layout.user.layoutConf')" />
</div>
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" :visible="state.isShowUserNewsPopover" :width="300" popper-class="el-popover-pupop-user-news">
<template #reference>
<el-badge :is-dot="false" @click="state.isShowUserNewsPopover = !state.isShowUserNewsPopover">
<el-popover @show="onShowMsgs" placement="bottom" trigger="click" :width="450">
<template #reference>
<div class="layout-navbars-breadcrumb-user-icon">
<el-badge :show-zero="false" :value="state.unreadMsgCount">
<SvgIcon name="bell" :title="$t('layout.user.news')" />
</el-badge>
</template>
<transition name="el-zoom-in-top">
<UserNews v-show="state.isShowUserNewsPopover" />
</transition>
</el-popover>
</div>
</div>
</template>
<UserNews ref="userNewsRef" @update:count="state.unreadMsgCount = $event" />
</el-popover>
<div class="layout-navbars-breadcrumb-user-icon mr-2" @click="onScreenfullClick">
<SvgIcon v-if="!state.isScreenfull" name="full-screen" :title="$t('layout.user.fullScreenOff')" />
<SvgIcon v-else name="crop" />
</div>
<el-dropdown trigger="click" :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link cursor-pointer">
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr-1" />
@@ -79,7 +81,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted, watch } from 'vue';
import { ref, computed, reactive, onMounted, watch, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
@@ -100,10 +102,12 @@ import EnumValue from '@/common/Enum';
const router = useRouter();
const searchRef = ref();
const userNewsRef = useTemplateRef('userNewsRef');
const state = reactive({
isScreenfull: false,
isShowUserNewsPopover: false,
disabledSize: '',
unreadMsgCount: 0,
});
const { userInfo } = storeToRefs(useUserInfo());
const themeConfigStore = useThemeConfig();
@@ -126,8 +130,15 @@ onMounted(() => {
initComponentSize();
isDark.value = themeConfig.isDark;
}
// 获取未读消息数量
state.unreadMsgCount = 0;
});
const onShowMsgs = () => {
userNewsRef.value?.loadMsgs(true);
};
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {

View File

@@ -1,117 +1,132 @@
<template>
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">{{ $t('layout.user.newTitle') }}</div>
<div class="head-box-btn" v-if="newsList.length > 0" @click="onAllReadClick">{{ $t('layout.user.newBtn') }}</div>
<div class="flex flex-col w-full rounded-md shadow-sm">
<!-- Header -->
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-2 text-sm text-gray-700">
<div class="font-medium">{{ $t('layout.user.newTitle') }}</div>
<div v-if="unreadCount > 0" class="color-primary cursor-pointer opacity-80 transition-opacity hover:opacity-100" @click="onRead()">
{{ $t('layout.user.newBtn') }}
</div>
</div>
<div class="content-box">
<template v-if="newsList.length > 0">
<div class="content-box-item" v-for="(v, k) in newsList" :key="k">
<div>{{ v.label }}</div>
<div class="content-box-msg">
{{ v.value }}
<!-- Content -->
<el-scrollbar height="350px" v-loading="loadingMsgs" class="px-4 py-2 text-sm">
<template v-if="msgs.length > 0">
<div
v-for="(v, k) in msgs"
:key="k"
class="pt-1 mt-0.5"
:style="{ backgroundColor: v.status == 1 ? 'var(--el-color-info-light-9)' : 'transparent' }"
@click="onRead(v)"
>
<div class="flex justify-between items-start">
<el-text size="small" tag="b" :type="EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.extra?.notifyType">
{{ $t(EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.label || '') }}
</el-text>
</div>
<div class="content-box-time">{{ v.time }}</div>
<div class="text-gray-500 mt-1 mb-1">{{ v.msg }}</div>
<div class="text-gray-500">{{ formatDate(v.createTime) }}</div>
<div class="mt-2 border-t border-gray-200"></div>
</div>
<el-button class="w-full mt-1" size="small" @click="loadMsgs()" v-if="!loadMoreDisable"> {{ $t('redis.loadMore') }} </el-button>
</template>
<el-empty :description="$t('layout.user.newDesc')" v-else></el-empty>
</div>
<div class="foot-box" @click="toMsgCenter" v-if="newsList.length > 0">{{ $t('layout.user.newGo') }}</div>
<el-empty v-if="msgs.length == 0 && !loadingMsgs" :image-size="100" :description="$t('layout.user.newDesc')" />
</el-scrollbar>
<!-- Footer -->
<!-- <div
v-if="msgs.length > 0"
class="color-primary flex h-9 items-center justify-center border-t border-gray-200 text-sm cursor-pointer opacity-80 transition-opacity hover:opacity-100"
@click="toMsgCenter"
>
{{ $t('layout.user.newGo') }}
</div> -->
</div>
</template>
<script lang="ts">
import { reactive, toRefs } from 'vue';
export default {
name: 'layoutBreadcrumbUserNews',
setup() {
const state = reactive({
newsList: [
{
label: '关于学习交流的通知',
value: 'QQ群号码 119699946',
time: '2021-09-08',
},
],
});
// 全部已读点击
const onAllReadClick = () => {
state.newsList = [];
};
// 前往通知中心点击
const toMsgCenter = () => {};
return {
onAllReadClick,
toMsgCenter,
...toRefs(state),
};
},
<script lang="ts" setup>
import { MsgSubtypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import { formatDate } from '@/common/utils/format';
import { personApi } from '@/views/personal/api';
import { useIntervalFn } from '@vueuse/core';
import { onMounted, ref, watchEffect } from 'vue';
const emit = defineEmits(['update:count']);
const msgQuery = ref<any>({
pageNum: 1,
pageSize: 10,
});
const loadMoreDisable = ref(true);
const loadingMsgs = ref(true);
const msgs = ref<Array<any>>([]);
const unreadCount = ref(0);
onMounted(() => {
useIntervalFn(
() => {
// 定时更新未读消息数
personApi.getUnreadMsgCount.request().then((res) => {
unreadCount.value = res;
});
},
10 * 1000,
{ immediate: true, immediateCallback: true }
);
});
watchEffect(() => {
emit('update:count', unreadCount.value);
});
const loadMsgs = async (research: boolean = false) => {
if (research) {
msgQuery.value.pageNum = 1;
msgs.value = [];
}
const msgList = await getMsgs();
msgs.value.push(...msgList.list);
msgQuery.value.pageNum += 1;
loadMoreDisable.value = msgList.total <= msgs.value.length;
};
const getMsgs = async () => {
try {
loadingMsgs.value = true;
return await personApi.getMsgs.request(msgQuery.value);
} catch (e) {
//
} finally {
loadingMsgs.value = false;
}
};
const onRead = async (msg: any = null) => {
if (msg && (msg.status == 1 || !msg.status)) {
return;
}
await personApi.readMsg.request({ id: msg?.id || 0 });
loadMsgs(true);
if (!msg) {
// 如果是全部已读,重置未读消息数
unreadCount.value = 0;
} else {
// 如果是单条已读,减少未读消息数
unreadCount.value = Math.max(unreadCount.value - 1, 0);
}
};
defineExpose({
loadMsgs,
});
const toMsgCenter = () => {};
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb-user-news {
.head-box {
display: flex;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
color: #333333;
justify-content: space-between;
height: 35px;
align-items: center;
.head-box-btn {
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
.content-box {
font-size: 13px;
.content-box-item {
padding-top: 12px;
&:last-of-type {
padding-bottom: 12px;
}
.content-box-msg {
color: #999999;
margin-top: 5px;
margin-bottom: 5px;
}
.content-box-time {
color: #999999;
}
}
}
.foot-box {
height: 35px;
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #ebeef5;
&:hover {
opacity: 1;
}
}
::v-deep(.el-empty__description p) {
font-size: 13px;
}
}
</style>
<style scoped lang="scss"></style>

View File

@@ -1,4 +1,3 @@
import { getNowUrl } from '@/common/utils/url';
import { defineStore } from 'pinia';
/**
@@ -12,14 +11,5 @@ export const useTagsViews = defineStore('tagsViews', {
setTagsViews(data: Array<TagsView>) {
this.tagsViews = data;
},
// 设置当前页面的tags view title
setNowTitle(title: string) {
this.tagsViews.forEach((item) => {
// console.log(getNowUrl(), item.path);
if (item.path == getNowUrl()) {
item.title = title;
}
});
},
},
});

View File

@@ -2,7 +2,7 @@
<div class="home-container personal">
<el-row :gutter="15">
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-col :xs="24" :sm="24">
<el-card shadow="hover" :header="$t('home.personalInfo')">
<div class="personal-user">
<div class="personal-user-left">
@@ -52,23 +52,6 @@
</div>
</el-card>
</el-col>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>{{ $t('home.msgNotify') }}</span>
<span @click="showMsgs" class="personal-info-more">{{ $t('common.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">{{ `[${$t(EnumValue.getLabelByValue(MsgTypeEnum, v.type))}] ${v.msg}` }}</a>
</li>
</ul>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="!mt-4 resource-info">
@@ -236,39 +219,11 @@
</el-card>
</el-col>
</el-row>
<el-dialog width="900px" :title="$t('common.msg')" v-model="msgDialog.visible">
<el-table border :data="msgDialog.msgs.list" size="small">
<el-table-column property="type" :label="$t('common.type')" width="60">
<template #default="scope">
{{ $t(EnumValue.getLabelByValue(MsgTypeEnum, scope.row.type)) }}
</template>
</el-table-column>
<el-table-column property="msg" :label="$t('common.msg')"></el-table-column>
<el-table-column property="createTime" :label="$t('common.time')" width="150">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-row type="flex" class="mt-1" 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>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, toRefs } from 'vue';
import { computed, onMounted, reactive } from 'vue';
// import * as echarts from 'echarts';
import { formatAxis, formatDate } from '@/common/utils/format';
import { indexApi } from './api';
@@ -285,8 +240,6 @@ import { getAllTagInfoByCodePaths } from '../ops/component/tag';
import { ElMessage } from 'element-plus';
import { getFileUrl, getUploadFileUrl } from '@/common/request';
import { saveUser } from '@/common/utils/storage';
import EnumValue from '../../common/Enum';
import { MsgTypeEnum } from './enums';
const router = useRouter();
const { userInfo } = storeToRefs(useUserInfo());
@@ -296,17 +249,6 @@ const state = reactive({
roles: [],
},
msgs: [],
msgDialog: {
visible: false,
query: {
pageSize: 10,
pageNum: 1,
},
msgs: {
list: [],
total: null,
},
},
resourceOpTableHeight: 180,
defaultLogSize: 5,
machine: {
@@ -331,8 +273,6 @@ const state = reactive({
},
});
const { msgDialog } = toRefs(state);
const roleInfo = computed(() => {
if (state.accountInfo.roles.length == 0) {
return '';
@@ -349,30 +289,12 @@ const currentTime = computed(() => {
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 getAccountInfo = async () => {
state.accountInfo = await personApi.accountInfo.request();
};
const getMsgs = async () => {
return await personApi.getMsgs.request(state.msgDialog.query);
};
const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.size >= 512 * 1024) {
ElMessage.error('头像不能超过512KB!');

View File

@@ -1,6 +0,0 @@
import { EnumValue } from '@/common/Enum';
export const MsgTypeEnum = {
Login: EnumValue.of(1, 'home.msgTypeLogin'),
Notify: EnumValue.of(2, 'home.msgTypeNotify'),
};

View File

@@ -151,7 +151,7 @@ import { ElMessage } from 'element-plus';
import { initRouter } from '@/router/index';
import { getRefreshToken, saveRefreshToken, saveToken, saveUser } from '@/common/utils/storage';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
import { RsaEncrypt } from '@/common/crypto';
import { getAccountLoginSecurity, getLdapEnabled } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo';

View File

@@ -1,5 +1,4 @@
import Api from '@/common/Api';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
export const machineApi = {
@@ -67,9 +66,9 @@ export const cmdConfApi = {
};
export function getMachineTerminalSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/terminal/${authCertName}?${joinClientParams()}`;
return `/machines/terminal/${authCertName}`;
}
export function getMachineRdpSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/rdp/${authCertName}`;
return `/machines/rdp/${authCertName}`;
}

View File

@@ -5,5 +5,7 @@ export const personApi = {
updateAccount: Api.newPut('/sys/accounts/self'),
authStatus: Api.newGet('/auth/oauth2/status'),
getMsgs: Api.newGet('/msgs/self'),
getUnreadMsgCount: Api.newGet('/msgs/self/unread/count'),
readMsg: Api.newGet('/msgs/self/read'),
unbindOauth2: Api.newGet('/auth/oauth2/unbind'),
};

View File

@@ -1,93 +1,87 @@
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import type { UserConfig } from 'vite';
import { loadEnv } from './src/common/utils/viteBuild';
import { CodeInspectorPlugin } from 'code-inspector-plugin';
import progress from 'vite-plugin-progress';
import tailwindcss from '@tailwindcss/vite';
import { ConfigEnv, defineConfig, loadEnv } from 'vite';
const pathResolve = (dir: string): any => {
return resolve(__dirname, '.', dir);
};
export default defineConfig(({ mode }: ConfigEnv) => {
const env = loadEnv(mode, process.cwd(), '');
const isProd = process.env.NODE_ENV === 'production';
const pathResolve = (dir: string): any => {
return resolve(__dirname, '.', dir);
};
const { VITE_PORT, VITE_OPEN, VITE_PUBLIC_PATH, VITE_EDITOR } = loadEnv();
const isProd = process.env.NODE_ENV === 'production';
const alias: Record<string, string> = {
'@': pathResolve('src/'),
};
const viteConfig: UserConfig = {
plugins: [
vue(),
tailwindcss(),
CodeInspectorPlugin({
bundler: 'vite',
editor: VITE_EDITOR as any,
}),
progress(),
],
root: process.cwd(),
resolve: {
alias,
},
base: isProd ? VITE_PUBLIC_PATH : './',
optimizeDeps: {
include: ['element-plus/es/locale/lang/zh-cn'],
},
server: {
host: '0.0.0.0',
port: VITE_PORT,
open: VITE_OPEN,
proxy: {
'/api': {
target: 'http://localhost:18888',
ws: true,
changeOrigin: true,
return {
base: isProd ? env.VITE_PUBLIC_PATH : './',
resolve: {
alias: {
'@': pathResolve('src/'),
},
},
},
build: {
outDir: 'dist',
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
entryFileNames: `assets/[hash]-[name].js`,
chunkFileNames: `assets/[hash]-[name].js`,
assetFileNames: `assets/[hash]-[name].[ext]`,
hashCharacters: 'hex',
advancedChunks: {
groups: [
{ name: 'vue-vendor', test: /[\\/]node_modules[\\/](vue|@vue|vue-router|pinia)[\\/]/ },
{ name: 'echarts', test: /(echarts)/i },
{ name: 'monaco', test: /(monaco-editor)/i },
],
plugins: [
vue(),
tailwindcss(),
CodeInspectorPlugin({
bundler: 'vite',
editor: env.VITE_EDITOR as any,
}),
progress(),
],
optimizeDeps: {
include: ['element-plus/es/locale/lang/zh-cn'],
},
server: {
host: '0.0.0.0',
port: Number.parseInt(env.VITE_PORT) || 8889,
open: env.VITE_OPEN === 'true',
proxy: {
'/api': {
target: 'http://localhost:18888',
ws: true,
changeOrigin: true,
},
},
},
},
define: {
__VUE_I18N_LEGACY_API__: JSON.stringify(false),
__VUE_I18N_FULL_INSTALL__: JSON.stringify(false),
__INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false),
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
},
build: {
outDir: 'dist',
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
entryFileNames: `assets/js/[hash]-[name].js`,
chunkFileNames: `assets/js/[hash]-[name].js`,
assetFileNames: `assets/[ext]/[hash]-[name].[ext]`,
hashCharacters: 'hex',
advancedChunks: {
groups: [
{ name: 'vue-vendor', test: /[\\/]node_modules[\\/](vue|@vue|vue-router|pinia)[\\/]/ },
{ name: 'charts', test: /[\\/]node_modules[\\/](echarts)[\\/]/ },
{ name: 'monaco', test: /[\\/]node_modules[\\/]monaco-editor[\\/]/ },
],
},
},
],
},
},
},
};
export default viteConfig;
define: {
__VUE_I18N_LEGACY_API__: JSON.stringify(false),
__VUE_I18N_FULL_INSTALL__: JSON.stringify(false),
__INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false),
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
},
},
},
],
},
},
};
});