Compare commits

...

10 Commits

Author SHA1 Message Date
meilin.huang
434c1fdfb3 refactor: 资源操作tab优化 2026-06-02 19:00:32 +08:00
zongyangleo
96ef4d2d6f !157 refactor: kafka操作优化
* feat: es新增导出功能
* refactor: kafka操作优化
2026-06-02 10:30:29 +00:00
meilin.huang
fab45f0823 refactor: 资源操作tab优化&修复tab点击定位树节点 2026-06-01 19:23:49 +08:00
meilin.huang
44b5f6ebfd refactor: 资源操作tab优化 2026-06-01 13:17:12 +08:00
zongyangleo
f234aff250 !156 各资源打开标签页重构
* refactor: 各资源打开标签页重构
2026-05-31 05:41:42 +00:00
saa99999
a17fa5a103 Fix CWE-347: JWT algorithm confusion + CWE-798: hardcoded credentials in example config (#131)
- Add HMAC algorithm verification in ParseToken to prevent JWT algorithm
  confusion attacks (CWE-347). Reject tokens with non-HMAC signing methods.
- Replace hardcoded secrets in config.yml.example with empty values
  (JWT key, DB password, AES key) to prevent users from deploying with
  weak/known credentials (CWE-798).
2026-05-27 19:10:12 +08:00
meilin.huang
519089d8d0 feat: sql脚本执行支持zip,统一读取body流,去除资源tagpath条件搜索 2026-05-26 19:31:05 +08:00
meilin.huang
871e9b8fdd feat: 首页优化&机器文件/文件夹上传实时进度通知修复 2026-05-22 20:36:19 +08:00
zongyangleo
daccc638a7 !155 feat: milvus 支持多账户
* feat: milvus 支持多账户
2026-05-22 11:26:27 +00:00
meilin.huang
c9daed9184 refactor: i18n msg优化等 2026-05-19 21:25:28 +08:00
224 changed files with 9020 additions and 6503 deletions

View File

@@ -22,7 +22,7 @@
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.24%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.26%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
@@ -31,7 +31,24 @@
## 前言
Web 版 **统一管理操作平台**,集成了对 Linux 系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite、ClickHouse 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis单机、哨兵、集群模式、MongoDB、Elasticsearch、Kafka、Milvus 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
Web 版 **统一资源管理操作平台**,集成了对 Linux 系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了丰富的数据库、缓存、搜索引擎及向量数据库的操作、数据同步与数据迁移功能。结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
### 🗄️ 数据库支持
#### 关系型数据库
| 数据库类型 | 支持状态 | 数据库类型 | 支持状态 | 数据库类型 | 支持状态 |
| :--------: | :------: | :--------: | :------: | :--------: | :------: |
| MySQL | ✅ | PostgreSQL | ✅ | Oracle | ✅ |
| SQL Server | ✅ | 达梦 | ✅ | 高斯 | ✅ |
| SQLite | ✅ | ClickHouse | ✅ | | |
#### 非关系型数据库
| 数据库类型 | 支持状态 | 数据库类型 | 支持状态 |
| :-----------: | :------: | :--------: | :------: |
| MongoDB | ✅ | Redis | ✅ |
| Elasticsearch | ✅ | Milvus | ✅ |
## 开发语言与主要框架

View File

@@ -19,7 +19,7 @@
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.26%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
@@ -28,7 +28,24 @@
## Preface
Web-based **Unified Management and Operation Platform**, integrating comprehensive operation support for Linux systems (including terminal management [terminal playback, command filtering], file management, script execution, process monitoring, and cronjob settings). It also provides data operation, data synchronization, and data migration for multiple databases (such as MySQL, PostgreSQL, Oracle, SQL Server, Dameng, Gauss, SQLite, ClickHouse, etc.). Additionally, it supports Redis operations (standalone, sentinel, and cluster modes) and MongoDB, Elasticsearch, Kafka, Milvus management, combined with work order process approval functionality to offer enterprises an all-in-one solution for operations and management.
Web-based **Unified Resource Management Platform**, integrating comprehensive operation support for Linux systems (including terminal management [terminal playback, command filtering], file management, script execution, process monitoring, and cronjob settings). It also provides data operation, data synchronization, and data migration for various databases, caches, search engines, and vector databases. Combined with work order process approval functionality, it offers enterprises an all-in-one solution for operations and management.
### 🗄️ Supported Databases
#### Relational Databases
| Database | Status | Database | Status | Database | Status |
| :--------: | :----: | :--------: | :----: | :------: | :----: |
| MySQL | ✅ | PostgreSQL | ✅ | Oracle | ✅ |
| SQL Server | ✅ | Dameng | ✅ | Gauss | ✅ |
| SQLite | ✅ | ClickHouse | ✅ | | |
#### Non-Relational Databases
| Database | Status | Database | Status |
| :-----------: | :----: | :------: | :----: |
| MongoDB | ✅ | Redis | ✅ |
| Elasticsearch | ✅ | Milvus | ✅ |
## Development languages and major frameworks

View File

@@ -19,12 +19,10 @@
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"asciinema-player": "^3.15.1",
"axios": "^1.16.0",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"echarts": "^6.0.0",
"element-plus": "^2.14.0",
"element-plus": "^2.14.1",
"js-base64": "^3.7.8",
"jsencrypt": "^3.5.4",
"json-bigint": "^1.0.0",
@@ -38,12 +36,12 @@
"shiki": "^4.0.2",
"shiki-stream": "^0.1.4",
"sortablejs": "^1.15.7",
"sql-formatter": "^15.7.3",
"sql-formatter": "^15.8.0",
"uuid": "^13.0.2",
"vue": "3.6.0-beta.11",
"vue-element-plus-x": "^2.0.3",
"vue-i18n": "^11.4.4",
"vue-router": "^5.0.7",
"vue-router": "^5.1.0",
"vuedraggable": "^4.1.0",
"x-markdown-vue": "0.0.200",
"xlsx": "^0.18.5"
@@ -69,7 +67,7 @@
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.13",
"vite": "^8.0.14",
"vite-plugin-progress": "0.0.7",
"vue-eslint-parser": "^10.4.0"
},

View File

@@ -0,0 +1,3 @@
allowBuilds:
'@logicflow/core': true
'@parcel/watcher': true

View File

