Compare commits

...

3 Commits

Author SHA1 Message Date
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
175 changed files with 3953 additions and 3436 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

@@ -20,7 +20,6 @@
"@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",
@@ -38,7 +37,7 @@
"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",
@@ -69,7 +68,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

@@ -134,6 +134,57 @@ class Api<T = any, P = any> {
};
}
/**
* 原始文件流上传请求(直接使用文件流作为 body参数通过 URL query 传递)
* @param file 文件对象
* @param queryParams URL 查询参数字符串
* @param options 上传选项(可包含自定义 headers
* @returns { abort: () => void } 返回中止方法
*/
uploadRaw(file: File, queryParams: string, options: UploadOptions & { headers?: Record<string, string> } = {}): { abort: () => void } {
const { onSuccess, onError, headers = {} } = options;
const url = `${config.baseApiUrl}${this.url}?${queryParams}&${joinClientParams()}`;
// 创建 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

@@ -2,7 +2,7 @@ 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 { Msg } from '@/hooks/useI18n';
import axios from 'axios';
import JSONBig from 'json-bigint';
import { useApiFetch } from '../hooks/useRequest';
@@ -56,7 +56,7 @@ export const baseUrl: string = config.baseApiUrl;
*/
function notifyErrorMsg(msg: string) {
// 危险通知
ElMessage.error(msg);
Msg.error(msg);
}
// create an axios instance

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

@@ -50,13 +50,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 +118,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')
@@ -429,7 +440,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 +464,7 @@ const downloadSelectedFile = async (filePath: string) => {
path: fullPath,
});
} catch (error: any) {
Msg.error('components.terminal.downloadFailed', { error: error.message });
return;
}
@@ -461,9 +473,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 +525,7 @@ const uploadFilesToCurrentPath = async (files: FileList) => {
const file = files[0];
// 使用统一的 HTTP 上传方法
const { uploadId, abort } = uploadFile(
uploadFile(
file,
{
machineId: props.machineId as number,
@@ -526,18 +537,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 +555,7 @@ const uploadFolderToCurrentPath = async (files: FileList) => {
// 获取当前路径
const currentPath = await getCurrentPathOrDefault();
// 使用文件夹上传
const { uploadId, abort } = uploadFolder(
uploadFolder(
files,
{
machineId: props.machineId as number,
@@ -559,18 +566,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 +620,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

@@ -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',
@@ -289,6 +299,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

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

@@ -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}',
@@ -298,6 +308,11 @@ export default {
uploadToPath: '文件将上传到路径: {path}',
uploadPathTip: '提示: 文件将上传到用户家目录(~)。如需上传到其他目录,请先在终端中执行 cd 命令切换到目标目录,然后使用拖拽上传功能。',
// 粘贴相关
manualPaste: '手动粘贴',
pasteManualHint: '当前环境无法自动读取剪贴板内容,请手动粘贴内容到下方输入框:',
pasteHere: '请在此处粘贴内容...',
// 机器文件上传进度通知
machineFileUpload: {
uploadProgress: '机器文件上传进度',

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

@@ -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();
@@ -624,24 +622,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

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

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

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

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

@@ -111,16 +111,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 +229,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,244 +1,112 @@
<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 +116,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 +144,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 +159,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 +188,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

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

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

@@ -158,7 +158,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 +305,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 +321,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

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

@@ -83,7 +83,7 @@ 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';
@@ -207,7 +207,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,7 @@
import Api from '@/common/Api';
import { registerSqlExecAborter, createSqlExecNotification } from '@/components/sysmsg/db/db-sql-exec-progress';
import { AesEncrypt } from '@/common/crypto';
import { joinClientParams } from '@/common/request';
import { registerSqlExecAborter } from '@/components/sysmsg/db/db-sql-exec-progress';
export const dbApi = {
// 获取权限列表
@@ -103,16 +103,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);
// 使用 URLSearchParams 构建查询参数
const queryParams = new URLSearchParams({
db: params.dbName,
uploadId: uploadId,
filename: file.name,
}).toString();
// 创建 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 +123,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

@@ -130,26 +130,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 } 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']);
@@ -379,7 +379,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 +456,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 +699,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 +729,7 @@ const onFormatSql = () => {
*/
const onCommit = () => {
getNowDbInst().runSql(props.dbName, 'COMMIT;');
ElMessage.success('COMMIT success');
Msg.success('COMMIT success');
};
/**
@@ -769,7 +769,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 +784,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 +798,7 @@ const handleSqlFileUpload = (options: any) => {
// 执行sql成功
const execSqlFileSuccess = (res: any) => {
if (res.code !== 200) {
ElMessage.error(res.msg);
Msg.error(res.msg);
}
};
@@ -857,7 +857,7 @@ const initMonacoEditor = () => {
try {
await onRunSql();
} catch (e: any) {
e.message && ElMessage.error(e.message);
e.message && Msg.error(e.message);
}
},
});
@@ -885,7 +885,7 @@ const initMonacoEditor = () => {
try {
await onRunSql(true);
} catch (e: any) {
e.message && ElMessage.error(e.message);
e.message && Msg.error(e.message);
}
},
});
@@ -912,7 +912,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

@@ -200,19 +200,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, 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();
@@ -698,7 +699,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

@@ -247,16 +247,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();
@@ -580,7 +580,7 @@ const onCancelCondition = () => {
*/
const onCommit = () => {
getNowDbInst().runSql(props.dbName, 'COMMIT;');
ElMessage.success('COMMIT success');
Msg.success('COMMIT success');
};
const onSelectByCondition = async () => {

View File

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

View File

@@ -165,23 +165,23 @@
</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 { useI18n } from 'vue-i18n';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import { ResourceOpCtx } from '@/views/ops/component/tag';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
import { DbDataOpComp } from '@/views/ops/db/resource';
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
import { useEventListener, useStorage } from '@vueuse/core';
import { ElCheckbox, ElMessageBox } from 'element-plus';
import { format as sqlFormatter } from 'sql-formatter';
import { defineAsyncComponent, getCurrentInstance, h, inject, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
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'));
@@ -313,7 +313,7 @@ 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);
@@ -364,7 +364,7 @@ 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);
@@ -470,7 +470,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 +523,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 +558,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 +599,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 +632,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;

View File

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

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

@@ -63,7 +63,7 @@ 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';
@@ -137,7 +137,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

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

@@ -267,13 +267,13 @@
</el-drawer>
</template>
<script setup lang="ts">
import { useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { computed, reactive, toRefs, useTemplateRef, watch } from 'vue';
import { dockerApi } from '../api';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { Rules } from '@/common/rule';
import { formatByteSize } from '@/common/utils/format';
import { deepClone } from '@/common/utils/object';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { computed, reactive, toRefs, useTemplateRef, watch } from 'vue';
import { dockerApi } from '../api';
const rules = {
name: [Rules.requiredInput('common.name')],
@@ -420,7 +420,7 @@ const btnOk = async () => {
state.submitForm.cmd = cmds;
}
await createExec();
useI18nOperateSuccessMsg();
Msg.operateSuccess();
emit('success', submitForm);
cancel();
};

View File

@@ -166,17 +166,17 @@
</template>
<script lang="ts" setup>
import EnumValue from '@/common/Enum';
import { formatByteSize, formatDate } from '@/common/utils/format';
import { fuzzyMatchField } from '@/common/utils/string';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { useDataState } from '@/hooks/useDataState';
import { Msg, useI18nConfirm } from '@/hooks/useI18n';
import { computed, defineAsyncComponent, onMounted, reactive, toRefs, watch } from 'vue';
import { dockerApi, getDockerExecSocketUrl } from '../api';
import { formatByteSize, formatDate } from '@/common/utils/format';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { ContainerStateEnum } from '../enums';
import { fuzzyMatchField } from '@/common/utils/string';
import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { useI18nConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import SvgIcon from '@/components/svgIcon/index.vue';
import { useDataState } from '@/hooks/useDataState';
import EnumValue from '@/common/Enum';
const ContainerLog = defineAsyncComponent(() => import('./ContainerLog.vue'));
const ContainerCreate = defineAsyncComponent(() => import('./ContainerCreate.vue'));
@@ -293,21 +293,21 @@ const setContainersStats = () => {
const containerRestart = async (param: any) => {
await dockerApi.containerRestart.request({ id: props.id, containerId: param.containerId });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
getContainers();
};
const containerStop = async (param: any) => {
await useI18nConfirm('docker.stopContainerConfirm', { name: param.name });
await dockerApi.containerStop.request({ id: props.id, containerId: param.containerId });
useI18nOperateSuccessMsg();
Msg.operateSuccess();
getContainers();
};
const containerRemove = async (param: any) => {
await useI18nConfirm('docker.removeContainerConfirm', { name: param.name });
await dockerApi.containerRemove.request({ id: props.id, containerId: param.containerId });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
getContainers();
};

View File

@@ -84,19 +84,18 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, toRefs } from 'vue';
import { dockerApi, getDockerExecSocketUrl } from '../api';
import { formatByteSize, formatDate } from '@/common/utils/format';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { ImageStateEnum } from '../enums';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { fuzzyMatchField } from '@/common/utils/string';
import TerminalBody from '@/components/terminal/TerminalBody.vue';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
import { formatByteSize, formatDate } from '@/common/utils/format';
import { getToken } from '@/common/utils/storage';
import { ElMessage } from 'element-plus';
import { i18n } from '@/i18n';
import { fuzzyMatchField } from '@/common/utils/string';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { Msg } from '@/hooks/useI18n';
import { computed, onMounted, reactive, toRefs } from 'vue';
import { dockerApi, getDockerExecSocketUrl } from '../api';
import { ImageStateEnum } from '../enums';
const props = defineProps({
id: {
@@ -179,7 +178,7 @@ const uploadImage = (content: any) => {
timeout: 3 * 60 * 60 * 1000,
})
.then(() => {
ElMessage.success(i18n.global.t('machine.uploadSuccess'));
Msg.success('machine.uploadSuccess');
setTimeout(() => {
getImages();
}, 3000);
@@ -187,12 +186,12 @@ const uploadImage = (content: any) => {
.catch(() => {
// state.uploadProgressShow = false;
});
ElMessage.info(i18n.global.t('docker.imageUploading'));
Msg.info('docker.imageUploading');
};
const uploadSuccess = (res: any) => {
if (res.code !== 200) {
ElMessage.error(res.msg);
Msg.error(res.msg);
}
};

View File

@@ -73,8 +73,7 @@
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { ElMessage } from 'element-plus';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { reactive, toRefs, useTemplateRef, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
@@ -97,7 +96,7 @@ const props = defineProps({
const dialogVisible = defineModel<boolean>('visible', { default: false });
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const emit = defineEmits(['cancel', 'val-change']);
const rules = {
tagCodePaths: [Rules.requiredSelect('tag.relateTag')],
@@ -162,18 +161,18 @@ const onTestConn = async (authCert: any) => {
}
await testConnExec(submitForm);
state.form.version = testConnRes.value.version.number;
ElMessage.success(t('es.connSuccess'));
Msg.success('es.connSuccess');
};
const onConfirm = async () => {
if (!state.form.version) {
ElMessage.warning(t('es.shouldTestConn'));
Msg.warning('es.shouldTestConn');
return;
}
await useI18nFormValidate(dbFormRef);
await saveInstanceExec(getReqForm());
useI18nSaveSuccessMsg();
Msg.saveSuccess();
state.form.id = saveInstanceRes as any;
emit('val-change', state.form);
onCancel();

View File

@@ -70,7 +70,7 @@ 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 { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
import { getTagPathSearchItem } from '../component/tag';
@@ -183,7 +183,7 @@ const deleteInstance = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
await esApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
} catch (err) {
//

View File

@@ -44,11 +44,11 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, watch } from 'vue';
import { esApi } from '@/views/ops/es/api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { ElMessage } from 'element-plus';
import { Msg } from '@/hooks/useI18n';
import { esApi } from '@/views/ops/es/api';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -133,11 +133,11 @@ const confirm = async () => {
await formRef.value.validate();
loading.value = true;
if (!formData.value.idxName) {
ElMessage.warning(t('es.requireIndexName'));
Msg.warning('es.requireIndexName');
return;
}
await esApi.proxyReq('put', props.instId, `/${formData.value.idxName}`, JSON.parse(formData.value.mappings));
ElMessage.success(t('common.saveSuccess'));
Msg.saveSuccess();
emit('success');
loading.value = false;
visible.value = false;

View File

@@ -25,10 +25,10 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { defineAsyncComponent, ref, watch } from 'vue';
import { Msg } from '@/hooks/useI18n';
import { esApi } from '@/views/ops/es/api';
import { ElMessage } from 'element-plus';
import { defineAsyncComponent, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
@@ -110,7 +110,7 @@ const onSaveDoc = async () => {
try {
data = JSON.parse(doc);
} catch (error) {
ElMessage.error(t('es.docJsonError'));
Msg.error('es.docJsonError');
loading.value = false;
return;
}
@@ -125,8 +125,7 @@ const onSaveDoc = async () => {
}, 2000);
await esApi.proxyReq('post', model.value.instId, `/${model.value.idxName}/_doc/${_id.value}`, data);
ElMessage.success(t('common.saveSuccess'));
Msg.saveSuccess();
setTimeout(() => {
visible.value = false;

View File

@@ -62,11 +62,10 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { defineAsyncComponent, reactive, ref, watch } from 'vue';
import { Msg, useI18nDeleteConfirm } from '@/hooks/useI18n';
import { esApi } from '@/views/ops/es/api';
import { ElMessage } from 'element-plus';
import { useI18nDeleteConfirm } from '@/hooks/useI18n';
import { defineAsyncComponent, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
const { t } = useI18n();
@@ -114,7 +113,7 @@ const onOk = async () => {
}
await esApi.proxyReq('put', state.instId, `/${state.idxName}/_settings`, { index: settings });
ElMessage.success(t('common.saveSuccess'));
Msg.saveSuccess();
}
};
@@ -168,14 +167,14 @@ const onRemoveAlias = async (name: string) => {
await useI18nDeleteConfirm(`${t('es.aliases')}: ${name}`);
await esApi.proxyReq('delete', state.instId, `/${state.idxName}/_alias/${name}`);
ElMessage.success(t('common.deleteSuccess'));
Msg.deleteSuccess();
await refreshAlias();
};
const onSubmitAddAlias = async () => {
aliasLoading.value = true;
await esApi.proxyReq('put', state.instId, `/${state.idxName}/_alias/${state.aliasesForm.name}`);
ElMessage.success(t('common.saveSuccess'));
Msg.saveSuccess();
await refreshAlias();
dialogFormVisible.value = false;
aliasLoading.value = false;

View File

@@ -95,12 +95,12 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { esApi } from '@/views/ops/es/api';
import { nextTick, reactive, ref, unref, watch } from 'vue';
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg, useI18nConfirm, useI18nDeleteConfirm } from '@/hooks/useI18n';
import { esApi } from '@/views/ops/es/api';
import { nextTick, reactive, ref, unref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -261,7 +261,7 @@ const doAddTemplate = async () => {
},
};
await esApi.proxyReq('put', props.instId, `/${state.v.api}/${state.form.name}`, data);
useI18nOperateSuccessMsg();
Msg.operateSuccess();
setTimeout(async () => {
state.addVisible = false;
@@ -273,7 +273,7 @@ const onDelTemplate = async (name: any) => {
await useI18nDeleteConfirm(name);
await useI18nConfirm('es.deleteTemplateConfirm', { name: name });
await esApi.proxyReq('delete', props.instId, `/${state.v.api}/${name}`);
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
setTimeout(async () => {
await fetchTemplates();

View File

@@ -39,10 +39,10 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { Msg } from '@/hooks/useI18n';
import { esApi } from '@/views/ops/es/api';
import { ElMessage } from 'element-plus';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const visible = defineModel<boolean>('visible');
@@ -78,7 +78,7 @@ const doBasicReindex = async () => {
let res = await esApi.proxyReq('POST', props.instId, `/_reindex${wfc}`, data);
// FIXME 如果是异步返回异步任务id添加到任务列表中可以在任务列表中查看状态
ElMessage.success(t('common.operateSuccess'));
Msg.operateSuccess();
};
</script>

View File

@@ -239,21 +239,21 @@
</template>
<script lang="tsx" setup>
import { defineAsyncComponent, inject, reactive, ref, toRefs, getCurrentInstance, onMounted } from 'vue';
import { defineAsyncComponent, getCurrentInstance, inject, onMounted, reactive, ref, toRefs } from 'vue';
import { ContextmenuItem } from '@/components/contextmenu';
import { useI18n } from 'vue-i18n';
import SvgIcon from '@/components/svgIcon/index.vue';
import { copyToClipboard } from '@/common/utils/string';
import { ElCheckbox, ElMessage } from 'element-plus';
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useIntervalFn } from '@vueuse/core';
import Api from '@/common/Api';
import { esApi } from '@/views/ops/es/api';
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
import { formatDocSize } from '@/common/utils/format';
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
import { copyToClipboard } from '@/common/utils/string';
import { ContextmenuItem } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg, useI18nConfirm, useI18nDeleteConfirm } from '@/hooks/useI18n';
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
import { esApi } from '@/views/ops/es/api';
import { EsOpComp } from '@/views/ops/es/resource';
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
import { useIntervalFn } from '@vueuse/core';
import { ElCheckbox } from 'element-plus';
import { useI18n } from 'vue-i18n';
const EsAddIndex = defineAsyncComponent(() => import('../component/EsAddIndex.vue'));
const EsDashboard = defineAsyncComponent(() => import('../component/EsDashboard.vue'));
@@ -381,16 +381,16 @@ const onIdxCopyName = async (data: any) => {
};
const onRefreshIdx = async (data: any) => {
await esApi.proxyReq('post', data.params.instId, `/${data.params.idxName}/_refresh`);
useI18nOperateSuccessMsg();
Msg.operateSuccess();
};
const onClearIdxCache = async (data: any) => {
await useI18nConfirm('es.clearCacheConfirm', { name: data.params.idxName });
await esApi.proxyReq('post', data.params.instId, `/${data.params.idxName}/_cache/clear`);
useI18nOperateSuccessMsg();
Msg.operateSuccess();
};
const onFlushIdx = async (data: any) => {
await esApi.proxyReq('post', data.params.instId, `/${data.params.idxName}/_flush`);
useI18nOperateSuccessMsg();
Msg.operateSuccess();
};
const onIdxReindex = async (data: any) => {
await onReindex(data.params.instId, data.params.idxName);
@@ -399,18 +399,18 @@ const onIdxClose = async (data: any) => {
await useI18nConfirm('es.closeIndexConfirm', { name: data.params.idxName });
await esApi.proxyReq('post', data.params.instId, `/${data.params.idxName}/_close`);
data.params.idx.status = 'close';
useI18nOperateSuccessMsg();
Msg.operateSuccess();
};
const onIdxOpen = async (data: any) => {
await useI18nConfirm('es.openIndexConfirm', { name: data.params.idxName });
await esApi.proxyReq('post', data.params.instId, `/${data.params.idxName}/_open`);
data.params.idx.status = 'open';
useI18nOperateSuccessMsg();
Msg.operateSuccess();
};
const onIdxDelete = async (data: any) => {
await useI18nDeleteConfirm(data.params.idxName);
await esApi.proxyReq('delete', data.params.instId, data.params.idxName);
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
await onRefreshIndices(data.params.instId, data.params.parentKey);
};
const onIdxBaseSearch = async (data: any) => {
@@ -719,7 +719,7 @@ const onEditDoc = async (src: any) => {
const onEditSelectDoc = async (dt: any) => {
if (dt.selectKeys.length > 1 || dt.selectKeys.length == 0) {
ElMessage.warning(t('common.pleaseSelectOne'));
Msg.warning('common.pleaseSelectOne');
return;
}
await onEditDoc(dt.selectKeys[0].src);
@@ -748,7 +748,7 @@ const doDeleteDoc = async (ids: any[]) => {
await esApi.proxyReq('post', dataTab.instId, `/${dataTab.idxName}/_delete_by_query`, {
query: { terms: { _id: ids } },
});
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
await refreshIndex(); // 删除后刷新索引
setTimeout(async () => {
await fetchIndexData(); // 删除后刷新数据

View File

@@ -69,18 +69,17 @@
</template>
<script lang="ts" setup>
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { reactive, toRefs, useTemplateRef, watchEffect } from 'vue';
import { machineApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { useI18n } from 'vue-i18n';
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { machineApi } from './api';
import { MachineProtocolEnum } from './enums';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
const { t } = useI18n();
@@ -159,20 +158,20 @@ const onTestConn = async (authCert: any) => {
const submitForm = getReqForm();
submitForm.authCerts = [authCert];
await testConnExec(submitForm);
ElMessage.success(t('machine.connSuccess'));
Msg.success('machine.connSuccess');
};
const onConfirm = async () => {
await useI18nFormValidate(machineFormRef);
if (state.form.authCerts.length == 0) {
ElMessage.error(t('machine.noAcErrMsg'));
Msg.error('machine.noAcErrMsg');
return false;
}
const submitForm = getReqForm();
await saveMachineExec(submitForm);
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('val-change', submitForm);
onCancel();
};

View File

@@ -268,7 +268,7 @@ 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 { useI18nDeleteConfirm, useI18nDeleteSuccessMsg } from '@/hooks/useI18n';
import { Msg, useI18nDeleteConfirm } from '@/hooks/useI18n';
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
@@ -503,7 +503,7 @@ const deleteMachine = async () => {
try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
await machineApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
search();
} catch (err) {
//

View File

@@ -98,8 +98,8 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { Msg } from '@/hooks/useI18n';
import { reactive, toRefs, watch } from 'vue';
import { machineApi } from './api';
const props = defineProps({
@@ -178,7 +178,7 @@ const confirmKillProcess = async (pid: any) => {
pid,
id: state.params.id,
});
ElMessage.success('kill success');
Msg.success('kill success');
state.params.name = '';
getProcess();
};

View File

@@ -64,16 +64,16 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { machineApi } from './api';
import { ScriptResultEnum } from './enums';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { DynamicFormEdit } from '@/components/dynamic-form';
import SvgIcon from '@/components/svgIcon/index.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { DynamicFormEdit } from '@/components/dynamic-form';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Msg, useI18nFormValidate } from '@/hooks/useI18n';
import { reactive, ref, toRefs, watch } from 'vue';
import { machineApi } from './api';
import { ScriptResultEnum } from './enums';
const props = defineProps({
data: {
@@ -147,7 +147,7 @@ const onConfirm = async () => {
state.form.params = JSON.stringify(state.params);
}
machineApi.saveScript.request(state.form).then(() => {
useI18nSaveSuccessMsg();
Msg.saveSuccess();
emit('submitSuccess');
onCancel();
});

View File

@@ -72,9 +72,9 @@
draggable
append-to-body
>
<TerminalBody
ref="terminal"
:cmd="terminalDialog.cmd"
<TerminalBody
ref="terminal"
:cmd="terminalDialog.cmd"
:socket-url="getMachineTerminalSocketUrl(props.authCertName)"
:machine-id="props.machineId"
:auth-cert-name="props.authCertName"
@@ -95,23 +95,19 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, Ref, defineAsyncComponent } from 'vue';
import { ElMessage } from 'element-plus';
import { DynamicFormDialog } from '@/components/dynamic-form';
import { TableColumn } from '@/components/pagetable';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { OptionsApi } from '@/components/pagetable/SearchForm/index';
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
import { defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
import { getMachineTerminalSocketUrl, machineApi } from './api';
import { ScriptResultEnum, ScriptTypeEnum } from './enums';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { DynamicFormDialog } from '@/components/dynamic-form';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { useI18n } from 'vue-i18n';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
import { OptionsApi } from '@/components/pagetable/SearchForm/index';
const ScriptEdit = defineAsyncComponent(() => import('./ScriptEdit.vue'));
const TerminalBody = defineAsyncComponent(() => import('@/components/terminal/TerminalBody.vue'));
const { t } = useI18n();
const props = defineProps({
machineId: { type: Number },
authCertName: { type: String },
@@ -225,7 +221,7 @@ const run = async (script: any) => {
});
if (noResult) {
ElMessage.success(t('machine.execCompleted'));
Msg.success('machine.execCompleted');
return;
}
state.resultDialog.result = res;
@@ -283,7 +279,7 @@ const deleteRow = async (rows: any) => {
machineId: props.machineId,
scriptId: rows.map((x: any) => x.id).join(','),
});
useI18nDeleteSuccessMsg();
Msg.deleteSuccess();
getScripts();
};

View File

@@ -1,4 +1,6 @@
import Api, { UploadOptions } from '@/common/Api';
import { createUploadFileNotification, registerUploadFileAborter } from '@/components/sysmsg/machine/machine-file-upload-progress';
import { createUploadFolderNotification, registerUploadFolderAborter } from '@/components/sysmsg/machine/machine-folder-upload-progress';
export const machineApi = {
// 获取权限列表
@@ -97,6 +99,11 @@ export interface UploadParams {
path: string;
/** 文件名 */
filename: string;
/** 是否创建进度通知 */
createProgressNotify?: boolean;
/** 是否是文件夹上传的一部分 */
isFolderUpload?: boolean;
}
/**
@@ -110,16 +117,37 @@ export function uploadFile(file: File, params: UploadParams, options: UploadOpti
// 业务层生成 uploadId
const uploadId = params.uploadId || `upload_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
const formData = new FormData();
formData.append('file', file);
formData.append('uploadId', uploadId);
formData.append('machineId', String(params.machineId));
formData.append('authCertName', params.authCertName);
formData.append('protocol', String(params.protocol));
formData.append('fileId', String(params.fileId));
formData.append('path', params.path);
// 构建查询参数
const queryParams = new URLSearchParams({
machineId: String(params.machineId),
authCertName: params.authCertName,
protocol: String(params.protocol),
fileId: String(params.fileId),
path: params.path,
uploadId: uploadId,
filename: file.name,
});
const { abort } = machineApi.uploadFile.upload(formData, options);
// 如果是文件夹上传,添加标识参数
if (params.isFolderUpload) {
queryParams.set('isFolderUpload', 'true');
}
// 直接使用文件流作为 body不包装为 FormData
const { abort } = machineApi.uploadFile.uploadRaw(file, queryParams.toString(), {
...options,
});
if (params.createProgressNotify !== false) {
createUploadFileNotification(uploadId, {
authCertName: params.authCertName,
path: params.path,
filename: file.name,
});
// 注册取消方法
registerUploadFileAborter(uploadId, abort);
}
return { uploadId, abort };
}
@@ -143,39 +171,124 @@ export interface FolderUploadParams {
}
/**
* 上传文件夹(使用 /upload-folder 接口)
* @param files 文件列表
* @param params 上传参数
* @param options 上传选项
* @returns { uploadId: string; abort: () => void } 返回包含 uploadId 和中止方法的对象
* 上传文件夹(逐个文件流式上传,保持目录结构)
*/
export function uploadFolder(files: FileList | File[], params: FolderUploadParams, options: UploadOptions = {}): { uploadId: string; abort: () => void } {
// 业务层生成 uploadId
const uploadId = params.uploadId || `upload_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
const uploadId = params.uploadId || `folder_${params.fileId}_${Date.now()}`;
const fileArray = Array.from(files);
const totalFiles = fileArray.length;
const totalSize = fileArray.reduce((sum, file) => sum + file.size, 0);
let isAborted = false;
const abortControllers: (() => void)[] = []; // 存储所有正在进行的上传的取消方法
const formData = new FormData();
formData.append('uploadId', uploadId);
formData.append('basePath', params.path);
formData.append('machineId', String(params.machineId));
formData.append('authCertName', params.authCertName);
formData.append('protocol', String(params.protocol));
// 添加所有文件
const paths: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const filepaths: string[] = [];
// 创建上传任务
const uploadTasks = fileArray.map((file, index) => {
const relativePath = (file as any).webkitRelativePath || file.name;
formData.append('files', file);
paths.push(relativePath);
}
const fullPath = `${params.path}/${relativePath}`;
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'));
// 添加路径数组
paths.forEach((path) => {
formData.append('paths', path);
filepaths.push(fullPath);
return () =>
new Promise<void>((resolve, reject) => {
console.log(`[FolderUpload] 开始上传 ${index + 1}/${totalFiles}:`, fullPath);
const { abort } = uploadFile(
file,
{
uploadId,
machineId: params.machineId,
authCertName: params.authCertName,
protocol: params.protocol,
fileId: params.fileId,
path: dirPath,
filename: file.name,
isFolderUpload: true,
createProgressNotify: false,
},
{
onSuccess: () => {
console.log(`[FolderUpload] 上传成功 ${index + 1}/${totalFiles}:`, fullPath);
resolve();
},
onError: (error) => {
console.log(`[FolderUpload] 上传失败 ${index + 1}/${totalFiles}:`, fullPath, error.message);
if (error.name === 'AbortError' || isAborted) {
reject(error);
} else {
options.onError?.(error);
resolve();
}
},
}
);
// 将当前文件的取消方法添加到列表中
abortControllers.push(abort);
});
});
// 使用 Api.upload 发起请求
const { abort } = machineApi.uploadFolder.upload(formData, options);
// 初始化进度通知
const folderName = fileArray[0]?.webkitRelativePath?.split('/')[0] || 'folder';
createUploadFolderNotification(uploadId, {
authCertName: params.authCertName,
path: params.path,
folderName,
totalFiles,
totalSize,
uploadedSize: 0,
uploadedFiles: 0,
finishedFiles: 0,
status: 'uploading',
files: new Map(filepaths.map((filepath) => [filepath, { path: filepath, status: 'waiting', progress: 0, currentSize: 0, totalSize: 0, timestamp: 0 }])),
});
return { uploadId, abort };
// 并发执行
const executeUploads = async () => {
const maxConcurrent = 3;
const running = new Set<Promise<void>>();
let index = 0;
const launchNext = () => {
if (index >= uploadTasks.length || isAborted) return;
const task = uploadTasks[index++]();
running.add(task);
task.finally(() => {
running.delete(task);
if (!isAborted) launchNext();
});
};
// 启动初始并发
for (let i = 0; i < maxConcurrent && i < uploadTasks.length; i++) {
launchNext();
}
// 等待所有完成
while (running.size > 0) {
await Promise.race(running);
}
if (!isAborted) {
console.log('[FolderUpload] 全部完成');
options.onSuccess?.();
}
};
executeUploads();
const res = {
uploadId,
abort: () => {
isAborted = true;
// 取消所有正在进行的上传请求
abortControllers.forEach((abort) => abort());
},
};
registerUploadFolderAborter(uploadId, res.abort);
return res;
}

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