mirror of
https://gitee.com/dromara/mayfly-go
synced 2026-05-25 12:25:19 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
871e9b8fdd | ||
|
|
daccc638a7 | ||
|
|
c9daed9184 | ||
|
|
b0a68bf12c | ||
|
|
7f94fd168b | ||
|
|
9b7e569b3a |
14
Dockerfile
14
Dockerfile
@@ -9,18 +9,24 @@ ARG MAYFLY_GO_URL=https://gitee.com/dromara/mayfly-go/releases/download/${MAYFLY
|
||||
|
||||
RUN wget -cO mayfly-go.zip ${MAYFLY_GO_URL} && \
|
||||
unzip mayfly-go.zip && \
|
||||
mv ${MAYFLY_GO_DIR_NAME}/* /opt
|
||||
|
||||
mv ${MAYFLY_GO_DIR_NAME} /opt/mayfly-go && \
|
||||
rm -rf mayfly-go.zip
|
||||
|
||||
FROM ${BASEIMAGES}
|
||||
|
||||
ARG TZ=Asia/Shanghai
|
||||
ENV TZ=${TZ}
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
COPY --from=builder /opt/mayfly-go /usr/local/bin/mayfly-go
|
||||
|
||||
# 从 builder 阶段复制完整目录
|
||||
COPY --from=builder /opt/mayfly-go/bin/mayfly-go /usr/local/bin/mayfly-go
|
||||
|
||||
# 设置执行权限
|
||||
RUN chmod +x /usr/local/bin/mayfly-go
|
||||
|
||||
WORKDIR /mayfly-go
|
||||
|
||||
EXPOSE 18888
|
||||
|
||||
CMD ["mayfly-go"]
|
||||
CMD ["mayfly-go"]
|
||||
|
||||
21
README.md
21
README.md
@@ -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 | ✅ |
|
||||
|
||||
## 开发语言与主要框架
|
||||
|
||||
|
||||
21
README_EN.md
21
README_EN.md
@@ -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
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@logicflow/core": "^2.2.1",
|
||||
"@logicflow/extension": "^2.2.1",
|
||||
"@logicflow/core": "^2.2.3",
|
||||
"@logicflow/extension": "^2.2.3",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
@@ -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",
|
||||
@@ -33,18 +32,17 @@
|
||||
"monaco-sql-languages": "^1.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode.vue": "^3.9.0",
|
||||
"qrcode.vue": "^3.9.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"shiki": "^4.0.2",
|
||||
"shiki-stream": "^0.1.4",
|
||||
"sortablejs": "^1.15.7",
|
||||
"sql-formatter": "^15.7.3",
|
||||
"trzsz": "^1.1.6",
|
||||
"sql-formatter": "^15.8.0",
|
||||
"uuid": "^13.0.2",
|
||||
"vue": "3.6.0-beta.11",
|
||||
"vue-element-plus-x": "^2.0.2",
|
||||
"vue-i18n": "^11.4.2",
|
||||
"vue-router": "^5.0.6",
|
||||
"vue-element-plus-x": "^2.0.3",
|
||||
"vue-i18n": "^11.4.4",
|
||||
"vue-router": "^5.0.7",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"x-markdown-vue": "0.0.200",
|
||||
"xlsx": "^0.18.5"
|
||||
@@ -70,7 +68,7 @@
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.12",
|
||||
"vite": "^8.0.14",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^10.4.0"
|
||||
},
|
||||
|
||||
3
frontend/pnpm-workspace.yaml
Normal file
3
frontend/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
allowBuilds:
|
||||
'@logicflow/core': true
|
||||
'@parcel/watcher': true
|
||||
@@ -18,6 +18,9 @@
|
||||
<router-view v-if="!themeConfig.isWatermark" />
|
||||
|
||||
<Setings />
|
||||
|
||||
<!-- 全局系统通知悬浮按钮 -->
|
||||
<GlobalNotificationFab />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
@@ -31,6 +34,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import EnumValue from './common/Enum';
|
||||
import { I18nEnum } from './common/commonEnum';
|
||||
import { saveThemeConfig } from './common/utils/storage';
|
||||
import GlobalNotificationFab from '@/components/sysmsg/GlobalNotificationFab.vue';
|
||||
|
||||
const Setings = defineAsyncComponent(() => import('@/layout/navBars/breadcrumb/setings.vue'));
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import request from './request';
|
||||
import { RequestOptions, useApiFetch } from '@/hooks/useRequest';
|
||||
import config from './config';
|
||||
import request, { joinClientParams } from './request';
|
||||
import { getToken } from './utils/storage';
|
||||
|
||||
/**
|
||||
* 文件上传选项
|
||||
*/
|
||||
export interface UploadOptions {
|
||||
/** 成功回调 */
|
||||
onSuccess?: () => void;
|
||||
/** 错误回调 */
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可用于各模块定义各自api请求
|
||||
@@ -77,6 +89,102 @@ class Api<T = any, P = any> {
|
||||
return request.xhrReq(this.method, this.url, param, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传请求
|
||||
* @param formData FormData 对象(调用方自行构建,包含文件和其他参数)
|
||||
* @param options 上传选项
|
||||
* @returns { abort: () => void } 返回中止方法
|
||||
*/
|
||||
upload(formData: FormData, options: UploadOptions = {}): { abort: () => void } {
|
||||
const { onSuccess, onError } = options;
|
||||
|
||||
const url = `${config.baseApiUrl}${this.url}?${joinClientParams()}`;
|
||||
|
||||
// 创建 AbortController 用于取消请求
|
||||
const abortController = new AbortController();
|
||||
|
||||
// 发起 fetch 请求
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: abortController.signal,
|
||||
})
|
||||
.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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 原始文件流上传请求(直接使用文件流作为 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 静态方法 **/
|
||||
|
||||
/**
|
||||
@@ -119,6 +227,14 @@ class Api<T = any, P = any> {
|
||||
static newDelete<T = any, P = any>(url: string): Api<T, P> {
|
||||
return Api.create<T, P>(url, 'delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件上传 api
|
||||
* @param url url
|
||||
*/
|
||||
static newUpload<T = any, P = any>(url: string): Api<T, P> {
|
||||
return Api.create<T, P>(url, 'upload');
|
||||
}
|
||||
}
|
||||
|
||||
export default Api;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
|
||||
// 消息子类型
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
||||
import syssocket from './syssocket';
|
||||
import { h, reactive } from 'vue';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||
|
||||
export async function initSysMsgs() {
|
||||
await registerDbSqlExecProgress();
|
||||
}
|
||||
|
||||
const sqlExecNotifyMap: Map<string, any> = new Map();
|
||||
|
||||
async function registerDbSqlExecProgress() {
|
||||
await syssocket.registerMsgHandler('sqlScriptRunProgress', function (message: any) {
|
||||
const content = message.params;
|
||||
const id = content.id;
|
||||
let progress = sqlExecNotifyMap.get(id);
|
||||
if (content.terminated) {
|
||||
if (progress != undefined) {
|
||||
progress.notification?.close();
|
||||
sqlExecNotifyMap.delete(id);
|
||||
progress = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress == undefined) {
|
||||
progress = {
|
||||
props: reactive(buildProgressProps()),
|
||||
notification: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
progress.props.progress.title = content.title;
|
||||
progress.props.progress.executedStatements = content.executedStatements;
|
||||
if (!sqlExecNotifyMap.has(id)) {
|
||||
progress.notification = ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: h(ProgressNotify, progress.props),
|
||||
type: 'info',
|
||||
showClose: false,
|
||||
});
|
||||
sqlExecNotifyMap.set(id, progress);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,16 @@ import { MsgSubtypeEnum } from './commonEnum';
|
||||
import EnumValue from './Enum';
|
||||
import { h } from 'vue';
|
||||
import { MessageRenderer } from '@/components/message/message';
|
||||
import { initMachineSysMsgs } from '@/components/sysmsg/machine';
|
||||
import { initDbSysMsgs } from '@/components/sysmsg/db';
|
||||
|
||||
/**
|
||||
* 初始化全局系统消息
|
||||
*/
|
||||
export function initSysMsgs() {
|
||||
initMachineSysMsgs();
|
||||
initDbSysMsgs();
|
||||
}
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
@@ -18,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
|
||||
*/
|
||||
@@ -32,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();
|
||||
|
||||
16
frontend/src/common/utils/file.ts
Normal file
16
frontend/src/common/utils/file.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 下载文件
|
||||
* @param url 文件下载地址
|
||||
*/
|
||||
export function downloadFile(url: string) {
|
||||
// 使用隐藏的 iframe 下载,避免页面闪烁
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = url;
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
// 1秒后移除 iframe
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe);
|
||||
}, 1000);
|
||||
}
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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字符串
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export const buildProgressProps = (): any => {
|
||||
return {
|
||||
progress: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
executedStatements: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<el-descriptions border size="small" :title="`${props.progress.title}`">
|
||||
<el-descriptions-item label="时间">{{ state.elapsedTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已处理">{{ progress.executedStatements }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, reactive } from 'vue';
|
||||
import { formatTime } from 'element-plus/es/components/countdown/src/utils';
|
||||
import { buildProgressProps } from './progress-notify';
|
||||
|
||||
const props = defineProps(buildProgressProps());
|
||||
|
||||
const state = reactive({
|
||||
elapsedTime: '00:00:00',
|
||||
});
|
||||
|
||||
let timer: any = undefined;
|
||||
const startTime = Date.now();
|
||||
|
||||
onMounted(async () => {
|
||||
timer = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
state.elapsedTime = formatTime(elapsed, 'HH:mm:ss');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
if (timer != undefined) {
|
||||
clearInterval(timer); // 在Vue实例销毁前,清除我们的定时器
|
||||
timer = undefined;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
211
frontend/src/components/sysmsg/GlobalNotificationFab.vue
Normal file
211
frontend/src/components/sysmsg/GlobalNotificationFab.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="globalNotificationState.activeCount > 0"
|
||||
class="fixed z-[2000]"
|
||||
:style="{ bottom: position.bottom + 'px', right: position.right + 'px' }"
|
||||
>
|
||||
<el-badge
|
||||
:value="globalNotificationState.activeCount"
|
||||
:max="99"
|
||||
class="cursor-move"
|
||||
@mousedown="startDrag"
|
||||
>
|
||||
<el-button
|
||||
circle
|
||||
type="primary"
|
||||
class="w-[50px] h-[50px] text-xl shadow-lg transition-all duration-300"
|
||||
:class="{ 'hover:scale-110 hover:shadow-xl': !isDragging }"
|
||||
@click="toggleNotificationPanel"
|
||||
>
|
||||
<SvgIcon name="Bell" />
|
||||
</el-button>
|
||||
</el-badge>
|
||||
|
||||
<!-- 展开的通知面板 -->
|
||||
<Transition name="slide-fade">
|
||||
<div
|
||||
v-if="isPanelVisible"
|
||||
class="absolute bottom-[60px] right-0 w-[420px] max-h-[500px] bg-white dark:bg-gray-800 rounded-lg shadow-2xl overflow-hidden z-[2001]"
|
||||
>
|
||||
<div class="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="m-0 text-base font-semibold text-gray-800 dark:text-gray-200">{{ $t('components.sysmsg.notifications.title') }}</h3>
|
||||
<el-button size="small" text @click="isPanelVisible = false">
|
||||
<SvgIcon name="Close" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-scrollbar max-height="400px">
|
||||
<div class="p-4">
|
||||
<!-- 直接展示所有通知 -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="task in allTasks" :key="task.id" class="p-2 bg-gray-50 dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-700">
|
||||
<!-- 显示通知标题 -->
|
||||
<div class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">{{ translateTitle(task.options.title) }}</div>
|
||||
<!-- 直接渲染原有组件 -->
|
||||
<component :is="task.component" v-bind="task.componentProps" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="globalNotificationState.activeCount === 0" :description="$t('common.noData')" :image-size="80" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { activeNotifications, globalNotificationState } from './global-notification-manager';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isPanelVisible = ref(false);
|
||||
|
||||
// 拖拽相关
|
||||
const STORAGE_KEY = 'global-notification-fab-position';
|
||||
const position = ref({ bottom: 20, right: 20 }); // 默认位置(对应 bottom-5 right-5)
|
||||
const isDragging = ref(false);
|
||||
const dragStart = ref({ x: 0, y: 0, initialBottom: 0, initialRight: 0 });
|
||||
const hasMoved = ref(false); // 标记是否发生了移动
|
||||
|
||||
const startDrag = (event: MouseEvent) => {
|
||||
// 只在左键拖拽时生效
|
||||
if (event.button !== 0) return;
|
||||
|
||||
isDragging.value = true;
|
||||
hasMoved.value = false;
|
||||
dragStart.value = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
initialBottom: position.value.bottom,
|
||||
initialRight: position.value.right,
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onDrag);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
|
||||
// 防止拖拽时选中文本
|
||||
document.body.style.userSelect = 'none';
|
||||
};
|
||||
|
||||
const onDrag = (event: MouseEvent) => {
|
||||
if (!isDragging.value) return;
|
||||
|
||||
const deltaY = event.clientY - dragStart.value.y;
|
||||
const deltaX = event.clientX - dragStart.value.x;
|
||||
|
||||
// 如果移动距离超过 3px,认为是拖拽而不是点击
|
||||
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
|
||||
hasMoved.value = true;
|
||||
}
|
||||
|
||||
// 更新位置(注意:鼠标向下移动时 bottom 应该减小)
|
||||
position.value.bottom = dragStart.value.initialBottom - deltaY;
|
||||
position.value.right = dragStart.value.initialRight - deltaX;
|
||||
|
||||
// 获取窗口尺寸用于边界限制
|
||||
const windowHeight = window.innerHeight;
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
// 确保不会移出屏幕(留出至少 50px 保证按钮可见)
|
||||
if (position.value.bottom < 0) position.value.bottom = 0;
|
||||
if (position.value.right < 0) position.value.right = 0;
|
||||
if (position.value.bottom > windowHeight - 50) position.value.bottom = windowHeight - 50;
|
||||
if (position.value.right > windowWidth - 50) position.value.right = windowWidth - 50;
|
||||
|
||||
// 如果发生了移动,阻止默认行为
|
||||
if (hasMoved.value) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging.value = false;
|
||||
document.removeEventListener('mousemove', onDrag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
|
||||
// 恢复文本选择
|
||||
document.body.style.userSelect = '';
|
||||
|
||||
// 保存位置到 localStorage
|
||||
savePosition();
|
||||
};
|
||||
|
||||
// 组件卸载时清理事件监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', onDrag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
});
|
||||
|
||||
// 保存位置到 localStorage
|
||||
const savePosition = () => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(position.value));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save notification fab position:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从 localStorage 加载位置
|
||||
const loadPosition = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
// 验证数据有效性
|
||||
if (typeof parsed.bottom === 'number' && typeof parsed.right === 'number') {
|
||||
position.value = parsed;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load notification fab position:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载保存的位置
|
||||
onMounted(() => {
|
||||
loadPosition();
|
||||
});
|
||||
|
||||
// 所有任务列表
|
||||
const allTasks = computed(() => {
|
||||
return Array.from(activeNotifications.values());
|
||||
});
|
||||
|
||||
// 翻译title(支持i18n key和直接文本)
|
||||
const translateTitle = (title: string): string => {
|
||||
// 如果包含点号,说明是i18n key,需要翻译
|
||||
if (title.includes('.')) {
|
||||
return t(title);
|
||||
}
|
||||
// 否则直接返回原文本
|
||||
return title;
|
||||
};
|
||||
|
||||
const toggleNotificationPanel = () => {
|
||||
// 如果发生了拖拽移动,不触发点击事件
|
||||
if (hasMoved.value) {
|
||||
hasMoved.value = false;
|
||||
return;
|
||||
}
|
||||
isPanelVisible.value = !isPanelVisible.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
86
frontend/src/components/sysmsg/db/DbSqlExecProgress.vue
Normal file
86
frontend/src/components/sysmsg/db/DbSqlExecProgress.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="w-full py-1">
|
||||
<el-row> <TagCodePath :code="progress.dbCode" /> / {{ progress.dbName }} </el-row>
|
||||
|
||||
<!-- 文件名 -->
|
||||
<div class="flex items-center gap-2 mb-2 mt-2">
|
||||
<SvgIcon name="Document" :size="16" class="text-primary flex-shrink-0" />
|
||||
<span class="flex-1 text-sm font-semibold text-gray-700 dark:text-gray-200 truncate" :title="progress.title">
|
||||
{{ progress.title }}
|
||||
</span>
|
||||
<!-- 取消按钮 -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<el-descriptions border size="small">
|
||||
<el-descriptions-item :label="$t('db.executedStatements')">{{ progress.executedStatements }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('db.elapsedTime')">{{ state.elapsedTime }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
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;
|
||||
title: string;
|
||||
executedStatements: number;
|
||||
terminated: boolean;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
progress?: Progress;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
progress: () => ({
|
||||
dbCode: '',
|
||||
dbName: '',
|
||||
title: '',
|
||||
executedStatements: 0,
|
||||
terminated: false,
|
||||
status: '',
|
||||
}),
|
||||
onCancel: undefined,
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
elapsedTime: '00:00:00',
|
||||
});
|
||||
|
||||
let timer: any = undefined;
|
||||
const startTime = Date.now();
|
||||
|
||||
onMounted(async () => {
|
||||
timer = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
state.elapsedTime = formatTime(elapsed, 'HH:mm:ss');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
if (timer != undefined) {
|
||||
clearInterval(timer); // 在Vue实例销毁前,清除我们的定时器
|
||||
timer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理取消执行
|
||||
const handleCancel = () => {
|
||||
if (props.onCancel) {
|
||||
cancelLoading.value = true;
|
||||
props.onCancel();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
118
frontend/src/components/sysmsg/db/db-sql-exec-progress.ts
Normal file
118
frontend/src/components/sysmsg/db/db-sql-exec-progress.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import syssocket from '@/common/syssocket';
|
||||
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 }>();
|
||||
|
||||
// 存储待注册的 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);
|
||||
sqlExecAborters.delete(id);
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
* @param abort 取消方法
|
||||
*/
|
||||
export function registerSqlExecAborter(execId: string, abort: () => void) {
|
||||
// 先检查通知是否已经存在
|
||||
const task = activeNotifications.get(execId);
|
||||
const progress = task?.componentProps?.progress || null;
|
||||
|
||||
if (progress) {
|
||||
// 通知已存在,直接注册
|
||||
sqlExecAborters.set(execId, { abort, progress });
|
||||
} else {
|
||||
// 通知还未创建,保存为 pending
|
||||
pendingSqlExecAborters.set(execId, abort);
|
||||
}
|
||||
}
|
||||
5
frontend/src/components/sysmsg/db/index.ts
Normal file
5
frontend/src/components/sysmsg/db/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { registerDbSqlExecProgress } from './db-sql-exec-progress';
|
||||
|
||||
export function initDbSysMsgs() {
|
||||
registerDbSqlExecProgress();
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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, NotificationTask>>(new Map());
|
||||
|
||||
// 悬浮通知状态
|
||||
export const globalNotificationState = reactive({
|
||||
activeCount: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新悬浮通知状态
|
||||
*/
|
||||
const updateNotificationState = () => {
|
||||
globalNotificationState.activeCount = activeNotifications.size;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取通知
|
||||
*/
|
||||
export function getNotification(id: string) {
|
||||
return activeNotifications.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新通知
|
||||
* @param id 通知唯一ID
|
||||
* @param category 通知类别(如:machineFileUpload, machineFolderUpload, sqlScript等)
|
||||
* @param content 通知内容
|
||||
* @param component 通知组件
|
||||
* @param componentProps 组件props
|
||||
* @param options 通知选项
|
||||
*/
|
||||
export const createOrUpdateNotification = (
|
||||
id: string,
|
||||
category: string,
|
||||
content: unknown,
|
||||
component: Component,
|
||||
componentProps: Record<string, any>,
|
||||
options: {
|
||||
title: string;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
) => {
|
||||
// 添加到活跃任务
|
||||
activeNotifications.set(id, {
|
||||
id,
|
||||
category,
|
||||
content,
|
||||
component,
|
||||
componentProps,
|
||||
options,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
updateNotificationState();
|
||||
};
|
||||
|
||||
/**
|
||||
* 完成通知
|
||||
* @param id 通知唯一ID
|
||||
* @param closeDelay 延迟关闭时间(毫秒)
|
||||
*/
|
||||
export const completeNotification = (id: string, closeDelay: number = 1000) => {
|
||||
// 延迟从活跃列表中移除
|
||||
setTimeout(() => {
|
||||
activeNotifications.delete(id);
|
||||
updateNotificationState();
|
||||
}, closeDelay);
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="w-full py-1">
|
||||
<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 items-center gap-2 mb-2">
|
||||
<SvgIcon name="Document" :size="16" class="text-primary flex-shrink-0" />
|
||||
<span class="flex-1 text-sm font-semibold text-gray-700 dark:text-gray-200 truncate" :title="progress.filename">
|
||||
{{ progress.filename }}
|
||||
</span>
|
||||
<!-- 取消按钮 -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="flex-1">
|
||||
<el-progress :percentage="percent" :status="progressStatus" :stroke-width="10" :show-text="false" />
|
||||
</div>
|
||||
<span class="text-sm font-bold text-primary min-w-[45px] text-right"> {{ percent }}% </span>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-md px-3 py-2">
|
||||
<div class="flex items-center justify-between text-xs gap-4">
|
||||
<span class="flex items-center gap-1.5 text-gray-600 dark:text-gray-400 font-medium">
|
||||
<SvgIcon name="Files" :size="14" />
|
||||
{{ $t('components.terminal.machineFileUpload.totalSize') }}
|
||||
<span class="font-mono font-semibold text-gray-800 dark:text-gray-200">
|
||||
{{ formatByteSize(progress.totalSize) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="flex items-center gap-1.5 text-gray-600 dark:text-gray-400 font-medium">
|
||||
<SvgIcon name="Upload" :size="14" />
|
||||
{{ $t('components.terminal.machineFileUpload.uploaded') }}
|
||||
<span class="font-mono font-semibold text-gray-800 dark:text-gray-200">
|
||||
{{ formatByteSize(progress.uploadedSize) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="flex items-center gap-1.5 text-gray-600 dark:text-gray-400 font-medium">
|
||||
<SvgIcon name="Odometer" :size="14" />
|
||||
{{ $t('components.terminal.machineFileUpload.speed') }}
|
||||
<span class="font-mono font-semibold text-primary">
|
||||
{{ speed }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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; // 文件路径
|
||||
filename: string;
|
||||
percent: number;
|
||||
uploadedSize: number;
|
||||
totalSize: number;
|
||||
timestamp?: number; // 时间戳,用于计算速度
|
||||
status: '' | 'complete' | 'error' | 'uploading';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
progress?: Progress;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
progress: () => ({
|
||||
authCertName: '',
|
||||
path: '',
|
||||
filename: '',
|
||||
percent: 0,
|
||||
uploadedSize: 0,
|
||||
totalSize: 0,
|
||||
timestamp: 0,
|
||||
status: '',
|
||||
}),
|
||||
onCancel: undefined,
|
||||
});
|
||||
|
||||
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 '';
|
||||
}
|
||||
});
|
||||
|
||||
// 计算百分比
|
||||
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));
|
||||
});
|
||||
|
||||
// 计算速度
|
||||
const lastTimestamp = ref(0);
|
||||
const lastUploadedSize = ref(0);
|
||||
|
||||
const speed = computed(() => {
|
||||
if (!props.progress.timestamp || !props.progress.uploadedSize) {
|
||||
return '0 B/s';
|
||||
}
|
||||
|
||||
// 首次更新,记录初始值
|
||||
if (lastTimestamp.value === 0) {
|
||||
lastTimestamp.value = props.progress.timestamp;
|
||||
lastUploadedSize.value = props.progress.uploadedSize;
|
||||
return '0 B/s';
|
||||
}
|
||||
|
||||
// 计算时间差和大小差
|
||||
const timeDiff = (props.progress.timestamp - lastTimestamp.value) / 1000; // 转换为秒
|
||||
const sizeDiff = props.progress.uploadedSize - lastUploadedSize.value;
|
||||
|
||||
// 更新时间戳和大小
|
||||
lastTimestamp.value = props.progress.timestamp;
|
||||
lastUploadedSize.value = props.progress.uploadedSize;
|
||||
|
||||
// 计算速度
|
||||
if (timeDiff <= 0) return '0 B/s';
|
||||
const speedBytes = sizeDiff / timeDiff;
|
||||
|
||||
// 格式化速度
|
||||
if (speedBytes < 1024) {
|
||||
return `${speedBytes.toFixed(0)} B/s`;
|
||||
} else if (speedBytes < 1024 * 1024) {
|
||||
return `${(speedBytes / 1024).toFixed(1)} KB/s`;
|
||||
} else {
|
||||
return `${(speedBytes / (1024 * 1024)).toFixed(1)} MB/s`;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理取消上传
|
||||
const handleCancel = () => {
|
||||
if (props.onCancel) {
|
||||
cancelLoading.value = true;
|
||||
props.onCancel();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<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 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
|
||||
:loading="cancelLoading"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 所有文件列表 -->
|
||||
<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>
|
||||
|
||||
<!-- 文件列表滚动区域 -->
|
||||
<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" />
|
||||
|
||||
<!-- 文件路径 -->
|
||||
<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 { ref, computed } from 'vue';
|
||||
|
||||
const cancelLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
progress: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
onCancel: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// 将 Map 转换为数组以便遍历
|
||||
const fileList = computed(() => {
|
||||
if (!props.progress.files || !(props.progress.files instanceof Map)) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(props.progress.files.values());
|
||||
});
|
||||
|
||||
// 格式化文件大小
|
||||
const formatSize = (bytes: number): string => {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 处理取消上传
|
||||
const handleCancel = () => {
|
||||
if (props.onCancel) {
|
||||
cancelLoading.value = true;
|
||||
props.onCancel();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
7
frontend/src/components/sysmsg/machine/index.ts
Normal file
7
frontend/src/components/sysmsg/machine/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { registerMachineFileUploadProgress } from './machine-file-upload-progress';
|
||||
import { registerFolderUploadProgressHandler } from './machine-folder-upload-progress';
|
||||
|
||||
export function initMachineSysMsgs() {
|
||||
registerMachineFileUploadProgress();
|
||||
registerFolderUploadProgressHandler();
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import syssocket from '@/common/syssocket';
|
||||
import { nextTick, reactive } from 'vue';
|
||||
import { activeNotifications, completeNotification, createOrUpdateNotification } from '../global-notification-manager';
|
||||
import MachineFileUploadProgress from './MachineFileUploadProgress.vue';
|
||||
|
||||
// 存储上传任务的取消方法
|
||||
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());
|
||||
|
||||
/**
|
||||
* 注册机器文件上传进度消息处理
|
||||
*/
|
||||
export async function registerMachineFileUploadProgress() {
|
||||
await syssocket.registerMsgHandler('machineFileUploadProgress', function (message: any) {
|
||||
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);
|
||||
uploadAborters.delete(uploadId);
|
||||
return;
|
||||
}
|
||||
|
||||
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 registerUploadFileAborter(uploadId: string, abort: () => void) {
|
||||
// 先检查通知是否已经存在
|
||||
const task = activeNotifications.get(uploadId);
|
||||
const progress = task?.componentProps?.progress || null;
|
||||
|
||||
if (progress) {
|
||||
// 通知已存在,直接注册
|
||||
uploadAborters.set(uploadId, { abort, progress });
|
||||
} else {
|
||||
// 通知还未创建,保存为 pending
|
||||
pendingAborters.set(uploadId, abort);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import syssocket from '@/common/syssocket';
|
||||
import { nextTick, reactive } from 'vue';
|
||||
import { activeNotifications, completeNotification, createOrUpdateNotification } from '../global-notification-manager';
|
||||
import MachineFolderUploadProgress from './MachineFolderUploadProgress.vue';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
|
||||
// 存储上传任务的取消方法
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册文件夹上传进度消息处理
|
||||
*/
|
||||
export async function registerFolderUploadProgressHandler() {
|
||||
await syssocket.registerMsgHandler('machineFolderUploadProgress', function (message: any) {
|
||||
const content = message.params;
|
||||
const uploadId = content.uploadId;
|
||||
|
||||
if (!uploadId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件夹上传:处理单文件进度和完成消息
|
||||
// 注意:文件夹上传时,单个文件的 complete/error 不关闭通知,只标记文件状态
|
||||
handleUploadFolderProgress(content);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册文件夹上传任务的取消方法
|
||||
* @param uploadId 上传ID
|
||||
* @param abort 取消方法
|
||||
*/
|
||||
export function registerUploadFolderAborter(uploadId: string, abort: () => void) {
|
||||
// 先检查通知是否已经存在
|
||||
const task = activeNotifications.get(uploadId);
|
||||
const progress = task?.componentProps?.progress || null;
|
||||
|
||||
if (progress) {
|
||||
// 通知已存在,直接注册
|
||||
folderUploadAborters.set(uploadId, { abort, progress });
|
||||
} else {
|
||||
// 通知还未创建,保存为 pending
|
||||
pendingFolderAborters.set(uploadId, abort);
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,26 +3,35 @@
|
||||
<div ref="terminalRef" class="h-full w-full" :style="{ background: getTerminalTheme().background }" />
|
||||
|
||||
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<Contextmenu ref="contextmenuRef" :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { Terminal, ITheme } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { SearchAddon } from '@xterm/addon-search';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { ITheme, Terminal } from '@xterm/xterm';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
import { storeToRefs } from 'pinia';
|
||||
import config from '@/common/config';
|
||||
import { createWebSocket, joinClientParams } from '@/common/request';
|
||||
import { downloadFile } from '@/common/utils/file';
|
||||
import { copyToClipboard, pasteFromClipboard } from '@/common/utils/string';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { Msg } from '@/hooks/useI18n';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { machineApi, uploadFile, uploadFolder } from '@/views/ops/machine/api';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import TerminalSearch from './TerminalSearch.vue';
|
||||
import { TerminalStatus } from './common';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
import themes from './themes.js';
|
||||
import { TrzszFilter } from 'trzsz';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { createWebSocket } from '@/common/request';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -42,12 +51,29 @@ const props = defineProps({
|
||||
socketUrl: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* 机器ID(用于文件传输)
|
||||
*/
|
||||
machineId: { type: Number, default: 0 },
|
||||
/**
|
||||
* 授权凭证名(用于文件传输)
|
||||
*/
|
||||
authCertName: { type: String, default: '' },
|
||||
/**
|
||||
* 文件ID(用于文件传输)
|
||||
*/
|
||||
fileId: { type: Number, default: 0 },
|
||||
/**
|
||||
* 协议类型(用于文件传输)
|
||||
*/
|
||||
protocol: { type: Number, default: 1 },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['statusChange']);
|
||||
|
||||
const terminalRef: any = ref(null);
|
||||
const terminalSearchRef: any = ref(null);
|
||||
const contextmenuRef: any = ref(null);
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
@@ -56,6 +82,11 @@ let term: Terminal;
|
||||
let socket: WebSocket;
|
||||
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// 静默模式标志:用于发送不显示的命令(如 pwd)
|
||||
let silentMode = false;
|
||||
let silentResolve: ((value: string) => void) | null = null;
|
||||
let silentBuffer = '';
|
||||
|
||||
const state = reactive({
|
||||
// 插件
|
||||
addon: {
|
||||
@@ -64,6 +95,15 @@ const state = reactive({
|
||||
weblinks: null as any,
|
||||
},
|
||||
status: -11,
|
||||
// 右键菜单
|
||||
contextmenu: {
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
items: [] as ContextmenuItem[],
|
||||
selectedItem: '',
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -171,6 +211,42 @@ const initSocket = async () => {
|
||||
console.log('terminal socket close...', e.reason);
|
||||
state.status = TerminalStatus.Disconnected;
|
||||
};
|
||||
|
||||
// 监听 WebSocket 消息,将服务器输出写入终端
|
||||
socket.onmessage = (e: MessageEvent) => {
|
||||
// 如果是静默模式,捕获输出但不显示
|
||||
if (silentMode && silentResolve) {
|
||||
silentBuffer += e.data;
|
||||
|
||||
// 使用正则表达式匹配绝对路径
|
||||
// 匹配以 / 开头,不包含空格、换行符的连续字符
|
||||
const pathMatch = silentBuffer.match(/(\/[\w\-\./_]*)/);
|
||||
|
||||
if (pathMatch && pathMatch[1]) {
|
||||
const path = pathMatch[1];
|
||||
console.log('[Silent Mode] Extracted path:', path);
|
||||
silentResolve(path);
|
||||
silentMode = false;
|
||||
silentResolve = null;
|
||||
silentBuffer = '';
|
||||
return; // 不写入终端
|
||||
}
|
||||
|
||||
// 如果缓冲区太大,超时处理
|
||||
if (silentBuffer.length > 500) {
|
||||
console.warn('[Silent Mode] Buffer too large, using default path');
|
||||
silentResolve('~');
|
||||
silentMode = false;
|
||||
silentResolve = null;
|
||||
silentBuffer = '';
|
||||
}
|
||||
|
||||
return; // 不写入终端
|
||||
}
|
||||
|
||||
// 正常模式,写入终端显示
|
||||
term.write(e.data);
|
||||
};
|
||||
};
|
||||
|
||||
const startHeartbeat = () => {
|
||||
@@ -200,38 +276,24 @@ const loadAddon = () => {
|
||||
state.addon.weblinks = weblinks;
|
||||
term.loadAddon(weblinks);
|
||||
|
||||
// 注册 trzsz
|
||||
// initialize trzsz filter
|
||||
const trzsz = new TrzszFilter({
|
||||
// write the server output to the terminal
|
||||
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
|
||||
// send the user input to the server
|
||||
sendToServer: sendData,
|
||||
// the terminal columns
|
||||
terminalColumns: term.cols,
|
||||
// there is a windows shell
|
||||
isWindowsShell: false,
|
||||
// 注册终端输入事件监听(将用户输入发送到 socket)
|
||||
term.onData((data: string) => sendData(data));
|
||||
term.onBinary((data: string) => sendData(data));
|
||||
|
||||
// 注册终端大小变化事件
|
||||
term.onResize((size: { cols: number; rows: number }) => {
|
||||
sendResize(size.cols, size.rows);
|
||||
});
|
||||
|
||||
// let trzsz process the server output
|
||||
socket?.addEventListener('message', (e) => trzsz.processServerOutput(e.data));
|
||||
// let trzsz process the user input
|
||||
term.onData((data) => trzsz.processTerminalInput(data));
|
||||
term.onBinary((data) => trzsz.processBinaryInput(data));
|
||||
term.onResize((size) => {
|
||||
sendResize(size.cols, size.rows);
|
||||
// tell trzsz the terminal columns has been changed
|
||||
trzsz.setTerminalColumns(size.cols);
|
||||
});
|
||||
// enable drag files or directories to upload
|
||||
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
|
||||
terminalRef.value.addEventListener('drop', (event: any) => {
|
||||
event.preventDefault();
|
||||
trzsz
|
||||
.uploadFiles(event.dataTransfer.items)
|
||||
.then(() => console.log('upload success'))
|
||||
.catch((err: any) => console.log(err));
|
||||
handleFileDrop(event.dataTransfer.items);
|
||||
});
|
||||
|
||||
// 添加右键菜单支持文件下载和上传
|
||||
setupContextMenu();
|
||||
};
|
||||
|
||||
// 写入内容至终端
|
||||
@@ -302,6 +364,282 @@ const closeSocket = () => {
|
||||
socket && socket.readyState === 1 && socket.close();
|
||||
};
|
||||
|
||||
// 设置右键菜单
|
||||
const setupContextMenu = () => {
|
||||
terminalRef.value.addEventListener('contextmenu', async (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
showContextMenu(event, term.getSelection());
|
||||
});
|
||||
};
|
||||
|
||||
// 显示组合右键菜单
|
||||
const showContextMenu = (event: MouseEvent, selectedText: string) => {
|
||||
state.contextmenu.selectedItem = selectedText;
|
||||
state.contextmenu.dropdown = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
|
||||
// 始终添加上传文件和上传文件夹按钮
|
||||
state.contextmenu.items = [
|
||||
new ContextmenuItem('copy', 'common.copy')
|
||||
.withIcon('CopyDocument')
|
||||
.withHideFunc(() => !selectedText)
|
||||
.withOnClick(() => {
|
||||
copyToClipboard(selectedText);
|
||||
}),
|
||||
new ContextmenuItem('paste', 'common.paste').withIcon('Document').withOnClick(async () => {
|
||||
let text = '';
|
||||
try {
|
||||
// 尝试从剪贴板读取
|
||||
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 {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
term.paste(text);
|
||||
focus();
|
||||
}
|
||||
}),
|
||||
new ContextmenuItem('download', 'components.terminal.downloadSelectedFile')
|
||||
.withIcon('Download')
|
||||
.withHideFunc(() => !selectedText)
|
||||
.withOnClick(() => {
|
||||
downloadSelectedFile(state.contextmenu.selectedItem);
|
||||
}),
|
||||
new ContextmenuItem('uploadFile', 'components.terminal.uploadFileToCurrentDir')
|
||||
.withIcon('Upload')
|
||||
.withHideFunc(() => !props.machineId || !props.authCertName)
|
||||
.withOnClick(() => {
|
||||
triggerFilesUpload();
|
||||
}),
|
||||
new ContextmenuItem('uploadFolder', 'components.terminal.uploadFolderToCurrentDir')
|
||||
.withIcon('Upload')
|
||||
.withHideFunc(() => !props.machineId || !props.authCertName)
|
||||
.withOnClick(() => {
|
||||
triggerFolderUpload();
|
||||
}),
|
||||
];
|
||||
|
||||
// 打开右键菜单
|
||||
contextmenuRef.value?.openContextmenu({});
|
||||
};
|
||||
|
||||
// 下载选中的文件
|
||||
const downloadSelectedFile = async (filePath: string) => {
|
||||
if (!props.machineId || !props.authCertName) {
|
||||
Msg.error('components.terminal.downloadFailed', { error: '缺少机器信息' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前路径
|
||||
const currentPath = await getCurrentPathOrDefault();
|
||||
|
||||
// 从完整路径中提取文件名
|
||||
const filename = filePath.trim().split('/').pop() || filePath.trim();
|
||||
|
||||
// 拼接完整路径
|
||||
const fullPath = currentPath.endsWith('/') ? `${currentPath}${filename}` : `${currentPath}/${filename}`;
|
||||
|
||||
// 先验证文件是否存在
|
||||
try {
|
||||
await machineApi.fileStat.request({
|
||||
machineId: props.machineId,
|
||||
protocol: props.protocol,
|
||||
fileId: props.fileId,
|
||||
authCertName: props.authCertName,
|
||||
path: fullPath,
|
||||
});
|
||||
} catch (error: any) {
|
||||
Msg.error('components.terminal.downloadFailed', { error: error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
downloadFile(
|
||||
`${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()}`
|
||||
);
|
||||
|
||||
Msg.success('components.terminal.startDownload', { file: fullPath });
|
||||
} catch (error: any) {
|
||||
Msg.error('components.terminal.downloadFailed', { error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 触发文件上传
|
||||
const triggerFilesUpload = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.style.display = 'none';
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
if (input.files && input.files.length > 0) {
|
||||
uploadFilesToCurrentPath(input.files);
|
||||
}
|
||||
document.body.removeChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
};
|
||||
|
||||
// 触发文件夹上传
|
||||
const triggerFolderUpload = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
(input as any).webkitdirectory = true;
|
||||
(input as any).directory = true;
|
||||
input.style.display = 'none';
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
if (input.files && input.files.length > 0) {
|
||||
uploadFolderToCurrentPath(input.files);
|
||||
}
|
||||
document.body.removeChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
};
|
||||
|
||||
// 上传文件到当前路径
|
||||
const uploadFilesToCurrentPath = async (files: FileList) => {
|
||||
try {
|
||||
// 获取当前路径
|
||||
const currentPath = await getCurrentPathOrDefault();
|
||||
|
||||
const file = files[0];
|
||||
|
||||
uploadFile(
|
||||
file,
|
||||
{
|
||||
machineId: props.machineId as number,
|
||||
authCertName: props.authCertName as string,
|
||||
protocol: props.protocol,
|
||||
fileId: props.fileId as number,
|
||||
path: currentPath,
|
||||
filename: file.name,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Msg.success('components.terminal.uploadSuccess');
|
||||
},
|
||||
onError: (error) => {
|
||||
Msg.error('components.terminal.uploadFailed', { error: error.message });
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error: any) {
|
||||
Msg.error('components.terminal.uploadFailed', { error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 上传文件夹到当前路径
|
||||
const uploadFolderToCurrentPath = async (files: FileList) => {
|
||||
try {
|
||||
// 获取当前路径
|
||||
const currentPath = await getCurrentPathOrDefault();
|
||||
|
||||
uploadFolder(
|
||||
files,
|
||||
{
|
||||
machineId: props.machineId as number,
|
||||
authCertName: props.authCertName as string,
|
||||
protocol: props.protocol,
|
||||
fileId: props.fileId as number,
|
||||
path: currentPath,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Msg.success('components.terminal.uploadSuccess');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
Msg.error('components.terminal.uploadFailed', { error: error.message });
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error: any) {
|
||||
Msg.error('components.terminal.uploadFailed', { error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前路径(静默模式,失败返回默认值)
|
||||
const getCurrentPathOrDefault = async (): Promise<string> => {
|
||||
try {
|
||||
return await getCurrentPath();
|
||||
} catch (e) {
|
||||
console.warn('获取当前路径失败,使用默认路径 ~:', e);
|
||||
return '~';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前路径(静默模式,不在终端显示)
|
||||
const getCurrentPath = (): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
reject('WebSocket 未连接');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置静默模式
|
||||
silentMode = true;
|
||||
silentResolve = resolve;
|
||||
silentBuffer = '';
|
||||
|
||||
// 发送 pwd 命令(使用 \r 模拟回车)
|
||||
sendData('pwd\r');
|
||||
|
||||
// 设置超时,防止永远等待
|
||||
setTimeout(() => {
|
||||
if (silentMode) {
|
||||
silentMode = false;
|
||||
silentResolve = null;
|
||||
silentBuffer = '';
|
||||
console.warn('[Silent Mode] Timeout getting current path');
|
||||
resolve('~'); // 超时返回默认路径
|
||||
}
|
||||
}, 2000); // 2秒超时
|
||||
});
|
||||
};
|
||||
|
||||
// 处理文件拖拽上传
|
||||
const handleFileDrop = async (items: DataTransferItemList) => {
|
||||
if (!props.machineId || !props.authCertName) {
|
||||
Msg.error('components.terminal.uploadFailed', { error: '缺少机器信息' });
|
||||
return;
|
||||
}
|
||||
|
||||
const files: File[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
await uploadFilesToCurrentPath(files as any);
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
console.log('in terminal body close');
|
||||
closeSocket();
|
||||
@@ -319,4 +657,6 @@ const getStatus = (): TerminalStatus => {
|
||||
|
||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss" scoped>
|
||||
// 终端容器样式
|
||||
</style>
|
||||
|
||||
@@ -78,6 +78,10 @@
|
||||
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
||||
:cmd="openTerminal.cmd"
|
||||
:socket-url="openTerminal.socketUrl"
|
||||
:machine-id="openTerminal.meta?.id || 0"
|
||||
:auth-cert-name="openTerminal.meta?.selectAuthCert?.name || ''"
|
||||
:file-id="0"
|
||||
:protocol="openTerminal.meta?.protocol || 1"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,13 +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',
|
||||
@@ -270,6 +282,35 @@ export default {
|
||||
previous: 'Previous',
|
||||
next: 'Next',
|
||||
noMatchMsg: 'No matching item is found',
|
||||
|
||||
// File transfer related
|
||||
downloadFile: 'Download File',
|
||||
downloadSelectedFile: 'Download Selected Path File',
|
||||
uploadFile: 'Upload File',
|
||||
uploadFileToCurrentDir: 'Upload File to Current Directory',
|
||||
uploadFolder: 'Upload Folder',
|
||||
uploadFolderToCurrentDir: 'Upload Folder to Current Directory',
|
||||
chooseUploadType: 'Choose Upload Type',
|
||||
startDownload: 'Start downloading file: {file}',
|
||||
downloadFailed: 'File download failed: {error}',
|
||||
uploadSuccess: 'File uploaded successfully',
|
||||
uploadFailed: 'File upload failed: {error}',
|
||||
uploading: 'Upload progress: {percent}%',
|
||||
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',
|
||||
uploaded: 'Uploaded',
|
||||
totalSize: 'Total Size',
|
||||
speed: 'Speed',
|
||||
},
|
||||
},
|
||||
crontab: {
|
||||
crontabInputPlaceholder: 'Click the left button to configure',
|
||||
@@ -329,5 +370,13 @@ export default {
|
||||
title: 'please select the icon',
|
||||
placeholder: 'please enter content search icon or select icon',
|
||||
},
|
||||
|
||||
// System message notifications
|
||||
sysmsg: {
|
||||
notifications: {
|
||||
title: 'System Notifications',
|
||||
closeAll: 'Close All',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -225,6 +225,14 @@ export default {
|
||||
|
||||
running: 'Running',
|
||||
waitRun: 'Wait Run',
|
||||
|
||||
// SQL execution
|
||||
sqlExecute: 'SQL Execution',
|
||||
executedStatements: 'Executed',
|
||||
elapsedTime: 'Elapsed',
|
||||
scriptFileUploadSuccess: 'SQL file [{filename}] executed successfully',
|
||||
scriptFileUploadCancelled: 'SQL file [{filename}] execution cancelled',
|
||||
scriptFileUploadFailed: 'SQL file [{filename}] execution failed: {error}',
|
||||
},
|
||||
es: {
|
||||
keywordPlaceholder: 'host / name / code',
|
||||
|
||||
45
frontend/src/i18n/en/home.ts
Normal file
45
frontend/src/i18n/en/home.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
@@ -141,5 +141,22 @@ export default {
|
||||
fileExceedsSysConf: 'The uploaded file exceeds the system configuration [{uploadMaxFileSize}]',
|
||||
fileUploadSuccess: 'Machine file upload successful',
|
||||
fileUploadFail: 'Machine file upload failed',
|
||||
fileUpload: 'File Upload',
|
||||
|
||||
// Folder upload progress
|
||||
folderUploadProgress: 'Folder Upload Progress',
|
||||
folderUpload: 'Folder Upload',
|
||||
uploading: 'Uploading',
|
||||
concurrentFiles: '{count} concurrent',
|
||||
fileList: 'File List',
|
||||
waiting: 'Waiting',
|
||||
|
||||
// Upload notifications
|
||||
uploadNotifications: {
|
||||
title: 'Upload Notifications',
|
||||
closeAll: 'Close All',
|
||||
folders: '{count} folders',
|
||||
files: '{count} files',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ function initI18n() {
|
||||
});
|
||||
|
||||
const themeConfig = getThemeConfig();
|
||||
const globalI18n = themeConfig.globalI18n || "zh-cn";
|
||||
const globalI18n = themeConfig?.globalI18n || 'zh-cn';
|
||||
|
||||
// https://vue-i18n.intlify.dev/guide/essentials/fallback.html#explicit-fallback-with-one-locale
|
||||
return createI18n({
|
||||
@@ -45,7 +45,7 @@ function initI18n() {
|
||||
silentFallbackWarn: true,
|
||||
fallbackWarn: false,
|
||||
locale: globalI18n,
|
||||
fallbackLocale: "zh-cn",
|
||||
fallbackLocale: 'zh-cn',
|
||||
messages,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,13 +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}',
|
||||
@@ -279,6 +291,35 @@ export default {
|
||||
previous: '上一个',
|
||||
next: '下一个',
|
||||
noMatchMsg: '未查询到匹配项',
|
||||
|
||||
// 文件传输相关
|
||||
downloadFile: '下载文件',
|
||||
downloadSelectedFile: '下载选中路径的文件',
|
||||
uploadFile: '上传文件',
|
||||
uploadFileToCurrentDir: '上传文件到当前目录',
|
||||
uploadFolder: '上传文件夹',
|
||||
uploadFolderToCurrentDir: '上传文件夹到当前目录',
|
||||
chooseUploadType: '请选择上传类型',
|
||||
startDownload: '开始下载文件: {file}',
|
||||
downloadFailed: '文件下载失败: {error}',
|
||||
uploadSuccess: '文件上传成功',
|
||||
uploadFailed: '文件上传失败: {error}',
|
||||
uploading: '上传进度: {percent}%',
|
||||
uploadToPath: '文件将上传到路径: {path}',
|
||||
uploadPathTip: '提示: 文件将上传到用户家目录(~)。如需上传到其他目录,请先在终端中执行 cd 命令切换到目标目录,然后使用拖拽上传功能。',
|
||||
|
||||
// 粘贴相关
|
||||
manualPaste: '手动粘贴',
|
||||
pasteManualHint: '当前环境无法自动读取剪贴板内容,请手动粘贴内容到下方输入框:',
|
||||
pasteHere: '请在此处粘贴内容...',
|
||||
|
||||
// 机器文件上传进度通知
|
||||
machineFileUpload: {
|
||||
uploadProgress: '机器文件上传进度',
|
||||
uploaded: '已上传',
|
||||
totalSize: '总大小',
|
||||
speed: '速度',
|
||||
},
|
||||
},
|
||||
crontab: {
|
||||
crontabInputPlaceholder: '可点击左边按钮配置',
|
||||
@@ -338,5 +379,13 @@ export default {
|
||||
title: '请选择图标',
|
||||
placeholder: '请输入内容搜索图标或者选择图标',
|
||||
},
|
||||
|
||||
// 系统消息通知
|
||||
sysmsg: {
|
||||
notifications: {
|
||||
title: '系统通知',
|
||||
closeAll: '全部关闭',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -219,5 +219,13 @@ export default {
|
||||
|
||||
running: '运行中',
|
||||
waitRun: '待运行',
|
||||
|
||||
// SQL执行
|
||||
sqlExecute: 'SQL执行',
|
||||
executedStatements: '已执行',
|
||||
elapsedTime: '已用时',
|
||||
scriptFileUploadSuccess: 'SQL文件【{filename}】执行成功',
|
||||
scriptFileUploadCancelled: 'SQL文件【{filename}】执行已取消',
|
||||
scriptFileUploadFailed: 'SQL文件【{filename}】执行失败: {error}',
|
||||
},
|
||||
};
|
||||
|
||||
45
frontend/src/i18n/zh-cn/home.ts
Normal file
45
frontend/src/i18n/zh-cn/home.ts
Normal 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: '已加载全部',
|
||||
},
|
||||
};
|
||||
@@ -142,5 +142,29 @@ export default {
|
||||
fileExceedsSysConf: '上传的文件超过系统配置的【{uploadMaxFileSize}】',
|
||||
fileUploadSuccess: '机器文件上传成功',
|
||||
fileUploadFail: '机器文件上传失败',
|
||||
fileUpload: '文件上传',
|
||||
|
||||
// 文件夹上传进度
|
||||
folderUploadProgress: '文件夹上传进度',
|
||||
folderUpload: '文件夹上传',
|
||||
uploading: '正在上传',
|
||||
concurrentFiles: '{count} 个并发',
|
||||
fileList: '文件列表',
|
||||
waiting: '等待中',
|
||||
|
||||
// 上传通知
|
||||
uploadNotifications: {
|
||||
title: '上传通知',
|
||||
closeAll: '全部关闭',
|
||||
folders: '{count} 个文件夹',
|
||||
files: '{count} 个文件',
|
||||
},
|
||||
},
|
||||
// 远程桌面相关
|
||||
'terminal-rdp': {
|
||||
sendCombinationKeySuccess: '发送组合键成功',
|
||||
clipboardSendSuccess: '发送剪贴板数据成功',
|
||||
clipboardInputRequired: '请输入需要粘贴的文本',
|
||||
httpsRequiredForClipboard: '只有 HTTPS 才可以访问剪贴板',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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(() => {});
|
||||
|
||||
@@ -16,7 +16,7 @@ import '@/theme/tailwind.css';
|
||||
import '@/assets/font/font.css';
|
||||
import '@/assets/icon/icon.js';
|
||||
import { getThemeConfig } from './common/utils/storage';
|
||||
import { initSysMsgs } from './common/sysmsgs';
|
||||
import { initSysMsgs } from './common/syssocket';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
|
||||
1
frontend/src/types/pinia.d.ts
vendored
1
frontend/src/types/pinia.d.ts
vendored
@@ -116,4 +116,5 @@ declare interface MilvusState {
|
||||
selectedDb: string,
|
||||
selectedCollection: string
|
||||
collections: any[],
|
||||
authCertName: string,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
146
frontend/src/views/home/resources/Base.vue
Normal file
146
frontend/src/views/home/resources/Base.vue
Normal 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>
|
||||
32
frontend/src/views/home/resources/Database.vue
Normal file
32
frontend/src/views/home/resources/Database.vue
Normal 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>
|
||||
31
frontend/src/views/home/resources/Docker.vue
Normal file
31
frontend/src/views/home/resources/Docker.vue
Normal 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>
|
||||
31
frontend/src/views/home/resources/ES.vue
Normal file
31
frontend/src/views/home/resources/ES.vue
Normal 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>
|
||||
32
frontend/src/views/home/resources/Kafka.vue
Normal file
32
frontend/src/views/home/resources/Kafka.vue
Normal 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>
|
||||
152
frontend/src/views/home/resources/Machine.vue
Normal file
152
frontend/src/views/home/resources/Machine.vue
Normal 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>
|
||||
33
frontend/src/views/home/resources/Milvus.vue
Normal file
33
frontend/src/views/home/resources/Milvus.vue
Normal 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>
|
||||
32
frontend/src/views/home/resources/Mongo.vue
Normal file
32
frontend/src/views/home/resources/Mongo.vue
Normal 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>
|
||||
31
frontend/src/views/home/resources/Redis.vue
Normal file
31
frontend/src/views/home/resources/Redis.vue
Normal 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>
|
||||
10
frontend/src/views/home/resources/index.ts
Normal file
10
frontend/src/views/home/resources/index.ts
Normal 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];
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
//
|
||||
|
||||
@@ -1,5 +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';
|
||||
|
||||
export const dbApi = {
|
||||
// 获取权限列表
|
||||
@@ -79,3 +81,62 @@ export const encryptField = async (param: any, field: string) => {
|
||||
}
|
||||
return param;
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传SQL文件并执行
|
||||
* @param file 文件对象
|
||||
* @param params 上传参数
|
||||
* @param options 上传选项
|
||||
* @returns { uploadId: string; abort: () => void } 返回包含 uploadId 和中止方法的对象
|
||||
*/
|
||||
export function uploadSqlFile(
|
||||
file: File,
|
||||
params: {
|
||||
dbId: number;
|
||||
dbName: string;
|
||||
},
|
||||
options: {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
} = {}
|
||||
): { uploadId: string; abort: () => void } {
|
||||
// 生成 uploadId
|
||||
const uploadId = `sql_exec_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
// 使用 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`);
|
||||
|
||||
// 使用 uploadRaw 直接传递文件流
|
||||
const { abort } = api.uploadRaw(file, queryParams, {
|
||||
onSuccess: () => {
|
||||
options.onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
options.onError?.(error);
|
||||
},
|
||||
});
|
||||
|
||||
// 创建SQL执行进度通知
|
||||
createSqlExecNotification(uploadId, {
|
||||
id: uploadId,
|
||||
title: file.name,
|
||||
dbCode: '',
|
||||
dbName: params.dbName,
|
||||
executedStatements: 0,
|
||||
terminated: false,
|
||||
status: 'uploading',
|
||||
clientId: '',
|
||||
});
|
||||
|
||||
// 注册取消器(在获取到abort方法后)
|
||||
registerSqlExecAborter(uploadId, abort);
|
||||
|
||||
return { uploadId, abort };
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<el-descriptions-item :span="1" :label="$t('common.code')">{{ state.detail.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.name')">{{ state.detail.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><TagCodePath :code="state.detail.code" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="Host">
|
||||
<SvgIcon :name="getDbDialect(state.detail.type).getInfo().icon" :size="20" />
|
||||
{{ state.detail.host }}:{{ state.detail.port }}
|
||||
@@ -34,6 +36,7 @@ import { reactive } from 'vue';
|
||||
import { dbApi } from '../api';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { getDbDialect } from '../dialect/index';
|
||||
import TagCodePath from '../../component/TagCodePath.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
class="sql-file-exec"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="execSqlFileSuccess"
|
||||
:http-request="handleSqlFileUpload"
|
||||
:headers="{ Authorization: token }"
|
||||
:action="getUploadSqlFileUrl()"
|
||||
:show-file-list="false"
|
||||
@@ -129,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 { dbApi, uploadSqlFile } from '../../api';
|
||||
import { DbInst } from '../../db';
|
||||
import { dbApi } from '../../api';
|
||||
|
||||
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']);
|
||||
|
||||
@@ -378,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;
|
||||
@@ -455,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;
|
||||
@@ -698,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);
|
||||
};
|
||||
@@ -728,7 +729,7 @@ const onFormatSql = () => {
|
||||
*/
|
||||
const onCommit = () => {
|
||||
getNowDbInst().runSql(props.dbName, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
Msg.success('COMMIT success');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -768,13 +769,36 @@ 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文件上传处理
|
||||
const handleSqlFileUpload = (options: any) => {
|
||||
const { file } = options;
|
||||
|
||||
const { uploadId, abort } = uploadSqlFile(
|
||||
file,
|
||||
{
|
||||
dbId: props.dbId as number,
|
||||
dbName: props.dbName as string,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Msg.success('db.scriptFileUploadSuccess', { filename: file.name });
|
||||
},
|
||||
onError: (error) => {
|
||||
Msg.error('db.scriptFileUploadFailed', { filename: file.name, error: error.message });
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { abort };
|
||||
};
|
||||
|
||||
// 执行sql成功
|
||||
const execSqlFileSuccess = (res: any) => {
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg);
|
||||
Msg.error(res.msg);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -833,7 +857,7 @@ const initMonacoEditor = () => {
|
||||
try {
|
||||
await onRunSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
e.message && Msg.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -861,7 +885,7 @@ const initMonacoEditor = () => {
|
||||
try {
|
||||
await onRunSql(true);
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
e.message && Msg.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -888,7 +912,7 @@ const initMonacoEditor = () => {
|
||||
try {
|
||||
await onFormatSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
e.message && Msg.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
//
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
//
|
||||
|
||||
@@ -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) {
|
||||
//
|
||||
|
||||
@@ -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) {
|
||||
//
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user