@@ -1,7 +1,7 @@
import { templateResolve } from '@/common/utils/string';
import { RequestOptions, useApiFetch } from '@/hooks/useRequest';
import config from './config';
import request, { joinClientParams } from './request';
import { getToken } from './utils/storage';
/**
* 文件上传选项
@@ -78,17 +78,6 @@ class Api<T = any, P = any> {
return (data.value as T) || (res as T);
}
/**
* xhr 请求对应的该api
* @param {Object} param 请求该api的参数
*/
async xhrReq(param: any = null, options: any = {}): Promise<T> {
if (this.beforeHandler) {
await this.beforeHandler(param);
}
return request.xhrReq(this.method, this.url, param, options);
}
/**
* 文件上传请求
* @param formData FormData 对象(调用方自行构建,包含文件和其他参数)
@@ -134,6 +123,87 @@ class Api<T = any, P = any> {
};
}
/**
* 原始文件流上传请求(直接使用文件流作为 body参数通过 URL query 传递)
* @param file 文件对象
* @param queryParams URL 查询参数对象(可选)
* @param options 上传选项(可包含自定义 headers
* @returns { abort: () => void } 返回中止方法
*/
uploadRaw(file: File, queryParams?: Record<string, string>, options: UploadOptions & { headers?: Record<string, string> } = {}): { abort: () => void } {
const { onSuccess, onError, headers = {} } = options;
// 构建 URL兼容没有 queryParams 的情况
let url = `${config.baseApiUrl}${this.url}`;
// 简单判断该url是否是restful风格
if (url.indexOf('{') != -1 && queryParams) {
url = templateResolve(url, queryParams);
}
const searchParams = new URLSearchParams();
// 添加业务参数
if (queryParams) {
Object.entries(queryParams).forEach(([key, value]) => {
searchParams.append(key, value);
});
}
// 添加客户端参数
const clientParams = joinClientParams();
if (clientParams) {
// 将 joinClientParams 返回的字符串追加到 searchParams
const clientParamsObj = new URLSearchParams(clientParams);
clientParamsObj.forEach((value, key) => {
searchParams.append(key, value);
});
}
// 拼接完整的 query string
const queryString = searchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
// 创建 AbortController 用于取消请求
const abortController = new AbortController();
// 构建请求头
const requestHeaders: Record<string, string> = {
...headers,
};
// 发起 fetch 请求,直接使用文件流作为 body
fetch(url, {
method: 'POST',
body: file,
signal: abortController.signal,
headers: requestHeaders,
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response;
})
.then(() => {
onSuccess?.();
})
.catch((error) => {
if (error.name === 'AbortError') {
return;
}
onError?.(new Error(`upload failed: ${error.message}`));
});
// 返回中止方法
return {
abort: () => {
abortController.abort();
},
};
}
/** 静态方法 **/
/**

View File

@@ -1,12 +1,12 @@
import { i18n } from '@/i18n';
import { ElMessage } from 'element-plus';
import { Msg } from '@/hooks/useI18n';
/**
* 不符合业务断言错误
*/
class AssertError extends Error {
constructor(message: string) {
ElMessage.error(message);
Msg.error(message);
super(message);
// 错误类名
this.name = 'AssertError';

View File

@@ -54,6 +54,7 @@ export const TagResourceTypePath = {
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
MilvusAuthCert: `${TagResourceTypeEnum.Milvus.value}/${TagResourceTypeEnum.AuthCert.value}`,
};
// 消息子类型

View File

@@ -1,20 +1,10 @@
import router from '../router';
import config from './config';
import { getClientId, getToken } from './utils/storage';
import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import JSONBig from 'json-bigint';
import { useApiFetch } from '../hooks/useRequest';
import Api from './Api';
// 配置 JSONBig将大数int64/uint64转为字符串避免精度丢失
// storeAsString: 将大数存储为字符串,而不是 BigNumber 对象
const JSONBigString = JSONBig({ storeAsString: true });
import config from './config';
import { getClientId, getToken } from './utils/storage';
export default {
request,
xhrReq,
get,
post,
put,
@@ -50,138 +40,6 @@ export const baseUrl: string = config.baseApiUrl;
// const baseUrl: string = 'http://localhost:18888/api';
// const baseWsUrl: string = config.baseWsUrl;
/**
* 通知错误消息
* @param msg 错误消息
*/
function notifyErrorMsg(msg: string) {
// 危险通知
ElMessage.error(msg);
}
// create an axios instance
const axiosInst = axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 60000, // request timeout
// 使用 json-bigint 处理响应数据,解决 int64/uint64 精度丢失问题
transformResponse: [
function (data) {
// 对响应数据进行转换
if (typeof data === 'string') {
try {
// 使用 JSONBigString 解析,大数会被转为字符串
return JSONBigString.parse(data);
} catch (err) {
// 如果解析失败,返回原始数据
return data;
}
}
return data;
},
],
});
// request interceptor
axiosInst.interceptors.request.use(
(config: any) => {
// do something before request is sent
const token = getToken();
if (token) {
// 设置token
config.headers['Authorization'] = token;
config.headers['ClientId'] = getClientId();
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// response interceptor
axiosInst.interceptors.response.use(
(response) => response,
(e: any) => {
const rejectPromise = Promise.reject(e);
if (axios.isCancel(e)) {
console.log('请求已取消');
return rejectPromise;
}
const statusCode = e.response?.status;
if (statusCode == 500) {
notifyErrorMsg('服务器未知异常');
return rejectPromise;
}
if (statusCode == 404) {
notifyErrorMsg('请求接口未找到');
return rejectPromise;
}
if (e.message) {
// 对响应错误做点什么
if (e.message.indexOf('timeout') != -1) {
notifyErrorMsg('网络请求超时');
return rejectPromise;
}
if (e.message == 'Network Error') {
notifyErrorMsg('网络连接错误');
return rejectPromise;
}
}
notifyErrorMsg('网络请求错误');
return rejectPromise;
}
);
/**
* xhr请求url
*
* @param method 请求方法
* @param url url
* @param params 参数
* @param options 可选
* @returns
*/
export function xhrReq(method: string, url: string, params: any = null, options: any = {}) {
if (!url) {
throw new Error('请求url不能为空');
}
// 简单判断该url是否是restful风格
if (url.indexOf('{') != -1) {
url = templateResolve(url, params);
}
const req: any = {
method,
url,
...options,
};
// post和put使用json格式传参
if (method === 'post' || method === 'put') {
req.data = params;
} else {
req.params = params;
}
return axiosInst
.request(req)
.then((response) => {
// 获取请求返回结果
const result: Result = response.data;
return parseResult(result);
})
.catch((e) => {
return Promise.reject(e);
});
}
/**
* fetch请求url
*
@@ -277,23 +135,3 @@ export function downloadFile(key: string) {
a.click();
a.remove();
}
function parseResult(result: Result) {
if (result.code === ResultEnum.SUCCESS) {
return result.data;
}
// 如果提示没有权限则移除token使其重新登录
if (result.code === ResultEnum.NO_PERMISSION) {
router.push({
path: '/401',
});
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
notifyErrorMsg(result.msg);
}
return Promise.reject(result);
}

View File

@@ -28,6 +28,31 @@ class SysSocket {
*/
categoryHandlers: Map<string, any> = new Map();
/**
* 重连定时器
*/
reconnectTimer: number | null = null;
/**
* 当前重连次数
*/
reconnectCount: number = 0;
/**
* 基础重连延迟(毫秒)
*/
baseReconnectDelay: number = 3000;
/**
* 是否正在重连
*/
isReconnecting: boolean = false;
/**
* 是否手动关闭
*/
isManualClose: boolean = false;
/**
* 初始化全局系统消息websocket
*/
@@ -42,52 +67,126 @@ class SysSocket {
}
console.log('init system ws');
try {
this.socket = await createWebSocket('/sysmsg');
this.socket.onmessage = async (event: { data: string }) => {
let message;
try {
message = JSON.parse(event.data);
} catch (e) {
console.error('解析ws消息失败', e);
return;
}
// 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理
const handler = this.categoryHandlers.get(message.category);
if (handler) {
handler(message);
return;
}
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: 0,
title,
message: h(MessageRenderer, { content: message.msg }),
type: msgSubtype?.extra.notifyType || 'info',
});
};
this.isManualClose = false;
await this.connect();
} catch (e) {
console.error('open system ws error', e);
}
}
/**
* 建立 WebSocket 连接
*/
private async connect(): Promise<void> {
this.socket = await createWebSocket('/sysmsg');
this.socket.onopen = () => {
console.log('WebSocket connected');
this.resetReconnect();
};
this.socket.onmessage = async (event: { data: string }) => {
let message;
try {
message = JSON.parse(event.data);
} catch (e) {
console.error('解析ws消息失败', e);
return;
}
// 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理
const handler = this.categoryHandlers.get(message.category);
if (handler) {
handler(message);
return;
}
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: 0,
title,
message: h(MessageRenderer, { content: message.msg }),
type: msgSubtype?.extra.notifyType || 'info',
});
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.socket.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.socket = null;
// 如果不是手动关闭,则尝试重连
if (!this.isManualClose) {
this.handleReconnect();
}
};
}
/**
* 处理重连逻辑
*/
private handleReconnect() {
if (this.isReconnecting) {
return;
}
this.isReconnecting = true;
this.reconnectCount++;
// 固定延迟重连策略:每 3 秒重试一次
const delay = this.baseReconnectDelay;
console.log(`WebSocket 将在 ${delay}ms 后尝试第 ${this.reconnectCount} 次重连`);
this.reconnectTimer = window.setTimeout(async () => {
this.isReconnecting = false;
try {
const token = getToken();
if (!token) {
console.warn('Token 不存在,停止重连');
return;
}
console.log(`尝试第 ${this.reconnectCount} 次重连 WebSocket`);
await this.connect();
} catch (e) {
console.error(`${this.reconnectCount} 次重连失败:`, e);
this.handleReconnect();
}
}, delay);
}
/**
* 重置重连状态
*/
private resetReconnect() {
this.isReconnecting = false;
this.reconnectCount = 0;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
destory() {
this.isManualClose = true;
this.resetReconnect();
this.socket?.close();
this.socket = null;
this.categoryHandlers?.clear();

View File

@@ -1,6 +1,6 @@
import { Msg } from '@/hooks/useI18n';
import { i18n } from '@/i18n';
import { v1 as uuidv1 } from 'uuid';
import Clipboard from 'clipboard';
import { ElMessage } from 'element-plus';
/**
* 模板字符串解析template = 'hahaha{name}_{id}' ,param = {name: 'hh', id: 1}
@@ -111,42 +111,67 @@ export function randomUuid() {
return uuidv1();
}
/**
* 从剪贴板粘贴文本
* @returns Promise<string> 剪贴板中的文本
* @throws Error 当无法访问剪贴板时抛出异常
*/
export async function pasteFromClipboard(): Promise<string> {
// navigator clipboard 需要https等安全上下文
if (navigator.clipboard && window.isSecureContext) {
// navigator clipboard 从剪贴板读文本
try {
const text = await navigator.clipboard.readText();
return text;
} catch (e: any) {
throw new Error(i18n.global.t('common.pasteFailed'));
}
}
// 非安全上下文HTTP 环境),无法读取剪贴板
throw new Error(i18n.global.t('common.pasteNotSupported'));
}
/**
* 拷贝文本至剪贴板
* @param txt 需要拷贝到剪贴板的文本
* @param selector click事件对应的元素selector默认为 #copyValue
* @returns
*/
export async function copyToClipboard(txt: string, selector: string = '#copyValue') {
export async function copyToClipboard(txt: string) {
// navigator clipboard 需要https等安全上下文
if (navigator.clipboard && window.isSecureContext) {
// navigator clipboard 向剪贴板写文本
try {
// 拷贝单元格数据
await navigator.clipboard.writeText(txt);
ElMessage.success('复制成功');
Msg.success('common.copySuccess');
} catch (e: any) {
ElMessage.error('复制失败');
Msg.error('common.copyFailed');
}
return;
}
let clipboard = new Clipboard(selector, {
text: function () {
return txt;
},
});
clipboard.on('success', () => {
ElMessage.success('复制成功');
// 释放内存
clipboard.destroy();
});
clipboard.on('error', () => {
// 不支持复制
ElMessage.error('该浏览器不支持自动复制');
// 释放内存
clipboard.destroy();
});
// 非安全上下文HTTP 环境),无法使用 Clipboard API
// 降级方案:创建临时 textarea 并使用 execCommand('copy')
try {
const textarea = document.createElement('textarea');
textarea.value = txt;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
Msg.success('common.copySuccess');
} else {
Msg.error('common.copyFailed');
}
} catch (e: any) {
Msg.error('common.copyNotSupported');
}
}
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {

View File

@@ -1,7 +1,7 @@
import { Msg } from '@/hooks/useI18n';
import * as monaco from 'monaco-editor';
import { VNode, h, render } from 'vue';
import MonacoEditorDialog from './MonacoEditorDialog.vue';
import * as monaco from 'monaco-editor';
import { ElMessage } from 'element-plus';
export type MonacoEditorDialogProps = {
content: string;
@@ -68,11 +68,11 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
try {
val = JSON.parse(value);
if (typeof val !== 'object') {
ElMessage.error('Invalid json');
Msg.error('Invalid json');
return;
}
} catch (e) {
ElMessage.error('Invalid json');
Msg.error('Invalid json');
return;
}
// 压缩json字符串

View File

@@ -21,6 +21,7 @@
<!-- Drawer 模式 -->
<el-drawer
:append-to-body="false"
:title="props.title"
v-model="dialogVisible"
:size="props.drawerSize || '50%'"
@@ -50,13 +51,14 @@
</template>
<script lang="ts" setup>
import { ElButton, ElDialog, ElDrawer } from 'element-plus';
import { ref, watch } from 'vue';
import { ElDialog, ElDrawer, ElButton, ElMessage } from 'element-plus';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { MonacoEditorDialogProps } from './MonacoEditorBox';
import { Msg } from '@/hooks/useI18n';
import { i18n } from '@/i18n';
import { registerCompletionItemProvider } from './completionItemProvider';
import { MonacoEditorDialogProps } from './MonacoEditorBox';
const editorRef: any = ref(null);
@@ -117,11 +119,11 @@ const confirm = async () => {
try {
val = JSON.parse(value);
if (typeof val !== 'object') {
ElMessage.error('Invalid json');
Msg.error('Invalid json');
return;
}
} catch (e) {
ElMessage.error('Invalid json');
Msg.error('Invalid json');
return;
}

View File

@@ -196,6 +196,7 @@ export interface PageTableProps {
border?: boolean; // 是否带有纵向边框 ==> 非必传默认为false
toolButton?: ('setting' | 'search')[] | boolean; // 是否显示表格功能按钮 ==> 非必传默认为true
searchCol?: any; // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 } | number 如 3
selectionData?: any[]; // 选中数据,声明为 prop 防止透传到 el-table 引发 Array.join 异常
}
// 接受父组件参数,配置默认值
@@ -209,6 +210,7 @@ const props = withDefaults(defineProps<PageTableProps>(), {
showSearch: false,
searchItems: () => [],
searchCol: () => ({ xs: 1, sm: 3, md: 3, lg: 4, xl: 5 }),
selectionData: () => [],
});
// 查询表单参数 ==> 非必传(默认为{pageNum:1, pageSize: 10}

View File

@@ -1,6 +1,6 @@
<template>
<div
v-if="globalNotificationState.hasActiveNotifications"
v-if="globalNotificationState.activeCount > 0"
class="fixed z-[2000]"
:style="{ bottom: position.bottom + 'px', right: position.right + 'px' }"
>

View File

@@ -9,7 +9,7 @@
{{ progress.title }}
</span>
<!-- 取消按钮 -->
<el-button v-if="!progress.terminated && progress.status !== 'cancelled'" type="danger" size="small" text @click="handleCancel">
<el-button v-if="!progress.terminated && progress.status !== 'cancelled'" type="danger" size="small" text :loading="cancelLoading" @click="handleCancel">
<SvgIcon name="Close" :size="14" />
{{ $t('common.cancel') }}
</el-button>
@@ -23,10 +23,12 @@
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive } from 'vue';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { formatTime } from 'element-plus/es/components/countdown/src/utils';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
const cancelLoading = ref(false);
interface Progress {
dbCode: string;
dbName: string;
@@ -77,6 +79,7 @@ onUnmounted(async () => {
// 处理取消执行
const handleCancel = () => {
if (props.onCancel) {
cancelLoading.value = true;
props.onCancel();
}
};

View File

@@ -1,7 +1,7 @@
import DbSqlExecProgress from './DbSqlExecProgress.vue';
import { createOrUpdateNotification, completeNotification, activeNotifications } from '../global-notification-manager';
import syssocket from '@/common/syssocket';
import { reactive, nextTick } from 'vue';
import { nextTick, reactive } from 'vue';
import { activeNotifications, completeNotification, createOrUpdateNotification } from '../global-notification-manager';
import DbSqlExecProgress from './DbSqlExecProgress.vue';
// 存储SQL执行任务的取消方法
const sqlExecAborters = new Map<string, { abort: () => void; progress?: any }>();
@@ -9,11 +9,32 @@ const sqlExecAborters = new Map<string, { abort: () => void; progress?: any }>()
// 存储待注册的 abort 方法(等待 WebSocket 消息到达)
const pendingSqlExecAborters = new Map<string, () => void>();
export interface SqlExecProgress {
id: string;
title: string;
dbCode: string;
dbName: string;
executedStatements: number;
terminated: boolean;
status: string;
clientId: string;
}
const sqlExecStates = reactive<Map<string, SqlExecProgress>>(new Map());
/**
* 注册数据库SQL执行进度消息处理
*/
export async function registerDbSqlExecProgress() {
await syssocket.registerMsgHandler('sqlScriptRunProgress', function (message: any) {
const content = message.params;
const id = content.id;
const progress = sqlExecStates.get(id);
if (!progress) {
return;
}
// SQL执行完成
if (content.terminated) {
completeNotification(id, 1000);
@@ -21,55 +42,62 @@ export async function registerDbSqlExecProgress() {
return;
}
// 构建组件props
const props = {
progress: reactive({
title: content.title || '',
executedStatements: content.executedStatements || 0,
terminated: content.terminated || false,
status: content.status || '',
dbCode: content.dbCode || '',
dbName: content.dbName || '',
}),
onCancel: () => {
const aborter = sqlExecAborters.get(id);
if (aborter) {
aborter.abort();
// 更新通知状态为取消
if (aborter.progress) {
nextTick(() => {
aborter.progress.status = 'cancelled';
aborter.progress.terminated = true;
});
// 延迟后关闭通知
setTimeout(() => {
completeNotification(id, 1000);
sqlExecAborters.delete(id);
}, 1500);
} else {
sqlExecAborters.delete(id);
}
}
},
};
// 创建或更新通知
createOrUpdateNotification(id, 'sqlScriptRun', content, DbSqlExecProgress, props, {
title: message.title || 'db.sqlExecute',
onCancel: props.onCancel,
});
// 如果有待注册的 abort 方法,现在注册
const pendingAbort = pendingSqlExecAborters.get(id);
if (pendingAbort) {
sqlExecAborters.set(id, { abort: pendingAbort, progress: props.progress });
pendingSqlExecAborters.delete(id);
}
progress.executedStatements = content.executedStatements || 0;
progress.terminated = content.terminated || false;
progress.status = content.status || '';
return;
});
}
/**
* 创建SQL执行进度通知
* @param id 执行ID
* @param data 进度数据
*/
export function createSqlExecNotification(id: string, data: SqlExecProgress) {
// 构建组件props
const props = {
progress: data,
onCancel: () => {
const aborter = sqlExecAborters.get(id);
if (aborter) {
aborter.abort();
// 更新通知状态为取消
if (aborter.progress) {
nextTick(() => {
aborter.progress.status = 'cancelled';
aborter.progress.terminated = true;
});
// 延迟后关闭通知
setTimeout(() => {
completeNotification(id, 1000);
sqlExecAborters.delete(id);
}, 1500);
} else {
sqlExecAborters.delete(id);
}
}
},
};
// 创建或更新通知
createOrUpdateNotification(id, 'sqlScriptRun', data, DbSqlExecProgress, props, {
title: data.title || 'db.sqlExecute',
onCancel: props.onCancel,
});
sqlExecStates.set(id, data);
// 如果有待注册的 abort 方法,现在注册
const pendingAbort = pendingSqlExecAborters.get(id);
if (pendingAbort) {
sqlExecAborters.set(id, { abort: pendingAbort, progress: props.progress });
pendingSqlExecAborters.delete(id);
}
}
/**
* 注册SQL执行任务的取消方法
* @param execId 执行ID

View File

@@ -1,14 +1,25 @@
import { reactive } from 'vue';
import { reactive, type Component } from 'vue';
// 通知任务接口定义
export interface NotificationTask {
id: string;
category: string;
content: unknown;
component: Component;
componentProps: Record<string, any>;
options: {
title: string;
onCancel?: () => void;
};
timestamp: number;
}
// 活跃通知任务映射表
export const activeNotifications = reactive<Map<string, any>>(new Map());
export const activeNotifications = reactive<Map<string, NotificationTask>>(new Map());
// 悬浮通知状态
export const globalNotificationState = reactive({
hasActiveNotifications: false,
activeCount: 0,
// 按类别统计
categoryCount: reactive<Map<string, number>>(new Map()),
});
/**
@@ -16,20 +27,15 @@ export const globalNotificationState = reactive({
*/
const updateNotificationState = () => {
globalNotificationState.activeCount = activeNotifications.size;
globalNotificationState.hasActiveNotifications = activeNotifications.size > 0;
// 按类别统计
const categoryMap = new Map<string, number>();
for (const [_, task] of activeNotifications) {
const category = task.category || 'default';
categoryMap.set(category, (categoryMap.get(category) || 0) + 1);
}
globalNotificationState.categoryCount.clear();
for (const [key, value] of categoryMap) {
globalNotificationState.categoryCount.set(key, value);
}
};
/**
* 获取通知
*/
export function getNotification(id: string) {
return activeNotifications.get(id);
}
/**
* 创建或更新通知
* @param id 通知唯一ID
@@ -42,12 +48,12 @@ const updateNotificationState = () => {
export const createOrUpdateNotification = (
id: string,
category: string,
content: any,
component: any,
componentProps: any,
content: unknown,
component: Component,
componentProps: Record<string, any>,
options: {
title: string;
onCancel?: () => void; // 取消回调
onCancel?: () => void;
}
) => {
// 添加到活跃任务
@@ -76,32 +82,3 @@ export const completeNotification = (id: string, closeDelay: number = 1000) => {
updateNotificationState();
}, closeDelay);
};
/**
* 关闭指定通知
* @param id 通知唯一ID
*/
export const closeNotification = (id: string) => {
activeNotifications.delete(id);
updateNotificationState();
};
/**
* 关闭指定类别的所有通知
* @param category 通知类别
*/
export const closeCategoryNotifications = (category: string) => {
for (const [id, task] of activeNotifications) {
if (task.category === category) {
closeNotification(id);
}
}
};
/**
* 关闭所有通知
*/
export const closeAllNotifications = () => {
activeNotifications.clear();
updateNotificationState();
};

View File

@@ -18,7 +18,7 @@
{{ progress.filename }}
</span>
<!-- 取消按钮 -->
<el-button v-if="progress.status === '' || progress.status === 'uploading'" type="danger" size="small" text @click="handleCancel">
<el-button v-if="progress.status === '' || progress.status === 'uploading'" type="danger" size="small" text :loading="cancelLoading" @click="handleCancel">
<SvgIcon name="Close" :size="14" />
{{ $t('common.cancel') }}
</el-button>
@@ -68,6 +68,8 @@ import { formatByteSize } from '@/common/utils/format';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
import { computed, ref } from 'vue';
const cancelLoading = ref(false);
interface Progress {
authCertName: string; // 授权凭证名
path: string; // 文件路径
@@ -159,6 +161,7 @@ const speed = computed(() => {
// 处理取消上传
const handleCancel = () => {
if (props.onCancel) {
cancelLoading.value = true;
props.onCancel();
}
};

View File

@@ -1,54 +1,97 @@
<template>
<div class="w-full py-2 max-w-[500px]">
<div class="w-full py-2 max-w-125">
<!-- 机器和路径信息 -->
<el-row>
<TagCodePath :code="progress.authCertName" />
</el-row>
<!-- 文件路径 -->
<div v-if="progress.path" class="mb-3 px-1">
<span class="text-xs text-gray-500 dark:text-gray-400 truncate block" :title="progress.path">
{{ progress.path }}
</span>
</div>
<!-- 文件夹信息 -->
<!-- 文件夹信息和取消按钮 -->
<div class="flex justify-between items-center mb-2">
<span class="font-semibold text-sm text-gray-700 dark:text-gray-200 truncate flex-1 mr-2" :title="progress.folderName">{{
progress.folderName
}}</span>
<span class="text-xs text-gray-500 dark:text-gray-400 mr-2">{{ progress.uploadedFiles }}/{{ progress.totalFiles }}</span>
<span class="font-semibold text-sm text-gray-700 dark:text-gray-200 truncate flex-1 mr-2" :title="progress.folderName">
{{ progress.folderName }}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400 mr-2">
{{ progress.uploadedFiles }}/{{ progress.totalFiles }}
<span class="ml-1">({{ formatSize(progress.uploadedSize) }}/{{ formatSize(progress.totalSize) }})</span>
</span>
<!-- 取消按钮 -->
<el-button v-if="progress.status === '' || progress.status === 'uploading'" type="danger" size="small" text @click="handleCancel">
<el-button
v-if="progress.status === '' || progress.status === 'uploading'"
type="danger"
size="small"
text
:loading="cancelLoading"
@click="handleCancel"
>
{{ $t('common.cancel') }}
</el-button>
</div>
<!-- 整体进度条 -->
<el-progress :percentage="percent" :status="progressStatus" :stroke-width="10" />
<!-- 所有文件列表 -->
<div v-if="fileList.length > 0" class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="text-xs font-semibold text-primary mb-2">{{ $t('machine.fileList') }}:</div>
<!-- 整体进度信息 -->
<div class="mt-1.5 flex justify-between items-center">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ formatSize(progress.uploadedSize) }} / {{ formatSize(progress.totalSize) }}</span>
<span class="text-xs font-semibold text-gray-700 dark:text-gray-200">{{ percent }}%</span>
</div>
<!-- 文件列表滚动区域 -->
<el-scrollbar max-height="200px">
<div
v-for="file in fileList"
:key="file.path"
class="flex items-center gap-2 py-1.5 px-2 text-xs hover:bg-gray-50 dark:hover:bg-gray-800 rounded"
>
<!-- 文件状态图标 -->
<SvgIcon
v-if="file.status === 'uploading'"
:size="12"
name="Loading"
color="var(--el-color-primary)"
class="animate-[rotating_2s_linear_infinite] shrink-0"
/>
<SvgIcon v-else-if="file.status === 'complete'" :size="12" name="CircleCheck" color="var(--el-color-success)" class="shrink-0" />
<SvgIcon v-else-if="file.status === 'error'" :size="12" name="CircleClose" color="var(--el-color-danger)" class="shrink-0" />
<div v-else class="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600 shrink-0" />
<!-- 正在上传的文件列表 -->
<div v-if="progress.uploadingFiles && progress.uploadingFiles.length > 0" class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="text-xs font-semibold text-primary mb-2">
{{ $t('machine.uploading') }} ({{ $t('machine.concurrentFiles', { count: progress.uploadingFiles.length }) }}):
</div>
<div v-for="(file, index) in progress.uploadingFiles" :key="index" class="flex items-center gap-1.5 py-1 text-xs text-gray-600 dark:text-gray-300">
<el-icon class="animate-[rotating_2s_linear_infinite] text-primary"><Loading /></el-icon>
<span class="flex-1 truncate">{{ file }}</span>
</div>
<!-- 文件路径 -->
<span class="flex-1 truncate text-gray-700 dark:text-gray-300" :title="file.path">
{{ file.path }}
</span>
<!-- 上传进度或状态 -->
<span v-if="file.status === 'uploading'" class="shrink-0 flex items-center gap-1.5">
<!-- 文件大小 -->
<span class="text-[10px] text-gray-500 dark:text-gray-400">
{{ formatSize(file.currentSize) }} / {{ formatSize(file.totalSize) }}
</span>
<!-- 传输速率 -->
<span v-if="file.speed" class="text-[10px] text-primary font-semibold"> {{ file.speed }}/s </span>
<!-- 进度百分比 -->
<span class="text-[10px] text-primary font-semibold"> {{ file.progress }}% </span>
</span>
<span v-else-if="file.status === 'complete'" class="text-xs text-success shrink-0">
{{ $t('common.complete') }}
</span>
<span v-else-if="file.status === 'error'" class="text-xs text-danger shrink-0">
{{ $t('common.error') }}
</span>
<span v-else class="text-xs text-gray-400 shrink-0">
{{ $t('machine.waiting') }}
</span>
</div>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
import { Loading } from '@element-plus/icons-vue';
import { computed } from 'vue';
import { ref, computed } from 'vue';
const cancelLoading = ref(false);
const props = defineProps({
progress: {
@@ -61,24 +104,12 @@ const props = defineProps({
},
});
const progressStatus = computed(() => {
if (props.progress.status === 'complete') {
return 'success';
} else if (props.progress.status === 'error') {
return 'danger';
} else if (props.progress.status === 'uploading') {
return 'primary';
} else {
return '';
// 将 Map 转换为数组以便遍历
const fileList = computed(() => {
if (!props.progress.files || !(props.progress.files instanceof Map)) {
return [];
}
});
// 计算百分比
const percent = computed(() => {
if (!props.progress.totalSize || !props.progress.uploadedSize) {
return 0;
}
return Math.min(100, Math.floor((props.progress.uploadedSize / props.progress.totalSize) * 100));
return Array.from(props.progress.files.values());
});
// 格式化文件大小
@@ -93,6 +124,7 @@ const formatSize = (bytes: number): string => {
// 处理取消上传
const handleCancel = () => {
if (props.onCancel) {
cancelLoading.value = true;
props.onCancel();
}
};

View File

@@ -1,7 +1,7 @@
import syssocket from '@/common/syssocket';
import { createOrUpdateNotification, completeNotification, activeNotifications } from '../global-notification-manager';
import { nextTick, reactive } from 'vue';
import { activeNotifications, completeNotification, createOrUpdateNotification } from '../global-notification-manager';
import MachineFileUploadProgress from './MachineFileUploadProgress.vue';
import { reactive, nextTick } from 'vue';
// 存储上传任务的取消方法
const uploadAborters = new Map<string, { abort: () => void; progress?: any }>();
@@ -9,6 +9,18 @@ const uploadAborters = new Map<string, { abort: () => void; progress?: any }>();
// 存储待注册的 abort 方法(等待 WebSocket 消息到达)
const pendingAborters = new Map<string, () => void>();
export interface FileUploadProgress {
authCertName: string;
path: string;
filename: string;
totalSize?: number; // 文件总大小
uploadedSize?: number; // 已上传大小
status?: 'uploading' | 'complete' | 'error';
timestamp?: number;
}
const fileStates = reactive<Map<string, FileUploadProgress>>(new Map());
/**
* 注册机器文件上传进度消息处理
*/
@@ -17,6 +29,11 @@ export async function registerMachineFileUploadProgress() {
const content = message.params;
const uploadId = content.uploadId;
const progress = fileStates.get(uploadId);
if (!progress) {
return;
}
// 上传完成或失败
if (content.status === 'complete' || content.status === 'error') {
completeNotification(uploadId, 1000);
@@ -24,63 +41,64 @@ export async function registerMachineFileUploadProgress() {
return;
}
// 构建组件props
const props = {
progress: reactive({
authCertName: content.authCertName || '',
path: content.path || '',
filename: content.filename || '',
uploadedSize: content.uploadedSize || 0,
totalSize: content.totalSize || 0,
timestamp: content.timestamp || 0,
status: content.status || 'uploading',
}),
onCancel: () => {
const aborter = uploadAborters.get(uploadId);
if (aborter) {
aborter.abort();
// 更新通知状态为取消
if (aborter.progress) {
nextTick(() => {
aborter.progress.status = 'error';
aborter.progress.filename = '已取消: ' + (aborter.progress.filename || '');
});
// 延迟后关闭通知
setTimeout(() => {
completeNotification(uploadId, 1000);
uploadAborters.delete(uploadId);
}, 1500);
} else {
uploadAborters.delete(uploadId);
}
}
},
};
// 创建或更新上传通知
createOrUpdateNotification(uploadId, 'machineFileUpload', content, MachineFileUploadProgress, props, {
title: message.title || 'machine.fileUpload',
onCancel: props.onCancel,
});
// 如果有待注册的 abort 方法,现在注册
const pendingAbort = pendingAborters.get(uploadId);
if (pendingAbort) {
console.log('[MachineFileUpload] Registering pending aborter for uploadId:', uploadId);
uploadAborters.set(uploadId, { abort: pendingAbort, progress: props.progress });
pendingAborters.delete(uploadId);
}
progress.status = content.status || 'uploading';
progress.uploadedSize = content.uploadedSize;
progress.timestamp = content.timestamp;
progress.totalSize = content.totalSize;
return;
});
}
export function createUploadFileNotification(uploadId: string, data: FileUploadProgress) {
// 构建组件props
const props = {
progress: data,
onCancel: () => {
const aborter = uploadAborters.get(uploadId);
if (aborter) {
aborter.abort();
// 更新通知状态为取消
if (aborter.progress) {
nextTick(() => {
aborter.progress.status = 'error';
aborter.progress.filename = '已取消: ' + (aborter.progress.filename || '');
});
// 延迟后关闭通知
setTimeout(() => {
completeNotification(uploadId, 1000);
uploadAborters.delete(uploadId);
}, 1500);
} else {
uploadAborters.delete(uploadId);
}
}
},
};
// 创建或更新上传通知
createOrUpdateNotification(uploadId, 'machineFileUpload', data, MachineFileUploadProgress, props, {
title: 'machine.fileUpload',
});
fileStates.set(uploadId, data);
// 如果有待注册的 abort 方法,现在注册
const pendingAbort = pendingAborters.get(uploadId);
if (pendingAbort) {
console.log('[MachineFileUpload] Registering pending aborter for uploadId:', uploadId);
uploadAborters.set(uploadId, { abort: pendingAbort, progress: props.progress });
pendingAborters.delete(uploadId);
}
}
/**
* 注册上传任务的取消方法
* @param uploadId 上传ID
* @param abort 取消方法
*/
export function registerUploadAborter(uploadId: string, abort: () => void) {
export function registerUploadFileAborter(uploadId: string, abort: () => void) {
// 先检查通知是否已经存在
const task = activeNotifications.get(uploadId);
const progress = task?.componentProps?.progress || null;

View File

@@ -1,7 +1,8 @@
import syssocket from '@/common/syssocket';
import { createOrUpdateNotification, completeNotification, activeNotifications } from '../global-notification-manager';
import { nextTick, reactive } from 'vue';
import { activeNotifications, completeNotification, createOrUpdateNotification } from '../global-notification-manager';
import MachineFolderUploadProgress from './MachineFolderUploadProgress.vue';
import { reactive, nextTick } from 'vue';
import { formatByteSize } from '@/common/utils/format';
// 存储上传任务的取消方法
const folderUploadAborters = new Map<string, { abort: () => void; progress?: any }>();
@@ -9,6 +10,155 @@ const folderUploadAborters = new Map<string, { abort: () => void; progress?: any
// 存储待注册的 abort 方法(等待 WebSocket 消息到达)
const pendingFolderAborters = new Map<string, () => void>();
export interface FolderUploadProgress {
authCertName: string;
path: string;
folderName: string;
totalFiles: number; // 文件夹总文件数
uploadedFiles: number; // 已上传文件数
finishedFiles: number; // 已完成(成功或失败)的文件数
totalSize: number; // 文件夹总大小
uploadedSize: number; // 已上传大小
status: 'uploading' | 'complete' | 'error';
files: Map<
string, // 文件路径
{
path: string;
status: 'waiting' | 'uploading' | 'complete' | 'error'; // 文件状态
progress: number;
currentSize: number; // 当前已上传大小
totalSize: number; // 文件总大小
timestamp: number; // 后端推送的时间戳(用于前端计算速度)
speed?: string;
}
>;
}
const folderStates = reactive<Map<string, FolderUploadProgress>>(new Map());
/**
* 更新文件夹上传进度(处理后端推送的单文件进度消息)
*/
export function handleUploadFolderProgress(content: any) {
const { uploadId, filename, path, uploadedSize, totalSize, status, timestamp } = content;
if (!uploadId || !filename) {
return;
}
const folderState = folderStates.get(uploadId);
if (!folderState) {
console.warn('[FolderUpload] 找不到 folderState:', uploadId);
return;
}
// 构建文件完整路径(后端推送的 path + filename
const backendFilePath = path ? `${path}/${filename}` : filename;
// 查找匹配的文件
let matchedFile = folderState.files.get(backendFilePath);
if (!matchedFile) {
console.warn('[FolderUpload] 未找到匹配的文件:', filename, '路径:', backendFilePath);
return;
}
// 更新 Map 中的状态
if (status === 'uploading' && totalSize > 0) {
// 如果已经是 complete 或 error不要覆盖
if (matchedFile.status === 'complete' || matchedFile.status === 'error') {
console.log('[FolderUpload] 忽略旧状态消息:', filename, matchedFile.status);
return;
}
matchedFile.status = 'uploading';
matchedFile.progress = Math.round((uploadedSize / totalSize) * 100);
matchedFile.currentSize = uploadedSize;
matchedFile.totalSize = totalSize;
// 计算传输速度(使用后端推送的 timestamp
if (timestamp) {
// 第一次推送时初始化
if (!matchedFile.timestamp) {
matchedFile.timestamp = timestamp;
} else {
const timeDiff = (timestamp - matchedFile.timestamp) / 1000; // 转换为秒
if (timeDiff > 0) {
const sizeDiff = uploadedSize - matchedFile.currentSize;
if (sizeDiff > 0) {
const speed = sizeDiff / timeDiff;
matchedFile.speed = formatByteSize(speed);
}
}
matchedFile.timestamp = timestamp;
}
}
} else if (status === 'complete') {
matchedFile.status = 'complete';
matchedFile.progress = 100;
matchedFile.currentSize = matchedFile.totalSize;
folderState.uploadedFiles = (folderState.uploadedFiles || 0) + 1;
folderState.finishedFiles = (folderState.finishedFiles || 0) + 1;
folderState.uploadedSize += uploadedSize;
console.log('[FolderUpload] 文件上传完成:', backendFilePath);
} else if (status === 'error') {
matchedFile.status = 'error';
matchedFile.progress = 0;
folderState.finishedFiles = (folderState.finishedFiles || 0) + 1;
console.log('[FolderUpload] 文件上传失败:', backendFilePath);
}
// 文件夹上传完成条件:所有文件都已完成(成功或失败)
if (folderState.finishedFiles === folderState.totalFiles) {
completeNotification(uploadId, 1000);
}
}
/**
* 创建文件夹上传通知
*/
export function createUploadFolderNotification(uploadId: string, data: FolderUploadProgress) {
const props = {
progress: data,
onCancel: () => {
const aborter = folderUploadAborters.get(uploadId);
if (aborter) {
aborter.abort();
if (aborter.progress) {
nextTick(() => {
aborter.progress.status = 'error';
aborter.progress.folderName = '已取消: ' + (aborter.progress.folderName || '');
});
setTimeout(() => {
completeNotification(uploadId, 1000);
folderUploadAborters.delete(uploadId);
folderStates.delete(uploadId);
}, 1500);
} else {
folderUploadAborters.delete(uploadId);
folderStates.delete(uploadId);
}
}
},
};
createOrUpdateNotification(uploadId, 'machineFolderUpload', data, MachineFolderUploadProgress, props, {
title: 'machine.folderUpload',
});
folderStates.set(uploadId, data);
// 注册 aborter
const pendingAbort = pendingFolderAborters.get(uploadId);
if (pendingAbort) {
folderUploadAborters.set(uploadId, { abort: pendingAbort, progress: props.progress });
pendingFolderAborters.delete(uploadId);
}
}
/**
* 注册文件夹上传进度消息处理
*/
@@ -21,65 +171,9 @@ export async function registerFolderUploadProgressHandler() {
return;
}
// 上传完成或失败
if (content.status === 'complete' || content.status === 'error') {
completeNotification(uploadId, 1000);
folderUploadAborters.delete(uploadId);
return;
}
// 构建组件props
const props = {
progress: reactive({
authCertName: content.authCertName || '',
path: content.path || '',
folderName: content.folderName || '',
totalFiles: content.totalFiles || 0,
uploadedFiles: content.uploadedFiles || 0,
totalSize: content.totalSize || 0,
uploadedSize: content.uploadedSize || 0,
uploadingFiles: content.uploadingFiles || [],
timestamp: content.timestamp || 0,
status: content.status || 'uploading',
}),
onCancel: () => {
const aborter = folderUploadAborters.get(uploadId);
if (aborter) {
aborter.abort();
// 更新通知状态为取消
if (aborter.progress) {
nextTick(() => {
aborter.progress.status = 'exception';
aborter.progress.folderName = '已取消: ' + (aborter.progress.folderName || '');
});
// 延迟后关闭通知
setTimeout(() => {
completeNotification(uploadId, 1000);
folderUploadAborters.delete(uploadId);
}, 1500);
} else {
folderUploadAborters.delete(uploadId);
}
}
},
};
// 创建或更新上传通知
if (content.status === 'uploading') {
createOrUpdateNotification(uploadId, 'machineFolderUpload', content, MachineFolderUploadProgress, props, {
title: message.title || 'machine.folderUpload',
onCancel: props.onCancel,
});
}
// 如果有待注册的 abort 方法,现在注册
const pendingAbort = pendingFolderAborters.get(uploadId);
if (pendingAbort) {
folderUploadAborters.set(uploadId, { abort: pendingAbort, progress: props.progress });
pendingFolderAborters.delete(uploadId);
}
// 文件夹上传:处理单文件进度和完成消息
// 注意:文件夹上传时,单个文件的 complete/error 不关闭通知,只标记文件状态
handleUploadFolderProgress(content);
});
}
@@ -88,7 +182,7 @@ export async function registerFolderUploadProgressHandler() {
* @param uploadId 上传ID
* @param abort 取消方法
*/
export function registerFolderUploadAborter(uploadId: string, abort: () => void) {
export function registerUploadFolderAborter(uploadId: string, abort: () => void) {
// 先检查通知是否已经存在
const task = activeNotifications.get(uploadId);
const progress = task?.componentProps?.progress || null;

View File

@@ -75,7 +75,7 @@ import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
import { useDebounceFn, useEventListener } from '@vueuse/core';
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
import { ElMessage } from 'element-plus';
import { Msg } from '@/hooks/useI18n';
import { joinClientParams } from '@/common/request';
import { MachineProtocolEnum } from '@/views/ops/machine/enums';
@@ -470,7 +470,7 @@ const openSendKeyboard = (keys: string[]) => {
for (let j = 0; j < keys.length; j++) {
state.client.sendKeyEvent(0, keys[j]);
}
ElMessage.success('发送组合键成功');
Msg.success('components.terminal-rdp.sendCombinationKeySuccess');
};
const exposes = {

View File

@@ -19,7 +19,7 @@
<script setup lang="ts">
import { reactive, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { Msg } from '@/hooks/useI18n';
const props = defineProps({
visible: { type: Boolean },
@@ -46,10 +46,10 @@ const onclose = () => {
const onsubmit = () => {
state.dialogVisible = false;
if (state.modelValue) {
ElMessage.success('发送剪贴板数据成功');
Msg.success('components.terminal-rdp.clipboardSendSuccess');
emits('submit', state.modelValue);
} else {
ElMessage.warning('请输入需要粘贴的文本');
Msg.warning('components.terminal-rdp.clipboardInputRequired');
}
};

View File

@@ -1,5 +1,6 @@
import Guacamole from './guacamole-common';
import { ElMessage } from 'element-plus';
import { i18n } from '@/i18n';
const clipboard = {};
@@ -77,7 +78,7 @@ clipboard.getLocalClipboard = async () => {
data: text,
};
} else {
ElMessage.warning('只有https才可以访问剪贴板');
Msg.warning('components.terminal-rdp.httpsRequiredForClipboard');
}
};

View File

@@ -19,14 +19,13 @@ import '@xterm/xterm/css/xterm.css';
import config from '@/common/config';
import { createWebSocket, joinClientParams } from '@/common/request';
import { downloadFile } from '@/common/utils/file';
import { copyToClipboard } from '@/common/utils/string';
import { copyToClipboard, pasteFromClipboard } from '@/common/utils/string';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { registerUploadAborter } from '@/components/sysmsg/machine/machine-file-upload-progress';
import { registerFolderUploadAborter } from '@/components/sysmsg/machine/machine-folder-upload-progress';
import { Msg } from '@/hooks/useI18n';
import { useThemeConfig } from '@/store/themeConfig';
import { machineApi, uploadFile, uploadFolder } from '@/views/ops/machine/api';
import { useDebounceFn, useEventListener } from '@vueuse/core';
import { ElMessage } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
@@ -391,15 +390,27 @@ const showContextMenu = (event: MouseEvent, selectedText: string) => {
copyToClipboard(selectedText);
}),
new ContextmenuItem('paste', 'common.paste').withIcon('Document').withOnClick(async () => {
let text = '';
try {
const text = await navigator.clipboard.readText();
if (text) {
term.paste(text);
focus();
// 尝试从剪贴板读取
text = await pasteFromClipboard();
} catch (error) {
// 读取失败(非 HTTPS 环境),弹出输入框让用户手动粘贴
try {
const { value: manualText } = await ElMessageBox.prompt(t('components.terminal.pasteManualHint'), t('components.terminal.manualPaste'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
inputType: 'textarea',
inputPlaceholder: t('components.terminal.pasteHere'),
});
text = manualText;
} catch {
// 用户取消
}
} catch (err) {
console.log(err);
ElMessage.error(t('common.pasteFailed'));
}
if (text) {
term.paste(text);
focus();
}
}),
new ContextmenuItem('download', 'components.terminal.downloadSelectedFile')
@@ -407,19 +418,22 @@ const showContextMenu = (event: MouseEvent, selectedText: string) => {
.withHideFunc(() => !selectedText)
.withOnClick(() => {
downloadSelectedFile(state.contextmenu.selectedItem);
}),
})
.withPermission('machine:file:upload'),
new ContextmenuItem('uploadFile', 'components.terminal.uploadFileToCurrentDir')
.withIcon('Upload')
.withHideFunc(() => !props.machineId || !props.authCertName)
.withOnClick(() => {
triggerFilesUpload();
}),
})
.withPermission('machine:file:upload'),
new ContextmenuItem('uploadFolder', 'components.terminal.uploadFolderToCurrentDir')
.withIcon('Upload')
.withHideFunc(() => !props.machineId || !props.authCertName)
.withOnClick(() => {
triggerFolderUpload();
}),
})
.withPermission('machine:file:upload'),
];
// 打开右键菜单
@@ -429,7 +443,7 @@ const showContextMenu = (event: MouseEvent, selectedText: string) => {
// 下载选中的文件
const downloadSelectedFile = async (filePath: string) => {
if (!props.machineId || !props.authCertName) {
ElMessage.error(t('components.terminal.downloadFailed', { error: '缺少机器信息' }));
Msg.error('components.terminal.downloadFailed', { error: '缺少机器信息' });
return;
}
@@ -453,6 +467,7 @@ const downloadSelectedFile = async (filePath: string) => {
path: fullPath,
});
} catch (error: any) {
Msg.error('components.terminal.downloadFailed', { error: error.message });
return;
}
@@ -461,9 +476,9 @@ const downloadSelectedFile = async (filePath: string) => {
`${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/download?path=${encodeURIComponent(fullPath)}&machineId=${props.machineId}&authCertName=${props.authCertName}&fileId=${props.fileId}&protocol=${props.protocol}&${joinClientParams()}`
);
ElMessage.success(t('components.terminal.startDownload', { file: fullPath }));
Msg.success('components.terminal.startDownload', { file: fullPath });
} catch (error: any) {
ElMessage.error(t('components.terminal.downloadFailed', { error: error.message }));
Msg.error('components.terminal.downloadFailed', { error: error.message });
}
};
@@ -513,8 +528,7 @@ const uploadFilesToCurrentPath = async (files: FileList) => {
const file = files[0];
// 使用统一的 HTTP 上传方法
const { uploadId, abort } = uploadFile(
uploadFile(
file,
{
machineId: props.machineId as number,
@@ -526,18 +540,15 @@ const uploadFilesToCurrentPath = async (files: FileList) => {
},
{
onSuccess: () => {
ElMessage.success(t('components.terminal.uploadSuccess'));
Msg.success('components.terminal.uploadSuccess');
},
onError: (error) => {
ElMessage.error(t('components.terminal.uploadFailed', { error: error.message }));
Msg.error('components.terminal.uploadFailed', { error: error.message });
},
}
);
// 注册取消方法
registerUploadAborter(uploadId, abort);
} catch (error: any) {
ElMessage.error(t('components.terminal.uploadFailed', { error: error.message }));
Msg.error('components.terminal.uploadFailed', { error: error.message });
}
};
@@ -547,8 +558,7 @@ const uploadFolderToCurrentPath = async (files: FileList) => {
// 获取当前路径
const currentPath = await getCurrentPathOrDefault();
// 使用文件夹上传
const { uploadId, abort } = uploadFolder(
uploadFolder(
files,
{
machineId: props.machineId as number,
@@ -559,18 +569,15 @@ const uploadFolderToCurrentPath = async (files: FileList) => {
},
{
onSuccess: () => {
ElMessage.success(t('components.terminal.uploadSuccess'));
Msg.success('components.terminal.uploadSuccess');
},
onError: (error: Error) => {
ElMessage.error(t('components.terminal.uploadFailed', { error: error.message }));
Msg.error('components.terminal.uploadFailed', { error: error.message });
},
}
);
// 注册取消方法
registerFolderUploadAborter(uploadId, abort);
} catch (error: any) {
ElMessage.error(t('components.terminal.uploadFailed', { error: error.message }));
Msg.error('components.terminal.uploadFailed', { error: error.message });
}
};
@@ -616,7 +623,7 @@ const getCurrentPath = (): Promise<string> => {
// 处理文件拖拽上传
const handleFileDrop = async (items: DataTransferItemList) => {
if (!props.machineId || !props.authCertName) {
ElMessage.error(t('components.terminal.uploadFailed', { error: '缺少机器信息' }));
Msg.error('components.terminal.uploadFailed', { error: '缺少机器信息' });
return;
}

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer v-model="visible" :before-close="cancel" size="50%" body-class="flex flex-col">
<el-drawer :append-to-body="false" v-model="visible" :before-close="cancel" size="50%" body-class="flex flex-col">
<template #header>
<DrawerHeader :header="props.title" :back="cancel">
<template #extra>

View File

@@ -45,9 +45,9 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, nextTick, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { SearchAddon, ISearchOptions } from '@xterm/addon-search';
import { useI18n } from 'vue-i18n';
import { Msg } from '@/hooks/useI18n';
const { t } = useI18n();
@@ -110,7 +110,7 @@ function searchKeywords(direction: any) {
res = props.searchAddon?.findPrevious(state.search.value, getSearchOptions(option));
}
if (!res) {
ElMessage.info(t('components.terminal.noMatchMsg'));
Msg.info('components.terminal.noMatchMsg');
}
}

View File

@@ -78,30 +78,64 @@ export function useI18nDetailTitle(i18nKey: string) {
return t('common.detailTitle', { name: t(i18nKey) });
}
export function useI18nOperateSuccessMsg() {
MsgSuccess('common.operateSuccess');
}
export function useI18nSaveSuccessMsg() {
MsgSuccess('common.saveSuccess');
}
export function useI18nDeleteSuccessMsg() {
MsgSuccess('common.deleteSuccess');
}
/**
* error msg
* @param msg msg(支持i8n msgkey)
* 国际化消息提示(基于 ElMessage
*/
export function MsgError(msg: string) {
ElMessage.error(i18n.global.t(msg));
}
export const Msg = {
/**
* 成功消息
* @param msg 消息内容(支持 i18n key
* @param params 国际化参数
*/
success(msg: string, params?: any) {
ElMessage.success(i18n.global.t(msg, params));
},
/**
* success msg
* @param msg msg(支持i8n msgkey)
*/
export function MsgSuccess(msg: string) {
ElMessage.success(i18n.global.t(msg));
}
/**
* 错误消息
* @param msg 消息内容(支持 i18n key
* @param params 国际化参数
*/
error(msg: string, params?: any) {
ElMessage.error(i18n.global.t(msg, params));
},
/**
* 警告消息
* @param msg 消息内容(支持 i18n key
* @param params 国际化参数
*/
warning(msg: string, params?: any) {
ElMessage.warning(i18n.global.t(msg, params));
},
/**
* 信息消息
* @param msg 消息内容(支持 i18n key
* @param params 国际化参数
*/
info(msg: string, params?: any) {
ElMessage.info(i18n.global.t(msg, params));
},
/**
* 保存成功消息
*/
saveSuccess() {
Msg.success('common.saveSuccess');
},
/**
* 删除成功消息
*/
deleteSuccess() {
Msg.success('common.deleteSuccess');
},
/**
* 操作成功消息
*/
operateSuccess() {
Msg.success('common.operateSuccess');
},
};

View File

@@ -1,16 +1,16 @@
import router from '@/router';
import Api from '@/common/Api';
import config from '@/common/config';
import openApi from '@/common/openApi';
import { Result, ResultEnum } from '@/common/request';
import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string';
import { ElMessage } from 'element-plus';
import { createFetch, UseFetchReturn } from '@vueuse/core';
import Api from '@/common/Api';
import { Result, ResultEnum } from '@/common/request';
import config from '@/common/config';
import { ref, unref } from 'vue';
import router from '@/router';
import { URL_401 } from '@/router/staticRouter';
import openApi from '@/common/openApi';
import { useThemeConfig } from '@/store/themeConfig';
import { createFetch, UseFetchReturn } from '@vueuse/core';
import JSONBig from 'json-bigint';
import { ref, unref } from 'vue';
import { Msg } from './useI18n';
const baseUrl: string = config.baseApiUrl;
@@ -77,6 +77,14 @@ export function useApiFetch<T, P = any>(api: Api, params?: P, reqOptions?: Reque
let paramsValue = unref(currentParam);
let apiUrl = url;
// 提取 ac授权凭证名参数始终以查询参数方式传递
let acValue: string | undefined;
if (paramsValue && paramsValue.ac) {
acValue = paramsValue.ac;
const { ac: _ac, ...restParams } = paramsValue;
paramsValue = restParams;
}
// 简单判断该url是否是restful风格
if (apiUrl.indexOf('{') != -1 && paramsValue) {
apiUrl = templateResolve(apiUrl, paramsValue);
@@ -108,7 +116,16 @@ export function useApiFetch<T, P = any>(api: Api, params?: P, reqOptions?: Reque
searchParam.append(key, val);
}
});
apiUrl = `${apiUrl}?${searchParam.toString()}`;
const qs = searchParam.toString();
if (qs) {
apiUrl = `${apiUrl}?${qs}`;
}
}
// ac 参数始终以查询参数形式附加到 URL
if (acValue) {
const separator = apiUrl.includes('?') ? '&' : '?';
apiUrl = `${apiUrl}${separator}ac=${encodeURIComponent(acValue)}`;
}
// 确保 FormData 的 body 不被 reqOptions 覆盖
@@ -181,23 +198,23 @@ async function execCustomFetch(uaf: UseFetchReturn<any>, reqOptions?: RequestOpt
const respStatus = uaf.response.value?.status;
if (respStatus == 404) {
ElMessage.error('url not found');
Msg.error('url not found');
return rejectPromise;
}
if (respStatus == 500) {
ElMessage.error('server error');
Msg.error('server error');
return rejectPromise;
}
console.error(e);
ElMessage.error('network error');
Msg.error('network error');
return rejectPromise;
}
}
const result: Result & { error: any; status: number } = uaf.data.value as any;
if (!result) {
ElMessage.error('network request failed');
Msg.error('network request failed');
return Promise.reject(result);
}
// es代理请求
@@ -258,7 +275,7 @@ async function execCustomFetch(uaf: UseFetchReturn<any>, reqOptions?: RequestOpt
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && resultCode != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
Msg.error(result.msg);
uaf.error.value = new Error(result.msg);
}

View File

@@ -48,15 +48,25 @@ export default {
basic: 'Basic',
other: 'Other',
reset: 'Reset',
online: 'Online',
offline: 'Offline',
total: 'Total',
enabled: 'Enabled',
disabled: 'Disabled',
success: 'Success',
fail: 'Fail',
complete: 'Complete',
error: 'Error',
requestFail: 'Request Fail',
previousStep: 'Previous Step',
nextStep: 'Next Step',
copy: 'Copy',
paste: 'Paste',
pasteFailed: 'Paste failed',
pasteNotSupported: 'Clipboard access is not supported in this environment. Please use Ctrl+V to paste',
copySuccess: 'Copy Success',
copyFailed: 'Copy failed',
copyNotSupported: 'This browser does not support auto copy',
copyCell: 'Copy Cell',
search: 'Search',
@@ -109,6 +119,8 @@ export default {
close: 'Close',
closeOther: 'Close Other',
closeAll: 'Close All',
closeLeft: 'Close Left',
closeRight: 'Close Right',
fullscreen: 'Fullscreen',
closeFullscreen: 'Close Fullscreen',
},
@@ -289,6 +301,11 @@ export default {
uploadToPath: 'File will be uploaded to: {path}',
uploadPathTip: 'Tip: File will be uploaded to home directory (~). To upload to another directory, use cd command first, then use drag-and-drop upload.',
// Paste related
manualPaste: 'Manual Paste',
pasteManualHint: 'Unable to read clipboard content automatically. Please paste your content into the input box below:',
pasteHere: 'Paste your content here...',
// Machine file upload progress notification
machineFileUpload: {
uploadProgress: 'Machine File Upload Progress',

View File

@@ -38,6 +38,7 @@ export default {
stopImageConfirm: 'Are you sure to stop image [{name}] ?',
export: 'Export',
imageUploading: 'Image uploading, please wait...',
uploadSuccess: 'Image uploaded successfully',
imageTips: 'Support manual input and select',
forcePull: 'Force Pull Image',
hostPortPlaceholder: '80',

View File

@@ -29,6 +29,8 @@ export default {
indexStats: 'Stats',
opViewColumns: 'Option View Columns',
opIndex: 'Index Management',
opDataManage: 'Data Management',
selectIndexFirst: 'Please select an index first',
opSearch: 'Search',
searchParamsPreview: 'Search Params Preview',
opBasicSearch: 'Basic Search',
@@ -98,6 +100,31 @@ export default {
text: 'Text',
startAnalyze: 'Start Analyze',
},
export: {
title: 'Export Data',
selectedCount: '{count} rows selected',
exportAll: 'Export All Data',
exportSelected: 'Export Selected Data',
exportQuery: 'Export Query Results',
exportType: 'Export Type',
csv: 'CSV File',
excel: 'Excel File',
json: 'JSON File',
confirm: 'Confirm Export',
exporting: 'Exporting...',
exportAllConfirm: 'Export all data from index [{name}] ({total} docs total). Continue?',
largeExportTip: 'Large dataset ({total} docs), will be exported via backend batch processing and compressed download',
selectAllFields: 'Select All Fields',
exportFields: 'Export Fields',
noData: 'No data to export',
phase: {
querying: 'Querying...',
exporting: 'Exporting...',
compressing: 'Compressing...',
completed: 'Completed',
unknown: 'Unknown',
},
},
contextmenu: {
index: {
addIndex: 'Add Index',

View File

@@ -0,0 +1,45 @@
export default {
home: {
personalInfo: 'Personal Information',
welcomeMsg: 'Welcome back, {name}!',
lastLoginIp: 'Last Login IP',
lastLoginTime: 'Last Login Time',
noOpRecord: 'No operation records',
// Resource Overview
resourceOverview: 'Resource Overview',
machineStats: 'Machine Statistics',
dbStats: 'Database Statistics',
redisStats: 'Redis Statistics',
mongoStats: 'MongoDB Statistics',
// Quick Access
quickAccess: 'Quick Access',
myMachines: 'Machines',
myDatabases: 'Databases',
myRedis: 'Redis',
myMongo: 'MongoDB',
myEs: 'Elasticsearch',
myMilvus: 'Milvus',
myContainers: 'Containers',
myKafka: 'Kafka',
// Machine Resource Status
machineResourceStatus: 'Machine Resource Status',
// System Load
systemLoad: 'System Load',
cpuUsage: 'CPU Usage',
memoryUsage: 'Memory Usage',
diskUsage: 'Disk Usage',
networkTraffic: 'Network Traffic',
// Recent Operations
recentOperations: 'Recent Operations',
viewAll: 'View All',
// Load Tips
loadMore: 'Scroll to load more...',
loadedAll: 'All loaded',
},
};

View File

@@ -148,6 +148,8 @@ export default {
folderUpload: 'Folder Upload',
uploading: 'Uploading',
concurrentFiles: '{count} concurrent',
fileList: 'File List',
waiting: 'Waiting',
// Upload notifications
uploadNotifications: {

View File

@@ -83,6 +83,12 @@ export default {
searchGroup: 'Enter group name',
selectGroupPlaceholder: 'select group',
Members: 'Members',
groupMembers: 'Consumer Group Members',
clientHost: 'Client Host',
clientID: 'Client ID',
instanceID: 'Instance ID',
memberID: 'Member ID',
assignedTopics: 'Assigned Topic Partitions',
partitionsFeatureComingSoon: 'Partitions feature coming soon',
},
},

View File

@@ -16,9 +16,12 @@ export default {
rootTag: 'Root Tag',
selectTagPlaceholder: 'Select the associated tag',
machineOp: 'Machine Operation',
machineTerminal: 'Machine Terminal',
machineFile: 'Machine File',
dbDataOp: 'Db Operation',
redisDataOp: 'Redis Operation',
esDataOp: 'Es Operation',
esIndexData: 'ES Index Data',
mongoDataOp: 'Mongo Operation',
allResource: 'All Resource',
mq: {

View File

@@ -48,15 +48,25 @@ export default {
basic: '基本',
other: '其他',
reset: '重置',
online: '在线',
offline: '离线',
total: '总数',
enabled: '启用',
disabled: '停用',
success: '成功',
fail: '失败',
complete: '已完成',
error: '错误',
requestFail: '请求失败',
previousStep: '上一步',
nextStep: '下一步',
copy: '复制',
paste: '粘贴',
pasteFailed: '粘贴失败',
pasteNotSupported: '当前环境不支持读取剪贴板,请使用 Ctrl+V 粘贴',
copySuccess: '复制成功',
copyFailed: '复制失败',
copyNotSupported: '该浏览器不支持自动复制',
copyCell: '复制单元格',
search: '搜索',
pleaseInput: '请输入{label}',
@@ -107,6 +117,8 @@ export default {
close: '关闭',
closeOther: '关闭其它',
closeAll: '全部关闭',
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
fullscreen: '当前页全屏',
closeFullscreen: '关闭全屏',
},
@@ -298,6 +310,11 @@ export default {
uploadToPath: '文件将上传到路径: {path}',
uploadPathTip: '提示: 文件将上传到用户家目录(~)。如需上传到其他目录,请先在终端中执行 cd 命令切换到目标目录,然后使用拖拽上传功能。',
// 粘贴相关
manualPaste: '手动粘贴',
pasteManualHint: '当前环境无法自动读取剪贴板内容,请手动粘贴内容到下方输入框:',
pasteHere: '请在此处粘贴内容...',
// 机器文件上传进度通知
machineFileUpload: {
uploadProgress: '机器文件上传进度',

View File

@@ -38,6 +38,7 @@ export default {
stopImageConfirm: '确定删除该镜像?',
export: '导出',
imageUploading: '镜像导入中,请稍后...',
uploadSuccess: '镜像导入成功',
imageTips: '支持手动输入并选择',
forcePull: '强制拉取镜像',
hostPortPlaceholder: '80',

View File

@@ -29,6 +29,8 @@ export default {
indexStats: '统计信息',
opViewColumns: '设置显示字段',
opIndex: '索引管理',
opDataManage: '数据管理',
selectIndexFirst: '请先选择索引',
opSearch: '搜索',
searchParamsPreview: '搜索条件预览',
opBasicSearch: '基础搜索',
@@ -97,6 +99,31 @@ export default {
text: '文本',
startAnalyze: '开始分析',
},
export: {
title: '导出数据',
selectedCount: '已选择 {count} 条数据',
exportAll: '导出所有数据',
exportSelected: '导出已选数据',
exportQuery: '导出查询结果',
exportType: '导出类型',
csv: 'CSV 文件',
excel: 'Excel 文件',
json: 'JSON 文件',
confirm: '确认导出',
exporting: '正在导出...',
exportAllConfirm: '将导出索引 [{name}] 的所有数据(共 {total} 条),确认继续吗?',
largeExportTip: '数据量较大(共 {total} 条),将通过后台分批查询并压缩后下载',
selectAllFields: '全选字段',
exportFields: '导出字段',
noData: '没有可导出的数据',
phase: {
querying: '正在查询...',
exporting: '正在导出数据...',
compressing: '正在压缩...',
completed: '导出完成',
unknown: '未知状态',
},
},
contextmenu: {
index: {
addIndex: '添加索引',

View File

@@ -0,0 +1,45 @@
export default {
home: {
personalInfo: '个人信息',
welcomeMsg: '欢迎回来,{name}',
lastLoginIp: '上次登录IP',
lastLoginTime: '上次登录时间',
noOpRecord: '暂无操作记录',
// 资源概览
resourceOverview: '资源概览',
machineStats: '机器统计',
dbStats: '数据库统计',
redisStats: 'Redis统计',
mongoStats: 'MongoDB统计',
// 快捷入口
quickAccess: '快捷入口',
myMachines: '机器',
myDatabases: '数据库',
myRedis: 'Redis',
myMongo: 'MongoDB',
myEs: 'Elasticsearch',
myMilvus: 'Milvus',
myContainers: '容器',
myKafka: 'Kafka',
// 机器资源状态
machineResourceStatus: '机器资源状态',
// 系统负载
systemLoad: '系统负载',
cpuUsage: 'CPU使用率',
memoryUsage: '内存使用率',
diskUsage: '磁盘使用率',
networkTraffic: '网络流量',
// 最近操作
recentOperations: '最近操作',
viewAll: '查看全部',
// 加载提示
loadMore: '滚动加载更多...',
loadedAll: '已加载全部',
},
};

View File

@@ -149,6 +149,8 @@ export default {
folderUpload: '文件夹上传',
uploading: '正在上传',
concurrentFiles: '{count} 个并发',
fileList: '文件列表',
waiting: '等待中',
// 上传通知
uploadNotifications: {
@@ -158,4 +160,11 @@ export default {
files: '{count} 个文件',
},
},
// 远程桌面相关
'terminal-rdp': {
sendCombinationKeySuccess: '发送组合键成功',
clipboardSendSuccess: '发送剪贴板数据成功',
clipboardInputRequired: '请输入需要粘贴的文本',
httpsRequiredForClipboard: '只有 HTTPS 才可以访问剪贴板',
},
};

View File

@@ -83,6 +83,12 @@ export default {
searchGroup: '输入组名称',
selectGroupPlaceholder: '选择分组',
Members: '成员',
groupMembers: '消费者组成员',
clientHost: '客户端地址',
clientID: '客户端 ID',
instanceID: '实例 ID',
memberID: '成员 ID',
assignedTopics: '分配的 Topic 分区',
partitionsFeatureComingSoon: '分区详情功能即将上线',
},
},

View File

@@ -18,9 +18,12 @@ export default {
rootTag: '根标签',
selectTagPlaceholder: '请选择关联标签',
machineOp: '机器操作',
machineTerminal: '机器终端',
machineFile: '机器文件',
dbDataOp: '数据库操作',
redisDataOp: 'Redis操作',
esDataOp: 'ES操作',
esIndexData: 'ES索引数据',
mongoDataOp: 'Mongo操作',
containerOp: '容器操作',
allResource: '所有资源',

View File

@@ -416,15 +416,13 @@
</template>
<script lang="ts" setup name="layoutBreadcrumbSeting">
import { nextTick, onMounted, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import ClipboardJS from 'clipboard';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getLocal, setLocal } from '@/common/utils/storage';
import { getLightColor } from '@/common/utils/theme';
import { setLocal, getLocal } from '@/common/utils/storage';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
import { nextTick, onMounted, ref, watch } from 'vue';
import themes from '@/components/terminal/themes';
import themes from '@/components/terminal/themes.js';
import { useWindowSize } from '@vueuse/core';
const copyConfigBtnRef = ref();
@@ -582,19 +580,16 @@ const initSetLayoutChange = () => {
themeConfig.value.isShowLogo = true;
themeConfig.value.isBreadcrumb = false;
themeConfig.value.isCollapse = false;
themeConfig.value.isTagsview = true;
themeConfig.value.isClassicSplitMenu = false;
} else if (themeConfig.value.layout === 'columns') {
themeConfig.value.isShowLogo = true;
themeConfig.value.isBreadcrumb = true;
themeConfig.value.isCollapse = false;
themeConfig.value.isTagsview = true;
themeConfig.value.isClassicSplitMenu = false;
} else {
themeConfig.value.isShowLogo = false;
themeConfig.value.isBreadcrumb = true;
themeConfig.value.isCollapse = false;
themeConfig.value.isTagsview = true;
themeConfig.value.isClassicSplitMenu = false;
}
@@ -624,24 +619,9 @@ const setLocalThemeConfigStyle = () => {
setLocal('themeConfigStyle', document.documentElement.style.cssText);
};
// 一键复制配置
const onCopyConfigClick = (target: any) => {
if (!target) {
return;
}
const onCopyConfigClick = () => {
let copyThemeConfig = getLocal('themeConfig');
copyThemeConfig.isDrawer = false;
const clipboard = new ClipboardJS(target, {
text: () => JSON.stringify(copyThemeConfig),
});
clipboard.on('success', () => {
themeConfig.value.isDrawer = false;
ElMessage.success('复制成功');
clipboard.destroy();
});
clipboard.on('error', () => {
ElMessage.error('复制失败');
clipboard.destroy();
});
};
const checkClientWidth = () => {

View File

@@ -72,23 +72,23 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted, watch, useTemplateRef, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
import { resetRoute } from '@/router/index';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import { useThemeConfig } from '@/store/themeConfig';
import { clearSession } from '@/common/utils/storage';
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
import openApi from '@/common/openApi';
import { getThemeConfig } from '@/common/utils/storage';
import { useDark, usePreferredDark } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import { I18nEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import openApi from '@/common/openApi';
import { clearSession, getThemeConfig } from '@/common/utils/storage';
import { Msg } from '@/hooks/useI18n';
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
import { resetRoute } from '@/router/index';
import { useThemeConfig } from '@/store/themeConfig';
import { useUserInfo } from '@/store/userInfo';
import { useDark, usePreferredDark } from '@vueuse/core';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import screenfull from 'screenfull';
import { computed, onMounted, reactive, ref, useTemplateRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
// const AiChatDialog = defineAsyncComponent(() => import('@/views/ai/AiChatDialog.vue'));
@@ -135,7 +135,7 @@ const onShowMsgs = () => {
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('暂不不支持全屏');
Msg.warning('暂不不支持全屏');
return false;
}
screenfull.toggle();
@@ -177,7 +177,7 @@ const onHandleCommandClick = (path: string) => {
resetRoute(); // 删除/重置路由
router.push('/login');
setTimeout(() => {
ElMessage.success(t('layout.user.logoutSuccess'));
Msg.success('layout.user.logoutSuccess');
}, 300);
})
.catch(() => {});

View File

@@ -1,8 +1,8 @@
import { defineStore } from 'pinia';
import { formatDate } from '@/common/utils/format';
import { useUserInfo } from '@/store/userInfo';
import { getServerConf, getSysStyleConfig } from '@/common/sysconfig';
import { formatDate } from '@/common/utils/format';
import { getLocal, getThemeConfig } from '@/common/utils/storage';
import { useUserInfo } from '@/store/userInfo';
import { defineStore } from 'pinia';
// 系统默认logo图标对应于@/assets/image/logo.svg
const logoIcon =

View File

@@ -116,4 +116,5 @@ declare interface MilvusState {
selectedDb: string,
selectedCollection: string
collections: any[],
authCertName: string,
}

View File

@@ -63,7 +63,7 @@
<script setup lang="ts" name="AiAssistant">
import { notBlankI18n } from '@/common/assert';
import { formatDate } from '@/common/utils/format';
import { useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { Msg } from '@/hooks/useI18n';
import { useThemeConfig } from '@/store/themeConfig';
import { ElMessageBox } from 'element-plus';
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
@@ -147,7 +147,7 @@ const onMenuCommand = async (command: ConversationMenuCommand, item: Conversatio
}).then(async ({ value }) => {
notBlankI18n(value, 'common.name');
await aiApi.renameSession.request({ sessionKey: item.key, title: value });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
loadSessions();
});
}

View File

@@ -1,5 +1,6 @@
<template>
<el-drawer
:append-to-body="false"
:title="title"
v-model="dialogVisible"
z-index="2000"

File diff suppressed because it is too large Load Diff

View File

@@ -1,160 +1,153 @@
import { createWebSocket } from '@/common/request';
import { ref, onBeforeUnmount, type Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { ElMessage } from 'element-plus';
import { Msg } from '@/hooks/useI18n';
import { onBeforeUnmount, ref, type Ref } from 'vue';
/**
* AI Chat WebSocket 连接管理 Hook
* 负责 WebSocket 连接、重连、消息收发等
*/
export function useAiChatWebSocket(
onMessage: (data: any) => void,
currentSessionId: Ref<string>,
isNewSession: Ref<boolean>
) {
const { t } = useI18n();
const socket = ref<WebSocket | null>(null);
const reconnectTimer = ref<any>(null);
const reconnectAttempts = ref(0);
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY = 3000;
export function useAiChatWebSocket(onMessage: (data: any) => void, currentSessionId: Ref<string>, isNewSession: Ref<boolean>) {
const socket = ref<WebSocket | null>(null);
const reconnectTimer = ref<any>(null);
const reconnectAttempts = ref(0);
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY = 3000;
/**
* 初始化 WebSocket 连接
*/
const initSocket = async () => {
try {
console.log('init chat ws...');
const ws = await createWebSocket(`/ai/chat`);
socket.value = ws;
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
// 会话隔离:只处理属于当前激活会话的消息
if (data.sessionId && data.sessionId !== currentSessionId.value) {
// 新会话首次收到后端返回的真实 sessionId更新并通知父组件
if (isNewSession.value) {
currentSessionId.value = data.sessionId;
} else {
console.log(`忽略不属于当前会话的消息: ${data.sessionId} !== ${currentSessionId.value}`);
/**
* 初始化 WebSocket 连接
*/
const initSocket = async () => {
try {
console.log('init chat ws...');
const ws = await createWebSocket(`/ai/chat`);
socket.value = ws;
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
// 会话隔离:只处理属于当前激活会话的消息
if (data.sessionId && data.sessionId !== currentSessionId.value) {
// 新会话首次收到后端返回的真实 sessionId更新并通知父组件
if (isNewSession.value) {
currentSessionId.value = data.sessionId;
} else {
console.log(`忽略不属于当前会话的消息: ${data.sessionId} !== ${currentSessionId.value}`);
return;
}
}
onMessage(data);
};
ws.onclose = (event) => {
console.log('chat ws 连接关闭:', event.code, event.reason);
if (!event.wasClean) {
attemptReconnect();
}
};
ws.onerror = (error) => {
console.error('chat ws 错误:', error);
};
// 连接成功,重置重连计数
reconnectAttempts.value = 0;
} catch (e) {
console.log('连接错误', e);
// 直接显示错误提示,不传递到消息处理器
Msg.error('ai.chat.connectionFailed');
attemptReconnect();
}
};
/**
* 尝试重连
*/
const attemptReconnect = () => {
if (reconnectAttempts.value >= MAX_RECONNECT_ATTEMPTS) {
console.warn('达到最大重连次数,停止重连');
Msg.error('ai.chat.connectionDisconnected');
return;
}
}
onMessage(data);
};
ws.onclose = (event) => {
console.log('chat ws 连接关闭:', event.code, event.reason);
if (!event.wasClean) {
attemptReconnect();
reconnectAttempts.value++;
console.log(`尝试第 ${reconnectAttempts.value} 次重连...`);
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value);
}
};
ws.onerror = (error) => {
console.error('chat ws 错误:', error);
};
reconnectTimer.value = setTimeout(() => {
initSocket();
}, RECONNECT_DELAY);
};
// 连接成功,重置重连计数
reconnectAttempts.value = 0;
} catch (e) {
console.log('连接错误', e);
// 直接显示错误提示,不传递到消息处理器
ElMessage.error(t('ai.chat.connectionFailed'));
attemptReconnect();
}
};
/**
* 清理 WebSocket 连接
*/
const cleanupSocket = () => {
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value);
reconnectTimer.value = null;
}
if (socket.value) {
socket.value.onclose = null;
socket.value.onerror = null;
socket.value.onmessage = null;
if (socket.value.readyState === WebSocket.OPEN || socket.value.readyState === WebSocket.CONNECTING) {
socket.value.close();
}
socket.value = null;
}
reconnectAttempts.value = 0;
};
/**
* 尝试重连
*/
const attemptReconnect = () => {
if (reconnectAttempts.value >= MAX_RECONNECT_ATTEMPTS) {
console.warn('达到最大重连次数,停止重连');
ElMessage.error(t('ai.chat.connectionDisconnected'));
return;
}
/**
* 发送消息
*/
const sendMessage = (type: 'text' | 'interruptResume', content: string) => {
// 检查 WebSocket 连接状态
if (!socket.value || socket.value.readyState === WebSocket.CLOSED || socket.value.readyState === WebSocket.CLOSING) {
console.warn('WebSocket 连接已关闭,尝试重连...');
reconnectAttempts.value++;
console.log(`尝试第 ${reconnectAttempts.value} 次重连...`);
// 如果正在重连中,等待重连完成
if (reconnectAttempts.value > 0 && reconnectAttempts.value < MAX_RECONNECT_ATTEMPTS) {
Msg.warning('ai.chat.reconnecting');
attemptReconnect();
return;
}
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value);
}
// 立即尝试重连
attemptReconnect();
Msg.error('ai.chat.connectionLost');
return;
}
reconnectTimer.value = setTimeout(() => {
initSocket();
}, RECONNECT_DELAY);
};
socket.value.send(
JSON.stringify({
type,
sessionId: currentSessionId.value,
content,
})
);
};
/**
* 清理 WebSocket 连接
*/
const cleanupSocket = () => {
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value);
reconnectTimer.value = null;
}
if (socket.value) {
socket.value.onclose = null;
socket.value.onerror = null;
socket.value.onmessage = null;
if (socket.value.readyState === WebSocket.OPEN || socket.value.readyState === WebSocket.CONNECTING) {
socket.value.close();
}
socket.value = null;
}
reconnectAttempts.value = 0;
};
/**
* 获取当前连接状态
*/
const isConnected = () => {
return socket.value && socket.value.readyState === WebSocket.OPEN;
};
/**
* 发送消息
*/
const sendMessage = (type: 'text' | 'interruptResume', content: string) => {
// 检查 WebSocket 连接状态
if (!socket.value || socket.value.readyState === WebSocket.CLOSED || socket.value.readyState === WebSocket.CLOSING) {
console.warn('WebSocket 连接已关闭,尝试重连...');
// 如果正在重连中,等待重连完成
if (reconnectAttempts.value > 0 && reconnectAttempts.value < MAX_RECONNECT_ATTEMPTS) {
ElMessage.warning(t('ai.chat.reconnecting'));
attemptReconnect();
return;
}
// 组件卸载时清理连接
onBeforeUnmount(() => {
cleanupSocket();
});
// 立即尝试重连
attemptReconnect();
ElMessage.error(t('ai.chat.connectionLost'));
return;
}
socket.value.send(
JSON.stringify({
type,
sessionId: currentSessionId.value,
content,
})
);
};
/**
* 获取当前连接状态
*/
const isConnected = () => {
return socket.value && socket.value.readyState === WebSocket.OPEN;
};
// 组件卸载时清理连接
onBeforeUnmount(() => {
cleanupSocket();
});
return {
initSocket,
sendMessage,
reconnectAttempts,
MAX_RECONNECT_ATTEMPTS,
};
return {
initSocket,
sendMessage,
reconnectAttempts,
MAX_RECONNECT_ATTEMPTS,
};
}

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<el-drawer :append-to-body="false" :title="props.title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -43,21 +43,18 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef, watch, onMounted } from 'vue';
import { procdefApi, procinstApi } from './api';
import { ElMessage } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { FlowBizType } from './enums';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import RedisRunCmdFlowBizForm from './flowbiz/redis/RedisRunCmdFlowBizForm.vue';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { Msg } from '@/hooks/useI18n';
import { defineAsyncComponent, reactive, shallowReactive, toRefs, useTemplateRef, watch } from 'vue';
import { procdefApi, procinstApi } from './api';
import FlowDesign from './components/flowdesign/FlowDesign.vue';
import { FlowBizType } from './enums';
import RedisRunCmdFlowBizForm from './flowbiz/redis/RedisRunCmdFlowBizForm.vue';
const DbSqlExecFlowBizForm = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecFlowBizForm.vue'));
const { t } = useI18n();
const props = defineProps({
title: {
type: String,
@@ -135,12 +132,12 @@ const btnOk = async () => {
await formRef.value.validate();
await bizFormRef.value.validateBizForm();
} catch (e: any) {
ElMessage.error(t('flow.procinstFormError'));
Msg.error('flow.procinstFormError');
return false;
}
await procinstStart();
ElMessage.success(t('flow.procinstStartSuccess'));
Msg.success('flow.procinstStartSuccess');
emit('val-change', modelValue.value);
//重置表单域
cancel();

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="title" v-model="visible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<el-drawer :append-to-body="false" :title="title" v-model="visible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="onCancel" />
</template>
@@ -51,18 +51,18 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { procdefApi } from './api';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { ProcdefStatus } from './enums';
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { reactive, ref, toRefs, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import MsgTmplSelect from '../msg/components/MsgTmplSelect.vue';
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
import { procdefApi } from './api';
import { ProcdefStatus } from './enums';
const { t } = useI18n();
@@ -119,7 +119,7 @@ watch(props, async (newValue: any) => {
const onSave = async () => {
await useI18nFormValidate(formRef);
await saveFlowDefExec();
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('val-change', state.form);
//重置表单域
formRef.value.resetFields();

View File

@@ -38,18 +38,18 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import { procdefApi, procinstApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import ProcdefEdit from './ProcdefEdit.vue';
import { ProcdefStatus } from './enums';
import TagCodePath from '../ops/component/TagCodePath.vue';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import { onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import TagCodePath from '../ops/component/TagCodePath.vue';
import ProcdefEdit from './ProcdefEdit.vue';
import { procdefApi } from './api';
import FlowDesignDrawer from './components/flowdesign/FlowDesignDrawer.vue';
import { ProcdefStatus } from './enums';
const { t } = useI18n();
@@ -133,7 +133,7 @@ const onDeleteProcdef = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join(', '));
await procdefApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
} catch (err) {
//
@@ -149,7 +149,7 @@ const onShowFlowDesign = async (data: any) => {
const onSaveFlowDesign = async (data: any) => {
await procdefApi.saveFlowDef.request({ id: state.flowDesignEditor.procdefId, flow: data });
useI18nSaveSuccessMsg();
Msg.saveSuccess();
state.flowDesignEditor.visible = false;
};
</script>

View File

@@ -1,5 +1,6 @@
<template>
<el-drawer
:append-to-body="false"
:title="props.title"
v-model="visible"
:before-close="cancel"
@@ -111,16 +112,15 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, defineAsyncComponent, shallowReactive } from 'vue';
import { procinstApi, procinstTaskApi } from './api';
import { ElMessage } from 'element-plus';
import { formatDate, formatTime } from '@/common/utils/format';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
import { formatTime } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { Msg } from '@/hooks/useI18n';
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
import { formatDate } from '@/common/utils/format';
import { defineAsyncComponent, reactive, shallowReactive, toRefs, watch } from 'vue';
import { procinstApi, procinstTaskApi } from './api';
import FlowDesign from './components/flowdesign/FlowDesign.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstStatus, ProcinstTaskStatus } from './enums';
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecBiz.vue'));
const RedisRunCmdBiz = defineAsyncComponent(() => import('./flowbiz/redis/RedisRunCmdBiz.vue'));
@@ -230,7 +230,7 @@ const btnOk = async () => {
try {
state.saveBtnLoading = true;
await api.request({ id: props.instTaskId, remark: state.form.remark });
ElMessage.success('操作成功');
Msg.operateSuccess();
cancel();
emit('val-change');
} finally {

View File

@@ -52,17 +52,17 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, Ref, defineAsyncComponent } from 'vue';
import { procinstApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/pagetable/SearchForm';
import ProcinstDetail from './ProcinstDetail.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
import { formatTime } from '@/common/utils/format';
import { useI18nDetailTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { Msg, useI18nDetailTitle } from '@/hooks/useI18n';
import { useUserInfo } from '@/store/userInfo';
import { defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import ProcinstDetail from './ProcinstDetail.vue';
import { procinstApi } from './api';
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
const { t } = useI18n();
@@ -130,7 +130,7 @@ const search = async () => {
const procinstCancel = async (data: any) => {
await procinstApi.cancel.request({ id: data.id });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
search();
};

View File

@@ -1,5 +1,6 @@
<template>
<el-drawer
:append-to-body="false"
:title="title"
v-model="visible"
:before-close="cancel"

View File

@@ -1,5 +1,6 @@
<template>
<el-drawer
:append-to-body="false"
body-class="!pt-2"
header-class="!mb-2"
:title="title"

View File

@@ -1,244 +1,113 @@
<template>
<div class="home-container personal">
<el-row :gutter="15">
<!-- 个人信息 -->
<el-col :xs="24" :sm="24">
<el-card shadow="hover" :header="$t('home.personalInfo')">
<div class="personal-user">
<div class="personal-user-left">
<el-upload
class="!h-full personal-user-left-upload"
:action="getUploadFileUrl(`avatar_${userInfo.username}`)"
:limit="1"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
:on-success="handleAvatarSuccess"
accept=".png,.jpg,.jpeg"
>
<img :src="userInfo.photo" />
</el-upload>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb-4">
{{ $t('home.welcomeMsg', { name: userInfo.name }) }}
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item !mb-1.5">
<div class="personal-item-label">{{ $t('common.username') }}</div>
<div class="personal-item-value">{{ userInfo.username }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item !mb-1.5">
<div class="personal-item-label">{{ $t('common.role') }}</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 !mb-1.5">
<div class="personal-item-label">{{ $t('home.lastLoginIp') }}</div>
<div class="personal-item-value">{{ userInfo.lastLoginIp }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item !mb-1.5">
<div class="personal-item-label">{{ $t('home.lastLoginTime') }}</div>
<div class="personal-item-value">{{ formatDate(userInfo.lastLoginTime) }}</div>
</el-col>
</el-row>
</el-col>
</el-row>
<div class="home-container personal overflow-x-hidden">
<!-- 个人信息卡片 -->
<div class="mb-4">
<el-card shadow="hover" :header="$t('home.personalInfo')">
<div class="flex flex-col sm:flex-row gap-4 items-center sm:items-start">
<div class="w-25 rounded overflow-hidden shrink-0">
<el-upload
class="h-full!"
:action="getUploadFileUrl(`avatar_${userInfo.username}`)"
:limit="1"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
:on-success="handleAvatarSuccess"
accept=".png,.jpg,.jpeg"
>
<img :src="userInfo.photo" class="w-full h-full rounded transition-transform duration-300 hover:scale-110" />
</el-upload>
</div>
<div class="flex-1 px-3.75">
<div class="mb-4 text-lg truncate">{{ $t('home.welcomeMsg', { name: userInfo.name }) }}</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1.5 text-[13px]">
<div class="flex items-center">
<span class="text-gray-500 mr-2 truncate">{{ $t('common.username') }}</span>
<span class="truncate">{{ userInfo.username }}</span>
</div>
<div class="flex items-center">
<span class="text-gray-500 mr-2 truncate">{{ $t('common.role') }}</span>
<span class="truncate">{{ roleInfo }}</span>
</div>
<div class="flex items-center">
<span class="text-gray-500 mr-2 truncate">{{ $t('home.lastLoginIp') }}</span>
<span class="truncate">{{ userInfo.lastLoginIp }}</span>
</div>
<div class="flex items-center">
<span class="text-gray-500 mr-2 truncate">{{ $t('home.lastLoginTime') }}</span>
<span class="truncate">{{ formatDate(userInfo.lastLoginTime) }}</span>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-card>
</div>
<el-row :gutter="20" class="!mt-4 resource-info">
<el-col :sm="12">
<el-card shadow="hover">
<div class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- 快捷入口卡片 -->
<div>
<el-card shadow="hover" class="h-105">
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('machine')">
<SvgIcon
class="mb-1 mr-1"
:size="28"
:name="TagResourceTypeEnum.Machine.extra.icon"
:color="TagResourceTypeEnum.Machine.extra.iconColor"
/>
<span class="">{{ state.machine.num }}</span>
</div>
</el-row>
<div class="flex justify-between items-center font-medium">
<span>{{ $t('home.quickAccess') }}</span>
</div>
</template>
<el-row>
<el-col :sm="24">
<el-table
:data="state.machine.opLogs"
:height="state.resourceOpTableHeight"
stripe
size="small"
:empty-text="$t('home.noOpRecord')"
>
<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-scrollbar :max-height="400">
<div class="p-3">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
<component v-for="comp of resourceComponents" :is="comp" @navigate="navigateToResource" />
</div>
</div>
</el-scrollbar>
</el-card>
</el-col>
</div>
<el-col :sm="12">
<el-card shadow="hover">
<!-- 最近操作记录 -->
<div>
<el-card shadow="hover" class="h-105">
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('db')">
<SvgIcon
class="mb-1 mr-1"
:size="28"
:name="TagResourceTypeEnum.DbInstance.extra.icon"
:color="TagResourceTypeEnum.DbInstance.extra.iconColor"
/>
<span class="">{{ state.db.num }}</span>
</div>
</el-row>
<div class="flex justify-between items-center font-medium">
<span>{{ $t('home.recentOperations') }}</span>
</div>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small" :empty-text="$t('home.noOpRecord')">
<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-table :data="state.recentOpLogs" :height="270" stripe size="small" :empty-text="$t('home.noOpRecord')">
<el-table-column prop="createTime" :label="$t('common.time')" show-overflow-tooltip width="140">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-row :gutter="20" class="!mt-4 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="mb-1 mr-1"
: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="$t('home.noOpRecord')">
<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-table-column prop="codePath" :label="$t('common.path')" min-width="150" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" width="60">
<template #default="scope">
<el-link @click="navigateToResource(scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</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="mb-1 mr-1"
: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="$t('home.noOpRecord')">
<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>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive } from 'vue';
// import * as echarts from 'echarts';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { getFileUrl, getUploadFileUrl } from '@/common/request';
import { formatAxis, formatDate } from '@/common/utils/format';
import { saveUser } from '@/common/utils/storage';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg } from '@/hooks/useI18n';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { useUserInfo } from '@/store/userInfo';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, onMounted, reactive } from 'vue';
import { useRouter } from 'vue-router';
import TagCodePath from '../ops/component/TagCodePath.vue';
import { resourceOpLogApi } from '../ops/tag/api';
import { personApi } from '../personal/api';
import { indexApi } from './api';
import { resourceComponents } from './resources';
const router = useRouter();
const { userInfo } = storeToRefs(useUserInfo());
@@ -248,28 +117,8 @@ const state = reactive({
roles: [],
},
msgs: [],
resourceOpTableHeight: 180,
defaultLogSize: 5,
machine: {
num: 0,
opLogs: [],
tagInfos: {},
},
db: {
num: 0,
opLogs: [],
tagInfos: {},
},
redis: {
num: 0,
opLogs: [],
tagInfos: {},
},
mongo: {
num: 0,
opLogs: [],
tagInfos: {},
},
defaultLogSize: 20,
recentOpLogs: [] as any[],
});
const roleInfo = computed(() => {
@@ -296,7 +145,7 @@ const getAccountInfo = async () => {
const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.size >= 512 * 1024) {
ElMessage.error('头像不能超过512KB!');
Msg.error('头像不能超过512KB!');
return false;
}
return true;
@@ -311,49 +160,20 @@ const handleAvatarSuccess = (response: any, uploadFile: any) => {
saveUser(newUser);
};
// 初始化数字滚动
// 初始化数
const initData = async () => {
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.Machine.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
state.machine.opLogs = res.list;
// 获取最近操作记录(不区分资源类型)
try {
const opLogsRes = await resourceOpLogApi.getAccountResourceOpLogs.request({
pageSize: state.defaultLogSize,
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.DbInstance.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
state.db.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
state.redis.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
state.mongo.opLogs = res.list;
});
indexApi.machineDashbord.request().then((res: any) => {
state.machine.num = res.machineNum;
});
indexApi.dbDashbord.request().then((res: any) => {
state.db.num = res.dbNum;
});
indexApi.redisDashbord.request().then((res: any) => {
state.redis.num = res.redisNum;
});
indexApi.mongoDashbord.request().then((res: any) => {
state.mongo.num = res.mongoNum;
});
state.recentOpLogs = opLogsRes.list || [];
} catch (error) {
console.error('Failed to load recent operation logs:', error);
}
};
// 快捷跳转
const toPage = (item: any, codePath = '') => {
let path;
useAutoOpenResource().setCodePath(codePath);
@@ -369,177 +189,11 @@ const toPage = (item: any, codePath = '') => {
router.push({ path });
};
// 资源导航
const navigateToResource = (codePath: string) => {
toPage('resource', codePath);
};
</script>
<style scoped lang="scss">
@use '@/theme/mixins/index.scss' as mixins;
.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 mixins.text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: gray;
@include mixins.text-ellipsis(1);
}
.personal-item-value {
@include mixins.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 mixins.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 {
overflow-x: hidden;
.home-card-item {
width: 100%;
height: 103px;
background: gray;
border-radius: 4px;
transition: all ease 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
transition: all ease 0.3s;
}
}
.home-card-item-box {
display: flex;
align-items: center;
position: relative;
overflow: hidden;
&: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;
}
.home-card-item-flex {
padding: 0 20px;
color: white;
.home-card-item-title,
.home-card-item-tip {
font-size: 13px;
}
.home-card-item-title-num {
font-size: 2vw;
}
.home-card-item-tip-num {
font-size: 13px;
}
}
}
}
</style>
<style scoped></style>

View File

@@ -0,0 +1,146 @@
<template>
<el-popover v-if="state.total > 0" placement="bottom" :width="650" trigger="hover" :show-after="300">
<template #reference>
<el-card shadow="hover" class="cursor-pointer transition-all hover:-translate-y-1" @click="onCardClick">
<div class="flex flex-col items-center">
<SvgIcon :size="32" :name="resourceIcon" :color="resourceColor" />
<div class="text-xs text-gray-600 text-center">{{ resourceLabel }}</div>
<div class="text-lg font-bold text-(--el-color-primary)">{{ state.total }}</div>
</div>
</el-card>
</template>
<!-- 资源悬浮展示 -->
<div class="resource-popover">
<div class="flex justify-between items-center mb-3 pb-2 border-b border-(--el-border-color-lighter) font-medium text-sm">
<span>{{ resourceLabel }}</span>
<span class="text-gray-500">({{ state.total }})</span>
</div>
<el-scrollbar ref="scrollbarRef" :max-height="180" @scroll="handleScroll">
<div v-loading="state.loading" class="min-w-0">
<div class="grid grid-cols-2 gap-2.5">
<div v-for="resource in state.resources" :key="resource.id" @click="navigateToResource(resource)">
<slot name="item" :resource="resource"></slot>
</div>
</div>
<!-- 加载更多提示 -->
<div v-if="state.hasMore" class="text-center py-3 text-xs text-gray-500">
<span class="italic">{{ $t('home.loadMore') }}</span>
</div>
<div v-else-if="state.resources.length > 0" class="text-center py-3 text-xs text-gray-400">
<span class="italic">{{ $t('home.loadedAll') }}</span>
</div>
</div>
</el-scrollbar>
</div>
</el-popover>
</template>
<script lang="ts" setup>
import SvgIcon from '@/components/svgIcon/index.vue';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { tagApi } from '@/views/ops/tag/api';
import type { ElScrollbar } from 'element-plus';
import { onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
const props = defineProps<{
resourceLabel: string;
resourceIcon: string;
resourceColor: string;
apiMethod: any;
}>();
const router = useRouter();
const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>();
// 内部状态
const state = reactive({
resources: [] as any[],
currentPage: 1,
pageSize: 6,
loading: false,
hasMore: true,
total: 0,
initialized: false,
});
onMounted(() => {
initLoad();
});
// 通用数据加载方法
const fetchData = async (pageNum: number) => {
try {
state.loading = true;
const res = await props.apiMethod.request({ pageSize: state.pageSize, pageNum });
return res;
} catch (error) {
console.error(`Failed to load ${props.resourceLabel}:`, error);
return { list: [], total: 0 };
} finally {
state.loading = false;
}
};
// 初始化加载数据
const initLoad = async () => {
if (state.initialized) return;
const res = await fetchData(1);
const items = res?.list || [];
state.total = res.total;
state.resources = items.slice(0, state.pageSize);
state.hasMore = state.resources.length < state.total;
state.initialized = true;
};
// 加载更多数据
const loadMore = async () => {
if (state.loading || !state.hasMore) return;
const nextPage = state.currentPage + 1;
const res = await fetchData(nextPage);
const newItems = res?.list || [];
state.resources = [...state.resources, ...newItems];
state.currentPage = nextPage;
state.hasMore = state.resources.length < res.total;
};
// 处理滚动
const handleScroll = () => {
if (!scrollbarRef.value) return;
const wrapRef = scrollbarRef.value.wrapRef;
if (!wrapRef) return;
// 滚动到底部时加载更多
if (wrapRef.scrollHeight - wrapRef.scrollTop - wrapRef.clientHeight < 20) {
loadMore();
}
};
// 跳转到资源
const navigateToResource = async (resource: any) => {
const tagResources = await tagApi.listByQuery.request({ codes: resource?.code });
useAutoOpenResource().setCodePath(tagResources?.[0]?.codePath);
router.push({ path: '/my-resource' });
};
// 点击卡片
const onCardClick = () => {
navigateToResource(state.resources?.[0]);
};
// 暴露方法给父组件
defineExpose({
initLoad,
state,
});
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,32 @@
<template>
<Base
:resource-label="$t('home.myDatabases')"
:resource-icon="ResourceTypeEnum.Db.extra.icon"
:resource-color="ResourceTypeEnum.Db.extra.iconColor"
:api-method="dbApi.dbs"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div class="flex flex-col gap-2 p-2.5 border border-[var(--el-border-color-lighter)] rounded-md bg-[var(--el-bg-color)] cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-[var(--el-text-color-primary)] flex-1 min-w-0">
<SvgIcon :name="getDbDialect(resource.type).getInfo().icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-[var(--el-text-color-secondary)]">
<div class="font-['Courier_New',monospace]">{{ resource.host || '-' }}:{{ resource.port || '-' }}</div>
<div v-if="resource.remark" class="overflow-hidden text-ellipsis whitespace-nowrap text-[11px]">{{ resource.remark }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { dbApi } from '@/views/ops/db/api';
import { getDbDialect } from '@/views/ops/db/dialect';
import Base from './Base.vue';
</script>

View File

@@ -0,0 +1,31 @@
<template>
<Base
:resource-label="$t('home.myContainers')"
:resource-icon="ResourceTypeEnum.Container.extra.icon"
:resource-color="ResourceTypeEnum.Container.extra.iconColor"
:api-method="dockerApi.page"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div class="flex flex-col gap-2 p-2.5 border border-[var(--el-border-color-lighter)] rounded-md bg-[var(--el-bg-color)] cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-[var(--el-text-color-primary)] flex-1 min-w-0">
<SvgIcon :name="ResourceTypeEnum.Container.extra.icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-[var(--el-text-color-secondary)]">
<div class="font-['Courier_New',monospace]">{{ resource.addr }}</div>
<div v-if="resource.remark" class="overflow-hidden text-ellipsis whitespace-nowrap text-[11px]">{{ resource.remark }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { dockerApi } from '@/views/ops/docker/api';
import Base from './Base.vue';
</script>

View File

@@ -0,0 +1,31 @@
<template>
<Base
:resource-label="$t('home.myEs')"
:resource-icon="ResourceTypeEnum.Es.extra.icon"
:resource-color="ResourceTypeEnum.Es.extra.iconColor"
:api-method="esApi.instances"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div class="flex flex-col gap-2 p-2.5 border border-[var(--el-border-color-lighter)] rounded-md bg-[var(--el-bg-color)] cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-[var(--el-text-color-primary)] flex-1 min-w-0">
<SvgIcon :name="ResourceTypeEnum.Es.extra.icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-[var(--el-text-color-secondary)]">
<div class="font-['Courier_New',monospace]">{{ resource.host || '-' }}:{{ resource.port || '-' }}</div>
<div v-if="resource.remark" class="overflow-hidden text-ellipsis whitespace-nowrap text-[11px]">{{ resource.remark }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { esApi } from '@/views/ops/es/api';
import Base from './Base.vue';
</script>

View File

@@ -0,0 +1,32 @@
<template>
<Base
:resource-label="$t('home.myKafka')"
:resource-icon="ResourceTypeEnum.MqKafka.extra.icon"
:resource-color="ResourceTypeEnum.MqKafka.extra.iconColor"
:api-method="mqApi.kafkaList"
ref="baseRef"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div class="flex flex-col gap-2 p-2.5 border border-[var(--el-border-color-lighter)] rounded-md bg-[var(--el-bg-color)] cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-[var(--el-text-color-primary)] flex-1 min-w-0">
<SvgIcon :name="ResourceTypeEnum.MqKafka.extra.icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-[var(--el-text-color-secondary)]">
<div class="font-['Courier_New',monospace]">{{ resource.hosts }}</div>
<div v-if="resource.remark" class="overflow-hidden text-ellipsis whitespace-nowrap text-[11px]">{{ resource.remark }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { mqApi } from '@/views/ops/mq/api';
import Base from './Base.vue';
</script>

View File

@@ -0,0 +1,152 @@
<template>
<Base
:resource-label="$t('home.myMachines')"
:resource-icon="ResourceTypeEnum.Machine.extra.icon"
:resource-color="ResourceTypeEnum.Machine.extra.iconColor"
:api-method="machineApi.list"
ref="baseRef"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div class="flex flex-col gap-2 p-2.5 border border-[var(--el-border-color-lighter)] rounded-md bg-[var(--el-bg-color)] cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-[var(--el-text-color-primary)] flex-1 min-w-0">
<SvgIcon :name="ResourceTypeEnum.Machine.extra.icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
<el-tag v-if="resource.stat" size="small" type="success">{{ $t('common.online') }}</el-tag>
<el-tag v-else size="small" type="danger">{{ $t('common.offline') }}</el-tag>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-[var(--el-text-color-secondary)]">
<div class="font-['Courier_New',monospace]">{{ resource.ip }}:{{ resource.port }}</div>
<div v-if="resource.stat" class="flex flex-col gap-1">
<div class="flex gap-2">
<span class="text-[11px] font-medium" :class="getCpuUsageClass(100 - resource.stat.cpuIdle) === 'stat-danger' ? 'text-[var(--el-color-danger)]' : getCpuUsageClass(100 - resource.stat.cpuIdle) === 'stat-warning' ? 'text-[var(--el-color-warning)]' : 'text-[var(--el-color-success)]'">
CPU: {{ (100 - resource.stat.cpuIdle).toFixed(0) }}%
</span>
</div>
<div class="flex gap-2">
<span
class="text-[11px] font-medium"
:class="getMemUsageClass(resource.stat.memTotal - resource.stat.memAvailable, resource.stat.memTotal) === 'stat-danger' ? 'text-[var(--el-color-danger)]' : getMemUsageClass(resource.stat.memTotal - resource.stat.memAvailable, resource.stat.memTotal) === 'stat-warning' ? 'text-[var(--el-color-warning)]' : 'text-[var(--el-color-success)]'"
>
MEM: {{ formatByteSize(resource.stat.memTotal - resource.stat.memAvailable) }} /
{{ formatByteSize(resource.stat.memTotal) }} ({{
(((resource.stat.memTotal - resource.stat.memAvailable) / resource.stat.memTotal) * 100).toFixed(0)
}}%)
</span>
</div>
<div v-if="resource.stat.fsInfos && resource.stat.fsInfos.length > 0" class="flex gap-2">
<span class="text-[11px] font-medium" :class="getDiskUsageClass(resource.stat.fsInfos) === 'stat-danger' ? 'text-[var(--el-color-danger)]' : getDiskUsageClass(resource.stat.fsInfos) === 'stat-warning' ? 'text-[var(--el-color-warning)]' : 'text-[var(--el-color-success)]'">
DISK: {{ getDiskUsed(resource.stat.fsInfos) }} / {{ getDiskTotal(resource.stat.fsInfos) }} ({{
getDiskUsage(resource.stat.fsInfos)
}})
</span>
</div>
<div v-else-if="resource.stat" class="flex gap-2">
<span class="text-[11px] text-[var(--el-text-color-secondary)]"> DISK: N/A </span>
</div>
</div>
<div v-else class="text-[11px] text-[var(--el-text-color-secondary)]">{{ $t('common.offline') }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { machineApi } from '@/views/ops/machine/api';
import Base from './Base.vue';
const formatByteSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
};
// 计算磁盘使用率
const getDiskUsage = (fSInfos: any[]) => {
if (!fSInfos || fSInfos.length === 0) return '0%';
let totalUsed = 0;
let totalFree = 0;
fSInfos.forEach((fs: any) => {
totalUsed += fs.used || 0;
totalFree += fs.free || 0;
});
const total = totalUsed + totalFree;
if (total === 0) return '0%';
const usage = (totalUsed / total) * 100;
return usage.toFixed(0) + '%';
};
// 获取CPU使用率颜色类
const getCpuUsageClass = (cpuUsage: number) => {
if (cpuUsage > 90) return 'stat-danger';
if (cpuUsage > 70) return 'stat-warning';
return 'stat-success';
};
// 获取内存使用率颜色类
const getMemUsageClass = (memUsed: number, memTotal: number) => {
if (memTotal === 0) return 'stat-success';
const usage = (memUsed / memTotal) * 100;
if (usage > 90) return 'stat-danger';
if (usage > 70) return 'stat-warning';
return 'stat-success';
};
// 获取磁盘使用率颜色类
const getDiskUsageClass = (fSInfos: any[]) => {
if (!fSInfos || fSInfos.length === 0) return 'stat-success';
let totalUsed = 0;
let totalFree = 0;
fSInfos.forEach((fs: any) => {
totalUsed += fs.used || 0;
totalFree += fs.free || 0;
});
const total = totalUsed + totalFree;
if (total === 0) return 'stat-success';
const usage = (totalUsed / total) * 100;
if (usage > 90) return 'stat-danger';
if (usage > 70) return 'stat-warning';
return 'stat-success';
};
// 计算磁盘已用空间
const getDiskUsed = (fSInfos: any[]) => {
if (!fSInfos || fSInfos.length === 0) return '0 B';
let totalUsed = 0;
fSInfos.forEach((fs: any) => {
totalUsed += fs.used || 0;
});
return formatByteSize(totalUsed);
};
// 计算磁盘总空间
const getDiskTotal = (fSInfos: any[]) => {
if (!fSInfos || fSInfos.length === 0) return '0 B';
let totalUsed = 0;
let totalFree = 0;
fSInfos.forEach((fs: any) => {
totalUsed += fs.used || 0;
totalFree += fs.free || 0;
});
return formatByteSize(totalUsed + totalFree);
};
</script>

View File

@@ -0,0 +1,33 @@
<template>
<Base
:resource-label="$t('home.myMilvus')"
:resource-icon="ResourceTypeEnum.Milvus.extra.icon"
:resource-color="ResourceTypeEnum.Milvus.extra.iconColor"
:api-method="milvusApi.list"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div
class="flex flex-col gap-2 p-2.5 border border-(--el-border-color-lighter) rounded-md bg-(--el-bg-color) cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]"
>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-(--el-text-color-primary) flex-1 min-w-0">
<SvgIcon :name="ResourceTypeEnum.Milvus.extra.icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-(--el-text-color-secondary)">
<div class="font-['Courier_New',monospace]">{{ resource.host }}</div>
<div v-if="resource.remark" class="overflow-hidden text-ellipsis whitespace-nowrap text-[11px]">{{ resource.remark }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { milvusApi } from '@/views/ops/milvus/api';
import Base from './Base.vue';
</script>

View File

@@ -0,0 +1,32 @@
<template>
<Base
:resource-label="$t('home.myMongo')"
:resource-icon="ResourceTypeEnum.Mongo.extra.icon"
:resource-color="ResourceTypeEnum.Mongo.extra.iconColor"
:api-method="mongoApi.mongoList"
ref="baseRef"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div class="flex flex-col gap-2 p-2.5 border border-[var(--el-border-color-lighter)] rounded-md bg-[var(--el-bg-color)] cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-[var(--el-text-color-primary)] flex-1 min-w-0">
<SvgIcon :name="ResourceTypeEnum.Mongo.extra.icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-[var(--el-text-color-secondary)]">
<div class="font-['Courier_New',monospace]">{{ resource.uri || '-' }}</div>
<div v-if="resource.remark" class="overflow-hidden text-ellipsis whitespace-nowrap text-[11px]">{{ resource.remark }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { mongoApi } from '@/views/ops/mongo/api';
import Base from './Base.vue';
</script>

View File

@@ -0,0 +1,31 @@
<template>
<Base
:resource-label="$t('home.myRedis')"
:resource-icon="ResourceTypeEnum.Redis.extra.icon"
:resource-color="ResourceTypeEnum.Redis.extra.iconColor"
:api-method="redisApi.redisList"
>
<!-- 自定义列表项 -->
<template #item="{ resource }">
<div class="flex flex-col gap-2 p-2.5 border border-[var(--el-border-color-lighter)] rounded-md bg-[var(--el-bg-color)] cursor-pointer transition-all duration-200 min-w-0 max-w-full box-border hover:border-[var(--el-color-primary-light-7)] hover:bg-[var(--el-fill-color-light)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)]">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 font-medium text-[13px] text-[var(--el-text-color-primary)] flex-1 min-w-0">
<SvgIcon :name="ResourceTypeEnum.Redis.extra.icon" :size="16" />
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{{ resource.name || resource.code }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5 text-[12px] text-[var(--el-text-color-secondary)]">
<div class="font-['Courier_New',monospace]">{{ resource.host || '-' }}:{{ resource.port || '-' }}</div>
<div v-if="resource.remark" class="overflow-hidden text-ellipsis whitespace-nowrap text-[11px]">{{ resource.remark }}</div>
</div>
</div>
</template>
</Base>
</template>
<script lang="ts" setup>
import { ResourceTypeEnum } from '@/common/commonEnum';
import SvgIcon from '@/components/svgIcon/index.vue';
import { redisApi } from '@/views/ops/redis/api';
import Base from './Base.vue';
</script>

View File

@@ -0,0 +1,10 @@
import Database from './Database.vue';
import Docker from './Docker.vue';
import ES from './ES.vue';
import Kafka from './Kafka.vue';
import Machine from './Machine.vue';
import Milvus from './Milvus.vue';
import Mongo from './Mongo.vue';
import Redis from './Redis.vue';
export const resourceComponents = [Machine, Database, Redis, Mongo, ES, Milvus, Docker, Kafka];

View File

@@ -132,25 +132,21 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref, toRefs, reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router';
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/crypto';
import { getAccountLoginSecurity, getLdapEnabled } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo';
import QrcodeVue from 'qrcode.vue';
import { personApi } from '@/views/personal/api';
import { getToken } from '@/common/utils/storage';
import { useThemeConfig } from '@/store/themeConfig';
import openApi from '@/common/openApi';
import { getFileUrl } from '@/common/request';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
const { t } = useI18n();
import { getAccountLoginSecurity, getLdapEnabled } from '@/common/sysconfig';
import { getRefreshToken, getToken, saveRefreshToken, saveToken, saveUser } from '@/common/utils/storage';
import { letterAvatar } from '@/common/utils/string';
import { Msg } from '@/hooks/useI18n';
import { initRouter } from '@/router/index';
import { useThemeConfig } from '@/store/themeConfig';
import { useUserInfo } from '@/store/userInfo';
import { personApi } from '@/views/personal/api';
import QrcodeVue from 'qrcode.vue';
import { nextTick, onMounted, reactive, ref, toRefs } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const rules = {
username: [Rules.requiredInput('common.username')],
@@ -409,7 +405,7 @@ const toIndex = async () => {
setTimeout(async () => {
// 关闭 loading
state.loading.signIn = true;
ElMessage.success(t('login.loginSuccessTip'));
Msg.success('login.loginSuccessTip');
// 水印设置用户信息
storesThemeConfig.setWatermarkUser();
}, 300);
@@ -429,7 +425,7 @@ const changePwd = async () => {
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
await openApi.changePwd(changePwdReq);
ElMessage.success(t('login.passwordChangeSuccessTip'));
Msg.success('login.passwordChangeSuccessTip');
state.loginForm.password = state.changePwdDialog.form.newPassword;
state.changePwdDialog.visible = false;
getCaptcha();

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<el-drawer :append-to-body="false" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -46,16 +46,16 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watchEffect, useTemplateRef, shallowReactive, computed } from 'vue';
import { channelApi } from '../api';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import EnumValue from '@/common/Enum';
import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { computed, reactive, shallowReactive, toRefs, useTemplateRef, watchEffect } from 'vue';
import { channelApi } from '../api';
import { ChannelStatusEnum, ChannelTypeEnum } from '../enums';
import EnumValue from '@/common/Enum';
import ChannelEmail from './ChannelEmail.vue';
import ChannelDing from './ChannelDing.vue';
import ChannelEmail from './ChannelEmail.vue';
const props = defineProps({
form: {
@@ -123,7 +123,7 @@ watchEffect(() => {
const btnOk = async () => {
await useI18nFormValidate(formRef);
await saveFormExec();
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('success', state.form);
//重置表单域
formRef.value.resetFields();

View File

@@ -26,12 +26,12 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import { onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { channelApi } from '../api';
import { ChannelStatusEnum, ChannelTypeEnum } from '../enums';
import ChannelEdit from './ChannelEdit.vue';
@@ -106,7 +106,7 @@ const editChannel = (data: any) => {
const deleteChannel = async () => {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.code).join('、'));
await channelApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<el-drawer :append-to-body="false" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -57,16 +57,16 @@
</template>
<script lang="ts" setup>
import { reactive, watchEffect, useTemplateRef, toRefs } from 'vue';
import { channelApi, tmplApi } from '../api';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import EnumValue from '@/common/Enum';
import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { ChannelStatusEnum, TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
import EnumValue from '@/common/Enum';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { reactive, toRefs, useTemplateRef, watchEffect } from 'vue';
import { channelApi, tmplApi } from '../api';
import { ChannelStatusEnum, ChannelTypeEnum, TmplStatusEnum, TmplTypeEnum } from '../enums';
const props = defineProps({
form: {
@@ -137,7 +137,7 @@ watchEffect(() => {
const btnOk = async () => {
await useI18nFormValidate(formRef);
await saveFormExec();
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('success', state.form);
//重置表单域
formRef.value.resetFields();

View File

@@ -57,17 +57,17 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { tmplApi } from '../api';
import { TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
import TmplEdit from './TmplEdit.vue';
import EnumValue from '@/common/Enum';
import { hasPerms } from '@/components/auth/auth';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
import { onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { tmplApi } from '../api';
import { ChannelTypeEnum, TmplStatusEnum, TmplTypeEnum } from '../enums';
import TmplEdit from './TmplEdit.vue';
const perms = {
saveTmpl: 'msg:tmpl:save',
@@ -155,7 +155,7 @@ const editTmpl = (data: any) => {
const deleteTmpl = async () => {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.code).join('、'));
await tmplApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
};
@@ -173,7 +173,7 @@ const sendMsg = async () => {
params: state.sendMsgDialog.params,
receiverIds: state.sendMsgDialog.receiverIds,
});
useI18nOperateSuccessMsg();
Msg.operateSuccess();
state.sendMsgDialog.visible = false;
};
</script>

View File

@@ -3,16 +3,13 @@
</template>
<script lang="ts" setup>
import { onMounted, toRaw, unref } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import openApi from '@/common/openApi';
import { useI18n } from 'vue-i18n';
import { Msg } from '@/hooks/useI18n';
import { onMounted, toRaw } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const { t } = useI18n();
onMounted(async () => {
try {
const queryParam = route.query;
@@ -29,7 +26,7 @@ onMounted(async () => {
}
const res: any = await openApi.oauth2Callback(queryParam);
ElMessage.success(t('system.oauth.authSuccess'));
Msg.success('system.oauth.authSuccess');
top?.opener.postMessage(toRaw(res), '*');
window.close();
} catch (e: any) {

View File

@@ -48,15 +48,12 @@
</template>
<script lang="ts" setup>
import { onMounted, reactive } from 'vue';
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
import { resourceAuthCertApi } from '../tag/api';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { Msg } from '@/hooks/useI18n';
import { onMounted, reactive } from 'vue';
import { resourceAuthCertApi } from '../tag/api';
import { AuthCertCiphertextTypeEnum, AuthCertTypeEnum } from '../tag/enums';
import ResourceAuthCertEdit from './ResourceAuthCertEdit.vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
resourceType: { type: Number },
@@ -127,7 +124,7 @@ const btnOk = async (authCert: any) => {
}
if (authCerts.value?.filter((x: any) => x.username == authCert.username).length > 0) {
ElMessage.error(t('ac.usernameExist'));
Msg.error('ac.usernameExist');
return;
}

View File

@@ -1,51 +1,6 @@
import { OptionsApi, SearchItem } from '@/components/pagetable/SearchForm';
import { ContextmenuItem } from '@/components/contextmenu';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { tagApi } from '../tag/api';
import { markRaw } from 'vue';
// 资源配置
export interface ResourceConfig {
order?: number;
resourceType: number; // 资源类型
rootNodeType: NodeType; // 资源根节点类型
// 资源管理组件配置
manager?: {
componentConf: ResourceComponentConfig; // 组件
countKey?: string; // 统计数keytab展示的数字对象key
permCode?: string; // 权限码
};
}
export interface ResourceComponentConfig {
name: string; // 名称
component?: any; // 组件
icon?: {
name: string;
color?: string;
};
}
export interface ResourceOpCtx {
/**
* 添加资源相关组件
* @param component 资源相关组件配置
* @returns 组件引用
*/
addResourceComponent(component: ResourceComponentConfig): Promise<any>;
/**
* 获取树节点
* @param nodeKey 节点key
*/
getTreeNode(nodeKey: string): any;
setCurrentTreeKey(nodeKey: string): void;
reloadTreeNode(nodeKey: string): void;
}
export class TagTreeNode {
/**
* 节点id
@@ -87,11 +42,6 @@ export class TagTreeNode {
// 节点组件
nodeComponent?: any;
/**
* 节点上下文
*/
ctx?: ResourceOpCtx;
static TagPath = -1;
constructor(key: any, label: string, type?: NodeType) {
@@ -101,7 +51,7 @@ export class TagTreeNode {
}
static new(parent: TagTreeNode, key: any, label: string, type?: NodeType) {
return new TagTreeNode(key, label, type).withContext(parent.ctx);
return new TagTreeNode(key, label, type);
}
withLabelRemark(labelRemark: any) {
@@ -134,14 +84,6 @@ export class TagTreeNode {
return this;
}
withContext(ctx: ResourceOpCtx | undefined | null) {
if (!ctx) {
return this;
}
this.ctx = ctx;
return this;
}
/**
* 加载子节点使用节点类型的loadNodesFunc去加载子节点
* @returns 子节点信息
@@ -223,24 +165,6 @@ export class NodeType {
}
}
/**
* 获取标签搜索项配置
* @param resourceType 资源类型
* @returns
*/
export function getTagPathSearchItem(resourceType: any) {
return SearchItem.select('tagPath', 'common.tag').withOptionsApi(
OptionsApi.new(tagApi.getResourceTagPaths, { resourceType }).withConvertFn((res: any) => {
return res.map((x: any) => {
return {
label: x,
value: x,
};
});
})
);
}
export function expandCodePath(codePath: string) {
const parts = codePath.split('/');
const result = [];

View File

@@ -1,6 +1,7 @@
<template>
<div class="db-list">
<el-drawer
:append-to-body="false"
:title="title"
v-model="dialogVisible"
@open="search"
@@ -158,7 +159,7 @@ import { hasPerms } from '@/components/auth/auth';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import { computed, defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import TagCodePath from '../component/TagCodePath.vue';
@@ -305,7 +306,7 @@ const editDb = (data: any) => {
const confirmEditDb = async (db: any) => {
db.instanceId = props.instance?.id;
await dbApi.saveDb.request(db);
useI18nSaveSuccessMsg();
Msg.saveSuccess();
search();
cancelEditDb();
};
@@ -321,7 +322,7 @@ const deleteDb = async () => {
for (let db of state.selectionData) {
await dbApi.deleteDb.request({ id: db.id });
}
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
} catch (err) {
//
} finally {

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -109,23 +109,19 @@
</template>
<script lang="ts" setup>
import { computed, reactive, toRefs, useTemplateRef, watchEffect } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { notBlankI18n } from '@/common/assert';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Rules } from '@/common/rule';
const { t } = useI18n();
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { computed, reactive, toRefs, useTemplateRef, watchEffect } from 'vue';
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import { dbApi } from './api';
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
const props = defineProps({
data: {
@@ -208,14 +204,14 @@ const testConn = async (authCert: any) => {
...submitForm.value,
authCerts: [authCert],
});
ElMessage.success(t('db.connSuccess'));
Msg.success('db.connSuccess');
};
const btnOk = async () => {
await useI18nFormValidate(dbFormRef);
notBlankI18n(submitForm.value.authCerts, 'db.acName');
await saveInstanceExec(submitForm.value);
useI18nSaveSuccessMsg();
Msg.saveSuccess();
state.form.id = saveInstanceRes as any;
emit('val-change', state.form);
cancel();

View File

@@ -76,18 +76,16 @@
</template>
<script lang="ts" setup>
import { TagResourceTypePath } from '@/common/commonEnum';
import { formatDate } from '@/common/utils/format';
import { hasPerms } from '@/components/auth/auth';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import SvgIcon from '@/components/svgIcon/index.vue';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
import { getTagPathSearchItem } from '../component/tag';
import TagCodePath from '../component/TagCodePath.vue';
import { dbApi } from './api';
import { getDbDialect } from './dialect';
@@ -110,7 +108,7 @@ const perms = {
saveDb: 'db:save',
};
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder'), getTagPathSearchItem(TagResourceTypePath.Db)];
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder')];
const columns = ref([
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(15),
@@ -207,7 +205,7 @@ const deleteInstance = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
} catch (err) {
//

View File

@@ -1,7 +1,6 @@
import Api from '@/common/Api';
import { AesEncrypt } from '@/common/crypto';
import { joinClientParams } from '@/common/request';
import { registerSqlExecAborter } from '@/components/sysmsg/db/db-sql-exec-progress';
import { createSqlExecNotification, registerSqlExecAborter } from '@/components/sysmsg/db/db-sql-exec-progress';
export const dbApi = {
// 获取权限列表
@@ -103,16 +102,18 @@ export function uploadSqlFile(
// 生成 uploadId
const uploadId = `sql_exec_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
const formData = new FormData();
formData.append('file', file);
formData.append('db', params.dbName);
formData.append('uploadId', uploadId);
// 构建查询参数对象
const queryParams: Record<string, string> = {
db: params.dbName,
uploadId: uploadId,
filename: file.name,
};
// 创建 Api 实例
const api = Api.newPost(`/dbs/${params.dbId}/exec-sql-file`);
// 使用 Api.upload 发起请求
const { abort } = api.upload(formData, {
// 使用 uploadRaw 直接传递文件流
const { abort } = api.uploadRaw(file, queryParams, {
onSuccess: () => {
options.onSuccess?.();
},
@@ -121,6 +122,18 @@ export function uploadSqlFile(
},
});
// 创建SQL执行进度通知
createSqlExecNotification(uploadId, {
id: uploadId,
title: file.name,
dbCode: '',
dbName: params.dbName,
executedStatements: 0,
terminated: false,
status: 'uploading',
clientId: '',
});
// 注册取消器在获取到abort方法后
registerSqlExecAborter(uploadId, abort);

View File

@@ -1,6 +1,6 @@
<template>
<div>
<div>
<div class="h-full flex flex-col">
<div class="flex-shrink-0">
<div class="card p-1! flex items-center justify-between">
<div>
<el-link @click="onRunSql()" underline="never" class="ml-3.5" icon="VideoPlay"> </el-link>
@@ -40,7 +40,7 @@
</div>
</div>
<el-splitter style="height: calc(100vh - 220px)" layout="vertical" @resize-end="onResizeTableHeight">
<el-splitter ref="splitterRef" class="flex-1 min-h-0" layout="vertical" @resize-end="onResizeTableHeight">
<el-splitter-panel :size="state.editorSize" max="80%">
<MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
</el-splitter-panel>
@@ -56,7 +56,14 @@
>
<el-tab-pane class="h-full!" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
<template #label>
<el-popover :show-after="1000" placement="top-start" :title="$t('db.execInfo')" trigger="hover" :width="300">
<el-popover
:show-after="1000"
placement="top-start"
:title="$t('db.execInfo')"
trigger="hover"
:width="300"
:teleported="false"
>
<template #reference>
<div>
<span>
@@ -130,26 +137,26 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
import { getToken } from '@/common/utils/storage';
import { notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
import config from '@/common/config';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getToken } from '@/common/utils/storage';
import { ElMessageBox } from 'element-plus';
import { format as sqlFormatter } from 'sql-formatter';
import { nextTick, onMounted, reactive, ref, toRefs, unref, useTemplateRef } from 'vue';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor } from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import DbTableData from '@/views/ops/db/component/table/DbTableData.vue';
import { DbInst } from '../../db';
import { dbApi, uploadSqlFile } from '../../api';
import { DbInst } from '../../db';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { joinClientParams } from '@/common/request';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { useI18n } from 'vue-i18n';
import { useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { Msg } from '@/hooks/useI18n';
import { useDebounceFn, useEventListener } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
const emits = defineEmits(['saveSqlSuccess']);
@@ -276,13 +283,17 @@ const onRemoveTab = (targetId: number) => {
}
};
const splitterRef = useTemplateRef<HTMLElement>('splitterRef');
const onResizeTableHeight = (index: number, sizes: number[]) => {
if (!sizes || sizes.length === 0) {
return;
}
const vh = window.innerHeight;
const plitpaneHeight = vh - 200;
// 基于splitter容器实际高度计算兼容全屏模式
const splitterEl = splitterRef.value?.$el || splitterRef.value;
const containerHeight = splitterEl ? (splitterEl as HTMLElement).getBoundingClientRect().height : window.innerHeight - 220;
const plitpaneHeight = containerHeight - 10;
let editorHeight = sizes[0];
if (editorHeight < 0 || editorHeight > plitpaneHeight - 43) {
@@ -290,7 +301,7 @@ const onResizeTableHeight = (index: number, sizes: number[]) => {
editorHeight = plitpaneHeight / 2;
}
let tableDataHeight = plitpaneHeight - editorHeight - 47;
let tableDataHeight = plitpaneHeight - editorHeight - 15;
state.editorSize = editorHeight;
state.tableDataHeight = tableDataHeight + 'px';
@@ -379,7 +390,7 @@ const runNonQuerySqls = async (sqls: string[], newTab: boolean) => {
i = state.execResTabs.findIndex((x) => x.id == state.activeTab);
execRes = state.execResTabs[i];
if (unref(execRes.loading)) {
ElMessage.error(t('db.currentSqlTabIsRunning'));
Msg.error('db.currentSqlTabIsRunning');
return;
}
id = execRes.id;
@@ -456,7 +467,7 @@ const runSql = async (sql: string, remark = '', newTab = false) => {
i = state.execResTabs.findIndex((x) => x.id == state.activeTab);
execRes = state.execResTabs[i];
if (unref(execRes.loading)) {
ElMessage.error(t('db.currentSqlTabIsRunning'));
Msg.error('db.currentSqlTabIsRunning');
return;
}
id = execRes.id;
@@ -699,7 +710,7 @@ const saveSql = async () => {
}
await dbApi.saveSql.request({ id: props.dbId, db: props.dbName, sql: sql, type: 1, name: sqlName });
useI18nSaveSuccessMsg();
Msg.saveSuccess();
// 保存sql脚本成功事件
emits('saveSqlSuccess', props.dbId, props.dbName);
};
@@ -729,7 +740,7 @@ const onFormatSql = () => {
*/
const onCommit = () => {
getNowDbInst().runSql(props.dbName, 'COMMIT;');
ElMessage.success('COMMIT success');
Msg.success('COMMIT success');
};
/**
@@ -769,7 +780,7 @@ const replaceSelection = (str: string, selection: any) => {
};
const beforeUpload = (file: File) => {
ElMessage.success(t('db.scriptFileUploadRunning', { filename: file.name }));
Msg.success('db.scriptFileUploadRunning', { filename: file.name });
};
// 自定义SQL文件上传处理
@@ -784,10 +795,10 @@ const handleSqlFileUpload = (options: any) => {
},
{
onSuccess: () => {
ElMessage.success(t('db.scriptFileUploadSuccess', { filename: file.name }));
Msg.success('db.scriptFileUploadSuccess', { filename: file.name });
},
onError: (error) => {
ElMessage.error(t('db.scriptFileUploadFailed', { filename: file.name, error: error.message }));
Msg.error('db.scriptFileUploadFailed', { filename: file.name, error: error.message });
},
}
);
@@ -798,7 +809,7 @@ const handleSqlFileUpload = (options: any) => {
// 执行sql成功
const execSqlFileSuccess = (res: any) => {
if (res.code !== 200) {
ElMessage.error(res.msg);
Msg.error(res.msg);
}
};
@@ -857,7 +868,7 @@ const initMonacoEditor = () => {
try {
await onRunSql();
} catch (e: any) {
e.message && ElMessage.error(e.message);
e.message && Msg.error(e.message);
}
},
});
@@ -885,7 +896,7 @@ const initMonacoEditor = () => {
try {
await onRunSql(true);
} catch (e: any) {
e.message && ElMessage.error(e.message);
e.message && Msg.error(e.message);
}
},
});
@@ -912,7 +923,7 @@ const initMonacoEditor = () => {
try {
await onFormatSql();
} catch (e: any) {
e.message && ElMessage.error(e.message);
e.message && Msg.error(e.message);
}
},
});

View File

@@ -15,16 +15,17 @@
</template>
<script lang="ts" setup>
import { toRefs, ref, reactive, onMounted } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
import { ElButton, ElDialog, ElInput, InputInstance } from 'element-plus';
import { onMounted, reactive, ref, toRefs } from 'vue';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { format as sqlFormatter } from 'sql-formatter';
import { SqlExecProps } from './SqlExecBox';
import { isTrue } from '@/common/assert';
import { Msg } from '@/hooks/useI18n';
import { i18n } from '@/i18n';
import { SqlExecProps } from './SqlExecBox';
const props = withDefaults(defineProps<SqlExecProps>(), {});
@@ -63,12 +64,12 @@ const runSql = async () => {
for (let re of res) {
if (re.errorMsg) {
isSuccess = false;
ElMessage.error(`${re.sql} ==>: ${re.errorMsg}`);
Msg.error(`${re.sql} ==>: ${re.errorMsg}`);
}
}
isTrue(isSuccess, 'exist run faild sql');
ElMessage.success(i18n.global.t('db.execSuccess'));
Msg.success('db.execSuccess');
} catch (e) {
runSuccess = false;
} finally {

View File

@@ -85,12 +85,13 @@
</template>
<script lang="ts" setup>
import { computed, nextTick, ref, Ref } from 'vue';
import { ElInput, ElMessage } from 'element-plus';
import { DataType } from '../../dialect/index';
import SvgIcon from '@/components/svgIcon/index.vue';
import MonacoEditorBox from '@/components/monaco/MonacoEditorBox';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg } from '@/hooks/useI18n';
import { ElInput } from 'element-plus';
import { computed, nextTick, ref, Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { DataType } from '../../dialect/index';
const { t } = useI18n();
@@ -146,7 +147,7 @@ const handleBlur = () => {
return;
}
if (props.dataType == DataType.Number && itemValue.value && !/^-?\d*\.?\d+$/.test(itemValue.value)) {
ElMessage.error(t('db.valueTypeNoMatch'));
Msg.error('db.valueTypeNoMatch');
return;
}
emit('update:modelValue', itemValue.value);

View File

@@ -1,176 +1,172 @@
<template>
<div class="db-table-data mt-1" :style="{ height: tableHeight }">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
ref="tableRef"
:header-height="showColumnTip && dbConfig.showColumnComment ? 48 : 30"
:row-height="30"
:row-class="rowClass"
:row-key="null"
:columns="state.columns"
:data="datas"
:width="width"
:height="height"
fixed
class="table"
:row-event-handlers="rowEventHandlers"
@scroll="onTableScroll"
>
<template #header="{ columns }">
<div v-for="(column, i) in columns" :key="i">
<div
:style="{
width: `${column.width}px`,
height: '100%',
textAlign: 'center',
borderRight: 'var(--el-table-border)',
borderTop: 'var(--el-table-border)',
}"
>
<!-- 行号列 -->
<div v-if="column.key == rowNoColumn.key" class="header-column-title">
<b class="el-text" tag="b"> {{ column.title }} </b>
</div>
<!-- 字段名列 -->
<div v-else style="position: relative" @mouseenter="showColumnAction(column)" @mouseleave="hideColumnAction">
<!-- 字段列的数据类型 -->
<div class="column-type">
<span v-if="column.dataTypeSubscript === 'icon-clock'">
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
</span>
<span class="text-[8px]!" v-else>{{ column.dataTypeSubscript }}</span>
</div>
<div v-if="showColumnTip">
<div class="header-column-title">
<b :title="column.remark" class="el-text cursor-pointer">
{{ column.title }}
</b>
</div>
<!-- 字段备注信息 -->
<div
v-if="dbConfig.showColumnComment"
style="color: var(--el-color-info-light-3)"
class="text-[10px]! el-text el-text--small is-truncated"
>
{{ column.columnComment }}
</div>
</div>
<div v-else class="header-column-title">
<b class="el-text"> {{ column.title }} </b>
</div>
<!-- 字段列右部分内容 -->
<div class="column-right">
<el-dropdown
@command="handleColumnCommand(column, $event)"
@visibleChange="onColumnActionVisibleChange(column, $event)"
trigger="click"
v-if="column.key !== rowNoColumn.key"
size="small"
placement="bottom-start"
>
<span class="column-actions-trigger">
<!-- 排序箭头图标 -->
<SvgIcon
v-if="
column.key == nowSortColumn?.key && !showColumnActions[column.key] && !columnActionVisible[column.key]
"
:color="'var(--el-color-primary)'"
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
:size="14"
/>
<!-- 更多操作图标 -->
<SvgIcon
v-if="columnActionVisible[column.key] || showColumnActions[column.key]"
name="MoreFilled"
:size="14"
:color="'var(--el-color-primary)'"
class="column-more-icon"
:class="{ 'column-more-icon-visible': columnActionVisible[column.key] || showColumnActions[column.key] }"
/>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="showColumnActionSort" command="sort-asc">
<SvgIcon name="top" class="mr-1" />
{{ $t('db.asc') }}
</el-dropdown-item>
<el-dropdown-item v-if="showColumnActionSort" command="sort-desc">
<SvgIcon name="bottom" class="mr-1" />
{{ $t('db.desc') }}
</el-dropdown-item>
<el-dropdown-item v-if="showColumnActionFixed && !column.fixed" command="fix">
<SvgIcon name="Paperclip" class="mr-1" />
{{ $t('db.fixed') }}
</el-dropdown-item>
<el-dropdown-item v-if="showColumnActionFixed && column.fixed" command="unfix">
<SvgIcon name="Minus" class="mr-1" />
{{ $t('db.cancelFiexd') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div ref="containerRef" class="db-table-data" :style="height ? { height } : {}">
<el-table-v2
ref="tableRef"
:header-height="showColumnTip && dbConfig.showColumnComment ? 48 : 30"
:row-height="30"
:row-class="rowClass"
:row-key="null"
:columns="state.columns"
:data="datas"
:width="state.containerWidth"
:height="state.containerHeight"
fixed
class="table"
:row-event-handlers="rowEventHandlers"
@scroll="onTableScroll"
>
<template #header="{ columns }">
<div v-for="(column, i) in columns" :key="i">
<div
:style="{
width: `${column.width}px`,
height: '100%',
textAlign: 'center',
borderRight: 'var(--el-table-border)',
borderTop: 'var(--el-table-border)',
}"
>
<!-- 行号列 -->
<div v-if="column.key == rowNoColumn.key" class="header-column-title">
<b class="el-text" tag="b"> {{ column.title }} </b>
</div>
</template>
<template #cell="{ rowData, column, rowIndex, columnIndex }">
<div @contextmenu="dataContextmenuClick($event, rowIndex, column, rowData)" class="table-data-cell">
<!-- 行号列 -->
<div v-if="column.key == rowNoColumn.key">
<b class="el-text el-text--small">
{{ (pageNum - 1) * pageSize + rowIndex + 1 }}
</b>
<!-- 字段名列 -->
<div v-else style="position: relative" @mouseenter="showColumnAction(column)" @mouseleave="hideColumnAction">
<!-- 字段列的数据类型 -->
<div class="column-type">
<span v-if="column.dataTypeSubscript === 'icon-clock'">
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
</span>
<span class="text-[8px]!" v-else>{{ column.dataTypeSubscript }}</span>
</div>
<!-- 数据列 -->
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
<div v-if="canEdit(rowIndex, columnIndex)">
<ColumnFormItem
v-model="rowData[column.key!]"
:data-type="column.dataType"
@blur="onExitEditMode(rowData, column, rowIndex)"
:column-name="column.columnName"
focus
/>
<div v-if="showColumnTip">
<div class="header-column-title">
<b :title="column.remark" class="el-text cursor-pointer">
{{ column.title }}
</b>
</div>
<div v-else :class="isUpdated(rowIndex, column.key) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
<span v-if="rowData[column.key!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<!-- 字段备注信息 -->
<div
v-if="dbConfig.showColumnComment"
style="color: var(--el-color-info-light-3)"
class="text-[10px]! el-text el-text--small is-truncated"
>
{{ column.columnComment }}
</div>
</div>
<span v-else :title="rowData[column.key!]" class="el-text el-text--small is-truncated">
{{ rowData[column.key!] }}
<div v-else class="header-column-title">
<b class="el-text"> {{ column.title }} </b>
</div>
<!-- 字段列右部分内容 -->
<div class="column-right">
<el-dropdown
@command="handleColumnCommand(column, $event)"
@visibleChange="onColumnActionVisibleChange(column, $event)"
trigger="click"
v-if="column.key !== rowNoColumn.key"
size="small"
placement="bottom-start"
>
<span class="column-actions-trigger">
<!-- 排序箭头图标 -->
<SvgIcon
v-if="
column.key == nowSortColumn?.key && !showColumnActions[column.key] && !columnActionVisible[column.key]
"
:color="'var(--el-color-primary)'"
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
:size="14"
/>
<!-- 更多操作图标 -->
<SvgIcon
v-if="columnActionVisible[column.key] || showColumnActions[column.key]"
name="MoreFilled"
:size="14"
:color="'var(--el-color-primary)'"
class="column-more-icon"
:class="{ 'column-more-icon-visible': columnActionVisible[column.key] || showColumnActions[column.key] }"
/>
</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="showColumnActionSort" command="sort-asc">
<SvgIcon name="top" class="mr-1" />
{{ $t('db.asc') }}
</el-dropdown-item>
<el-dropdown-item v-if="showColumnActionSort" command="sort-desc">
<SvgIcon name="bottom" class="mr-1" />
{{ $t('db.desc') }}
</el-dropdown-item>
<el-dropdown-item v-if="showColumnActionFixed && !column.fixed" command="fix">
<SvgIcon name="Paperclip" class="mr-1" />
{{ $t('db.fixed') }}
</el-dropdown-item>
<el-dropdown-item v-if="showColumnActionFixed && column.fixed" command="unfix">
<SvgIcon name="Minus" class="mr-1" />
{{ $t('db.cancelFiexd') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<template v-if="state.loading" #overlay>
<div class="el-loading-mask flex flex-col items-center justify-center">
<div>
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="28" />
<el-text class="ml-1" tag="b">{{ $t('db.execTime') }} - {{ state.execTime.toFixed(1) }}s</el-text>
</div>
<div v-if="loading && abortFn" class="mt-2!">
<el-button @click="cancelLoading" type="info" size="small" plain>{{ $t('common.cancel') }}</el-button>
</div>
</div>
</template>
<template #empty>
<el-empty class="text-center" :description="props.emptyText" :image-size="60" />
</template>
</el-table-v2>
</div>
</div>
</template>
</el-auto-resizer>
<template #cell="{ rowData, column, rowIndex, columnIndex }">
<div @contextmenu="dataContextmenuClick($event, rowIndex, column, rowData)" class="table-data-cell">
<!-- 行号列 -->
<div v-if="column.key == rowNoColumn.key">
<b class="el-text el-text--small">
{{ (pageNum - 1) * pageSize + rowIndex + 1 }}
</b>
</div>
<!-- 数据列 -->
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
<div v-if="canEdit(rowIndex, columnIndex)">
<ColumnFormItem
v-model="rowData[column.key!]"
:data-type="column.dataType"
@blur="onExitEditMode(rowData, column, rowIndex)"
:column-name="column.columnName"
focus
/>
</div>
<div v-else :class="isUpdated(rowIndex, column.key) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
<span v-if="rowData[column.key!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<span v-else :title="rowData[column.key!]" class="el-text el-text--small is-truncated">
{{ rowData[column.key!] }}
</span>
</div>
</div>
</div>
</template>
<template v-if="state.loading" #overlay>
<div class="el-loading-mask flex flex-col items-center justify-center">
<div>
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="28" />
<el-text class="ml-1" tag="b">{{ $t('db.execTime') }} - {{ state.execTime.toFixed(1) }}s</el-text>
</div>
<div v-if="loading && abortFn" class="mt-2!">
<el-button @click="cancelLoading" type="info" size="small" plain>{{ $t('common.cancel') }}</el-button>
</div>
</div>
</template>
<template #empty>
<el-empty class="text-center" :description="props.emptyText" :image-size="60" />
</template>
</el-table-v2>
<el-dialog @close="state.genTxtDialog.visible = false" v-model="state.genTxtDialog.visible" :title="state.genTxtDialog.title" width="1000px">
<template #header>
@@ -200,19 +196,20 @@
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref, computed } from 'vue';
import { ElInput, ElMessage } from 'element-plus';
import { copyToClipboard } from '@/common/utils/string';
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue';
import { exportCsv, exportExcel, exportFile } from '@/common/utils/export';
import { formatDate } from '@/common/utils/format';
import { copyToClipboard } from '@/common/utils/string';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue';
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
import { useIntervalFn, useStorage } from '@vueuse/core';
import { ElInput } from 'element-plus';
import { Ref, computed, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Msg } from '../../../../../hooks/useI18n';
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue';
import DbTableDataForm from './DbTableDataForm.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -258,7 +255,7 @@ const props = defineProps({
},
height: {
type: String,
default: '600px',
default: '',
},
pageSize: {
type: Number,
@@ -272,6 +269,8 @@ const props = defineProps({
const contextmenuRef = ref();
const tableRef = ref();
const containerRef = useTemplateRef<HTMLElement>('containerRef');
let resizeObserver: ResizeObserver | null = null;
// 用于控制列操作按钮的显示
const showColumnActions = ref({} as any);
@@ -369,6 +368,9 @@ let nowUpdateCell: Ref<NowUpdateCell> = ref(null) as any;
// 选中的数据, key->rowIndex value->primaryKeyValue
const selectionRowsMap = ref(new Map<number, any>());
// 最后一次点击的行索引,用于 shift 批量选择
let lastSelectedRowIndex: number | null = null;
// 更新单元格 key-> rowIndex value -> 更新行
const cellUpdateMap = ref(new Map<number, UpdatedRow>());
@@ -386,6 +388,8 @@ const state = reactive({
columns: [] as any,
loading: false,
tableHeight: '600px',
containerHeight: 600,
containerWidth: 800,
execTime: 0,
contextmenu: {
dropdown: {
@@ -406,7 +410,7 @@ const state = reactive({
},
});
const { tableHeight, datas } = toRefs(state);
const { containerHeight, containerWidth, datas } = toRefs(state);
const dbConfig = useStorage('dbConfig', DbThemeConfig);
@@ -486,6 +490,22 @@ onMounted(async () => {
state.tableHeight = props.height;
state.loading = props.loading;
// 使用 ResizeObserver 自动测量容器尺寸,确保 el-table-v2 固定表头 + body滚动
if (containerRef.value) {
const rect = containerRef.value.getBoundingClientRect();
state.containerHeight = rect.height || 600;
state.containerWidth = rect.width || 800;
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { height, width } = entry.contentRect;
if (height > 0) state.containerHeight = height;
if (width > 0) state.containerWidth = width;
}
});
resizeObserver.observe(containerRef.value);
}
state.dbId = props.dbId;
state.dbType = getNowDbInst().type;
dbDialect = getDbDialect(state.dbType);
@@ -501,12 +521,14 @@ onMounted(async () => {
onBeforeUnmount(() => {
endLoading();
resizeObserver?.disconnect();
});
const setTableData = (datas: any) => {
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
selectionRowsMap.value.clear();
cellUpdateMap.value.clear();
lastSelectedRowIndex = null;
// formatDataValues(datas);
state.datas = datas;
setTableColumns(props.columns);
@@ -632,7 +654,7 @@ const isSelection = (rowIndex: number): boolean => {
*/
const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
if (isMultiple) {
// 如果重复点击,则取消选中数据
// 如果重复点击,则取消选中数据
if (selectionRowsMap.value.get(rowIndex)) {
selectionRowsMap.value.delete(rowIndex);
return;
@@ -641,6 +663,21 @@ const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
selectionRowsMap.value.clear();
}
selectionRowsMap.value.set(rowIndex, rowData);
lastSelectedRowIndex = rowIndex;
};
/**
* Shift 批量选择:选中起始行到当前行之间的所有行
*/
const selectionRowRange = (startIndex: number, endIndex: number) => {
const from = Math.min(startIndex, endIndex);
const to = Math.max(startIndex, endIndex);
for (let i = from; i <= to; i++) {
const rowData = state.datas[i];
if (rowData) {
selectionRowsMap.value.set(i, rowData);
}
}
};
/**
@@ -651,11 +688,17 @@ const rowEventHandlers = {
const event = e.event;
const rowIndex = e.rowIndex;
const rowData = e.rowData;
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
// 按住ctrl/meta点击则多选切换
if (event.ctrlKey || event.metaKey) {
selectionRow(rowIndex, rowData, true);
return;
}
// 按住shift点击则批量选择起始行到当前行
if (event.shiftKey && lastSelectedRowIndex !== null) {
selectionRowsMap.value.clear();
selectionRowRange(lastSelectedRowIndex, rowIndex);
return;
}
selectionRow(rowIndex, rowData);
},
};
@@ -698,7 +741,7 @@ const onDeleteData = async () => {
const onEditRowData = () => {
const selectionDatas = Array.from(selectionRowsMap.value.values());
if (selectionDatas.length > 1) {
ElMessage.warning(t('db.onlySelectOneData'));
Msg.warning('db.onlySelectOneData');
return;
}
const data = selectionDatas[0];

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-row>
<div class="h-full flex flex-col gap-1">
<el-row class="flex-shrink-0">
<el-col :span="8">
<div class="mt-1">
<el-link :disabled="state.loading" @click="onRefresh()" icon="refresh" underline="never" class="ml-1"> </el-link>
@@ -12,6 +12,7 @@
width="auto"
:title="$t('db.tableFieldConf')"
trigger="click"
:teleported="false"
@hide="triggerCheckedColumns"
>
<div><el-input v-model="checkedShowColumns.searchKey" size="small" :placeholder="$t('db.columnFilterPlaceholder')" /></div>
@@ -45,16 +46,16 @@
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" underline="never"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" effect="dark" content="commit" placement="top">
<el-tooltip :show-after="500" effect="dark" content="commit" placement="top" :teleported="false">
<el-link @click="onCommit()" type="success" icon="CircleCheck" underline="never"> </el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.submitUpdate')" placement="top">
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.submitUpdate')" placement="top" :teleported="false">
<el-link @click="submitUpdateFields()" type="success" underline="never" class="!text-[12px]">{{ $t('common.submit') }}</el-link>
</el-tooltip>
<el-divider v-if="hasUpdatedFileds" direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.cancelUpdate')" placement="top">
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.cancelUpdate')" placement="top" :teleported="false">
<el-link @click="cancelUpdateFields" type="warning" underline="never" class="!text-[12px]">{{ $t('common.cancel') }}</el-link>
</el-tooltip>
</div>
@@ -74,6 +75,7 @@
highlight-first-item
value-key="columnName"
ref="condInputRef"
:teleported="false"
>
<template #suffix>
<SvgIcon @click="onSelectByCondition" name="search" />
@@ -95,7 +97,7 @@
</template>
<template #prepend>
<el-popover :visible="state.condPopVisible" trigger="click" :width="320" placement="right">
<el-popover :visible="state.condPopVisible" trigger="click" :width="320" placement="right" :teleported="false">
<template #reference>
<el-button @click.stop="chooseCondColumnName" style="color: var(--el-color-success)" text size="small">
{{ $t('db.selectColumn') }}
@@ -133,13 +135,13 @@
<db-table-data
ref="dbTableRef"
class="flex-1 min-h-0 overflow-hidden"
:db-id="dbId"
:db="dbName"
:data="datas"
:table="tableName"
:columns="columns"
:loading="loading"
:height="tableHeight"
:page-size="pageSize"
:page-num="pageNum"
:show-column-tip="true"
@@ -149,7 +151,7 @@
@data-delete="onRefresh"
></db-table-data>
<el-row type="flex" class="mt-2" :gutter="10" justify="space-between" style="user-select: none">
<el-row type="flex" class="flex-shrink-0" :gutter="10" justify="space-between" style="user-select: none">
<el-col :span="12">
<el-text
id="copyValue"
@@ -185,7 +187,7 @@
<el-link class="op-page" underline="never" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
<el-link class="op-page" underline="never" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
<div style="width: 90px" class="op-page ml-2">
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange" :teleported="false">
<el-option
style="font-size: 12px; height: 24px; line-height: 24px"
v-for="(op, i) in pageSizes"
@@ -206,7 +208,7 @@
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="500px">
<el-row gutter="5">
<el-col :span="5">
<el-select v-model="conditionDialog.condition">
<el-select v-model="conditionDialog.condition" :teleported="false">
<el-option label="=" value="="> </el-option>
<el-option label="LIKE" value="LIKE"> </el-option>
<el-option label=">" value=">"> </el-option>
@@ -247,16 +249,16 @@
<script lang="ts" setup>
import { computed, onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { DbInst } from '@/views/ops/db/db';
import DbTableData from './DbTableData.vue';
import { DbDialect } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import { useEventListener } from '@vueuse/core';
import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
import DbTableDataForm from './DbTableDataForm.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg } from '@/hooks/useI18n';
import { DbInst } from '@/views/ops/db/db';
import { DbDialect } from '@/views/ops/db/dialect';
import { useEventListener } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import DbTableData from './DbTableData.vue';
import DbTableDataForm from './DbTableDataForm.vue';
const { t } = useI18n();
@@ -273,10 +275,6 @@ const props = defineProps({
type: String,
required: true,
},
tableHeight: {
type: [String],
default: '600px',
},
});
const dbTableRef: Ref = ref(null);
@@ -325,7 +323,6 @@ const state = reactive({
title: '',
visible: false,
},
tableHeight: '600px',
hasUpdatedFileds: false,
dbDialect: {} as DbDialect,
@@ -340,20 +337,12 @@ const state = reactive({
const { datas, condition, loading, columns, checkedShowColumns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } =
toRefs(state);
watch(
() => props.tableHeight,
(newValue: any) => {
state.tableHeight = newValue;
}
);
const getNowDbInst = () => {
return DbInst.getInst(props.dbId);
};
onMounted(async () => {
console.log('in table data mounted');
state.tableHeight = props.tableHeight;
await onRefresh();
state.dbDialect = getNowDbInst().getDialect();
@@ -580,7 +569,7 @@ const onCancelCondition = () => {
*/
const onCommit = () => {
getNowDbInst().runSql(props.dbName, 'COMMIT;');
ElMessage.success('COMMIT success');
Msg.success('COMMIT success');
};
const onSelectByCondition = async () => {

View File

@@ -1,5 +1,5 @@
<template>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="75%">
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="75%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -128,13 +128,13 @@
</template>
<script lang="ts" setup>
import { computed, reactive, ref, toRefs, watch, useTemplateRef, nextTick, Ref } from 'vue';
import { ElMessage } from 'element-plus';
import SqlExecBox from '../sqleditor/SqlExecBox';
import { DbDialect, DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
import { DbInst } from '../../db';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { Msg } from '@/hooks/useI18n';
import { computed, nextTick, reactive, ref, Ref, toRefs, useTemplateRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { DbInst } from '../../db';
import { DbDialect, DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
import SqlExecBox from '../sqleditor/SqlExecBox';
const { t } = useI18n();
@@ -345,7 +345,7 @@ const deleteIndex = (index: any) => {
const submit = async () => {
let sql = genSql();
if (!sql) {
ElMessage.warning(t('db.noChange'));
Msg.warning('db.noChange');
return;
}
SqlExecBox({

View File

@@ -1,7 +1,7 @@
<template>
<div class="db-table">
<div class="db-table flex flex-col gap-1">
<el-row class="mb-1">
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right" :teleported="false">
<template #reference>
<el-button v-auth="'db:data:export'" :disabled="state.dumpInfo.tables?.length == 0" class="ml-1" type="success" size="small">
{{ $t('db.dump') }}
@@ -353,4 +353,11 @@ const onSubmitSql = async (row: { tableName: string }) => {
await getTables();
};
</script>
<style lang="scss"></style>
<style lang="scss">
.db-table {
> .el-table {
flex: 1;
min-height: 0;
}
}
</style>

View File

@@ -1,15 +1,15 @@
/* eslint-disable no-unused-vars */
import { dbApi } from './api';
import { getTextWidth } from '@/common/utils/string';
import SqlExecBox from './component/sqleditor/SqlExecBox';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor, languages, Position } from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { dbApi } from './api';
import SqlExecBox from './component/sqleditor/SqlExecBox';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
import { Msg } from '@/hooks/useI18n';
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
import { DbGetDbNamesMode } from './enums';
import { ElMessage } from 'element-plus';
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
@@ -232,7 +232,7 @@ export class DbInst {
});
for (let re of res) {
if (re.errorMsg) {
ElMessage.error(`${re.sql} -> 执行失败: ${re.errorMsg}`);
Msg.error(`${re.sql} -> 执行失败: ${re.errorMsg}`);
}
}
return res;
@@ -404,7 +404,7 @@ export class DbInst {
* @param inst 数据库实例,后端返回的列表接口中的信息
* @returns DbInst
*/
static async getOrNewInst(inst: any) {
static getOrNewInst(inst: any) {
if (!inst) {
throw new Error('inst不能为空');
}
@@ -427,7 +427,9 @@ export class DbInst {
dbInst.databases = inst.databases;
if (dbInst.databases?.[0]) {
dbInst.version = await dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] });
dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] }).then((version) => {
dbInst.version = version;
});
}
dbInstCache.set(dbInst.id, dbInst);

View File

@@ -1,5 +1,5 @@
<template>
<div class="db-sql-exec h-full">
<div class="db-sql-exec h-full flex flex-col">
<el-row>
<el-col :span="24" v-if="state.db">
<el-descriptions :column="4" size="small" border>
@@ -27,6 +27,7 @@
width="auto"
:title="$t('db.dbShowSetting')"
trigger="click"
:teleported="false"
>
<el-row>
<el-checkbox
@@ -79,18 +80,18 @@
</el-col>
</el-row>
<div id="data-exec" class="mt-1">
<div id="data-exec" ref="dataExecRef" class="mt-1 flex-1 min-h-0 overflow-visible">
<el-tabs
v-if="state.tabs.size > 0"
type="card"
@tab-remove="onRemoveTab"
@tab-change="onTabChange"
v-model="state.activeName"
class="h-full! w-full"
class="db-data-tabs w-full"
>
<el-tab-pane class="h-full!" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label>
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250" :teleported="false">
<template #reference>
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="text-[12px]!">{{ dt.label }}</span>
</template>
@@ -119,7 +120,6 @@
:db-id="dt.dbId"
:db-name="dt.db"
:table-name="dt.params.table"
:table-height="state.dataTabsTableHeight"
:ref="(el: any) => (dt.componentRef = el)"
></db-table-data-op>
@@ -165,23 +165,21 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, getCurrentInstance, h, inject, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from '../db';
import { ResourceOpCtx } from '@/views/ops/component/tag';
import { dbApi } from '../api';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { getDbDialect } from '../dialect/index';
import { useEventListener, useStorage } from '@vueuse/core';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
import { format as sqlFormatter } from 'sql-formatter';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
import { ResourceOpCtx, ResourceOpCtxKey } from '@/views/ops/resource/resourceOp';
import { useEventListener, useStorage } from '@vueuse/core';
import { ElCheckbox, ElMessageBox } from 'element-plus';
import { format as sqlFormatter } from 'sql-formatter';
import { defineAsyncComponent, h, inject, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
import { DbDataOpComp } from '@/views/ops/db/resource';
import { dbApi } from '../api';
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from '../db';
import { getDbDialect } from '../dialect/index';
const DbTableOp = defineAsyncComponent(() => import('../component/table/DbTableOp.vue'));
const DbSqlEditor = defineAsyncComponent(() => import('../component/sqleditor/DbSqlEditor.vue'));
@@ -192,6 +190,11 @@ const { t } = useI18n();
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
const props = defineProps<{
dbInfo: any;
db: string;
}>();
const emits = defineEmits(['init']);
const tabContextmenuRef: any = useTemplateRef('tabContextmenuRef');
@@ -227,7 +230,6 @@ const state = reactive({
dropdown: { x: 0, y: 0 },
items: tabContextmenuItems,
},
dataTabsTableHeight: '600px',
tablesOpHeight: '600',
dbServerInfo: {
loading: true,
@@ -256,28 +258,35 @@ const { nowDbInst, tableCreateDialog } = toRefs(state);
const dbConfig = useStorage('dbConfig', DbThemeConfig);
onMounted(() => {
changeDb(props.dbInfo, props.db);
state.reloadStatus = !dbConfig.value.cacheTable;
emits('init', { name: DbDataOpComp.name, ref: getCurrentInstance()?.exposed });
setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度
useEventListener(window, 'resize', setHeight);
});
const dataExecRef = useTemplateRef<HTMLElement>('dataExecRef');
onBeforeUnmount(() => {
dispposeCompletionItemProvider('sql');
});
/**
* 设置editor高度和数据表高度
* 设置editor高度和数据表高度(基于容器位置计算,兼容全屏模式)
*/
const setHeight = () => {
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
state.tablesOpHeight = window.innerHeight - 225 + 'px';
const el = document.getElementById('data-exec');
if (el) {
const rect = el.getBoundingClientRect();
state.tablesOpHeight = window.innerHeight - rect.top - 60 + 'px';
} else {
state.tablesOpHeight = window.innerHeight - 225 + 'px';
}
};
// 选择数据库,改变当前正在操作的数据库信息
const changeDb = async (db: any, dbName: string) => {
state.nowDbInst = await DbInst.getOrNewInst(db);
const changeDb = (db: any, dbName: string) => {
state.nowDbInst = DbInst.getOrNewInst(db);
state.nowDbInst.databases = db.databases;
state.db = dbName;
};
@@ -287,7 +296,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
if (tableName == '') {
return;
}
await changeDb(db, dbName);
changeDb(db, dbName);
const key = `tableData:${db.id}.${dbName}.${tableName}`;
let tab = state.tabs.get(key);
@@ -313,10 +322,10 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
// 新建查询tab
const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
if (!dbName || !db.id) {
ElMessage.warning(t('db.noDbInstMsg'));
Msg.warning('db.noDbInstMsg');
return;
}
await changeDb(db, dbName);
changeDb(db, dbName);
const dbId = db.id;
let label;
@@ -364,10 +373,10 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
const addTablesOpTab = async (db: any) => {
const dbName = db.db;
if (!db || !db.id) {
ElMessage.warning(t('db.noDbInstMsg'));
Msg.warning('db.noDbInstMsg');
return;
}
await changeDb(db, dbName);
changeDb(db, dbName);
const dbId = db.id;
let key = `tablesOp:${dbId}.${dbName}`;
@@ -470,7 +479,7 @@ const deleteSql = async (dbId: any, db: string, sqlName: string) => {
try {
await useI18nDeleteConfirm(sqlName);
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
reloadSqls(dbId, db);
} catch (err) {
//
@@ -523,11 +532,11 @@ const onDeleteTable = async (data: any) => {
for (let re of res) {
if (re.errorMsg) {
success = false;
ElMessage.error(`${re.sql} -> ${re.errorMsg}`);
Msg.error(`${re.sql} -> ${re.errorMsg}`);
}
}
if (success) {
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
setTimeout(() => {
parentKey && reloadNode(parentKey);
}, 1000);
@@ -558,7 +567,7 @@ const onRenameTable = async (data: any) => {
tableData.tableName = promptValue.value;
let sql = nowDbInst.value.getDialect().getModifyTableInfoSql(tableData);
if (!sql) {
ElMessage.warning(t('db.noChange'));
Msg.warning('db.noChange');
return;
}
@@ -599,7 +608,7 @@ const onCopyTable = async (data: any) => {
if (action === 'confirm') {
// 执行sql
dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
useI18nOperateSuccessMsg();
Msg.operateSuccess();
setTimeout(() => {
parentKey && reloadNode(parentKey);
}, 1000);
@@ -632,7 +641,7 @@ const getNowDbInfo = () => {
const loadTables = async (dbInfo: any) => {
if (!dbInfo || !dbInfo.id) {
ElMessage.warning(t('db.noDbInstMsg'));
Msg.warning('db.noDbInstMsg');
return;
}
let { id, db } = dbInfo;
@@ -642,7 +651,12 @@ const loadTables = async (dbInfo: any) => {
return tables;
};
const onRefresh = () => {
state.tabs.clear();
};
defineExpose({
onRefresh,
onChangeDb: changeDb,
loadTables,
loadTableData,
@@ -663,15 +677,29 @@ defineExpose({
<style lang="scss" scoped>
.db-sql-exec {
#data-exec {
::v-deep(.el-tabs) {
::v-deep(.db-data-tabs) {
height: 100%;
display: flex;
flex-direction: column;
--el-tabs-header-height: 30px;
}
::v-deep(.el-tabs__header) {
margin: 0 0 5px;
> .el-tabs__header {
margin: 0 0 5px;
flex-shrink: 0;
.el-tabs__item {
padding: 0 5px;
.el-tabs__item {
padding: 0 5px;
}
}
> .el-tabs__content {
flex: 1;
min-height: 0;
overflow: visible;
}
.el-tab-pane {
height: 100%;
}
}

View File

@@ -1,14 +1,16 @@
import { ContextmenuItem } from '@/components/contextmenu';
import { NodeType, TagTreeNode, ResourceConfig } from '../../component/tag';
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
import { defineAsyncComponent } from 'vue';
import { dbApi } from '../api';
import { sleep } from '@/common/utils/loading';
import { DbInst } from '../db';
import { schemaDbTypes } from '../dialect/index';
import { i18n } from '@/i18n';
import { formatByteSize } from '@/common/utils/format';
import { sleep } from '@/common/utils/loading';
import { i18n } from '@/i18n';
import type { ResourceConfig } from '@/views/ops/resource/resource';
import { defineAsyncComponent } from 'vue';
import { NodeType, TagTreeNode } from '../../component/tag';
import { createResourceOpTab } from '../../resource/resourceOp';
import { dbApi } from '../api';
import { DbInst } from '../db';
import { getDbDialect, schemaDbTypes } from '../dialect/index';
const DbInstList = defineAsyncComponent(() => import('../InstanceList.vue'));
const DbDataOp = defineAsyncComponent(() => import('./DbDataOp.vue'));
@@ -37,19 +39,14 @@ const SqlIcon = {
color: '#f56c6c',
};
export const DbDataOpComp = {
name: 'tag.dbDataOp',
component: DbDataOp,
icon: DbIcon,
};
// node节点点击时触发改变db事件
const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
const params = nodeData.params;
if (params.db) {
const compRef = await nodeData.ctx?.addResourceComponent(DbDataOpComp);
compRef.onChangeDb(
{
const getDbOpTab = async (params: any) => {
const tabKey = `${params.instCode}.${params.dbCode}.${params.db}`;
return await createResourceOpTab({
key: tabKey,
name: `${params.name}/${params.db}`,
component: DbDataOp,
componentProps: {
dbInfo: {
id: params.id,
host: `${params.host}`,
name: params.name,
@@ -57,18 +54,24 @@ const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
tagPath: params.tagPath,
databases: params.dbs,
},
params.db
);
}
db: params.db,
},
tabComponentProps: {
icon: { name: getDbDialect(params.type)?.getInfo().icon },
},
});
};
const getDbOpTabCompInst = async (params: any) => {
return (await getDbOpTab(params)).componentInstance;
};
const ContextmenuItemRefresh = new ContextmenuItem('refresh', 'common.refresh')
.withIcon('RefreshRight')
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).reloadNode(node.key));
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.reloadNode(node.key));
// 数据库实例节点类型
export const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
parentNode.ctx?.addResourceComponent(DbDataOpComp);
const tagPath = parentNode.params.tagPath;
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
@@ -110,7 +113,7 @@ export const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value)
x.username = authCerts[x.authCertName]?.username;
x.instCode = params.instCode;
x.dbCode = x.code;
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
return TagTreeNode.new(parentNode, `${parentNode.key}.${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
});
})
.withContextMenuItems([ContextmenuItemRefresh]);
@@ -141,37 +144,32 @@ export const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode
});
// 数据库节点
export const NodeTypeDb = new NodeType(223)
.withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
params.parentKey = parentNode.key;
// pg类数据库会多一层schema
if (schemaDbTypes.includes(params.type)) {
const { id, db } = params;
const schemaNames = await dbApi.pgSchemas.request({ id, db });
return schemaNames.map((sn: any) => {
// 将db变更为 db/schema;
const nParams = { ...params };
nParams.schema = sn;
nParams.db = nParams.db + '/' + sn;
nParams.dbs = schemaNames;
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
.withParams(nParams)
.withIcon(SchemaIcon);
});
}
export const NodeTypeDb = new NodeType(223).withContextMenuItems([ContextmenuItemRefresh]).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
params.parentKey = parentNode.key;
// pg类数据库会多一层schema
if (schemaDbTypes.includes(params.type)) {
const { id, db } = params;
const schemaNames = await dbApi.pgSchemas.request({ id, db });
return schemaNames.map((sn: any) => {
// 将db变更为 db/schema;
const nParams = { ...params };
nParams.schema = sn;
nParams.db = nParams.db + '/' + sn;
// nParams.dbs = schemaNames;
return TagTreeNode.new(parentNode, `${parentNode.key}/${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
});
}
return getNodeTypeTables(parentNode);
})
.withNodeClickFunc(nodeClickChangeDb);
return getNodeTypeTables(parentNode);
});
export const getNodeTypeTables = (parentNode: TagTreeNode) => {
const params = parentNode.params;
let tableKey = `${params.id}.${params.db}.table-menu`;
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
let tableKey = `${parentNode.key}.table-menu`;
let sqlKey = `${parentNode.key}.sql-menu`;
return [
TagTreeNode.new(parentNode, `${params.id}.${params.db}.table-menu`, i18n.global.t('db.table'), NodeTypeTableMenu)
TagTreeNode.new(parentNode, tableKey, i18n.global.t('db.table'), NodeTypeTableMenu)
.withParams({
...params,
key: tableKey,
@@ -185,25 +183,22 @@ export const getNodeTypeTables = (parentNode: TagTreeNode) => {
};
// postgres schema模式
export const NodeTypePostgresSchema = new NodeType(224)
.withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
params.parentKey = parentNode.key;
return getNodeTypeTables(parentNode);
})
.withNodeClickFunc(nodeClickChangeDb);
export const NodeTypePostgresSchema = new NodeType(224).withContextMenuItems([ContextmenuItemRefresh]).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
params.parentKey = parentNode.key;
return getNodeTypeTables(parentNode);
});
// 数据库表菜单节点
const NodeTypeTableMenu = new NodeType(4)
.withContextMenuItems([
ContextmenuItemRefresh,
new ContextmenuItem('createTable', 'db.createTable').withIcon('Plus').withOnClick(async (parentNode: TagTreeNode) => {
(await parentNode.ctx?.addResourceComponent(DbDataOpComp))?.onEditTable(parentNode);
(await getDbOpTabCompInst(parentNode.params))?.onEditTable(parentNode);
}),
new ContextmenuItem('tablesOp', 'db.tableOp').withIcon('Setting').withOnClick(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
(await parentNode.ctx?.addResourceComponent(DbDataOpComp)).addTablesOpTab({
(await getDbOpTabCompInst(params))?.addTablesOpTab({
id: params.id,
db: params.db,
type: params.type,
@@ -212,24 +207,19 @@ const NodeTypeTableMenu = new NodeType(4)
}),
])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
const params = parentNode.params;
const compRef = await getDbOpTabCompInst(params);
// // 获取当前库的所有表信息
const tables = await compRef.loadTables(params);
let { id, db, type, schema, version } = params;
let dbTableSize = 0;
const tablesNode = tables.map((x: any) => {
const tableSize = x.dataLength + x.indexLength;
dbTableSize += tableSize;
const key = `${id}.${db}.${x.tableName}`;
const key = `${parentNode.key}.${x.tableName}`;
return TagTreeNode.new(parentNode, key, x.tableName, NodeTypeTable)
.withIsLeaf(true)
.withParams({
id,
db,
type,
schema,
version,
...params,
key: key,
parentKey: parentNode.key,
tableName: x.tableName,
@@ -244,72 +234,59 @@ const NodeTypeTableMenu = new NodeType(4)
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
return tablesNode;
});
// .withNodeDblclickFunc((node: TagTreeNode) => {
// const params = node.params;
// addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
// });
// 数据库sql模板菜单节点
const NodeTypeSqlMenu = new NodeType(225)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
const id = params.id;
const db = params.db;
const dbs = params.dbs;
// 加载用户保存的sql脚本
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
return sqls.map((x: any) => {
return TagTreeNode.new(parentNode, `${id}.${db}.${x.name}`, x.name, NodeTypeSql)
.withIsLeaf(true)
.withParams({ id, db, dbs, sqlName: x.name })
.withIcon(SqlIcon);
});
})
.withNodeClickFunc(nodeClickChangeDb);
const NodeTypeSqlMenu = new NodeType(225).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
const id = params.id;
const db = params.db;
const dbs = params.dbs;
// 加载用户保存的sql脚本
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
return sqls.map((x: any) => {
return TagTreeNode.new(parentNode, `${parentNode.key}.${x.name}`, x.name, NodeTypeSql)
.withIsLeaf(true)
.withParams({ id, db, dbs, sqlName: x.name })
.withIcon(SqlIcon);
});
});
// 表节点类型
const NodeTypeTable = new NodeType(226)
.withContextMenuItems([
new ContextmenuItem('copyTable', 'db.copyTable')
.withIcon('copyDocument')
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onCopyTable(node)),
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onCopyTable(node)),
new ContextmenuItem('renameTable', 'db.renameTable')
.withIcon('edit')
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onRenameTable(node)),
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onRenameTable(node)),
new ContextmenuItem('editTable', 'db.editTable')
.withIcon('edit')
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onEditTable(node)),
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onEditTable(node)),
new ContextmenuItem('delTable', 'db.delTable')
.withIcon('Delete')
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onDeleteTable(node)),
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onDeleteTable(node)),
new ContextmenuItem('ddl', 'DDL')
.withIcon('Document')
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onGenDdl(node)),
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onGenDdl(node)),
])
.withNodeClickFunc(async (node: TagTreeNode) => {
const params = node.params;
(await node.ctx?.addResourceComponent(DbDataOpComp)).loadTableData({ id: params.id, nodeKey: node.key }, params.db, params.tableName);
(await getDbOpTabCompInst(node.params))?.loadTableData({ id: params.id, nodeKey: node.key }, params.db, params.tableName);
});
// sql模板节点类型
const NodeTypeSql = new NodeType(227)
.withNodeClickFunc(async (parentNode: TagTreeNode) => {
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
const params = parentNode.params;
compRef.addQueryTab({ id: params.id, nodeKey: parentNode.key, dbs: params.dbs }, params.db, params.sqlName);
(await getDbOpTabCompInst(params))?.addQueryTab({ id: params.id, nodeKey: parentNode.key, dbs: params.dbs }, params.db, params.sqlName);
})
.withContextMenuItems([
new ContextmenuItem('delSql', 'common.delete')
.withIcon('delete')
.withOnClick(async (node: TagTreeNode) =>
(await node.ctx?.addResourceComponent(DbDataOpComp)).deleteSql(node.params.id, node.params.db, node.params.sqlName)
),
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.deleteSql(node.params.id, node.params.db, node.params.sqlName)),
]);
const getSqlMenuNodeKey = (dbId: number, db: string) => {
return `${dbId}.${db}.sql-menu`;
};
export default {
order: 2,
resourceType: ResourceTypeEnum.Db.value,

View File

@@ -1,6 +1,6 @@
<template>
<div class="sync-task-edit">
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -210,22 +210,21 @@
</template>
<script lang="ts" setup>
import { computed, reactive, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/db/dialect';
import { Rules } from '@/common/rule';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
import { Rules } from '@/common/rule';
import { DbDataSyncDuplicateStrategyEnum } from '@/views/ops/db/sync/enums';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { dbApi } from '@/views/ops/db/api';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/db/dialect';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import { DbDataSyncDuplicateStrategyEnum } from '@/views/ops/db/sync/enums';
import { computed, reactive, ref, toRefs, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -396,7 +395,7 @@ watch(tabActiveName, async (newValue: string) => {
}
});
if (fields.size < (state.form.fieldMap?.length || 0)) {
ElMessage.warning(t('db.fieldMapError'));
Msg.warning('db.fieldMapError');
state.previewInsertSql = '';
return;
}
@@ -443,19 +442,19 @@ const loadDbTables = async (dbId: number, db: string) => {
const handleGetSrcFields = async () => {
// 执行sql获取字段信息
if (!state.form.dataSql || !state.form.dataSql.trim()) {
ElMessage.warning(t('db.noDataSqlMsg'));
Msg.warning('db.noDataSqlMsg');
return;
}
// 判断sql是否是查询语句
if (!/^select/i.test(state.form.dataSql.trim()!)) {
ElMessage.warning(t('db.notSelectSql'));
Msg.warning('db.notSelectSql');
return;
}
// 判断是否有多条sql
if (/;/i.test(state.form.dataSql!)) {
ElMessage.warning(t('db.notOneSql'));
Msg.warning('db.notOneSql');
return;
}
@@ -481,7 +480,7 @@ const handleGetSrcFields = async () => {
});
if (res.length && !res[0].columns) {
ElMessage.warning(t('db.notColumnSql'));
Msg.warning('db.notColumnSql');
return;
}
@@ -532,7 +531,7 @@ const btnOk = async () => {
const reqForm: any = { ...state.form };
reqForm.fieldMap = JSON.stringify(state.form.fieldMap);
await saveExec(reqForm);
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('val-change', state.form);
cancel();
};

View File

@@ -49,14 +49,14 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { Msg, useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from '@/views/ops/db/sync/enums';
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
@@ -144,14 +144,14 @@ const edit = async (data: any) => {
const run = async (id: any) => {
await useI18nConfirm('db.runConfirm');
await dbSyncApi.runDatasyncTask.request({ taskId: id });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
setTimeout(search, 1000);
};
const stop = async (id: any) => {
await useI18nConfirm('db.stopConfirm');
await dbSyncApi.stopDatasyncTask.request({ taskId: id });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
search();
};
@@ -164,7 +164,7 @@ const log = async (data: any) => {
const updStatus = async (id: any, status: 1 | -1) => {
try {
await dbSyncApi.updateDatasyncTaskStatus.request({ taskId: id, status });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
search();
} catch (err) {
//
@@ -175,7 +175,7 @@ const del = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
await dbSyncApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
} catch (err) {
//

View File

@@ -1,6 +1,6 @@
<template>
<div class="db-transfer-edit">
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -154,18 +154,17 @@
<script lang="ts" setup>
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
import { getDbDialect, getDbDialectMap } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
import { deepClone } from '@/common/utils/object';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { dbApi } from '@/views/ops/db/api';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getDbDialect, getDbDialectMap } from '@/views/ops/db/dialect';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -394,12 +393,12 @@ const btnOk = async () => {
}
if (!reqForm.checkedKeys) {
ElMessage.error(t('db.noTransferTableMsg'));
Msg.error('db.noTransferTableMsg');
return false;
}
await saveExec(reqForm);
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('val-change', state.form);
cancel();
};

View File

@@ -78,21 +78,20 @@
</template>
<script lang="ts" setup>
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
import { getDbDialect } from '@/views/ops/db/dialect';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { ElMessage } from 'element-plus';
import { hasPerms } from '@/components/auth/auth';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getClientId } from '@/common/utils/storage';
import FileInfo from '@/components/file/FileInfo.vue';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
import { getClientId } from '@/common/utils/storage';
import { hasPerms } from '@/components/auth/auth';
import FileInfo from '@/components/file/FileInfo.vue';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import { Msg, useI18nDeleteConfirm, useI18nFormValidate } from '@/hooks/useI18n';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getDbDialect } from '@/views/ops/db/dialect';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
import { DbTransferFileStatusEnum } from '@/views/ops/db/transfer/enums';
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -175,18 +174,18 @@ const state = reactive({
onConfirm: async function () {
await useI18nFormValidate(runFormRef);
if (state.runDialog.runForm.targetDbType !== state.runDialog.runForm.dbType) {
ElMessage.warning(t('db.targetDbTypeSelectError', { dbType: state.runDialog.runForm.dbType }));
Msg.warning('db.targetDbTypeSelectError', { dbType: state.runDialog.runForm.dbType });
return false;
}
state.runDialog.runForm.clientId = getClientId();
await dbTransferApi.dbTransferFileRun.request(state.runDialog.runForm);
useI18nOperateSuccessMsg();
Msg.operateSuccess();
state.runDialog.onCancel();
await search();
},
onSelectRunTargetDb: function (param: any) {
if (param.type !== state.runDialog.runForm.dbType) {
ElMessage.warning(t('db.targetDbTypeSelectError', { dbType: state.runDialog.runForm.dbType }));
Msg.warning('db.targetDbTypeSelectError', { dbType: state.runDialog.runForm.dbType });
}
},
},
@@ -205,7 +204,7 @@ const onDel = async function () {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
await dbTransferApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
await search();
} catch (err) {
//

View File

@@ -83,17 +83,17 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { getDbDialect } from '@/views/ops/db/dialect';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { Msg, useI18nConfirm, useI18nDeleteConfirm } from '@/hooks/useI18n';
import { getDbDialect } from '@/views/ops/db/dialect';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
import { DbTransferRunningStateEnum } from '@/views/ops/db/transfer/enums';
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
const DbTransferFile = defineAsyncComponent(() => import('./DbTransferFile.vue'));
@@ -190,7 +190,7 @@ const edit = async (data: any) => {
const stop = async (id: any) => {
await useI18nConfirm('db.stopConfirm');
await dbTransferApi.stopDbTransferTask.request({ taskId: id });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
search();
};
@@ -205,7 +205,7 @@ const onReRun = async (data: any) => {
await useI18nConfirm('db.runConfirm');
try {
let res = await dbTransferApi.runDbTransferTask.request({ taskId: data.id });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
// 拿到日志id之后弹出日志弹窗
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
} catch (e) {
@@ -226,7 +226,7 @@ const openFiles = async (data: any) => {
const updStatus = async (id: any, status: 1 | -1) => {
try {
await dbTransferApi.updateDbTransferTaskStatus.request({ taskId: id, status });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
search();
} catch (err) {
//
@@ -237,7 +237,7 @@ const del = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
await dbTransferApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
} catch (err) {
//

View File

@@ -58,16 +58,14 @@
</template>
<script lang="ts" setup>
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { formatDate } from '@/common/utils/format';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { useRoute } from 'vue-router';
import { getTagPathSearchItem } from '../component/tag';
import { dockerApi } from './api';
const ContainerConfEdit = defineAsyncComponent(() => import('./CotainerConfEdit.vue'));
@@ -82,10 +80,7 @@ const props = defineProps({
const route = useRoute();
const pageTableRef: Ref<any> = ref(null);
const searchItems = [
SearchItem.input('keyword', 'common.keyword').withPlaceholder('redis.keywordPlaceholder'),
getTagPathSearchItem(TagResourceTypeEnum.Container.value),
];
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('redis.keywordPlaceholder')];
const columns = ref([
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(15),
@@ -137,7 +132,7 @@ const deleteConf = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
await dockerApi.delConf.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
} catch (err) {
//

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="onCancel" />
</template>
@@ -30,16 +30,12 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, useTemplateRef } from 'vue';
import { dockerApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
const { t } = useI18n();
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { reactive, toRefs, useTemplateRef, watch } from 'vue';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { dockerApi } from './api';
const props = defineProps({
container: {
@@ -95,13 +91,13 @@ watch(dialogVisible, () => {
const onTestConn = async () => {
await useI18nFormValidate(formRef);
// await testConnExec();
ElMessage.success(t('ac.connSuccess'));
Msg.success('ac.connSuccess');
};
const onConfirm = async () => {
await useI18nFormValidate(formRef);
await saveConfExec();
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('val-change', state.form);
onCancel();
};

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer title="Docker" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="true" size="80%">
<el-drawer :append-to-body="false" title="Docker" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="true" size="80%">
<template #header>
<DrawerHeader :header="props.host" :back="cancel">
<template #extra>

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