Compare commits

...

56 Commits

Author SHA1 Message Date
meilin.huang
847f5c7c90 feat: v1.11.0 2026-05-13 20:01:05 +08:00
meilin.huang
f23b243fc5 refactor: 移除antlr4减小包体积&ai助手优化 2026-05-08 20:45:13 +08:00
meilin.huang
3768cef62d feat: ai助手优化等
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 22:37:10 +08:00
zongyangleo
1e1ded4db8 !153 fix:还原达梦驱动,修复数据库多级 ssh 跳 bug
* fix:还原达梦驱动,修复数据库多级 ssh 跳 bug
* feat: 支持milvus操作
2026-04-27 05:13:40 +00:00
meilin.huang
13f76f4b35 fix: 数据库导出问题
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 00:28:58 +08:00
希有
c796e05232 !151 fix(db-data-sync): 优化数据同步排序逻辑,避免首次未同步完后续增量同步时漏数据或重复数据
* fix(db-data-sync): 优化数据同步排序逻辑,避免首次未同步完后续增量同步时漏数据或重复数据
* ```
2026-04-21 09:25:26 +00:00
meilin.huang
f207517d35 feat: ai助手优化、请求库优化 2026-04-21 17:22:21 +08:00
meilin.huang
6f5069567e feat: 初步新增ai助手 2026-04-15 12:47:10 +08:00
Coder慌
13ce0e9396 !150 增加clickhouse支持
Merge pull request !150 from 希有/dev
2026-04-15 03:55:09 +00:00
xiyou
e0144f7310 增加clickhouse支持 2026-04-15 10:09:17 +08:00
zongyangleo
8ba87c1895 !149 fix:连接池修复和guacd自动重连
* 连接池修复和guacd自动重连
2026-04-07 13:29:31 +00:00
meilin.huang
414e4b0b36 refactor: 代码优化&依赖升级 2026-03-18 20:58:41 +08:00
zongyangleo
bfa41c3621 !148 refactor: 支持kafka操作
* fix: 达梦连接问题修复
* refactor: 支持kafka操作
2026-03-18 09:00:55 +00:00
meilin.huang
84ab496308 refactor: 前端生产路由改为history、依赖版本升级 2026-03-05 20:31:57 +08:00
fanzhouqi
1f27283ab7 !147 fix:过滤状态不是online的数据库
* fix:过滤状态不是online的数据库
2026-02-26 12:16:38 +00:00
meilin.huang
f91b89f38a refactor: 注释优化 2026-02-10 18:12:41 +08:00
meilin.huang
9bb9861d88 refactor: 参数绑定等优化 2026-02-07 13:12:07 +08:00
meilin.huang
403d1c45e5 refactor: sshtunnel等 2026-02-01 13:35:23 +08:00
meilin.huang
400db0402a refactor: 包优化&其他问题修复 2026-01-25 14:16:16 +08:00
fanzhouqi
f0ae178183 !143 fix:mysql查询时,如果出现列名一样,会覆盖数据,github issues #124
* fix:mysql查询时,如果出现列名一样,会覆盖数据,github issues #124
2026-01-23 09:13:14 +00:00
meilin.huang
4641e448d2 fix: 数据库迁移、同步保存时定时任务未清除问题 2026-01-21 12:22:54 +08:00
meilin.huang
f0de65b7ce refactor: 协程启动优化、tagviews调整 2026-01-20 19:45:46 +08:00
meilin.huang
0472c5101f feat: 数据库迁移至文件支持zip格式、go启动协程时panic优化、菜单资源优化 2026-01-18 20:40:11 +08:00
meilin.huang
185cd6f82b fix: postgres导出调整等 2026-01-14 20:38:06 +08:00
meilin.huang
aa6ad39b83 feat: 新增数据库导出按钮权限等 2026-01-07 21:27:25 +08:00
Coder慌
047b57f890 !141 fix(dbi): concurrent map read and write in GetDbDataType
Merge pull request !141 from 希有/fix-concurrent-map-panic
2026-01-05 12:08:41 +00:00
meilin.huang
a18417ab26 fix: 问题修复 2026-01-05 20:07:17 +08:00
xiyou
20fcf557d5 fix(dbi): concurrent map read and write in GetDbDataType 2025-12-27 19:36:13 +08:00
meilin.huang
5598ddf93c feat: 工单流程新增ai任务,支持退回、数据导出支持excel、其他优化等 2025-12-08 20:50:16 +08:00
meilin.huang
3017460cc7 refactor: 去除无用组件等 2025-11-16 09:11:09 +08:00
Coder慌
4836a770c4 !139 feat(es):增加ES实例中对HTTP/HTTPS协议的支持,默认使用HTTP协议,使用https时默认证书免校验
Merge pull request !139 from davidathena/dev
2025-10-28 11:25:47 +00:00
fudawei
e6c89fad1b feat(es):增加ES实例中对HTTPS协议的支持,默认证书免校验 2025-10-23 15:29:27 +08:00
meilin.huang
dba19b1e66 fix: editor提示被遮挡问题修复等 2025-10-18 11:21:33 +08:00
davidathena
4e30bdb7cc !138 fix: 后端数据连接断开后报空指针异常后程序中断问题
* fix(connection):fix the bug for nil error in connection when connection reset
* fix(sqleditor): fix the spell error in sql editor
2025-10-18 03:15:25 +00:00
meilin.huang
4ac57cd140 refactor: 标签不可移动,资源选择优化等 2025-10-07 15:41:19 +08:00
meilin.huang
c4d52ce47a feat: 资源操作新增右键菜单操作等 2025-09-17 21:23:12 +08:00
meilin.huang
54d0688571 fix: 名称调整等 2025-09-14 20:53:47 +08:00
meilin.huang
66d5fd6ca4 feat: 容器操作优化等 2025-09-06 21:32:48 +08:00
时光似水戏流年
25195b6360 !137 现在执行sql只执行当前光标所在的sql(分号分割的),如果要执行全部sql需要先全选,再执行
* 现在执行sql只执行当前sql(分号分割的),如果要执行全部sql需要先全选,再执行
2025-09-02 11:12:04 +00:00
meilin.huang
e02ecf053f feat: 资源操作统一管理&容器操作 2025-08-31 21:46:10 +08:00
meilin.huang
c86f2ad412 refactor: 样式优化 2025-08-19 19:44:14 +08:00
meilin.huang
82fd97e06a fix: file文件缺失 2025-08-08 12:55:10 +08:00
meilin.huang
614a144f60 refactor: 样式优化 2025-08-04 21:02:27 +08:00
meilin.huang
7d344c71e1 refactor: 消息模块调整 & 样式优化 2025-08-02 22:08:56 +08:00
meilin.huang
6ad6c69660 refactor: 消息模块重构,infra包路径简写等 2025-07-27 21:02:48 +08:00
meilin.huang
e96379b6c0 fix: vite配置调整 2025-07-07 12:05:55 +08:00
meilin.huang
f7480f3bac refactor: cast包替换 2025-06-27 12:17:45 +08:00
meilin.huang
54d3a5b368 fix: sql执行记录根据关键词搜索问题修复等 2025-06-22 10:52:06 +08:00
meilin.huang
7eb4d064ea feat: 机器脚本新增分配、组件属性类型不匹配警告调整 2025-06-16 20:13:03 +08:00
meilin.huang
cc66fcddf5 refactor: 动态路由调整&分隔面板使用element自带组件 2025-06-09 21:18:55 +08:00
meilin.huang
aac4c2b42b fix: 机器计划任务、数据库迁移任务初始化问题修复 2025-06-01 20:39:54 +08:00
meilin.huang
7a17042276 refactor: pool get options支持不创建连接 2025-05-29 20:24:48 +08:00
Coder慌
42fbfd3c47 !136 fix: 连接池修复
Merge pull request !136 from zongyangleo/dev_0529
2025-05-29 04:39:31 +00:00
zongyangleo
e273ade0b0 fix: 连接池修复 2025-05-29 11:38:29 +08:00
meilin.huang
bcaa4563ac fix: ssh tunnel检测导致死锁问题调整 2025-05-27 22:56:54 +08:00
meilin.huang
e0c01d4561 fix: 移除隧道连接时检测是否正在使用 2025-05-26 22:33:51 +08:00
964 changed files with 63199 additions and 410390 deletions

6
.gitignore vendored
View File

@@ -18,9 +18,15 @@
**/vendor/
.idea
.vscode
.qoder
out
server/docs/docker-compose
server/config.yml
server/ip2region.xdb
mayfly-go.log
mayfly-go-linux-amd64/
.DS_Store
__debug_*

47
AGENTS.md Normal file
View File

@@ -0,0 +1,47 @@
# Mayfly-Go
你是一位全栈开发工程师,参与 Mayfly-Go 项目的开发。
## 技术栈
- **后端**: Go 1.26+, GORM, Gin, 自定义 IOC 依赖注入框架
- **前端**: Vue 3 (Composition API) + TypeScript 6.x + Vite 8.x + Element Plus + Tailwind CSS 4.x + Pinia
## 常用命令
```bash
# 后端
cd server && go run main.go
cd server && go test ./...
# 前端
cd frontend && pnpm dev
cd frontend && pnpm build
cd frontend && pnpm lint
```
## 全局边界
-**Always**: 后端遵循 Clean Architecture 分层api → application → domain → infra
-**Always**: 所有错误必须处理,禁止 `result, _ := doSomething()`
-**Always**: 前端所有展示文本使用 i18n`$t()` / `t()`),禁止硬编码
- ⚠️ **Ask first**: 修改 pkg/ 或 common/ 下的公共接口
- 🚫 **Never**: 在 application/domain/infra 层使用 `biz.ErrIsNil`,必须返回 error
- 🚫 **Never**: 前端直接调用 axios必须通过 API 封装
## 详细规范
- @./docs/server/architecture.md — 分层架构与目录规范
- @./docs/server/api.md — API 层规范
- @./docs/server/application.md — Application 层规范
- @./docs/server/domain.md — Domain 层规范
- @./docs/server/infrastructure.md — Infrastructure 层规范
- @./docs/server/concurrent.md — 并发与 Panic 处理
- @./docs/server/security.md — 安全与权限
- @./docs/server/quality.md — 代码质量与 Git 提交
- @./docs/server/i18n.md — 后端国际化规范
- @./docs/frontend/overview.md — 前端综合示例与技术栈
- @./docs/frontend/component.md — 组件开发规范
- @./docs/frontend/api.md — API 定义与调用
- @./docs/frontend/i18n.md — 国际化规范
- @./docs/frontend/style.md — 样式与 UI 规范

View File

@@ -1,16 +1,16 @@
# 构建前端资源
FROM m.daocloud.io/docker.io/node:18-bookworm-slim AS fe-builder
FROM m.daocloud.io/docker.io/node:22-bookworm-slim AS fe-builder
WORKDIR /mayfly
COPY frontend .
RUN yarn config set registry 'https://registry.npmmirror.com' && \
yarn install && \
yarn build
RUN npm config set registry 'https://registry.npmmirror.com' && \
npm install && \
npm run build
# 构建后端资源
FROM m.daocloud.io/docker.io/golang:1.23 AS be-builder
FROM m.daocloud.io/docker.io/golang:1.26 AS be-builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly

View File

@@ -31,7 +31,7 @@
## 前言
Web 版 **统一管理操作平台**,集成了对 Linux 系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis单机、哨兵、集群模式 MongoDB 、Es 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
Web 版 **统一管理操作平台**,集成了对 Linux 系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite、ClickHouse 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis单机、哨兵、集群模式、MongoDB、Elasticsearch、Kafka、Milvus 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
## 开发语言与主要框架
@@ -51,42 +51,32 @@ http://go.mayfly.run
#### 首页
![首页](https://foruda.gitee.com/images/1714378104294194769/149fd257_1240250.png "屏幕截图")
![首页](https://foruda.gitee.com/images/1757163736351080323/afb6b330_1240250.png "屏幕截图")
#### 机器操作
#### 资源管理
##### 状态查看
![资源树](https://foruda.gitee.com/images/1757163958991119284/83eb2171_1240250.png "屏幕截图")
![机器状态查看](https://foruda.gitee.com/images/1714378556642584686/93c46ec0_1240250.png "屏幕截图")
#### 资源操作
##### ssh 终端
![终端操作](https://foruda.gitee.com/images/1757164093410206293/1c7dda30_1240250.png)
![终端操作](https://foruda.gitee.com/images/1714378353790214943/2864ba66_1240250.png "屏幕截图")
##### 文件操作
![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件操作](https://foruda.gitee.com/images/1757164149388450531/0542398c_1240250.png)
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### 数据库操作
![sql编辑器](https://foruda.gitee.com/images/1757164386318836686/c3b17a52_1240250.png)
##### sql 编辑器
![选表查数据](https://foruda.gitee.com/images/1757164281011401749/5109485f_1240250.png)
![sql编辑器](https://foruda.gitee.com/images/1714378747473077515/3c9387c0_1240250.png "屏幕截图")
##### 在线增删改查数据
![选表查数据](https://foruda.gitee.com/images/1714378625059063750/3951e5a8_1240250.png "屏幕截图")
#### Redis 操作
![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### Mongo 操作
![redis操作](https://foruda.gitee.com/images/1757164442298752845/4af1b296_1240250.png)
![mongo操作](https://foruda.gitee.com/images/1714378916425714642/77fc0ed9_1240250.png "屏幕截图")
![es操作](https://foruda.gitee.com/images/1757164553845346963/b5b70381_1240250.png)
![容器操作](https://foruda.gitee.com/images/1757164625186816754/2b195e25_1240250.png)
#### 工单流程审批
![流程审批](https://foruda.gitee.com/images/1714379057627690037/ad136862_1240250.png "屏幕截图")

View File

@@ -28,7 +28,7 @@
## 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, etc.). Additionally, it supports Redis operations (standalone, sentinel, and cluster modes) and MongoDB、Es management, combined with work order process approval functionality to offer enterprises an all-in-one solution for operations and management.
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.
## Development languages and major frameworks
@@ -46,40 +46,30 @@ account/passwordtest/test123.
![首页](https://foruda.gitee.com/images/1714378104294194769/149fd257_1240250.png "屏幕截图")
#### Machine Operation
#### Resource Manage
##### Status
![资源树](https://foruda.gitee.com/images/1757163958991119284/83eb2171_1240250.png "屏幕截图")
![机器状态查看](https://foruda.gitee.com/images/1714378556642584686/93c46ec0_1240250.png "屏幕截图")
#### Resource Operation
##### SSH Terminal
![终端操作](https://foruda.gitee.com/images/1757164093410206293/1c7dda30_1240250.png)
![终端操作](https://foruda.gitee.com/images/1714378353790214943/2864ba66_1240250.png "屏幕截图")
##### File Operation
![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件操作](https://foruda.gitee.com/images/1757164149388450531/0542398c_1240250.png)
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### Database Operation
![sql编辑器](https://foruda.gitee.com/images/1757164386318836686/c3b17a52_1240250.png)
##### SQL Editor
![选表查数据](https://foruda.gitee.com/images/1757164281011401749/5109485f_1240250.png)
![sql编辑器](https://foruda.gitee.com/images/1714378747473077515/3c9387c0_1240250.png "屏幕截图")
##### Add, delete, update and check data online
![选表查数据](https://foruda.gitee.com/images/1714378625059063750/3951e5a8_1240250.png "屏幕截图")
#### Redis Operation
![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### Mongo Operation
![redis操作](https://foruda.gitee.com/images/1757164442298752845/4af1b296_1240250.png)
![mongo操作](https://foruda.gitee.com/images/1714378916425714642/77fc0ed9_1240250.png "屏幕截图")
![es操作](https://foruda.gitee.com/images/1757164553845346963/b5b70381_1240250.png)
![容器操作](https://foruda.gitee.com/images/1757164625186816754/2b195e25_1240250.png)
#### Work order process approval
![流程审批](https://foruda.gitee.com/images/1714379057627690037/ad136862_1240250.png "屏幕截图")

View File

@@ -1,201 +1,312 @@
#bin/bash
#!/bin/bash
#==============================================
# Mayfly-Go Release Build Tool
# 前后端打包编译至指定目录,快速制作发行版
#==============================================
set -e # 遇到错误立即退出
#----------------------------------------------
# 前后端打包编译至指定目录,即快速制作发行版
# 全局配置
#----------------------------------------------
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVER_DIR="${PROJECT_ROOT}/server"
FRONTEND_DIR="${PROJECT_ROOT}/frontend"
BINARY_NAME="mayfly-go"
project_path=`pwd`
# 构建后的二进制执行文件名
exec_file_name="mayfly-go"
# web项目目录
web_folder="${project_path}/frontend"
# server目录
server_folder="${project_path}/server"
# 构建目标配置:名称|操作系统|架构
BUILD_TARGETS=(
"linux-amd64|linux|amd64"
"linux-arm64|linux|arm64"
"windows|windows|amd64"
"mac|darwin|amd64"
)
function echo_red() {
echo -e "\033[1;31m$1\033[0m"
}
function echo_green() {
echo -e "\033[1;32m$1\033[0m"
}
function echo_yellow() {
#----------------------------------------------
# 工具函数
#----------------------------------------------
print_header() {
echo -e "\033[1;33m$1\033[0m"
}
function buildWeb() {
cd ${web_folder}
copy2Server=$1
echo_yellow "-------------------Start bundling frontends-------------------"
yarn run build
if [ "${copy2Server}" == "2" ] ; then
echo_green 'Copy the packaged static files to server/static/static'
rm -rf ${server_folder}/static/static && mkdir -p ${server_folder}/static/static && cp -r ${web_folder}/dist/* ${server_folder}/static/static
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>End of packaging frontend<<<<<<<<<<<<<<<<<<<<\n"
print_success() {
echo -e "\033[1;32m$1\033[0m"
}
function build() {
cd ${project_path}
print_error() {
echo -e "\033[1;31m$1\033[0m" >&2
}
# 打包产物的输出目录
toFolder=$1
os=$2
arch=$3
copyDocScript=$4
print_info() {
echo -e "\033[1;34m$1\033[0m"
}
echo_yellow "-------------------Start a bundle build - ${os}-${arch}-------------------"
to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}
cd ${server_folder}
echo_green "Package build executables..."
#----------------------------------------------
# 构建函数
#----------------------------------------------
build_frontend() {
print_header "\n>>> Building frontend..."
cd "${FRONTEND_DIR}"
npm run build
# 拷贝到 server 静态目录
print_success ">>> Copying frontend assets to server/static/static"
rm -rf "${SERVER_DIR}/static/static"
mkdir -p "${SERVER_DIR}/static/static"
cp -r "${FRONTEND_DIR}/dist/"* "${SERVER_DIR}/static/static/"
cd "${PROJECT_ROOT}"
}
execFileName=${exec_file_name}
# 如果是windows系统,可执行文件需要添加.exe结尾
if [ "${os}" == "windows" ];then
execFileName="${execFileName}.exe"
build_backend() {
local output_dir="$1"
local os_name="$2"
local arch="$3"
local copy_resources="$4"
local binary_file="${BINARY_NAME}"
local target_name="${os_name}-${arch}"
print_header "\n>>> Building backend: ${target_name}"
# Windows 需要 .exe 后缀
if [ "${os_name}" = "windows" ]; then
binary_file="${BINARY_NAME}.exe"
fi
# 编译
cd "${SERVER_DIR}"
go mod tidy
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
if [ -d ${toFolder} ] ; then
echo_green "The desired folder already exists. Clear the folder"
sudo rm -rf ${toFolder}
CGO_ENABLED=0 GOOS="${os_name}" GOARCH="${arch}" \
go build -trimpath -ldflags="-w" -o "${binary_file}" main.go
# 准备输出目录
local bin_dir="${output_dir}/bin"
if [ -d "${output_dir}" ]; then
print_info " Output directory exists, cleaning..."
rm -rf "${output_dir}"
fi
echo_green "Create '${toFolder}' Directory"
mkdir ${toFolder}
echo_green "Move binary to '${toFolder}'"
mv ${server_folder}/${execFileName} ${toFolder}
# if [ "${copy2Server}" == "1" ] ; then
# echo_green "拷贝前端静态页面至'${toFolder}/static'"
# mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
# fi
if [ "${copyDocScript}" == "1" ] ; then
echo_green "Copy resources such as scripts [config.yml.example、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml.example ${toFolder}
mv ${toFolder}/config.yml.example ${toFolder}/config.yml
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/readme_en.txt ${toFolder}
cp ${server_folder}/resources/script/startup.sh ${toFolder}
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
mkdir -p "${bin_dir}"
# 移动二进制文件到 bin 目录
mv "${SERVER_DIR}/${binary_file}" "${bin_dir}/"
# 拷贝资源文件
if [ "${copy_resources}" = "1" ]; then
print_info " Copying config and scripts..."
cp "${SERVER_DIR}/config.yml.example" "${output_dir}/config.yml"
cp "${SERVER_DIR}/readme.txt" "${output_dir}/"
cp "${SERVER_DIR}/readme_en.txt" "${output_dir}/"
cp "${SERVER_DIR}/resources/script/mayfly-go.sh" "${output_dir}/"
chmod +x "${output_dir}/mayfly-go.sh"
fi
echo_yellow ">>>>>>>>>>>>>>>>>>> ${os}-${arch} - Bundle build complete <<<<<<<<<<<<<<<<<<<<\n"
print_success ">>> Build complete: ${target_name}"
cd "${PROJECT_ROOT}"
}
function buildLinuxAmd64() {
build "$1/mayfly-go-linux-amd64" "linux" "amd64" $2
}
function buildLinuxArm64() {
build "$1/mayfly-go-linux-arm64" "linux" "arm64" $2
}
function buildWindows() {
build "$1/mayfly-go-windows" "windows" "amd64" $2
}
function buildMac() {
build "$1/mayfly-go-mac" "darwin" "amd64" $2
}
function buildDocker() {
echo_yellow "-------------------Start building the docker image-------------------"
imageVersion=$1
imageName="mayfly/mayfly-go:${imageVersion}"
docker build --no-cache --platform linux/amd64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
echo_green "The docker image is built -> [${imageName}]"
echo_yellow "-------------------Finished building the docker image-------------------"
}
function buildxDocker() {
echo_yellow "-------------------The docker buildx build image starts-------------------"
imageVersion=$1
imageName="ccr.ccs.tencentyun.com/mayfly/mayfly-go:${imageVersion}"
docker buildx build --no-cache --push --platform linux/amd64,linux/arm64 --build-arg MAYFLY_GO_VERSION="${imageVersion}" -t "${imageName}" .
echo_green "The docker multi-architecture version image is built -> [${imageName}]"
echo_yellow "-------------------The docker buildx image is finished-------------------"
}
function runBuild() {
read -p "Select build version [0 | Other->Other than docker image 1->linux-amd64 2->linux-arm64 3->windows 4->mac 5->docker 6->docker buildx]: " buildType
toPath="."
imageVersion="latest"
copyDocScript="1"
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
# 构建结果的目的路径
read -p "Please enter the build product output directory [default current path]: " toPath
if [ ! -d ${toPath} ] ; then
echo_red "Build product output directory does not exist!"
exit;
fi
if [ "${toPath}" == "" ] ; then
toPath="."
fi
read -p "Whether to copy documents & Scripts [0-> No 1-> Yes][Default yes]: " copyDocScript
if [ "${copyDocScript}" == "" ] ; then
copyDocScript="1"
fi
# 进入目标路径,并赋值全路径
cd ${toPath}
toPath=`pwd`
# read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
runBuildWeb="2"
# 编译web前端
buildWeb ${runBuildWeb}
build_docker() {
local version="$1"
local use_buildx="$2"
local image_name
local build_cmd
if [ "${use_buildx}" = "1" ]; then
image_name="ccr.ccs.tencentyun.com/mayfly/mayfly-go:${version}"
build_cmd="docker buildx build --no-cache --push --platform linux/amd64,linux/arm64"
print_header "\n>>> Building Docker image (multi-arch): ${image_name}"
else
image_name="mayfly/mayfly-go:${version}"
build_cmd="docker build --no-cache --platform linux/amd64"
print_header "\n>>> Building Docker image: ${image_name}"
fi
${build_cmd} --build-arg MAYFLY_GO_VERSION="${version}" -t "${image_name}" "${PROJECT_ROOT}"
print_success ">>> Docker image built: ${image_name}"
}
if [[ "${buildType}" == "5" ]] || [[ "${buildType}" == "6" ]] ; then
read -p "Please enter the docker image version (default latest) : " imageVersion
cleanup_frontend() {
print_info "\n>>> Cleaning up temporary frontend assets..."
rm -rf "${SERVER_DIR}/static/static/"{assets,config.js,index.html}
print_success ">>> Cleanup complete"
}
if [ "${imageVersion}" == "" ] ; then
imageVersion="latest"
fi
compress_package() {
local source_dir="$1"
local output_dir="$2"
local package_name
package_name="$(basename "${source_dir}")"
print_header "\n>>> Compressing package: ${package_name}"
cd "${output_dir}"
# 统一使用 zip 格式,跨平台兼容性最好
zip -r "${package_name}.zip" "${package_name}"/
rm -rf "${package_name}"
print_success ">>> Compressed: ${package_name}.zip"
cd "${PROJECT_ROOT}"
}
#----------------------------------------------
# 主流程
#----------------------------------------------
main() {
# 显示菜单
print_header "========================================"
print_header " Mayfly-Go Release Build Tool"
print_header "========================================"
echo ""
echo "Build Options:"
echo " [0] All Platforms (linux-amd64, linux-arm64, windows, mac)"
echo " [1] Linux AMD64"
echo " [2] Linux ARM64"
echo " [3] Windows"
echo " [4] macOS"
echo " [5] Docker Image"
echo " [6] Docker Multi-arch (buildx)"
echo ""
read -p "Select build option [0-6] (default: 0): " build_type
build_type=${build_type:-0}
# 验证输入
if ! [[ "${build_type}" =~ ^[0-6]$ ]]; then
print_error "Error: Invalid option. Please enter a number between 0 and 6."
exit 1
fi
case ${buildType} in
"1")
buildLinuxAmd64 ${toPath} ${copyDocScript}
;;
"2")
buildLinuxArm64 ${toPath} ${copyDocScript}
;;
"3")
buildWindows ${toPath} ${copyDocScript}
;;
"4")
buildMac ${toPath} ${copyDocScript}
;;
# 初始化配置
local output_dir="."
local docker_version="latest"
local copy_resources="1"
local compress_output="0"
local is_docker=0
# Docker 构建
if [[ "${build_type}" == "5" || "${build_type}" == "6" ]]; then
is_docker=1
echo ""
read -p "Enter Docker image version (default: latest): " docker_version
docker_version=${docker_version:-latest}
else
# 二进制构建
echo ""
read -p "Enter output directory (default: current): " output_dir
output_dir=${output_dir:-.}
# 验证并获取绝对路径
if [ "${output_dir}" != "." ] && [ ! -d "${output_dir}" ]; then
print_error "Error: Directory '${output_dir}' does not exist."
exit 1
fi
output_dir="$(cd "${output_dir}" && pwd)"
echo ""
read -p "Copy config & scripts? [Y/n] (default: Y): " copy_input
if [ "$(to_lower "${copy_input}")" = "n" ]; then
copy_resources="0"
fi
echo ""
read -p "Compress package? [y/N] (default: N): " compress_input
if [ "$(to_lower "${compress_input}")" = "y" ]; then
compress_output="1"
fi
# 构建前端
echo ""
build_frontend
fi
# 显示配置摘要
echo ""
print_header "Build Configuration:"
# 获取构建类型名称
local type_names=("All Platforms" "Linux AMD64" "Linux ARM64" "Windows" "macOS" "Docker Image" "Docker Multi-arch")
echo " Type: ${type_names[${build_type}]}"
if [ "${is_docker}" = "1" ]; then
echo " Version: ${docker_version}"
else
echo " Output: ${output_dir}"
echo " Resources: $([ "${copy_resources}" = "1" ] && echo "Yes" || echo "No")"
echo " Compress: $([ "${compress_output}" = "1" ] && echo "Yes" || echo "No")"
fi
echo ""
# 确认构建
read -p "Continue? [Y/n] (default: Y): " confirm
if [ "$(to_lower "${confirm}")" = "n" ]; then
print_info "Build cancelled."
exit 0
fi
# 执行构建
echo ""
print_header "Starting build..."
case "${build_type}" in
"1"|"2"|"3"|"4")
# 单个平台构建
local target="${BUILD_TARGETS[$((build_type-1))]}"
IFS='|' read -r name os arch <<< "${target}"
build_backend "${output_dir}/mayfly-go-${name}" "${os}" "${arch}" "${copy_resources}"
;;
"5")
buildDocker ${imageVersion}
;;
build_docker "${docker_version}" "0"
;;
"6")
buildxDocker ${imageVersion}
;;
build_docker "${docker_version}" "1"
;;
*)
buildLinuxAmd64 ${toPath} ${copyDocScript}
buildLinuxArm64 ${toPath} ${copyDocScript}
buildWindows ${toPath} ${copyDocScript}
buildMac ${toPath} ${copyDocScript}
;;
# 构建所有平台
print_info "Building all platforms..."
for target in "${BUILD_TARGETS[@]}"; do
IFS='|' read -r name os arch <<< "${target}"
build_backend "${output_dir}/mayfly-go-${name}" "${os}" "${arch}" "${copy_resources}"
done
;;
esac
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
echo_green "Delete static assets under ['${server_folder}/static/static']."
# 删除静态资源文件保留一个favicon.ico否则后端启动会报错
rm -rf ${server_folder}/static/static/assets
rm -rf ${server_folder}/static/static/config.js
rm -rf ${server_folder}/static/static/index.html
# 清理临时文件
if [ "${is_docker}" = "0" ]; then
cleanup_frontend
fi
# 压缩输出
if [ "${compress_output}" = "1" ] && [ "${is_docker}" = "0" ]; then
case "${build_type}" in
"1"|"2"|"3"|"4")
local target="${BUILD_TARGETS[$((build_type-1))]}"
IFS='|' read -r name os arch <<< "${target}"
compress_package "${output_dir}/mayfly-go-${name}" "${output_dir}"
;;
*)
print_info "Compressing all packages..."
for target in "${BUILD_TARGETS[@]}"; do
IFS='|' read -r name os arch <<< "${target}"
compress_package "${output_dir}/mayfly-go-${name}" "${output_dir}"
done
;;
esac
fi
# 完成
echo ""
print_success "========================================"
print_success " Build Completed Successfully!"
print_success "========================================"
}
runBuild
# 执行主函数
main

37
docs/frontend/api.md Normal file
View File

@@ -0,0 +1,37 @@
---
trigger: always_on
---
# API 定义与调用规范
## API 定义
```typescript
import Api from '@/common/Api';
export const accountApi = {
list: Api.newGet('/sys/accounts'),
save: Api.newPost('/sys/accounts'),
update: Api.newPut('/sys/accounts/{id}'),
del: Api.newDelete('/sys/accounts/{id}'),
changeStatus: Api.newPut('/sys/accounts/change-status/{id}/{status}'),
};
```
## 调用模式
```typescript
// 简单请求
await accountApi.del.request({ id: row.id });
// 响应式(用于 loading 状态)
const { execute, isFetching } = accountApi.list.useApi();
// 表格集成
<page-table :page-api="accountApi.list" />
```
## 边界
-**Always**: API 定义集中放在 `api.ts`
- 🚫 **Never**: 直接调用 axios必须通过 API 封装

164
docs/frontend/component.md Normal file
View File

@@ -0,0 +1,164 @@
---
trigger: always_on
---
# 组件开发规范
## 代码组织顺序
```
Imports
Props/Emits
常量定义 (as const)
类型定义
响应式数据
计算属性
监听器
工具函数
事件处理方法 (on 开头)
```
## 命名规范
- **事件方法**: 必须以 `on` 开头(`onSubmit`, `onDelete`, `onEdit`
- **变量/函数**: camelCase
- **常量**: UPPER_SNAKE_CASE + `as const`
- **组件**: PascalCase
- **文件**: 组件用 PascalCase其他用小写
## Props & Emits
```vue
<script lang="ts" setup>
interface Props {
visible?: boolean;
data?: any;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
data: null,
});
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'success'): void;
}>();
</script>
```
## 双向绑定规范
### 使用 defineModel (Vue 3.4+)
**必须使用 `defineModel` 实现双向绑定**,替代旧的 `computed` + `emit('update:xxx')` 模式。
#### 基本用法
```vue
<script lang="ts" setup>
// 单个 v-model
const modelValue = defineModel<string>('modelValue', {
default: '',
});
// 命名 v-model
const authCertName = defineModel<string>('authCertName');
const machineId = defineModel<number>('machineId');
</script>
```
#### 内部字段联动更新
当组件内部有多个字段,需要联动更新外部的 `modelValue` 时,使用 `watch` 监听:
```vue
<script lang="ts" setup>
import { watch } from 'vue';
const authCertName = defineModel<string>('authCertName');
const machineName = defineModel<string>('machineName');
const selectNode = defineModel<string>('modelValue', { default: '' });
// 监听内部字段变化,自动更新 selectNode
watch(
[authCertName, machineName],
() => {
selectNode.value = authCertName.value
? `${machineName.value} > ${authCertName.value}`
: '';
},
{ immediate: true }
);
</script>
```
#### 规范要点
-**Always**: 使用 `defineModel` 替代 `computed` + `emit('update:xxx')`
-**Always**: 为 `defineModel` 提供合适的 `default`
- 🚫 **Never**: 使用旧的 `computed` getter/setter 模式实现双向绑定
## 图标使用规范
### 统一使用 SvgIcon 组件
**所有图标必须使用 `SvgIcon` 组件**,禁止使用 `<el-icon>` 配合导入图标组件。
```vue
<!-- 正确使用 SvgIcon -->
<SvgIcon name="Monitor" :size="20" />
<SvgIcon name="check" class="text-success" />
<!-- 错误使用 el-icon + 导入 -->
<el-icon><Check /></el-icon>
```
**规范要点**
- ✅ 使用 `name` 属性指定图标,`size` 属性控制大小
- ✅ 图标名称使用 PascalCase 或 kebab-case
- 🚫 禁止使用 `<el-icon>` 和导入 `@element-plus/icons-vue`
- 🚫 禁止通过 class 设置图标大小
### 自定义 SVG 图标
项目支持在 `assets/icon` 目录下添加自定义 SVG 图标。
#### 目录结构
```
frontend/src/assets/icon/
├── db/ # 数据库图标mysql.svg, postgres.svg...
├── machine/ # 机器图标
└── ...
```
#### 使用方法
**格式**: `name="icon {目录}/{文件名}"`(不含 .svg
```vue
<SvgIcon name="icon db/mysql" :size="20" />
```
#### 添加步骤
1. 将 SVG 文件放到 `frontend/src/assets/icon/` 对应子目录
2. 文件名使用小写 + 连字符(如 `mysql.svg`
3. 使用 `name="icon db/mysql"` 引用
#### 注意事项
- ✅ SVG 必须有 `viewBox` 属性
- ✅ 使用 `size` 属性控制大小
- ✅ 图标颜色继承当前元素的 `color`
- 🚫 不要在 SVG 中硬编码颜色值
- 🚫 文件名不要使用大写或下划线
## 边界
-**Always**: 使用 Composition API + `<script setup>`
-**Always**: 事件方法以 `on` 开头
-**Always**: 移除无用的导入import和无用的字段、变量、函数
- 🚫 **Never**: 保留未使用的代码或注释掉的代码
- 🚫 **Never**: 使用固定高度计算,优先用 Flexbox

45
docs/frontend/i18n.md Normal file
View File

@@ -0,0 +1,45 @@
---
trigger: always_on
---
# 国际化规范
## 文件组织
```
src/i18n/
├── zh-cn/
│ ├── common.ts
│ ├── system.ts
│ ├── ai.ts
│ └── ...
└── en/
├── common.ts
├── system.ts
└── ...
```
- 按模块拆分,每个业务模块一个文件
- 命名空间以模块名为根 key`system.account.name`
## 使用方式
```vue
<template>
<h1>{{ $t('system.account.name') }}</h1>
<el-button>{{ $t('common.save') }}</el-button>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const message = t('common.success');
</script>
```
## 边界
-**Always**: 所有展示文本通过 `$t()``t()` 获取
-**Always**: 枚举的 label 必须是国际化 key
-**Always**: 新增模块时在 `zh-cn/``en/` 下同时创建语言文件
- 🚫 **Never**: 在组件中直接写中文/英文文本

134
docs/frontend/overview.md Normal file
View File

@@ -0,0 +1,134 @@
---
trigger: always_on
---
# 前端开发规范
## 技术栈
Vue 3 (Composition API) + TypeScript 5.x + Vite 5.x + Element Plus + Tailwind CSS 3.x + Pinia
## 综合示例:列表页 + 编辑对话框
### 枚举定义
```typescript
// src/views/system/enums.ts
import { EnumValue } from '@/common/Enum';
export const AccountStatusEnum = {
Enable: EnumValue.of(1, 'system.account.statusEnable').tagTypeSuccess(),
Disable: EnumValue.of(-1, 'system.account.statusDisable').tagTypeDanger(),
};
```
### API 定义
```typescript
// src/views/system/api.ts
import Api from '@/common/Api';
export const accountApi = {
list: Api.newGet('/sys/accounts'),
save: Api.newPost('/sys/accounts'),
update: Api.newPut('/sys/accounts/{id}'),
del: Api.newDelete('/sys/accounts/{id}'),
};
```
### 列表页
```vue
<template>
<page-table ref="pageTableRef" :page-api="accountApi.list" :search-items="searchItems" v-model:query-form="query" :columns="columns">
<template #tableHeader>
<el-button v-auth="'account:add'" type="primary" @click="onAdd">
{{ $t('common.create') }}
</el-button>
</template>
<template #action="{ data }">
<el-button link v-auth="'account:edit'" @click="onEdit(data)">
{{ $t('common.edit') }}
</el-button>
</template>
</page-table>
<AccountEdit v-model:visible="editVisible" :data="editData" @success="onEditSuccess" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { accountApi } from '../api';
import { AccountStatusEnum } from '../enums';
import PageTable from '@/components/pagetable/PageTable.vue';
import { SearchItem, TableColumn } from '@/components/pagetable';
import AccountEdit from './AccountEdit.vue';
const pageTableRef = ref();
const editVisible = ref(false);
const editData = ref<any>(null);
const query = ref({ username: '', status: null as number | null, pageNum: 1, pageSize: 10 });
const searchItems = [SearchItem.input('username', 'common.username'), SearchItem.select('status', 'common.status', AccountStatusEnum)];
const columns = [
TableColumn.new('username', 'common.username'),
TableColumn.new('status', 'common.status').typeTag(AccountStatusEnum),
TableColumn.new('action', 'common.operation').isSlot().fixedRight(),
];
const onAdd = () => { editData.value = null; editVisible.value = true; };
const onEdit = (row: any) => { editData.value = row; editVisible.value = true; };
const onEditSuccess = () => { editVisible.value = false; pageTableRef.value?.search(); };
</script>
```
### 编辑对话框
```vue
<template>
<el-dialog v-model="visible" :title="dialogTitle" width="500px" @close="onDialogClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item :label="$t('common.username')" prop="username">
<el-input v-model="form.username" :disabled="!!form.id" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onCancel">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ $t('common.confirm') }}</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { accountApi } from '../api';
import { useI18nOperateSuccessMsg } from '@/hooks/useI18n';
const props = defineProps<{ visible?: boolean; data?: any }>();
const emit = defineEmits(['update:visible', 'success']);
const formRef = ref();
const submitting = ref(false);
const form = reactive({ id: undefined, username: '', name: '', status: 1 });
const visible = computed({ get: () => props.visible, set: (val) => emit('update:visible', val) });
const { t } = useI18n();
const dialogTitle = computed(() => (form.id ? t('system.account.editAccount') : t('system.account.addAccount')));
watch(() => props.data, (newVal) => { newVal ? Object.assign(form, newVal) : resetForm(); }, { immediate: true });
const resetForm = () => { form.id = undefined; form.username = ''; form.name = ''; form.status = 1; formRef.value?.clearValidate(); };
const onSubmit = async () => {
await formRef.value?.validate();
submitting.value = true;
try {
form.id ? await accountApi.update.request(form) : await accountApi.save.request(form);
useI18nOperateSuccessMsg();
visible.value = false;
emit('success');
} finally { submitting.value = false; }
};
const onCancel = () => { visible.value = false; };
const onDialogClose = () => { resetForm(); };
</script>
```

35
docs/frontend/style.md Normal file
View File

@@ -0,0 +1,35 @@
---
trigger: always_on
---
# 样式与 UI 规范
## Tailwind CSS
优先使用 Tailwind 工具类,支持 `dark:` 前缀:
```vue
<template>
<div class="flex items-center justify-between p-4 bg-white dark:bg-gray-800">
<span class="text-sm text-gray-600 dark:text-gray-300">Label</span>
</div>
</template>
```
## 权限控制
按钮权限使用 `v-auth` 指令:
```vue
<el-button v-auth="'account:add'" type="primary" @click="onAdd">新增</el-button>
```
## 类型安全
- 避免 `any`,使用可选链 `?.`
- 使用 TypeScript 严格模式
## 边界
-**Always**: 优先使用 Tailwind CSS
- 🚫 **Never**: 使用固定高度计算,优先用 Flexbox

59
docs/server/api.md Normal file
View File

@@ -0,0 +1,59 @@
---
trigger: always_on
---
# API 层规范
## Handler 标准结构
```go
type Db struct {
dbApp application.Db `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
}
// @router /api/dbs [get]
func (d *Db) Dbs(rc *req.Ctx) {
queryCond := req.BindQuery[entity.DbQuery](rc) // 1. 绑定参数
loginAccount := rc.GetLoginAccount() // 2. 获取上下文
result, err := d.dbApp.GetPageList(queryCond) // 3. 调用应用层
biz.ErrIsNil(err) // 4. 断言错误仅API层
rc.ResData = result // 5. 返回结果
}
```
## 路由配置
```go
func (d *Db) ReqConfs() *req.Confs {
return req.NewConfs("/dbs",
req.NewGet("", d.Dbs),
req.NewPost("", d.Save).Log(req.NewLogSaveI(imsg.LogDbSave)),
req.NewDelete(":dbId", d.DeleteDb).Log(req.NewLogSaveI(imsg.LogDbDelete)),
)
}
```
## 断言边界
**✅ API 层可用断言**
```go
func (d *Db) Save(rc *req.Ctx) {
form := req.BindFormAndValid[form.DbForm](rc)
biz.IsTrue(form.InstanceId > 0, "实例ID不能为空")
biz.ErrIsNil(d.dbApp.SaveDb(rc, &entity.Db{Name: form.Name}))
rc.ResData = "保存成功"
}
```
**🚫 Application 层禁止断言,必须返回 error**
```go
func (d *dbAppImpl) SaveDb(ctx context.Context, db *entity.Db) error {
if db.Name == "" {
return errorx.NewBiz("名称不能为空")
}
return d.Save(ctx, db)
}
```

View File

@@ -0,0 +1,51 @@
---
trigger: always_on
---
# Application 层规范
## 接口与实现
```go
type Db interface {
base.App[*entity.Db]
GetPageList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
SaveDb(ctx context.Context, entity *entity.Db) error
}
type dbAppImpl struct {
base.AppImpl[*entity.Db, repository.Db]
dbInstanceApp Instance `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
}
var _ Db = (*dbAppImpl)(nil)
func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
// 1. 参数校验返回error
if dbEntity.Name == "" {
return errorx.NewBiz("名称不能为空")
}
// 2. 业务检查
oldDb := &entity.Db{Name: dbEntity.Name, InstanceId: dbEntity.InstanceId}
if dbEntity.Id == 0 && d.GetByCond(oldDb) == nil {
return errorx.NewBizI(ctx, imsg.ErrDbNameExist)
}
// 3. 持久化
return d.Save(ctx, dbEntity)
}
```
## 错误处理
```go
// 普通业务错误
return errorx.NewBiz("数据库名称已存在")
// 国际化错误
return errorx.NewBizI(ctx, imsg.ErrDbNameExist)
```
## 边界
-**Always**: 参数校验后返回 error禁止 panic
- 🚫 **Never**: 在 application 层使用 `biz.ErrIsNil``biz.IsTrue`

View File

@@ -0,0 +1,59 @@
---
trigger: always_on
---
# Go 分层架构与目录规范
## 分层目录
```
internal/{module}/
├── api/ # HTTP请求处理、参数绑定、响应返回
│ ├── form/ # 请求表单结构体
│ └── vo/ # 响应视图对象
├── application/ # 业务逻辑编排、事务控制
│ └── dto/ # 数据传输对象
├── domain/ # 核心业务逻辑、实体定义
│ ├── entity/ # 领域实体
│ └── repository/ # 仓储接口定义
├── infra/ # 数据持久化、外部服务调用
│ └── persistence/ # 仓储实现
├── imsg/ # 国际化消息定义
└── init/ # 模块初始化(依赖注册、路由注册)
```
## 命名规范
- **模块/包名**: 小写无分隔符(`machine`, `dbinstance`
- **文件名**: 小写+下划线(`db.go`, `db_sql_exec.go`
- **结构体/常量**: PascalCase
- **接口**: 以 `er` 结尾或名词(`Reader`, `Repository`
- **变量/函数**: camelCase
## IOC 依赖注入
```go
// 1. 定义接口
type Db interface {
base.App[*entity.Db]
GetPageList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
}
// 2. 实现接口并注入依赖
type dbAppImpl struct {
base.AppImpl[*entity.Db, repository.Db]
dbInstanceApp Instance `inject:"T"` // T=按类型注入
tagApp tagapp.TagTree `inject:"T"`
}
var _ Db = (*dbAppImpl)(nil)
// 3. 模块初始化时注册
func init() {
ioc.Register(&dbAppImpl{})
}
```
## 边界
-**Always**: 依赖单向流动,上层依赖下层接口,禁止反向依赖
- 🚫 **Never**: 跨层直接调用具体实现,必须通过接口

69
docs/server/concurrent.md Normal file
View File

@@ -0,0 +1,69 @@
---
trigger: always_on
---
# 并发与 Panic 处理规范
## 统一 Panic 捕获gox.Recover
**核心原则**:严禁手动编写 `defer func() { recover() }`,必须使用 `gox.Recover()`
### 场景1仅记录日志
```go
func (s *Service) ProcessData(data []byte) {
defer gox.Recover()
result := parseData(data)
saveToDB(result)
}
```
### 场景2Panic 转 Error 返回
```go
func (s *Service) SaveUser(ctx context.Context, user *entity.User) (err error) {
defer gox.Recover(func(e error) {
err = fmt.Errorf("保存用户失败: %w", e)
})
if err := validateUser(user); err != nil {
return err
}
return s.repo.Insert(ctx, user)
}
```
### 场景3Goroutine 安全启动
```go
// ✅ 推荐
gox.Go(func() {
sendNotification(userId, message)
})
// 🚫 禁止
go func() {
sendNotification(userId, message)
}()
```
## Context 传递
所有阻塞操作必须接受 `context.Context`
```go
func (d *dbAppImpl) SaveDb(ctx context.Context, entity *entity.Db) error {
return d.GetRepo().Insert(ctx, entity)
}
```
## 错误组使用
```go
eg, ctx := errgroup.WithContext(context.Background())
for _, task := range tasks {
eg.Go(func() error {
return process(ctx, task)
})
}
err := eg.Wait()
```

44
docs/server/domain.md Normal file
View File

@@ -0,0 +1,44 @@
---
trigger: always_on
---
# Domain 层规范
## 实体定义
```go
package entity
import "mayfly-go/pkg/model"
type Db struct {
model.Model // 必须嵌入基础模型
model.ExtraData // 辅助字段(展示用、非查询条件)
Code string `json:"code" gorm:"size:32;not null;index:idx_db_code"`
Name string `json:"name" gorm:"size:255;not null;"`
InstanceId uint64 `json:"instanceId" gorm:"not null;"`
}
type Status int8
const (
StatusActive Status = 1
StatusInactive Status = 0
)
```
## ExtraData 使用原则
-**使用 ExtraData**: 前端展示字段、关联名称、状态文本、可选扩展信息
- 🚫 **必须独立字段**: 查询条件、排序字段、分组统计、索引字段、核心业务字段
## Repository 接口
```go
package repository
type Db interface {
base.Repo[*entity.Db]
GetDbList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
}
```

113
docs/server/i18n.md Normal file
View File

@@ -0,0 +1,113 @@
---
trigger: always_on
---
# 后端国际化i18n规范
## 文件组织
每个业务模块在 `internal/{module}/imsg/` 目录下维护国际化消息:
```
internal/{module}/imsg/
├── imsg.go # 消息ID常量定义MsgId
├── zh_cn.go # 中文语言包
└── en.go # 英文语言包
```
### imsg.go — 常量定义
```go
package imsg
import (
"mayfly-go/internal/pkg/consts"
"mayfly-go/pkg/i18n"
)
func init() {
i18n.AppendLangMsg(i18n.Zh_CN, Zh_CN)
i18n.AppendLangMsg(i18n.En, En)
}
const (
LogDbSave = iota + consts.ImsgNumDb
LogDbDelete
ErrDbNameExist
)
```
### zh_cn.go — 中文语言包
```go
package imsg
import "mayfly-go/pkg/i18n"
var Zh_CN = map[i18n.MsgId]string{
LogDbSave: "保存数据库配置",
LogDbDelete: "删除数据库配置",
ErrDbNameExist: "该实例下数据库名已存在",
}
```
### en.go — 英文语言包
```go
package imsg
import "mayfly-go/pkg/i18n"
var En = map[i18n.MsgId]string{
LogDbSave: "Save database configuration",
LogDbDelete: "Delete database configuration",
ErrDbNameExist: "The database name already exists in this instance",
}
```
## 消息ID编号规则
各模块起始编号定义在 `internal/pkg/consts/consts.go`,新增模块需注册唯一起始值:
```go
const (
ImsgNumSys = 10000
ImsgNumAuth = 20000
ImsgNumDb = 60000
ImsgNumAi = 140000
// ...
)
```
模块内使用 `iota + consts.ImsgNum{Xxx}` 自增,避免全局冲突。
## 使用方式
### 国际化业务错误
```go
return errorx.NewBizI(ctx, imsg.ErrDbNameExist)
```
### 国际化操作日志
```go
req.NewPost("", d.Save).Log(req.NewLogSaveI(imsg.LogDbSave))
```
### 模板变量替换
```go
// 定义ErrDbNotAccess = "未配置数据库【{{.dbName}}】的操作权限"
errorx.NewBizI(ctx, imsg.ErrDbNotAccess, "dbName", dbName)
// 或直接使用 i18n 包
i18n.T(imsg.DataSyncSuccessMsg, "count", 100)
i18n.TC(ctx, imsg.DataSyncSuccessMsg, "count", 100)
```
## 边界
-**Always**: 新增模块时必须同步创建 `imsg.go``zh_cn.go``en.go` 三个文件
-**Always**: 操作日志消息以 `Log` 开头,业务错误以 `Err` 开头
- 🚫 **Never**: 在 `errorx.NewBiz("硬编码中文")` 中直接使用硬编码文本,必须走国际化

View File

@@ -0,0 +1,45 @@
---
trigger: always_on
---
# Infrastructure 层规范
## Repository 实现
```go
package persistence
type dbRepoImpl struct {
base.RepoImpl[*entity.Db]
}
func newDbRepo() repository.Db {
return &dbRepoImpl{}
}
func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error) {
pd := model.NewCond().
Eq("instance_id", condition.InstanceId).
In("code", condition.Codes).
Like("name", condition.Name)
list := []*entity.DbListPO{}
return gormx.PageByCond(d.GetModel(), pd, condition.PageParam, list)
}
```
## GORMX 常用操作
```go
// 条件构建
pd := model.NewCond().Eq("status", 1).In("id", ids).Like("name", keyword)
// 分页查询
result, err := gormx.PageByCond(repo.GetModel(), pd, pageParam, &list)
// 单条查询
err := gormx.GetByCond(repo.GetModel(), pd, &entity)
// 更新
err := gormx.UpdateByCond(repo.GetModel(), values, pd)
```

62
docs/server/quality.md Normal file
View File

@@ -0,0 +1,62 @@
---
trigger: always_on
---
# 代码质量与 Git 规范
## 函数长度
- 单个函数不超过 100 行
- 复杂逻辑拆分为私有方法
## Error 处理
```go
// ✅ 完整处理
result, err := doSomething()
if err != nil {
logx.Errorf("操作失败: %v", err)
return errorx.NewBiz("操作失败")
}
// 🚫 忽略错误
result, _ := doSomething()
```
## 资源释放
```go
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
```
## 魔法数字
```go
const MaxRetryCount = 3
if retry > MaxRetryCount { ... } // ✅
if retry > 3 { ... } // 🚫
```
## Git 提交格式
```
<type>(<scope>): <subject>
<body>
```
**Type 类型**: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
**示例**:
```
feat(db): 添加数据库备份功能
- 实现定时备份任务
- 支持增量备份和全量备份
Closes #123
```

26
docs/server/security.md Normal file
View File

@@ -0,0 +1,26 @@
---
trigger: always_on
---
# 安全与权限规范
## 权限控制
```go
// 路由级别
req.NewPost(":dbId/exec-sql", d.ExecSql).RequiredPermissionCode("db:sqlscript:run")
// 代码级别
biz.IsTrue(account.HasPermission("db:sqlscript:run"), "无权限执行SQL")
```
## 敏感信息
- 资源密码使用 AES 加密存储
- `aes.key``jwt.key` 必须使用随机字符串
## OWASP 安全准则
- 防范 SQL 注入:使用参数化查询
- 防范 XSS输出转义
- 防范 CSRF配合前端同源策略

View File

@@ -5,6 +5,10 @@ VITE_PORT = 8889
VITE_OPEN = false
# public path 配置线上环境路径(打包)
VITE_PUBLIC_PATH = ''
VITE_PUBLIC_PATH = './'
VITE_EDITOR=idea
VITE_EDITOR=idea
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

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

View File

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

View File

@@ -1,76 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
extends: ['plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
rules: {
// http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/html-self-closing': 'off',
'vue/no-multiple-template-root': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-model-argument': 'off',
'vue/no-arrow-functions-in-watch': 'off',
'vue/no-template-key': 'off',
'vue/no-v-html': 'off',
'vue/no-unused-vars': 'off',
'vue/comment-directive': 'off',
'vue/no-parsing-error': 'off',
'vue/no-deprecated-v-on-native-modifier': 'off',
'vue/multi-word-component-names': 'off',
'no-useless-escape': 'off',
'no-sparse-arrays': 'off',
'no-prototype-builtins': 'off',
'no-constant-condition': 'off',
'no-use-before-define': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'generator-star-spacing': 'off',
'no-unreachable': 'off',
'no-multiple-template-root': 'off',
'no-unused-vars': 'off',
'no-v-model-argument': 'off',
'no-case-declarations': 'off',
// 'no-console': 'error',
'no-redeclare': 'off',
},
};

106
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,106 @@
// import js from '@eslint/js';
// import tseslint from 'typescript-eslint';
// import vuePlugin from 'eslint-plugin-vue';
// import vueParser from 'vue-eslint-parser';
// export default tseslint.config(
// {
// ignores: [
// '*.sh',
// 'node_modules',
// 'lib',
// '*.md',
// '*.scss',
// '*.woff',
// '*.ttf',
// '.vscode',
// '.idea',
// 'dist',
// 'mock',
// 'public',
// 'bin',
// 'build',
// 'config',
// 'index.html',
// 'src/assets',
// ],
// },
// js.configs.recommended,
// ...tseslint.configs.recommended,
// ...vuePlugin.configs['flat/recommended'],
// {
// files: ['**/*.{js,ts,tsx,vue}'],
// languageOptions: {
// ecmaVersion: 2021,
// sourceType: 'module',
// parser: vueParser,
// parserOptions: {
// parser: tseslint.parser,
// },
// globals: {
// browser: true,
// es2021: true,
// node: true,
// console: true,
// window: true,
// document: true,
// setTimeout: true,
// },
// },
// plugins: {
// vue: vuePlugin,
// },
// rules: {
// '@typescript-eslint/ban-ts-ignore': 'off',
// '@typescript-eslint/explicit-function-return-type': 'off',
// '@typescript-eslint/no-explicit-any': 'off',
// '@typescript-eslint/no-var-requires': 'off',
// '@typescript-eslint/no-empty-function': 'off',
// '@typescript-eslint/no-use-before-define': 'off',
// '@typescript-eslint/ban-ts-comment': 'off',
// '@typescript-eslint/ban-types': 'off',
// '@typescript-eslint/no-non-null-assertion': 'off',
// '@typescript-eslint/explicit-module-boundary-types': 'off',
// '@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
// '@typescript-eslint/no-unused-vars': 'off',
// // Vue rules
// 'vue/html-indent': ['error', 4],
// 'vue/script-indent': ['error', 4],
// 'vue/custom-event-name-casing': 'off',
// 'vue/attributes-order': 'off',
// 'vue/one-component-per-file': 'off',
// 'vue/html-closing-bracket-newline': 'off',
// 'vue/max-attributes-per-line': 'off',
// 'vue/multiline-html-element-content-newline': 'off',
// 'vue/singleline-html-element-content-newline': 'off',
// 'vue/attribute-hyphenation': 'off',
// 'vue/html-self-closing': 'off',
// 'vue/no-multiple-template-root': 'off',
// 'vue/require-default-prop': 'off',
// 'vue/no-v-model-argument': 'off',
// 'vue/no-arrow-functions-in-watch': 'off',
// 'vue/no-template-key': 'off',
// 'vue/no-v-html': 'off',
// 'vue/no-unused-vars': 'off',
// 'vue/comment-directive': 'off',
// 'vue/no-parsing-error': 'off',
// 'vue/no-deprecated-v-on-native-modifier': 'off',
// 'vue/multi-word-component-names': 'off',
// // JavaScript rules
// 'no-useless-escape': 'off',
// 'no-sparse-arrays': 'off',
// 'no-prototype-builtins': 'off',
// 'no-constant-condition': 'off',
// 'no-use-before-define': 'off',
// 'no-restricted-globals': 'off',
// 'no-restricted-syntax': 'off',
// 'generator-star-spacing': 'off',
// 'no-unreachable': 'off',
// 'no-unused-vars': 'off',
// 'no-case-declarations': 'off',
// 'no-redeclare': 'off',
// },
// }
// );

View File

@@ -1,7 +1,10 @@
<!DOCTYPE html>
<html lang="zh_CN">
<app-config />
<head>
<base href="/" />
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -14,7 +17,7 @@
<body>
<div id="app"></div>
<script type="application/javascript" src="./config.js"></script>
<script type="application/javascript" src="/config.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>

View File

@@ -10,64 +10,69 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@logicflow/core": "^2.0.13",
"@logicflow/extension": "^2.0.18",
"@vueuse/core": "^13.2.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"asciinema-player": "^3.9.0",
"axios": "^1.6.2",
"@element-plus/icons-vue": "^2.3.2",
"@logicflow/core": "^2.2.1",
"@logicflow/extension": "^2.2.1",
"@vueuse/core": "^14.3.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"asciinema-player": "^3.15.1",
"axios": "^1.16.0",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.9.11",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.2",
"monaco-sql-languages": "^0.14.0",
"monaco-themes": "^0.4.5",
"dayjs": "^1.11.20",
"echarts": "^6.0.0",
"element-plus": "^2.14.0",
"js-base64": "^3.7.8",
"jsencrypt": "^3.5.4",
"json-bigint": "^1.0.0",
"mermaid": "^11.15.0",
"monaco-editor": "^0.55.1",
"monaco-sql-languages": "^1.0.0",
"nprogress": "^0.2.0",
"pinia": "^3.0.2",
"qrcode.vue": "^3.6.0",
"pinia": "^3.0.4",
"qrcode.vue": "^3.9.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.6",
"splitpanes": "^4.0.3",
"sql-formatter": "^15.6.1",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.14",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.1",
"vuedraggable": "^4.1.0"
"shiki": "^4.0.2",
"shiki-stream": "^0.1.4",
"sortablejs": "^1.15.7",
"sql-formatter": "^15.7.3",
"trzsz": "^1.1.6",
"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",
"vuedraggable": "^4.1.0",
"x-markdown-vue": "0.0.200",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.6",
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.0",
"@types/crypto-js": "^4.2.2",
"@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.14",
"autoprefixer": "^10.4.21",
"code-inspector-plugin": "^0.20.9",
"dotenv": "^16.3.1",
"eslint": "^9.25.1",
"eslint-plugin-vue": "^10.0.0",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"sass": "^1.89.0",
"tailwindcss": "^4.1.7",
"typescript": "^5.8.2",
"vite": "^6.3.5",
"@types/node": "^22.19.18",
"@types/nprogress": "^0.2.3",
"@types/sortablejs": "^1.15.9",
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.59.2",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/compiler-sfc": "^3.5.34",
"autoprefixer": "^10.5.0",
"code-inspector-plugin": "^1.5.1",
"eslint": "^10.3.0",
"eslint-plugin-vue": "^10.9.1",
"postcss": "^8.5.14",
"prettier": "^3.8.3",
"sass": "^1.99.0",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
"vite-plugin-progress": "0.0.7",
"vue-eslint-parser": "^10.1.3"
"vue-eslint-parser": "^10.4.0"
},
"browserslist": [
"> 1%",

View File

@@ -1,4 +1,4 @@
module.exports = {
export default {
// 一行最多多少个字符
printWidth: 160,
// 指定每个缩进级别的空格数

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,39 +1,39 @@
<template>
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
<div class="h-full">
<el-watermark
:zIndex="10000000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
:content="themeConfig.watermarkText"
class="!h-full"
>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
<el-config-provider
:size="getGlobalComponentSize"
:locale="getGlobalI18n"
:button="{ autoInsertSpace: false, round: true }"
:dialog="{ alignCenter: true, transition: 'dialog-bounce' }"
>
<el-watermark
:zIndex="100000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
:content="themeConfig.watermarkText"
class="h-full!"
>
<router-view />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
</div>
<Setings />
</el-config-provider>
</template>
<script setup lang="ts" name="app">
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue';
import { onMounted, nextTick, watch, computed, defineAsyncComponent } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import LockScreen from '@/layout/lockScreen/index.vue';
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
import mittBus from '@/common/utils/mitt';
import { useIntervalFn } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import EnumValue from './common/Enum';
import { I18nEnum } from './common/commonEnum';
import { saveThemeConfig } from './common/utils/storage';
const setingsRef = ref();
const Setings = defineAsyncComponent(() => import('@/layout/navBars/breadcrumb/setings.vue'));
const route = useRoute();
const themeConfigStores = useThemeConfig();
@@ -42,19 +42,9 @@ const { themeConfig } = storeToRefs(themeConfigStores);
// 定义变量内容
const { locale, t } = useI18n();
// 布局配置弹窗打开
const openSetingsDrawer = () => {
setingsRef.value.openDrawer();
};
// 页面加载时
onMounted(() => {
nextTick(() => {
// 监听布局配置弹窗点击打开
mittBus.on('openSetingsDrawer', () => {
openSetingsDrawer();
});
// 初始化系统主题
themeConfigStores.initThemeConfig();
});
@@ -120,11 +110,6 @@ const refreshWatermarkTime = () => {
themeConfigStores.setWatermarkNowTime();
};
// 页面销毁时,关闭监听布局配置
onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {});
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,

View File

@@ -0,0 +1 @@
<svg t="1775984689718" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9497" width="48" height="48"><path d="M710.8 98.1H318.5c-120 0-217.6 97.6-217.6 217.6V708c0 120 97.6 217.6 217.6 217.6h392.3c120 0 217.6-97.6 217.6-217.6V315.7c0.1-120-97.6-217.6-217.6-217.6z m-30 57.4L652.6 214c-8.8 18.3-27.7 30.2-48.1 30.2H424.9c-20.4 0-39.2-11.9-48.1-30.2l-28.2-58.6h332.2zM871.1 708c0 88.4-71.9 160.3-160.3 160.3H318.5c-88.4 0-160.3-71.9-160.3-160.3V315.7c0-77.4 55.2-142.2 128.2-157l38.6 80.2c18.3 38.1 57.5 62.7 99.7 62.7h179.6c42.3 0 81.4-24.6 99.7-62.7l38.6-80.2c73.1 14.9 128.2 79.6 128.2 157V708z" p-id="9498"></path><path d="M486.9 408.2c-4.6-9.9-14.1-15.8-24.3-16.4-0.6 0-1.3-0.1-1.9-0.1-0.7 0-1.4 0.1-2.1 0.1-10.1 0.7-19.6 6.5-24.2 16.4l-142.7 306c-6.7 14.4-0.5 31.4 13.9 38.1 3.9 1.8 8 2.7 12.1 2.7 10.8 0 21.1-6.1 26-16.6l34.4-73.8h165.1l34.4 73.8c4.9 10.4 15.2 16.6 26 16.6 4.1 0 8.2-0.9 12.1-2.7 14.4-6.7 20.6-23.8 13.9-38.1l-142.7-306z m-82 199.1l55.8-119.7 55.8 119.7H404.9zM683.1 391.2c-15.8 0-28.7 12.8-28.7 28.7v306.9c0 15.8 12.8 28.7 28.7 28.7s28.7-12.8 28.7-28.7V419.9c0-15.9-12.8-28.7-28.7-28.7z" p-id="9499"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg t="1775984826390" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11872" width="48" height="48"><path d="M762.112 325.632h-225.536v-65.792c49.408-11.264 86.528-55.296 86.528-108.288 0-61.184-49.92-110.848-111.104-110.848s-111.104 49.664-111.104 110.848c0 52.736 37.12 97.024 86.528 108.288v65.792h-225.536c-87.808 0-158.976 71.424-158.976 158.976V706.56c0 87.808 71.424 158.976 158.976 158.976h500.224c87.808 0 158.976-71.424 158.976-158.976v-221.696c0-87.808-71.424-159.232-158.976-159.232z m-312.064-174.08c0-34.048 27.904-61.952 61.952-61.952s61.952 27.904 61.952 61.952-27.904 61.952-61.952 61.952-61.952-27.904-61.952-61.952zM872.192 706.56c0 60.672-49.408 110.08-110.08 110.08H261.888c-60.672 0-110.08-49.408-110.08-110.08v-221.696c0-60.672 49.408-110.08 110.08-110.08h500.224c60.672 0 110.08 49.408 110.08 110.08V706.56zM724.224 934.4H299.776c-13.568 0-24.576 11.008-24.576 24.576s11.008 24.576 24.576 24.576h424.192c13.568 0 24.576-11.008 24.576-24.576s-10.752-24.576-24.32-24.576zM29.696 478.464c-13.568 0-24.576 11.008-24.576 24.576v185.088c0 13.568 11.008 24.576 24.576 24.576s24.576-11.008 24.576-24.576v-185.088c0-13.568-11.008-24.576-24.576-24.576zM994.304 478.464c-13.568 0-24.576 11.008-24.576 24.576v185.088c0 13.568 11.008 24.576 24.576 24.576s24.576-11.008 24.576-24.576v-185.088c0-13.568-11.008-24.576-24.576-24.576z" p-id="11873"></path><path d="M349.184 467.968c-70.4 0-127.488 57.088-127.488 127.488 0 70.4 57.344 127.488 127.488 127.488s127.744-57.088 127.744-127.488c-0.256-70.144-57.344-127.488-127.744-127.488z m0 206.08c-43.264 0-78.592-35.328-78.592-78.592s35.328-78.592 78.592-78.592 78.592 35.328 78.592 78.592-35.328 78.592-78.592 78.592zM674.816 467.968c-70.4 0-127.744 57.088-127.744 127.488 0 70.4 57.344 127.488 127.744 127.488s127.488-57.088 127.488-127.488c0.256-70.144-57.088-127.488-127.488-127.488z m0 206.08c-43.264 0-78.592-35.328-78.592-78.592s35.328-78.592 78.592-78.592 78.592 35.328 78.592 78.592-35.328 78.592-78.592 78.592z" p-id="11874"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 150 150">
<defs>
<style>
.cls-1 {
fill: #161616;
stroke-width: 0px;
}
</style>
</defs>
<path class="cls-1" d="M27,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
<path class="cls-1" d="M49.2,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
<path class="cls-1" d="M71.4,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
<path class="cls-1" d="M93.6,25.7c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v97.7c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2V25.7Z"/>
<path class="cls-1" d="M115.9,64.6c0-.6.5-1.2,1.2-1.2h8.8c.6,0,1.2.5,1.2,1.2v19.9c0,.6-.5,1.2-1.2,1.2h-8.8c-.6,0-1.2-.5-1.2-1.2v-19.9Z"/>
</svg>

After

Width:  |  Height:  |  Size: 962 B

View File

@@ -0,0 +1 @@
<svg t="1756305127175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22356" width="48" height="48"><path d="M959.718832 123.963683C872.444401 50.185297 704.593576 0.299912 511.850044 0.299912S151.255687 50.185297 63.981255 123.963683C23.193205 158.453578 0 198.04198 0 240.22962v543.840672c0 132.461193 229.132871 239.929708 511.850044 239.929708s511.850044-107.468515 511.850044-239.929708v-543.840672c0-42.18764-23.193205-81.776042-63.981256-116.265937zM87.774285 189.64444c19.794201-21.893586 50.685151-43.087377 89.373816-61.182075 42.287611-19.794201 92.073025-35.489603 147.956653-46.586352C384.087474 70.17944 446.869081 64.281168 511.850044 64.281168s127.76257 5.898272 186.745289 17.594845c55.883628 11.096749 105.669042 26.792151 147.956654 46.586352 38.688665 18.094699 69.579615 39.28849 89.373816 61.182075 15.795372 17.494875 23.793029 34.489896 23.793029 50.48521 0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 273.319926 63.981255 256.324905 63.981255 240.22962s7.997657-33.090306 23.79303-50.58518zM63.981255 356.495558c87.274431 73.778385 255.125256 123.66377 447.868789 123.66377s360.594357-49.885385 447.868788-123.66377v155.254515c0 16.095285-7.997657 33.090306-23.793029 50.48521-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.794201-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 544.740408 63.981255 527.745387 63.981255 511.750073V356.495558z m895.737577 427.574734c0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182076-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182076C71.978912 817.160597 63.981255 800.165576 63.981255 784.070292V627.91604c87.274431 73.778385 255.125256 123.66377 447.868789 123.663771s360.594357-49.885385 447.868788-123.663771v156.154252z" p-id="22357"></path><path d="M167.950796 519.847701m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22358"></path><path d="M167.950796 791.768037m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22359"></path></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg t="1756305474315" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24277" width="48" height="48"><path d="M960 0H0v1024h1024V0.146286h-64V0z m-640 960.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m0-256h-256V256.146286H640v192h-256V256.146286h-64v192h-256V256.146286h896v191.853714z" p-id="24278"></path></svg>

After

Width:  |  Height:  |  Size: 547 B

View File

@@ -0,0 +1 @@
<svg t="1756107672203" class="icon" viewBox="0 0 1472 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5144" width="48" height="48"><path d="M1449.66628 358.737a233.848 233.848 0 0 0-166.348-35.445 268.717 268.717 0 0 0-108.127-152.273l-31.158-20.026-22.265 30.455a258.736 258.736 0 0 0-22.01 265.39 177.353 177.353 0 0 1-74.28 21.241h-24.953V309.536H830.08228V0H624.44928v154.768H287.27328v154.704H118.68528V468.08H8.44728L3.26528 504.42a493.032 493.032 0 0 0 95.97 353.3c90.149 110.11 234.232 165.964 428.284 165.964a749.848 749.848 0 0 0 585.42-255.025 804.871 804.871 0 0 0 139.86-226.874c187.718-3.391 213.246-134.359 214.27-139.99l4.863-27.447-22.01-15.61z m-766.291-49.84v-92.068h87.717v92.068h-87.717z m-337.176 154.64v-92.068h87.59v92.068h-87.59z m168.588 0v-92.068h87.589v92.068h-87.589z m168.588 0v-92.068h87.717v92.068h-87.717z m170.38-92.068h87.524v92.068h-87.525v-92.068zM683.37428 62.125h87.717v92.003h-87.717V62.125zM514.78728 216.829h87.589v92.068h-87.525v-92.068z m-168.588 0h87.59v92.068h-87.59v-92.068zM177.61228 371.47h87.525v92.068H177.61228v-92.068zM527.19928 938.4a609.348 609.348 0 0 1-235-40.564 399.493 399.493 0 0 0 151.058-66.092 44.018 44.018 0 0 0 7.87-57.582 39.54 39.54 0 0 0-54.575-11.9 375.18 375.18 0 0 1-215.998 62.508 262.639 262.639 0 0 1-19.194-21.433 392.455 392.455 0 0 1-79.591-249.523h943.9a250.035 250.035 0 0 0 155.216-62.06l4.99-4.671a682.157 682.157 0 0 1-658.42 451.636z m699.432-482.412l-25.144-1.215-15.163-21.178a186.566 186.566 0 0 1-21.626-161.358 145.619 145.619 0 0 1 42.483 100.769l-1.663 60.525 54.83-18.682a205.505 205.505 0 0 1 111.07-1.664 170.123 170.123 0 0 1-144.787 42.803zM544.41028 629.31a69.738 69.738 0 1 1-66.412 69.674 68.139 68.139 0 0 1 66.412-69.674z m0 85.413a15.74 15.74 0 1 0-14.971-15.675 15.291 15.291 0 0 0 14.97 15.675z m0 0" p-id="5145"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -47,6 +47,7 @@ function convertSvgToSymbol(svgString, symbolId) {
iconNames.push(`icon ${name}`);
svgsymbols += convertSvgToSymbol(allSvgIcons[path].default, name);
}
svgsymbols += '</svg>';
var t = (t = document.getElementsByTagName('script'))[t.length - 1],

View File

@@ -0,0 +1 @@
<svg t="1756286353957" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19008" width="48" height="48"><path d="M853.333333 554.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-170.666666a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 682.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 896h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 853.333333v-170.666666a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 725.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM853.333333 42.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128V170.666667a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 170.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 384h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 341.333333V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 213.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" p-id="19009"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M254.816 286.4a317.504 317.504 0 0 1 450.144 0c124.512 124.704 124.512 326.688 0 451.392a317.76 317.76 0 0 1-450.144-0.192l-203.136-203.84a30.848 30.848 0 0 1 0-43.584L254.816 286.4z m401.92 59.84a233.28 233.28 0 0 0-330.656 0L176.864 496a22.784 22.784 0 0 0 0 32l149.44 149.92a233.312 233.312 0 0 0 330.688 0 234.88 234.88 0 0 0-0.192-331.648h-0.064zM972.384 492.64l-90.592-91.008a7.552 7.552 0 0 0-12.8 7.104 481.984 481.984 0 0 1 0 211.2c-1.664 7.584 7.488 12.48 12.8 7.136l90.592-90.944a30.912 30.912 0 0 0 0-43.52z" fill="#0077FF"></path><path d="M493.28 354.784c86.784 0 157.216 71.456 157.216 159.52 0 88.16-70.336 159.616-157.216 159.616-86.848 0-157.28-71.456-157.28-159.616 0-88.064 70.4-159.52 157.28-159.52z" fill="#A6D0FF"></path></svg>

After

Width:  |  Height:  |  Size: 872 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M475.19999999 84.5568c202.7008 0 362.6496 71.0912 373.50400001 163.6608l0.40959999 4.5568h0.5632v232.2432H795.19999999V364.288c-63.1552 48.3328-175.5648 80.5888-307.5584 82.5088l-12.4416 0.0768c-133.1968 0-247.7312-30.8224-313.93279999-78.08L155.2 364.288v136.7552c0 63.5136 128.6144 126.208 319.99999999 126.208 63.1808 0 119.5264-6.8352 166.656-18.2784-4.9408 23.552-6.4 43.5968-4.4032 60.2112-48.7936 10.6752-103.7056 16.6144-162.2528 16.6144-133.1968 0-247.7312-30.7968-313.93279999-78.08l-6.0672-4.5056v125.824c0 63.5136 128.6144 126.2336 319.99999999 126.2336 74.3168 0 139.1616-9.4464 190.6688-24.7296l15.18080001 55.5008a631.04 631.04 0 0 1-89.6256 19.584 803.8656 803.8656 0 0 1-116.22400001 8.192c-206.7456 0-369.3312-73.984-374.3488-169.1392l-0.128-4.5568V252.7744h0.56320001C107.32799999 158.0032 269.1712 84.5824 475.19999999 84.5824z m335.18080001 637.696c12.3648 0 22.4 10.0608 22.39999999 22.4256l-0.0768 74.112a22.3744 22.3744 0 0 1 8.96-9.3184c15.4112-8.704 27.0336-24.6528 33.408-46.592a22.4 22.4 0 1 1 43.008 12.4928c-9.6 33.024-28.416 58.4704-54.39999999 73.1136a22.4 22.4 0 0 1-30.92480001-9.216v40.7296a22.4 22.4 0 0 1-44.79999999 0V744.704c0-12.3648 10.0608-22.4 22.4256-22.4z m-15.6672-184.7808a22.784 22.784 0 0 1 31.51359999 0.256c9.8816 9.8816 24.6528 26.624 40.06400001 47.36 25.3184 34.048 44.2624 68.4544 53.24799999 101.9136a22.4256 22.4256 0 0 1-43.3664 11.3408c-9.8816-36.6848-35.584-76.3392-65.8432-111.488-39.7824 46.1824-69.76 97.152-69.75999999 138.5984 0 36.992 13.056 67.4048 33.89439999 81.5616l5.632 5.3248a22.4 22.4 0 0 1-30.77119999 31.7696c-33.8432-22.9376-53.5552-67.3792-53.55520001-118.656 0-39.1424 18.1248-81.8944 48.2816-125.8752a461.312 461.312 0 0 1 50.688-62.1056zM475.19999999 143.0016c-187.7504 0.0512-314.7776 60.416-319.53919999 122.7264 4.8128 62.2336 131.7888 122.5984 319.53919999 122.5984s314.7776-60.3648 319.5392-122.6496C789.92639999 203.4176 662.95039999 143.0016 475.19999999 143.0016z" ></path></svg>
<svg t="1756389060526" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29147" width="48" height="48"><path d="M465.454545 9.402182c245.697939 0 439.575273 86.171152 452.732122 198.376727l0.496485 5.523394h0.682666v281.506909H853.333333V348.470303c-76.551758 58.585212-212.805818 97.683394-372.79806 100.010667l-15.080728 0.093091c-161.450667 0-300.280242-37.360485-380.524606-94.642425L77.575758 348.470303v165.763879c0 76.986182 155.896242 152.979394 387.878787 152.979394 76.582788 0 144.880485-8.285091 202.007273-22.155637-5.988848 28.547879-7.757576 52.844606-5.337212 72.983273-59.143758 12.939636-125.703758 20.138667-196.670061 20.138667-161.450667 0-300.280242-37.329455-380.524606-94.642424l-7.354181-5.461334v152.51394c0 76.986182 155.896242 153.010424 387.878787 153.010424 90.08097 0 168.680727-11.450182 231.113697-29.975273l18.40097 67.273697a764.89697 764.89697 0 0 1-108.637091 23.738182 974.382545 974.382545 0 0 1-140.877576 9.929697c-250.600727 0-447.674182-89.677576-453.756121-205.017212l-0.155151-5.523394V213.302303h0.682666C19.549091 98.428121 215.722667 9.433212 465.454545 9.433212z m406.279758 772.964848c14.987636 0 27.151515 12.194909 27.151515 27.182546l-0.093091 89.832727a27.120485 27.120485 0 0 1 10.860606-11.29503c18.680242-10.550303 32.768-29.882182 40.494546-56.475152a27.151515 27.151515 0 1 1 52.130909 15.142788c-11.636364 40.029091-34.443636 70.873212-65.939394 88.622546a27.151515 27.151515 0 0 1-37.484606-11.17091v49.369213a27.151515 27.151515 0 0 1-54.30303 0V809.580606c0-14.987636 12.194909-27.151515 27.182545-27.151515z m-18.990545-223.976727a27.61697 27.61697 0 0 1 38.198303 0.310303c11.977697 11.977697 29.882182 32.271515 48.562424 57.406061 30.68897 41.270303 53.651394 82.97503 64.54303 123.531636a27.182545 27.182545 0 0 1-52.565333 13.746424c-11.977697-44.466424-43.132121-92.532364-79.80994-135.136969-48.221091 55.978667-84.557576 117.76-84.557575 167.99806 0 44.838788 15.825455 81.702788 41.084121 98.862546l6.826667 6.454303a27.151515 27.151515 0 0 1-37.298425 38.508606c-41.022061-27.803152-64.915394-81.671758-64.915394-143.825455 0-47.445333 21.969455-99.265939 58.523152-152.576a559.166061 559.166061 0 0 1 61.44-75.279515zM465.454545 80.244364C237.878303 80.306424 83.905939 153.475879 78.134303 229.003636 83.968 304.407273 237.878303 377.607758 465.454545 377.607758S847.003152 304.407273 852.774788 228.941576C846.941091 153.475879 693.030788 80.244364 465.454545 80.244364z" p-id="29148"></path></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M668.8 460.8c64 0 118.4-54.4 118.4-118.4S732.8 224 668.8 224s-118.4 54.4-118.4 118.4c0 12.8 3.2 22.4 6.4 35.2L502.4 416c-22.4-28.8-57.6-48-92.8-57.6v-64c54.4-12.8 92.8-60.8 92.8-115.2 0-64-54.4-118.4-118.4-118.4s-118.4 54.4-118.4 118.4c0 54.4 38.4 102.4 92.8 115.2v67.2c-80 16-134.4 96-118.4 176 12.8 60.8 57.6 105.6 118.4 118.4v70.4c-54.4 12.8-92.8 57.6-92.8 115.2 0 64 54.4 118.4 118.4 118.4s118.4-54.4 118.4-118.4c0-54.4-38.4-102.4-92.8-115.2v-70.4c35.2-6.4 70.4-22.4 92.8-54.4l54.4 41.6c-3.2 9.6-6.4 22.4-6.4 35.2 3.2 64 54.4 115.2 121.6 115.2 64-3.2 115.2-54.4 115.2-121.6-3.2-64-54.4-115.2-118.4-115.2-35.2 0-67.2 16-89.6 41.6l-54.4-41.6c6.4-16 9.6-35.2 9.6-54.4 0-16-3.2-35.2-9.6-48l54.4-38.4c19.2 32 54.4 48 89.6 44.8z m0-176c32 0 54.4 25.6 54.4 54.4s-25.6 54.4-54.4 54.4c-32 0-54.4-25.6-54.4-54.4-3.2-28.8 22.4-54.4 54.4-54.4z m-345.6-105.6c0-32 25.6-54.4 54.4-54.4 32 0 54.4 25.6 54.4 54.4s-25.6 54.4-54.4 54.4c-28.8 3.2-54.4-22.4-54.4-54.4z m115.2 662.4c0 32-22.4 57.6-54.4 57.6s-57.6-22.4-57.6-54.4 22.4-57.6 54.4-57.6 57.6 25.6 57.6 54.4z m-54.4-249.6c-44.8-3.2-76.8-41.6-73.6-86.4 3.2-38.4 35.2-70.4 73.6-73.6 44.8 0 80 35.2 80 80 0 41.6-38.4 76.8-80 80z m284.8 32c28.8 0 54.4 22.4 54.4 54.4s-22.4 54.4-51.2 54.4h-3.2c-32 0-54.4-25.6-54.4-57.6s25.6-54.4 54.4-54.4v3.2z" fill="#0171F1"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M750.08 657.92c-16.384 0-30.208 2.56-44.032 8.192l-44.032-52.224c33.28-38.4 55.296-90.624 55.296-145.92s-19.456-101.888-52.224-140.288l27.648-24.576c16.384 8.192 33.28 13.824 52.224 13.824 63.488 0 115.712-52.224 115.712-115.712s-52.224-115.712-115.712-115.712c-63.488 0-115.712 52.224-115.712 115.712 0 13.824 2.56 30.208 8.192 41.472l-35.84 30.208c-34.304-19.968-73.216-30.208-112.64-30.208-44.032 0-85.504 13.824-121.344 32.768l-41.472-52.224c0-5.632 2.56-13.824 2.56-19.456 0-40.96-32.768-74.24-73.728-74.24h-0.512c-40.96 0-74.24 32.768-74.24 73.728v0.512c0 40.96 32.768 74.24 73.728 74.24H262.656L306.688 332.8c-27.648 38.4-46.592 85.504-46.592 134.656 0 35.84 8.192 71.68 24.576 101.888l-35.84 30.208c-8.192-5.632-19.456-5.632-30.208-5.632-49.664 0-90.624 41.472-90.624 90.624 0 49.664 41.472 90.624 90.624 90.624s90.624-41.472 90.624-90.624c0-8.192 0-16.384-2.56-24.576l30.208-27.648c41.472 35.84 93.696 57.856 154.112 57.856 33.28 0 63.488-5.632 90.624-19.456l46.592 57.856c-11.264 19.456-19.456 44.032-19.456 68.608 0 77.312 63.488 140.288 143.36 140.288s143.36-63.488 143.36-140.288c0.512-75.776-65.536-139.264-145.408-139.264z m-261.632-11.264c-99.328 0-181.76-79.872-181.76-178.688C306.688 368.64 389.12 289.28 488.448 289.28s181.76 79.872 181.76 178.688c0 99.328-82.432 178.688-181.76 178.688zM430.592 465.408c0.512 18.432-13.824 33.28-32.256 33.792-18.432 0.512-33.28-13.824-33.792-32.256v-1.536c0-18.432 14.848-32.768 33.28-32.768 18.432-0.512 32.768 14.336 32.768 32.768z m88.064 0c0 18.432-14.848 33.28-32.768 33.28-18.432 0-33.28-14.848-33.28-32.768 0-18.432 14.848-32.768 33.28-32.768s32.768 13.824 32.768 32.256z m91.136 0c0 18.432-14.848 33.28-32.768 33.28s-33.28-14.848-33.28-32.768c0-18.432 14.848-32.768 33.28-32.768 17.92-1.024 32.768 13.824 32.768 32.256z" fill="#2B85FB"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M897.8125003 599.75c-0.37500029 8.58750029-11.73750029 18.18749971-35.06250058 30.375-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.95000029-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.3125-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.7375-23.81249971-11.40000029-35.96249971-20.99999971-36.37499942-30.07500029v90.97499971c0 9.07499971 12.52500029 18.71250029 36.37499942 30.11250058 47.7 22.79999971 312.48749971 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.35000029 52.94999971-27.60000029 301.76250029-129.89999971 349.79999942-154.95000029 24.4125-12.7125 35.25000029-22.6125 35.25000029-31.57499971v-89.70000029l-0.18749971-0.07499971z" fill="" ></path><path d="M897.77500001 451.43749971c-0.37500029 8.58750029-11.73750029 18.15000029-35.02500029 30.33750058-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.94999942-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.35000029-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.77500029-23.81249971-11.3625-35.96249971-20.99999971-36.37499942-30.0375v90.97500058c0 9.07499971 12.52500029 18.675 36.37499942 30.07499942 47.7 22.79999971 312.45000029 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.3125 52.94999971-27.60000029 301.76250029-129.9375 349.79999942-154.94999942 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125v-89.70000029l-0.225-0.03750029z" fill="" ></path><path d="M897.77500001 297.61250029c0.45-9.15000029-11.51250029-17.17499971-35.58750029-26.02500029-46.8-17.13750029-294.11250029-115.57500029-341.47499942-132.93749971-47.3625-17.325-66.63750029-16.61249971-122.25000058 3.375C342.7375003 161.93750029 79.41249972 265.24999971 32.5750003 283.55000029c-23.43750029 9.225-34.875 17.73749971-34.50000058 26.81249942V401.37499971c0 9.07499971 12.52500029 18.675 36.37500029 30.07500029 47.7 22.79999971 312.45000029 129.78749971 354.30000029 149.77500029 41.85 19.98749971 71.25000029 20.25 124.23749942-7.35000029 52.94999971-27.60000029 301.76250029-129.9375 349.80000029-154.95000029 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125V297.61250029h-0.30000058zM320.31250001 383.75l208.53749971-32.02499971-63 92.3625-145.49999942-60.33750029z m461.25-83.17500029l-123.33750029 48.75000029-13.3875 5.24999971-123.26249971-48.74999942 136.575-54 123.37499971 48.74999942z m-362.09999971-89.36249942l-20.17500029-37.20000058 62.92500029 24.60000058 59.32499942-19.42500058-16.04999971 38.43750058 60.45000029 22.64999942-77.9625 8.1-17.47500029 42.00000029-28.19999971-46.83750029-90-8.1 67.1625-24.22499942z m-155.3625 52.49999971c61.57500029 0 111.44999971 19.31249971 111.44999971 43.16249971s-49.87500029 43.2-111.44999971 43.2-111.4875-19.38750029-111.4875-43.2c0-23.85 49.91249971-43.2 111.4875-43.2z" fill="" ></path></svg>
<svg t="1756388835244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25729" width="48" height="48"><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="25730"></path></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -1 +1,9 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621859009605" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9709" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M820.203922 812.172549H684.67451v-45.176471h112.439215V279.090196H633.47451l-85.333334 277.082353c-3.011765 10.039216-12.047059 16.062745-22.086274 16.062745-10.039216 0-19.07451-7.027451-21.082353-17.066667l-71.278431-280.094117h-180.705883V762.980392h120.470589v45.176471H229.898039c-12.047059 0-22.086275-10.039216-22.086274-22.086275V252.988235c0-12.047059 10.039216-22.086275 22.086274-22.086274H451.764706c10.039216 0 19.07451 7.027451 22.086274 17.066666l55.215687 218.854902L595.32549 250.980392c3.011765-9.035294 12.047059-16.062745 21.082353-16.062745h202.792157c12.047059 0 22.086275 10.039216 22.086275 22.086275v533.082353c1.003922 12.047059-9.035294 22.086275-21.082353 22.086274z m0 0" fill="#e25813" p-id="9710"></path><path d="M731.858824 425.662745c4.015686-12.047059-2.007843-25.098039-14.054902-29.113725-12.047059-4.015686-25.098039 2.007843-29.113726 14.054902L563.2 766.996078h-73.286275L371.45098 410.603922c-4.015686-12.047059-17.066667-18.070588-28.109804-14.054902-12.047059 4.015686-18.070588 17.066667-14.054901 28.109804l123.482352 371.45098c3.011765 9.035294 12.047059 15.058824 21.082353 15.058823h72.282353l-53.207843 160.627451 46.180392 2.007844 192.752942-548.141177z" fill="#2c2c2c" p-id="9711"></path></svg>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1621859009605" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="9709" xmlns:xlink="http://www.w3.org/1999/xlink"
width="200" height="200">
<defs><style type="text/css"></style></defs>
<path d="M820.203922 812.172549H684.67451v-45.176471h112.439215V279.090196H633.47451l-85.333334 277.082353c-3.011765 10.039216-12.047059 16.062745-22.086274 16.062745-10.039216 0-19.07451-7.027451-21.082353-17.066667l-71.278431-280.094117h-180.705883V762.980392h120.470589v45.176471H229.898039c-12.047059 0-22.086275-10.039216-22.086274-22.086275V252.988235c0-12.047059 10.039216-22.086275 22.086274-22.086274H451.764706c10.039216 0 19.07451 7.027451 22.086274 17.066666l55.215687 218.854902L595.32549 250.980392c3.011765-9.035294 12.047059-16.062745 21.082353-16.062745h202.792157c12.047059 0 22.086275 10.039216 22.086275 22.086275v533.082353c1.003922 12.047059-9.035294 22.086275-21.082353 22.086274z m0 0" fill="#e25813" p-id="9710" stroke-width="30" stroke="#e25813"></path>
<path d="M731.858824 425.662745c4.015686-12.047059-2.007843-25.098039-14.054902-29.113725-12.047059-4.015686-25.098039 2.007843-29.113726 14.054902L563.2 766.996078h-73.286275L371.45098 410.603922c-4.015686-12.047059-17.066667-18.070588-28.109804-14.054902-12.047059 4.015686-18.070588 17.066667-14.054901 28.109804l123.482352 371.45098c3.011765 9.035294 12.047059 15.058824 21.082353 15.058823h72.282353l-53.207843 160.627451 46.180392 2.007844 192.752942-548.141177z" fill="#2c2c2c" p-id="9711" stroke-width="30" stroke="#2c2c2c"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -3,8 +3,10 @@ import { RequestOptions, useApiFetch } from '@/hooks/useRequest';
/**
* 可用于各模块定义各自api请求
* T 请求返回的数据类型
* P 请求参数类型
*/
class Api {
class Api<T = any, P = any> {
/**
* 请求url
*/
@@ -45,29 +47,30 @@ class Api {
/**
* 响应式使用该api
* @param params 响应式params
* @param param 请求参数
* @param reqOptions 其他可选值
* @returns
*/
useApi<T>(params: any = null, reqOptions?: RequestOptions) {
return useApiFetch<T>(this, params, reqOptions);
useApi(param?: P, reqOptions?: RequestOptions) {
return useApiFetch<T, P>(this, param, reqOptions);
}
/**
* fetch 请求对应的该api
* @param {Object} param 请求该api的参数
* @param options options
*/
async request(param: any = null, options: any = {}): Promise<any> {
async request(param?: P, options: any = {}): Promise<T> {
const { execute, data } = this.useApi(param, options);
const res = await execute();
return data.value || res;
return (data.value as T) || (res as T);
}
/**
* xhr 请求对应的该api
* @param {Object} param 请求该api的参数
*/
async xhrReq(param: any = null, options: any = {}): Promise<any> {
async xhrReq(param: any = null, options: any = {}): Promise<T> {
if (this.beforeHandler) {
await this.beforeHandler(param);
}
@@ -81,40 +84,40 @@ class Api {
* @param url url
* @param method 请求方法(get,post,put,delete...)
*/
static create(url: string, method: string): Api {
return new Api(url, method);
static create<T = any, P = any>(url: string, method: string): Api<T> {
return new Api<T, P>(url, method);
}
/**
* 创建get api
* @param url url
*/
static newGet(url: string): Api {
return Api.create(url, 'get');
static newGet<T = any, P = any>(url: string): Api<T, P> {
return Api.create<T, P>(url, 'get');
}
/**
* new post api
* @param url url
*/
static newPost(url: string): Api {
return Api.create(url, 'post');
static newPost<T = any, P = any>(url: string): Api<T, P> {
return Api.create<T, P>(url, 'post');
}
/**
* new put api
* @param url url
*/
static newPut(url: string): Api {
return Api.create(url, 'put');
static newPut<T = any, P = any>(url: string): Api<T, P> {
return Api.create<T, P>(url, 'put');
}
/**
* new delete api
* @param url url
*/
static newDelete(url: string): Api {
return Api.create(url, 'delete');
static newDelete<T = any, P = any>(url: string): Api<T, P> {
return Api.create<T, P>(url, 'delete');
}
}

View File

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

View File

@@ -32,7 +32,7 @@ export function isTrue(condition: boolean, msgOrI18nKey: string) {
* @param msg 错误消息
*/
export function notBlank(obj: any, msg: string) {
if (obj == null || obj == undefined || obj == '') {
if (obj == null || obj == undefined || !obj) {
throw new AssertError(msg);
}
if (Array.isArray(obj) && obj.length == 0) {

View File

@@ -9,14 +9,22 @@ export const I18nEnum = {
En: EnumValue.of('en', 'English').setExtra({ icon: 'icon layout/en', el: enLocale }),
};
export const LinkTypeEnum = {
Iframes: EnumValue.of(1, 'ifrmaes'),
Link: EnumValue.of(2, 'link'),
};
// 资源类型
export const ResourceTypeEnum = {
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Machine: EnumValue.of(1, 'tag.machine').setExtra({ icon: 'icon machine/machine', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
Db: EnumValue.of(2, 'tag.db').setExtra({ icon: 'icon db/db', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
Es: EnumValue.of(6, 'ES实例').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Container: EnumValue.of(7, 'tag.container').setExtra({ icon: 'icon docker/docker', iconColor: 'var(--el-color-primary)' }),
MqKafka: EnumValue.of(8, 'tag.mq.kafka').setExtra({ icon: 'icon mq/kafka', iconColor: 'var(--el-color-primary)' }),
Milvus: EnumValue.of(9, 'tag.milvus').setExtra({ icon: 'icon milvus/milvus', iconColor: 'var(--el-color-primary)' }),
};
// 标签关联的资源类型
@@ -30,8 +38,13 @@ export const TagResourceTypeEnum = {
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
AuthCert: ResourceTypeEnum.AuthCert,
Container: ResourceTypeEnum.Container,
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
MqKafka: ResourceTypeEnum.MqKafka,
Milvus: ResourceTypeEnum.Milvus,
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
};
// 标签关联的资源类型路径
@@ -42,3 +55,31 @@ export const TagResourceTypePath = {
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
};
// 消息子类型
export const MsgSubtypeEnum = {
UserLogin: EnumValue.of('user.login', 'login.login').setExtra({
notifyType: 'primary',
}),
MachineFileUploadSuccess: EnumValue.of('machine.file.upload.success', 'machine.fileUploadSuccess').setExtra({
notifyType: 'success',
}),
MachineFileUploadFail: EnumValue.of('machine.file.upload.fail', 'machine.fileUploadFail').setExtra({
notifyType: 'danger',
}),
DbDumpFail: EnumValue.of('db.dump.fail', 'db.dbDumpFail').setExtra({
notifyType: 'danger',
}),
SqlScriptRunSuccess: EnumValue.of('db.sqlscript.run.success', 'db.sqlScriptRunSuccess').setExtra({
notifyType: 'success',
}),
SqlScriptRunFail: EnumValue.of('db.sqlscript.run.fail', 'db.sqlScriptRunFail').setExtra({
notifyType: 'danger',
}),
FlowUserTaskTodo: EnumValue.of('flow.usertask.todo', 'flow.todoTask').setExtra({
notifyType: 'primary',
}),
};

View File

@@ -1,8 +1,28 @@
/**
* 获取应用配置。
* 需要后端将index.html文件中的<app-config />标签替换为script标签并将配置项挂载到全局变量window.__APP_CONFIG__ 上
* @returns 应用配置
*/
export function getAppConfig() {
return (window as any)?.__APP_CONFIG__;
}
export function getBaseApiUrl() {
const config = getAppConfig();
console.log('app config: ', config);
if (config) {
if (!config.CTX_PATH) {
return window.location.host;
}
return window.location.host + config.CTX_PATH;
}
let path = window.location.pathname;
if (path == '/') {
return window.location.host;
}
if (path.endsWith('/')) {
// 去除最后一个/
return window.location.host + path.replace(/\/$/, '');
@@ -13,9 +33,6 @@ export function getBaseApiUrl() {
const config = {
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl || location.protocol + '//' + getBaseApiUrl()}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.10.0',
};
export default config;

View File

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

View File

@@ -4,9 +4,14 @@ import { getClientId, getToken } from './utils/storage';
import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import JSONBig from 'json-bigint';
import { useApiFetch } from '../hooks/useRequest';
import Api from './Api';
// 配置 JSONBig将大数int64/uint64转为字符串避免精度丢失
// storeAsString: 将大数存储为字符串,而不是 BigNumber 对象
const JSONBigString = JSONBig({ storeAsString: true });
export default {
request,
xhrReq,
@@ -58,6 +63,22 @@ function notifyErrorMsg(msg: string) {
const axiosInst = axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 60000, // request timeout
// 使用 json-bigint 处理响应数据,解决 int64/uint64 精度丢失问题
transformResponse: [
function (data) {
// 对响应数据进行转换
if (typeof data === 'string') {
try {
// 使用 JSONBigString 解析,大数会被转为字符串
return JSONBigString.parse(data);
} catch (err) {
// 如果解析失败,返回原始数据
return data;
}
}
return data;
},
],
});
// request interceptor
@@ -204,6 +225,24 @@ function getApiUrl(url: string) {
return baseUrl + url + '?' + joinClientParams();
}
/**
* 创建 websocket
*/
export const createWebSocket = (url: string): Promise<WebSocket> => {
return new Promise<WebSocket>((resolve, reject) => {
const clientParam = (url.includes('?') ? '&' : '?') + joinClientParams();
const socket = new WebSocket(`${config.baseWsUrl}${url}${clientParam}`);
socket.onopen = () => {
resolve(socket);
};
socket.onerror = (e) => {
reject(e);
};
});
};
// 组装客户端参数,包括 token 和 clientId
export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,11 @@
import * as XLSX from 'xlsx';
/**
* 导出CSV文件
* @param filename 文件名
* @param columns 列信息
* @param datas 数据
*/
export function exportCsv(filename: string, columns: string[], datas: []) {
// 二维数组
const cvsData = [columns];
@@ -30,6 +38,11 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
exportFile(`${filename}.csv`, csvString);
}
/**
* 导出文件
* @param filename 文件名
* @param content 文件内容
*/
export function exportFile(filename: string, content: string) {
// 导出
let link = document.createElement('a');
@@ -42,4 +55,79 @@ export function exportFile(filename: string, content: string) {
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link); // 下载完成后移除元素
}
/**
* 计算字符串显示宽度(考虑中英文字符差异)
* @param str 要计算的字符串
* @returns 计算后的宽度值
*/
function getStringWidth(str: string): number {
if (!str) return 0;
// 统计中文字符数量(包括中文标点)
const chineseChars = str.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g);
const chineseCount = chineseChars ? chineseChars.length : 0;
// 英文字符数量
const englishCount = str.length - chineseCount;
// 中文字符按2个单位宽度计算英文字符按1个单位宽度计算
return chineseCount * 2 + englishCount;
}
/**
* 导出Excel文件
* @param filename 文件名
* @param sheets 多个工作表数据,每个工作表包含名称、列信息和数据
* 示例: [{name: 'Sheet1', columns: ['列1', '列2'], datas: [{col1: '值1', col2: '值2'}]}]
*/
export function exportExcel(filename: string, sheets: { name: string; columns: string[]; datas: any[] }[]) {
// 创建工作簿
const wb = XLSX.utils.book_new();
// 处理每个工作表
sheets.forEach((sheet) => {
// 准备表头
const headers: any = {};
sheet.columns.forEach((col) => {
headers[col] = col;
});
// 准备数据
const data = [headers, ...sheet.datas];
// 创建工作表
const ws = XLSX.utils.json_to_sheet(data, { skipHeader: true });
// 设置列宽自适应
const colWidths: { wch: number }[] = [];
sheet.columns.forEach((col, index) => {
// 计算列宽:取表头和前几行数据的最大宽度
let maxWidth = getStringWidth(col); // 表头宽度
const checkCount = Math.min(sheet.datas.length, 10); // 只检查前10行数据
for (let i = 0; i < checkCount; i++) {
const cellData = sheet.datas[i][col];
const cellStr = cellData ? String(cellData) : '';
const cellWidth = getStringWidth(cellStr);
if (cellWidth > maxWidth) {
maxWidth = cellWidth;
}
}
// 设置最小宽度为8最大宽度为80
colWidths.push({ wch: Math.min(Math.max(maxWidth + 2, 8), 80) });
});
// 应用列宽设置
ws['!cols'] = colWidths;
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, sheet.name);
});
// 导出文件
XLSX.writeFile(wb, `${filename}.xlsx`);
}

View File

@@ -6,7 +6,7 @@ import dayjs from 'dayjs';
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
* @returns 格式化后内容
*/
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
export function formatDate(date?: string | number | Date, format: string = 'YYYY-MM-DD HH:mm:ss') {
if (!date) {
return '';
}
@@ -126,3 +126,47 @@ export function formatAxis(param: any) {
else if (hour < 22) return '晚上好';
else return '夜里好';
}
/**
* 格式化数据为美观的 JSON 字符串
*
* - 如果输入是对象,直接格式化为缩进 JSON
* - 如果输入是 JSON 字符串,先解析为对象再格式化
* - 如果解析失败,返回原始值的字符串形式
* - 如果输入为空值,返回空字符串
*
* @param val - 要格式化的数据(对象或 JSON 字符串)
* @returns 格式化后的 JSON 字符串,带 2 空格缩进
*
* @example
* ```ts
* // 格式化对象
* formatJson({ name: 'test', value: 123 })
* // 输出: '{\n "name": "test",\n "value": 123\n}'
*
* // 格式化 JSON 字符串
* formatJson('{"name":"test"}')
* // 输出: '{\n "name": "test"\n}'
*
* // 处理无效输入
* formatJson(null)
* // 输出: ''
* ```
*/
export function formatJson(val: any) {
if (!val) {
return '';
}
try {
// 如果val是字符串尝试解析为对象后再格式化
let data = val;
if (typeof val === 'string') {
data = JSON.parse(val);
}
return JSON.stringify(data, null, 2);
} catch {
// 如果解析失败,直接返回原始值的字符串形式
return String(val);
}
}

View File

@@ -1,5 +1,7 @@
import { nextTick } from 'vue';
import '@/theme/loading.scss';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
/**
* 页面全局 Loading
@@ -9,33 +11,57 @@ import '@/theme/loading.scss';
export const NextLoading = {
// 创建 loading
start: () => {
// 如果已经存在loading元素则不重复创建
if (document.querySelector('.loading-next')) {
return;
}
const bodys: Element = document.body;
const div = <HTMLElement>document.createElement('div');
div.setAttribute('class', 'loading-next');
const { themeConfig } = storeToRefs(useThemeConfig());
if (themeConfig.value.isDark) {
div.classList.add('dark');
}
const htmls = `
<div class="loading-next-box">
<div class="loading-next-box-warp">
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
</div>
</div>
`;
<div class="loading-next-box">
<div class="loading-next-box-warp">
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
</div>
</div>
`;
div.innerHTML = htmls;
bodys.insertBefore(div, bodys.childNodes[0]);
// 插入到body的第一个子元素之前避免影响布局
if (bodys.firstChild) {
bodys.insertBefore(div, bodys.firstChild);
} else {
bodys.appendChild(div);
}
},
// 移除 loading
done: (time: number = 1000) => {
done: (time: number = 500) => {
nextTick(() => {
setTimeout(() => {
const el = <HTMLElement>document.querySelector('.loading-next');
el?.parentNode?.removeChild(el);
if (el) {
// 添加淡出效果
el.style.transition = 'opacity 0.3s ease-out';
el.style.opacity = '0';
setTimeout(() => {
el?.parentNode?.removeChild(el);
}, 300);
}
}, time);
});
},

View File

@@ -1,8 +0,0 @@
// https://www.npmjs.com/package/mitt
import mitt, { Emitter } from 'mitt';
// 类型
const emitter: Emitter<any> = mitt<any>();
// 导出
export default emitter;

View File

@@ -106,7 +106,7 @@ export function deepClone(
result = Array.isArray(obj) ? [] : {};
hash.set(obj, result);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
let value = obj[key];
value = callback(key, value);
result[key] = deepClone(value, callback, hash);

View File

@@ -12,7 +12,13 @@ import { ElMessage } from 'element-plus';
export function templateResolve(template: string, param: any) {
return template.replace(/\{\w+\}/g, (word) => {
const key = word.substring(1, word.length - 1);
const value = param[key];
let value;
// 兼容FormData类型的参数
if (param instanceof FormData) {
value = param.get(key);
} else {
value = param[key];
}
if (value != null || value != undefined) {
return value;
}

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { useUserInfo } from '@/store/userInfo';
* @param code 权限code
* @returns
*/
export function hasPerm(code: string) {
export function hasPerm(code: string): boolean {
if (!code) {
return true;
}
@@ -17,7 +17,7 @@ export function hasPerm(code: string) {
* @returns {"xxx:save": true} key->permission code
* @param permCodes
*/
export function hasPerms(permCodes: any[]) {
export function hasPerms(permCodes: any[]): Record<string, boolean> {
const res = {} as { [key: string]: boolean };
for (let permCode of permCodes) {
if (hasPerm(permCode)) {

View File

@@ -1,7 +1,7 @@
<template>
<transition @enter="onEnter" name="el-zoom-in-center">
<div
aria-hidden="true"
:aria-hidden="state.isShow ? 'false' : 'true'"
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
@@ -126,7 +126,7 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
};
const headerContextmenuClick = (event: any, data: any) => {
const headerContextmenuClick = (event: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为
};

View File

@@ -12,8 +12,9 @@ const props = defineProps({
required: true,
},
value: {
type: [Object, String, Number],
type: [Object, String, Number, null, Boolean],
required: true,
default: () => null,
},
});
@@ -40,7 +41,7 @@ onMounted(() => {
});
const convert = (value: any) => {
const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
const enumValue = EnumValue.getEnumByValue(props.enums, value);
if (!enumValue) {
state.enumLabel = '-';
state.type = 'danger';
@@ -50,8 +51,8 @@ const convert = (value: any) => {
state.enumLabel = enumValue?.label || '';
if (enumValue.tag) {
state.color = enumValue.tag.color;
state.type = enumValue.tag.type;
state.color = enumValue.tag.color || '';
state.type = enumValue.tag.type || defaultType;
} else {
state.type = defaultType;
}

View File

@@ -1,16 +1,30 @@
<template>
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
</el-tooltip>
<el-button v-if="loading" :loading="loading" name="loading" link type="primary" />
{{ fileDetail?.filename }}
<template v-else>
<el-tooltip :content="fileSize" placement="left">
<el-link
v-if="props.canDownload"
target="_blank"
rel="noopener noreferrer"
icon="Download"
type="primary"
:href="getFileUrl(props.fileKey)"
></el-link>
</el-tooltip>
{{ fileDetail?.filename }}
<!-- 文件大小显示 -->
<span v-if="props.showFileSize && fileDetail?.size" class="file-size">({{ fileSize }})</span>
</template>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { computed, onMounted, Ref, ref, watch } from 'vue';
import openApi from '@/common/openApi';
import { getFileUrl } from '@/common/request';
import { formatByteSize } from '@/common/utils/format';
const props = defineProps({
fileKey: {
type: String,
@@ -23,8 +37,14 @@ const props = defineProps({
type: Boolean,
default: true,
},
showFileSize: {
type: Boolean,
default: false,
},
});
const loading: Ref<boolean> = ref(false);
onMounted(async () => {
setFileInfo();
});
@@ -38,23 +58,38 @@ watch(
}
);
const fileSize = computed(() => {
return fileDetail.value?.size ? formatByteSize(fileDetail.value.size) : '';
});
const fileDetail: any = ref({});
const setFileInfo = async () => {
if (!props.fileKey) {
return;
}
if (props.files && props.files.length > 0) {
const file: any = props.files.find((file: any) => {
return file.fileKey === props.fileKey;
});
fileDetail.value = file;
return;
}
try {
if (!props.fileKey) {
return;
}
loading.value = true;
if (props.files && props.files.length > 0) {
const file: any = props.files.find((file: any) => {
return file.fileKey === props.fileKey;
});
fileDetail.value = file;
return;
}
const files = await openApi.getFileDetail([props.fileKey]);
fileDetail.value = files?.[0];
const files = await openApi.getFileDetail([props.fileKey]);
fileDetail.value = files?.[0];
} finally {
loading.value = false;
}
};
</script>
<style lang="scss"></style>
<style lang="scss" scoped>
.file-size {
margin-left: 1px;
color: #909399;
font-size: 8px;
}
</style>

View File

@@ -1,14 +1,16 @@
<template>
<el-form-item v-bind="$attrs">
<template #label>
{{ props.label }}
<div class="flex items-center">
{{ props.label }}
<el-tooltip :placement="props.placement">
<template #content>
<span v-html="props.tooltip"></span>
</template>
<SvgIcon name="QuestionFilled" />
</el-tooltip>
<el-tooltip :placement="props.placement">
<template #content>
<span v-html="props.tooltip"></span>
</template>
<SvgIcon name="QuestionFilled" class="ml-1" />
</el-tooltip>
</div>
</template>
<!-- 遍历父组件传入的 solts 透传给子组件 -->
@@ -24,11 +26,11 @@ import { useSlots } from 'vue';
const props = defineProps({
label: {
type: String,
require: true,
required: true,
},
tooltip: {
type: String,
require: true,
required: true,
},
placement: {
type: String,

View File

@@ -0,0 +1,129 @@
import { ElLink, ElText } from 'element-plus';
import { defineAsyncComponent, defineComponent, h } from 'vue';
type Size = 'large' | 'default' | 'small';
interface ComponentConfig {
component: any;
getDefaultProps?: (size: Size) => Record<string, any>;
}
const linkConf = {
component: ElLink,
getDefaultProps: (size: Size) => {
return {
type: 'primary',
verticalAlign: 'baseline',
style: {
fontSize: size === 'small' ? '12px' : size === 'large' ? '16px' : '14px',
verticalAlign: 'baseline',
},
};
},
};
const components = {
'el-link': linkConf,
a: linkConf,
'error-text': {
component: ElText,
getDefaultProps: (size: Size) => {
return {
type: 'danger',
size,
};
},
},
'machine-info': {
component: defineAsyncComponent(() => import('@/views/ops/machine/component/MachineDetail.vue')),
getDefaultProps: (size: Size) => {
return {
size,
};
},
},
'db-info': {
component: defineAsyncComponent(() => import('@/views/ops/db/component/DbDetail.vue')),
getDefaultProps: (size: Size) => {
return {
size,
};
},
},
} as Record<string, ComponentConfig>;
export const MessageRenderer = defineComponent({
props: {
content: String,
size: {
type: String as () => Size,
default: 'default',
},
},
setup(props) {
const parseContent = (content: string) => {
if (!content) {
return [h('span', '')];
}
// 创建一个包装容器来处理HTML内容
const container = document.createElement('div');
container.innerHTML = content;
const parseNode = (node: Node): any => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement;
const tagName = element.tagName.toLowerCase();
let attrs: Record<string, any> = {};
// 提取属性
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
const componentConf = components[tagName];
if (!componentConf) {
return h(tagName, attrs, Array.from(element.childNodes).map(parseNode));
}
// 存在默认组件配置,则合并
if (componentConf.getDefaultProps) {
const defaultProps = componentConf.getDefaultProps(props.size);
attrs = {
...defaultProps,
...attrs,
};
}
return h(componentConf.component, attrs, {
default: () => Array.from(element.childNodes).map(parseNode),
});
}
return '';
};
return Array.from(container.childNodes).map(parseNode);
};
return () => {
// 根据 size 属性确定根元素的 class
const rootClass = props.size === 'small' ? 'text-sm' : props.size === 'large' ? 'text-lg' : 'text-base';
try {
const elements = parseContent(props.content || '');
return h('div', { class: rootClass }, elements);
} catch (e) {
console.error('消息渲染失败:', e);
return h('div', { class: rootClass }, props.content || '');
}
};
},
});

View File

@@ -1,7 +1,7 @@
<template>
<div class="monaco-editor-custom relative h-full">
<div class="monaco-editor-content" ref="monacoTextareaRef" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable size="small">
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value" />
</el-select>
</div>
@@ -34,15 +34,9 @@ import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineComplet
import { editor, languages } from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/
// import Monokai from 'monaco-themes/themes/Monokai.json'
// import Active4D from 'monaco-themes/themes/Active4D.json'
// import ahe from 'monaco-themes/themes/All Hallows Eve.json'
// import bop from 'monaco-themes/themes/Birds of Paradise.json'
// import krTheme from 'monaco-themes/themes/krTheme.json'
// import Dracula from 'monaco-themes/themes/Dracula.json'
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import SolarizedLight from './themes/Solarized-light.json';
import SolarizedDark from './themes/Solarized-dark.json';
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
import { ElOption, ElSelect } from 'element-plus';
@@ -96,7 +90,11 @@ const languageArr = [
},
{
value: 'html',
label: 'XML/HTML',
label: 'Html',
},
{
value: 'xml',
label: 'Xml',
},
{
value: 'python',
@@ -155,6 +153,7 @@ const defaultOptions = {
scrollBeyondLastLine: false,
lineNumbers: 'on',
lineNumbersMinChars: 3,
fixedOverflowWidgets: true, // 使弹出层不被容器限制
} as editor.IStandaloneEditorConstructionOptions;
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
@@ -167,6 +166,9 @@ self.MonacoEnvironment = {
if (label === 'json') {
return new JsonWorker();
}
if (label === 'html') {
return new HtmlWorker();
}
return new EditorWorker();
},
};
@@ -225,15 +227,18 @@ const initMonacoEditorIns = () => {
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
monaco.editor.defineTheme('SolarizedDark', SolarizedDark);
defaultOptions.language = state.languageMode;
defaultOptions.theme = themeConfig.value.editorTheme;
let options = Object.assign(defaultOptions, props.options as any);
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, options);
// 监听内容改变,双向绑定
monacoEditorIns.onDidChangeModelContent(() => {
modelValue.value = monacoEditorIns.getModel()?.getValue();
});
if (!options.readOnly) {
// 监听内容改变,双向绑定
monacoEditorIns.onDidChangeModelContent(() => {
modelValue.value = monacoEditorIns.getModel()?.getValue();
});
}
};
const changeLanguage = (value: any) => {
@@ -322,7 +327,7 @@ defineExpose({ getEditor, format, focus });
z-index: 2;
right: 10px;
top: 10px;
max-width: 130px;
max-width: 100px;
}
border: 1px solid var(--el-border-color-light, #ebeef5);

View File

@@ -9,16 +9,18 @@ export type MonacoEditorDialogProps = {
language: string;
height?: string;
width?: string;
options?: any; // 可选项如字体大小等
options?: any; // 可选项,如字体大小等
canChangeLang?: boolean; // 是否可以切换语言
showConfirmButton?: boolean;
confirmFn?: Function; // 点击确认的回调函数入参editor value
confirmFn?: Function; // 点击确认的回调函数,入参editor value
closeFn?: Function; // 点击取消 或 关闭弹窗的回调函数
completionItemProvider?: monaco.languages.CompletionItemProvider; // 自定义补全项
useDrawer?: boolean; // 是否使用drawer而不是dialog,默认false
drawerSize?: string | number; // drawer尺寸,默认'50%'
};
const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
const boxId = 'monaco-editor-dialog-id';
const boxId = props.useDrawer ? 'monaco-editor-drawer-id' : 'monaco-editor-dialog-id';
let boxInstance: VNode;
const container = document.getElementById(boxId);
@@ -35,6 +37,9 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
if (props.content === undefined) {
props.content = '';
}
if (props.useDrawer === undefined) {
props.useDrawer = false;
}
// 创建 虚拟dom
boxInstance = h(MonacoEditorDialog, {
@@ -53,6 +58,7 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
}
// 移除 container DOM 元素
document.body.removeChild(container);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
props.closeFn && props.closeFn();
},
onConfirm: () => {
@@ -72,6 +78,7 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
// 压缩json字符串
value = JSON.stringify(val);
}
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
props.confirmFn && props.confirmFn(value);
},
});

View File

@@ -1,6 +1,7 @@
<template>
<div>
<el-dialog :title="props.title" v-model="dialogVisible" :width="props.width" @close="close">
<!-- Dialog 模式 -->
<el-dialog :title="props.title" v-model="dialogVisible" :width="props.width" @close="close" v-if="!props.useDrawer">
<monaco-editor
ref="editorRef"
:height="props.height"
@@ -17,12 +18,40 @@
</span>
</template>
</el-dialog>
<!-- Drawer 模式 -->
<el-drawer
:title="props.title"
v-model="dialogVisible"
:size="props.drawerSize || '50%'"
@close="close"
:destroy-on-close="true"
:close-on-click-modal="true"
class="monaco-editor-drawer"
v-else
>
<monaco-editor
ref="editorRef"
:height="props.height || 'calc(100vh - 120px)'"
class="editor"
:language="props.language"
v-model="modelValue"
:options="props.options"
:can-change-mode="props.canChangeLang"
/>
<template #footer>
<div class="drawer-footer">
<el-button @click="dialogVisible = false">{{ i18n.global.t('common.cancel') }}</el-button>
<el-button v-if="props.showConfirmButton" @click="confirm" type="primary">{{ i18n.global.t('common.confirm') }}</el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { ElDialog, ElButton, ElMessage } from 'element-plus';
import { ElDialog, ElDrawer, ElButton, ElMessage } from 'element-plus';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { MonacoEditorDialogProps } from './MonacoEditorBox';
@@ -45,6 +74,18 @@ const dialogVisible = defineModel<boolean>('visible', {
const emit = defineEmits(['close', 'confirm']);
const formatXML = function (xml: string, tab?: string) {
let formatted = '',
indent = '';
tab = tab || ' ';
xml.split(/>\s*</).forEach(function (node) {
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
formatted += indent + '<' + node + '>\r\n';
if (node.match(/^<?\w[^>]*[^\/]$/)) indent += tab;
});
return formatted.substring(1, formatted.length - 3);
};
watch(
() => props.language,
() => {
@@ -103,26 +144,11 @@ const close = () => {
}, 200);
};
const formatXML = function (xml: string, tab?: string) {
let formatted = '',
indent = '';
tab = tab || ' ';
xml.split(/>\s*</).forEach(function (node) {
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
formatted += indent + '<' + node + '>\r\n';
if (node.match(/^<?\w[^>]*[^\/]$/)) indent += tab;
});
return formatted.substring(1, formatted.length - 3);
};
function compressHTML(html: string) {
return (
html
.replace(/[\r\n\t]+/g, ' ') // 移除换行符和制表符
// .replace(/<!--[\s\S]*?-->/g, '') // 移除注释
.replace(/\s{2,}/g, ' ') // 合并多个空格为一个空格
.replace(/>\s+</g, '><')
); // 移除标签之间的空格
return html
.replace(/[\r\n\t]+/g, ' ') // 移除换行符和制表符
.replace(/\s{2,}/g, ' ') // 合并多个空格为一个空格
.replace(/>\s+</g, '><'); // 移除标签之间的空格
}
</script>
<style lang="scss" scoped>
@@ -130,4 +156,23 @@ function compressHTML(html: string) {
font-size: 9pt;
font-weight: 600;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 10px 0;
}
:deep(.monaco-editor-drawer) {
.el-drawer__header {
margin-bottom: 20px;
}
.el-drawer__body {
padding: 0;
}
.el-drawer__footer {
padding: 0 20px;
}
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<div class="h-full">
<monaco-editor
ref="editorRef"
:height="props.height"
class="editor"
language="text"
v-model="modelValue"
:options="{
readOnly: true,
}"
:can-change-mode="false"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, watch } from 'vue';
import { useWebSocket } from '@vueuse/core';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const props = defineProps({
height: {
type: String,
default: '100%',
},
wsUrl: {
type: String,
default: '',
},
});
const websocketUrl = ref(props.wsUrl);
const { data } = useWebSocket(websocketUrl);
const editorRef = useTemplateRef<InstanceType<typeof MonacoEditor>>('editorRef');
const modelValue = defineModel<string>('modelValue', {
type: String,
default: '',
});
watch(data, (value) => {
// eslint-disable-next-line no-control-regex
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
setTimeout(() => {
revealLastLine();
}, 200);
});
const reload = (wsUrl: string) => {
modelValue.value = '';
websocketUrl.value = wsUrl;
revealLastLine();
};
const revealLastLine = () => {
const editor = editorRef.value?.getEditor();
const lineCount = editor?.getModel()?.getLineCount();
editor?.revealLine(lineCount || 0);
};
defineExpose({
reload,
});
</script>
<style lang="scss" scoped>
.editor {
font-size: 9pt;
font-weight: 600;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<template>
<div class="h-full flex flex-col flex-1 overflow-hidden">
<transition name="el-zoom-in-top">
<transition name="page-table-search-form">
<!-- 查询表单 -->
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
<!-- 遍历父组件传入的 solts 透传给子组件 -->
@@ -21,7 +21,7 @@
<div class="flex">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="flex">
<el-dropdown v-if="searchItems?.length > 1">
<el-dropdown v-if="props.searchItems?.length > 1">
<SvgIcon :size="16" name="CaretBottom" class="!mr-1 !mt-1.5 simple-search-form-btn" />
<template #dropdown>
<el-dropdown-menu>
@@ -54,7 +54,7 @@
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button
v-if="showToolButton('search') && searchItems?.length > 1"
v-if="showToolButton('search') && props.searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
circle
@click="isShowSearch = !isShowSearch"
@@ -68,7 +68,7 @@
trigger="click"
>
<div v-for="(item, index) in tableColumns" :key="index">
<el-checkbox v-model="item.show" :label="$t(item.label)" :true-value="true" :false-value="false" />
<el-checkbox v-model="item.show" :label="$t(item.label)" :true-value="1" :false-value="0" />
</div>
<template #reference>
<el-button icon="Operation" circle :size="props.size"></el-button>
@@ -171,9 +171,9 @@ import EnumTag from '@/components/enumtag/EnumTag.vue';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
import Api from '@/common/Api';
import SearchForm from '@/components/SearchForm/index.vue';
import { SearchItem } from '../SearchForm/index';
import SearchFormItem from '../SearchForm/components/SearchFormItem.vue';
import SearchForm from '@/components/pagetable/SearchForm/index.vue';
import { SearchItem } from './SearchForm/index';
import SearchFormItem from './SearchForm/components/SearchFormItem.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { usePageTable } from '@/hooks/usePageTable';
import { ElInput, ElTable } from 'element-plus';
@@ -365,4 +365,22 @@ defineExpose({
total,
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.page-table-search-form-enter-active {
transition: all 0.3s ease-out;
}
.page-table-search-form-leave-active {
transition: all 0.3s ease-in;
}
.page-table-search-form-enter-from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
.page-table-search-form-leave-to {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
</style>

View File

@@ -37,11 +37,11 @@
</template>
<script setup lang="ts" name="SearchForm">
import { computed, ref } from 'vue';
import { BreakPoint } from '@/components/Grid/interface/index';
import { BreakPoint } from '@/components/pagetable/Grid/interface/index';
import { Delete, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import SearchFormItem from './components/SearchFormItem.vue';
import Grid from '@/components/Grid/index.vue';
import GridItem from '@/components/Grid/components/GridItem.vue';
import Grid from '@/components/pagetable/Grid/index.vue';
import GridItem from '@/components/pagetable/Grid/components/GridItem.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { SearchItem } from './index';

View File

@@ -71,9 +71,9 @@ export class TableColumn {
formatFunc: Function;
/**
* 是否显示该列
* 是否显示该列,1显示 0不显示
*/
show: boolean = true;
show: number = 1;
/**
* 是否展示美化按钮主要用于美化json文本等
@@ -225,13 +225,19 @@ export class TableColumn {
let maxData;
// 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) {
let nowData = tableData[i];
let nowValue = getValueByPath(nowData, prop);
const nowData = tableData[i];
const nowValue = getValueByPath(nowData, prop);
if (!nowValue) {
continue;
}
// 转为字符串比较长度
let nowText = nowValue + '';
let nowText;
if (typeof nowValue === 'object') {
nowText = JSON.stringify(nowValue);
} else {
nowText = nowValue + '';
}
if (nowText.length > maxWidthText.length) {
maxWidthText = nowText;
maxWidthValue = nowValue;

View File

@@ -66,7 +66,7 @@
import Guacamole from './guac/guacamole-common';
import { getMachineRdpSocketUrl } from '@/views/ops/machine/api';
import clipboard from './guac/clipboard';
import { reactive, ref } from 'vue';
import { onUnmounted, reactive, ref } from 'vue';
import { TerminalStatus } from '@/components/terminal/common';
import ClipboardDialog from '@/components/terminal-rdp/guac/ClipboardDialog.vue';
import { TerminalExpose } from '@/components/terminal-rdp/index';
@@ -77,6 +77,7 @@ import { useDebounceFn, useEventListener } from '@vueuse/core';
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
import { ElMessage } from 'element-plus';
import { joinClientParams } from '@/common/request';
import { MachineProtocolEnum } from '@/views/ops/machine/enums';
const viewportRef = ref({} as any);
const displayRef = ref({} as any);
@@ -91,6 +92,10 @@ const props = defineProps({
type: String,
required: true,
},
protocol: {
type: Number,
default: 2, // 2=RDP, 3=VNC
},
clipboardList: {
type: Array,
default: () => [],
@@ -189,7 +194,7 @@ const installClipboard = () => {
};
const installResize = () => {
// 在resize事件结束后300毫秒执行
// 在 resize 事件结束后 300 毫秒执行,使用防抖
useEventListener('resize', useDebounceFn(resize, 300));
};
@@ -333,19 +338,21 @@ const resize = () => {
const width = parseInt(String(box.clientWidth));
const height = parseInt(String(box.clientHeight));
// VNC 协议只发送尺寸不重连RDP 协议在连接状态下发送尺寸,未连接时重连
if (state.display.getWidth() !== width || state.display.getHeight() !== height) {
if (state.status !== TerminalStatus.Connected) {
connect(width, height);
} else {
// VNC 协议protocol=3只发送尺寸变化不触发重连
if (props.protocol === MachineProtocolEnum.Vnc.value) {
// VNC: 仅发送尺寸
state.client.sendSize(width, height);
} else {
// RDP: 未连接时重连,已连接时发送尺寸
if (state.status !== TerminalStatus.Connected) {
connect(width, height);
} else {
state.client.sendSize(width, height);
}
}
}
// setting timeout so display has time to get the correct size
// setTimeout(() => {
// const scale = Math.min(box.clientWidth / Math.max(state.display.getWidth(), 1), box.clientHeight / Math.max(state.display.getHeight(), 1));
// state.display.scale(scale);
// console.log(state.size, scale);
// }, 100);
};
const handleMouseState = (mouseState: any, showCursor = false) => {
@@ -477,6 +484,10 @@ const exposes = {
setRemoteClipboard: onsubmitClipboard,
} as TerminalExpose;
onUnmounted(() => {
disconnect();
});
defineExpose(exposes);
</script>

View File

@@ -18,10 +18,11 @@ import { useThemeConfig } from '@/store/themeConfig';
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
import TerminalSearch from './TerminalSearch.vue';
import { TerminalStatus } from './common';
import { useDebounceFn, useEventListener, useIntervalFn } from '@vueuse/core';
import themes from './themes';
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();
@@ -53,6 +54,7 @@ const { themeConfig } = storeToRefs(useThemeConfig());
// 终端实例
let term: Terminal;
let socket: WebSocket;
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
const state = reactive({
// 插件
@@ -110,7 +112,6 @@ const initTerm = async () => {
cursorBlink: true,
disableStdin: false,
allowProposedApi: true,
fastScrollModifier: 'ctrl',
theme: getTerminalTheme(),
});
@@ -124,7 +125,7 @@ const initTerm = async () => {
// 注册窗口大小监听器
useEventListener('resize', useDebounceFn(fitTerminal, 400));
initSocket();
await initSocket();
// 注册其他插件
loadAddon();
@@ -140,33 +141,31 @@ const initTerm = async () => {
});
};
const initSocket = () => {
const initSocket = async () => {
if (!props.socketUrl) {
return;
}
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
// 监听socket连接
socket.onopen = () => {
// 注册心跳
useIntervalFn(sendPing, 15000);
state.status = TerminalStatus.Connected;
focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendData(props.cmd + ' \r');
}
};
// 监听socket错误信息
socket.onerror = (e: Event) => {
try {
socket = await createWebSocket(`${props.socketUrl}${props.socketUrl.includes('?') ? '&' : '?'}rows=${term?.rows}&cols=${term?.cols}`);
} catch (e) {
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
state.status = TerminalStatus.Error;
console.log('连接错误', e);
};
return;
}
// 注册心跳
startHeartbeat();
state.status = TerminalStatus.Connected;
focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendData(props.cmd + ' \r');
}
socket.onclose = (e: CloseEvent) => {
console.log('terminal socket close...', e.reason);
@@ -174,6 +173,22 @@ const initSocket = () => {
};
};
const startHeartbeat = () => {
stopHeartbeat();
console.log('terminal start heartbeat');
heartbeatTimer = setInterval(() => {
sendPing();
}, 10000);
};
const stopHeartbeat = () => {
if (heartbeatTimer) {
console.log('terminal stop heartbeat');
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
};
const loadAddon = () => {
// 注册搜索组件
const searchAddon = new SearchAddon();
@@ -282,6 +297,7 @@ const sendData = (key: any) => {
};
const closeSocket = () => {
stopHeartbeat();
// 关闭 websocket
socket && socket.readyState === 1 && socket.close();
};

View File

@@ -0,0 +1,20 @@
import { ref } from 'vue';
export function useDataState<KeyType, ValueType extends number | boolean | string>() {
const dataState = ref(new Map<KeyType, ValueType>());
const setState = (key: KeyType, value: ValueType) => {
dataState.value.set(key, value as any);
};
const getState = (key: KeyType): ValueType => {
const result = dataState.value.get(key);
return result as ValueType;
};
return {
dataState,
setState,
getState,
};
}

View File

@@ -10,9 +10,13 @@ import { ref, unref } from 'vue';
import { URL_401 } from '@/router/staticRouter';
import openApi from '@/common/openApi';
import { useThemeConfig } from '@/store/themeConfig';
import JSONBig from 'json-bigint';
const baseUrl: string = config.baseApiUrl;
// 配置 JSONBig将大数int64/uint64转为字符串避免精度丢失
const JSONBigString = JSONBig({ storeAsString: true });
const useCustomFetch = createFetch({
baseUrl: baseUrl,
combination: 'chain',
@@ -20,7 +24,7 @@ const useCustomFetch = createFetch({
immediate: false,
timeout: 600000,
// beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
async beforeFetch({ options }) {
async beforeFetch({ url, options }) {
const token = getToken();
const headers = new Headers(options.headers || {});
@@ -31,38 +35,50 @@ const useCustomFetch = createFetch({
const themeConfig = useThemeConfig().themeConfig;
headers.set('Content-Type', 'application/json');
// 如果不是 FormData才设置 Content-Type
if (!(options.body instanceof FormData)) {
headers.set('Content-Type', 'application/json');
}
headers.set('Accept-Language', themeConfig?.globalI18n);
options.headers = headers;
return { options };
return { url, options };
},
async afterFetch(ctx) {
ctx.data = await ctx.response.json();
async afterFetch(ctx: any) {
// 使用 json-bigint 解析响应数据,解决 int64/uint64 精度丢失问题
const responseText = await ctx.response.text();
try {
ctx.data = JSONBigString.parse(responseText);
} catch (err) {
// 如果解析失败,尝试使用原生 JSON.parse
try {
ctx.data = JSON.parse(responseText);
} catch {
ctx.data = responseText;
}
}
return ctx;
},
},
});
interface EsReq {
esProxyReq: boolean;
esProxyReq?: boolean;
}
export interface RequestOptions extends RequestInit, EsReq {}
export function useApiFetch<T>(api: Api, params: any = null, reqOptions?: RequestOptions) {
const uaf = useCustomFetch<T>(api.url, {
export function useApiFetch<T, P = any>(api: Api, params?: P, reqOptions?: RequestOptions) {
const currentParam = ref(params);
const uaf: any = useCustomFetch<T>(api.url, {
async beforeFetch({ url, options }) {
options.method = api.method;
if (!params) {
return;
}
let paramsValue = unref(params);
let paramsValue = unref(currentParam);
let apiUrl = url;
// 简单判断该url是否是restful风格
if (apiUrl.indexOf('{') != -1) {
if (apiUrl.indexOf('{') != -1 && paramsValue) {
apiUrl = templateResolve(apiUrl, paramsValue);
}
@@ -70,44 +86,75 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions?: Reques
paramsValue = await api.beforeHandler(paramsValue);
}
if (paramsValue) {
const method = options.method?.toLowerCase();
// post和put使用json格式传参
if (method === 'post' || method === 'put') {
options.body = JSON.stringify(paramsValue);
// post和put使用json格式传参如果是FormData则直接使用
const method = options.method?.toLowerCase();
if ((method === 'post' || method === 'put') && paramsValue) {
if (paramsValue instanceof FormData) {
options.body = paramsValue;
// 对于 FormData删除 Content-Type header让浏览器自动设置 multipart/form-data 和 boundary
if (options.headers instanceof Headers) {
options.headers.delete('Content-Type');
} else if (options.headers && typeof options.headers === 'object') {
delete (options.headers as any)['Content-Type'];
}
} else {
const searchParam = new URLSearchParams();
Object.keys(paramsValue).forEach((key) => {
const val = paramsValue[key];
if (val) {
searchParam.append(key, val);
}
});
apiUrl = `${apiUrl}?${searchParam.toString()}`;
options.body = JSON.stringify(paramsValue);
}
} else if (paramsValue && method !== 'post' && method !== 'put') {
const searchParam = new URLSearchParams();
Object.keys(paramsValue).forEach((key) => {
const val = paramsValue[key];
if (val) {
searchParam.append(key, val);
}
});
apiUrl = `${apiUrl}?${searchParam.toString()}`;
}
// 确保 FormData 的 body 不被 reqOptions 覆盖
const finalOptions = {
...options,
...reqOptions,
};
// 如果原始 options.body 是 FormData优先保留
if (options.body instanceof FormData) {
finalOptions.body = options.body;
// 对于 FormData不要设置 Content-Type让浏览器自动设置 multipart/form-data 和 boundary
if (finalOptions.headers instanceof Headers) {
finalOptions.headers.delete('Content-Type');
} else if (finalOptions.headers && typeof finalOptions.headers === 'object') {
delete (finalOptions.headers as any)['Content-Type'];
}
}
return {
url: apiUrl,
options: {
...options,
...reqOptions,
},
options: finalOptions,
};
},
onFetchError: (ctx) => {
onFetchError: (ctx: { data: any }) => {
if (reqOptions?.esProxyReq) {
uaf.data = { value: JSON.parse(ctx.data) };
// 使用 json-bigint 解析错误响应
try {
const errorText = typeof ctx.data === 'string' ? ctx.data : JSON.stringify(ctx.data);
uaf.data = { value: JSONBigString.parse(errorText) };
} catch {
uaf.data = { value: ctx.data };
}
return Promise.resolve(uaf.data);
}
return ctx;
},
}) as any;
});
// 统一处理后的返回结果如果直接使用uaf.data则数据会出现由{code: x, data: {}} -> data 的变化导致某些结果绑定报错
const data = ref<T | null>(null);
return {
execute: async function () {
execute: async function (executeParam?: P) {
if (executeParam !== undefined) {
currentParam.value = executeParam;
}
await execCustomFetch(uaf, reqOptions);
data.value = uaf.data.value;
},

131
frontend/src/i18n/en/ai.ts Normal file
View File

@@ -0,0 +1,131 @@
export default {
ai: {
assistant: {
newSession: 'New Session',
startNewConversation: 'Start a New Conversation',
selectOrCreateSession: 'Select a session or create a new one',
},
chat: {
connectionFailed: 'Connection failed, please check your network or contact the administrator',
connectionDisconnected: 'Connection disconnected, please refresh the page to retry',
thinking: 'Thinking',
toolCall: 'Tool Call',
toolCallResult: 'Tool Call Result',
type: 'Type',
processed: 'Processed',
interrupts: ' interrupts',
submitting: '(Submitting...)',
reconnecting: 'Reconnecting to server, please wait...',
connectionLost: 'Connection lost, trying to reconnect...',
customTriggerDialogTitle: 'Custom Trigger Symbol Selection',
},
interrupt: {
approval: {
title: 'Approval Interrupt',
toolInfo: 'Tool Info',
toolName: 'Tool Name',
description: 'Description',
executionParams: 'Execution Parameters',
interruptId: 'Interrupt ID',
operationRecord: 'Operation Record',
operationType: 'Operation Type',
operationTime: 'Operation Time',
additionalData: 'Additional Data',
approve: 'Approve',
reject: 'Reject',
approved: 'Approved',
rejected: 'Rejected',
pendingApproval: 'Pending Approval',
pendingSubmit: 'Pending Submit',
selected: 'Selected: ',
rejectTitle: 'Rejection Reason',
rejectReasonPlaceholder: 'Please enter the reason for rejection',
rejectReasonRequired: 'Rejection reason is required',
},
confirmation: {
title: 'Confirmation',
pleaseSelect: 'Please Select',
interruptId: 'Interrupt ID',
operationRecord: 'Operation Record',
operationType: 'Operation Type',
operationTime: 'Operation Time',
selectionResult: 'Selection Result',
confirm: 'Confirm',
cancel: 'Cancel',
confirmed: 'Confirmed',
cancelled: 'Cancelled',
pendingConfirmation: 'Pending Confirmation',
},
generic: {
interrupt: 'Interrupt',
operationInterrupted: 'Operation Interrupted',
details: 'Details',
interruptId: 'Interrupt ID',
operationRecord: 'Operation Record',
operationType: 'Operation Type',
operationTime: 'Operation Time',
additionalData: 'Additional Data',
confirm: 'Confirm',
reject: 'Reject',
processed: 'Processed',
rejected: 'Rejected',
pending: 'Pending',
rejectTitle: 'Rejection Reason',
rejectReasonPlaceholder: 'Please enter the reason for rejection',
rejectReasonRequired: 'Rejection reason is required',
},
assetSelection: {
title: 'Asset Selection',
selectAsset: 'Select Asset',
selected: 'Selected',
pending: 'Pending',
toolInfo: 'Tool Info',
toolName: 'Tool Name',
toolDesc: 'Tool Description',
availableAssets: 'Available Assets ({count})',
noAssets: 'No available assets',
operationRecord: 'Operation Record',
selectedAsset: 'Selected Asset',
operationTime: 'Operation Time',
confirm: 'Confirm Selection',
cancel: 'Cancel',
dbType: 'Database Type',
protocol: 'Protocol',
sshTunnel: 'SSH Tunnel',
authCert: 'Auth Certificate',
selectDbHint: 'Please select database instance and specific database',
},
paramCompletion: {
title: 'Complete Parameters',
completeParam: 'Complete Parameters',
completed: 'Completed',
pending: 'Pending',
toolInfo: 'Tool Info',
toolName: 'Tool Name',
confirm: 'Confirm',
cancel: 'Cancel',
dbType: 'Database Type',
selectDbHint: 'Please select database instance and specific database',
selectMachineHint: 'Please select the machine to operate',
machineIp: 'Machine IP',
authCert: 'Auth Certificate',
missingParams: 'Missing Parameters',
unsupportedType: 'Unsupported parameter type',
enterParamsHint: 'Please enter the following parameters',
confirmTriggered: 'Selected, please submit all',
cancelTriggered: 'Selection cancelled',
},
status: {
interrupted: 'Interrupted',
resumed: 'Resumed',
},
action: {
approve: 'Approve',
reject: 'Reject',
confirm: 'Confirmed',
cancel: 'Cancelled',
},
batchSubmit: 'Submit All ({count})',
},
},
};

View File

@@ -5,6 +5,7 @@ export default {
edit: 'Edit',
delete: 'Delete',
detail: 'Details',
apply: 'Apply',
add: 'Add',
save: 'Save',
close: 'Close',
@@ -49,10 +50,13 @@ export default {
reset: 'Reset',
success: 'Success',
fail: 'Fail',
requestFail: 'Request Fail',
previousStep: 'Previous Step',
nextStep: 'Next Step',
copy: 'Copy',
copySuccess: 'Copy Success',
copyCell: 'Copy Cell',
search: 'Search',
pleaseInput: 'Please enter {label}',
pleaseSelect: 'Please select {label}',
@@ -69,6 +73,8 @@ export default {
fieldNotEmpty: '{field} cannot be empty',
selectAll: 'Select all',
MultiPlaceholder: 'Multiple are separated by commas',
appSlogan: 'Simple, efficient and secure',
preview: 'Preview',
},
layout: {
user: {
@@ -150,8 +156,6 @@ export default {
isUniqueOpened: 'Menu accordion',
isFixedHeader: 'Fixed header',
isClassicSplitMenu: 'Classic layout split menu',
isLockScreen: 'Open the lock screen',
lockScreenTime: 'screen locking(s/s)',
interfaceDisplay: 'Interface display',
isShowLogo: 'Sidebar logo',
isBreadcrumb: 'Open breadcrumb',
@@ -192,145 +196,6 @@ export default {
btnTwo: 'Update now',
btnTwoLoading: 'Updating',
},
menu: {
index: 'Home Page',
personalCenter: 'Personal Center',
tag: 'Tag',
tagTree: 'Tag Tree',
tagSave: 'Save Tag',
tagDelete: 'Delete Tag',
authorization: 'Authorization',
authorizationBase: 'Base Permission',
authorizationSave: 'Save Authorization',
authorizationDelete: 'Delete Authorization',
team: 'Team',
teamSave: 'Save Team',
teamDelete: 'Delete Team',
teamMemberAdd: 'Add Member',
teamMemberDelete: 'Delete Member',
teamTagSave: 'Save Team Tag',
machine: 'Machine',
machineOp: 'Machine Operation',
machineOpBase: 'Base Permission',
machineList: 'Machine List',
machineBase: 'Base Permission',
machineCreate: 'Create Machine',
machineEdit: 'Edit Machine',
machineDelete: 'Delete Machine',
machineTerminal: 'Machine Terminal',
machineFileConf: 'File',
machineFileConfCreate: 'File-Add Config',
machineFileConfDelete: 'File-Delete Config',
machineFileCreate: 'File-Create',
machineFileDelete: 'File-Delete',
machineFileWrite: 'File-Write',
machineFileUpload: 'File-Upload',
machineScript: 'Script',
machineScriptSave: 'Script-Save',
machineScriptDelete: 'Script-Delete',
machineScriptRun: 'Script-Run',
machineKillprocess: 'Kill Process',
machineCronJob: 'Cron Job',
machineCronJobSvae: 'Cron Job-Save',
machineCronJobDelete: 'Cron Job-Delete',
machineSecurityConfig: 'Security Config',
machineSecurityCmdSvae: 'Cmd Config-Save',
machineSecurityCmdDelete: 'Cmd Config-Delete',
dbms: 'DBMS',
dbDataOp: 'Data Operation',
dbDataOpBase: 'Base Permission',
dbDataOpSqlScriptRun: 'SQL Script Run',
dbInstance: 'DB Instance',
dbInstanceBase: 'Base Permission',
dbInstanceSave: 'Save Instance',
dbInstanceDelete: 'Delete Instance',
dbBase: 'Db Base Permission',
dbSave: 'Save Db',
dbDelete: 'Delete Db',
dbDataSync: 'Data Sync',
dbDataSyncBase: 'Base Permission',
dbDataSyncSave: 'Save Sync Task',
dbDataSyncDelete: 'Delete Sync Task',
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
dbDataSyncLog: 'Sync Log',
dbTransfer: 'DB Transfer',
dbTransferBase: 'Base Permission',
dbTransferSave: 'Save Transfer Task',
dbTransferDelete: 'Delete Transfer Task',
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
dbTransferRun: 'Run Transfer Task',
dbTransferRunLog: 'Transfer Log',
dbTransferFileShow: 'ransfer File-Show',
dbTransferFileDelete: 'Transfer File-Delete',
dbTransferFileDownload: 'Transfer File-Download',
dbTransferFileRun: 'Transfer File-Run',
redis: 'Redis',
redisDataOp: 'Data Operation',
redisDataOpBase: 'Base Permission',
redisDataOpSave: 'Save Data',
redisDataOpDelete: 'Delete Data',
redisManage: 'Redis Manage',
redisManageBase: 'Base Permission',
mongo: 'Mongo',
mongoDataOp: 'Data Operation',
mongoDataOpBase: 'Base Permission',
mongoDataOpSave: 'Save Data',
mongoDataOpDelete: 'Delete Data',
mongoManage: 'Mongo Manage',
mongoManageBase: 'Base Permission',
flow: 'Flow',
myTask: 'My Task',
myFlow: 'My Flow',
flowProcDef: 'Process Define',
flowProcDefSave: 'Save Process Define',
flowProcDefDelete: 'Delete Process Define',
msgManage: 'Message',
channel: 'Message Channel',
msgChannelBase: 'Base Permission',
saveMsgChannel: 'Save Message Channel',
delMsgChannel: 'Delete Message Channel',
msgTmpl: 'Message Template',
msgTmplBase: 'Base Permission',
saveMsgTmpl: 'Save Message Template',
delMsgTmpl: 'Delete Message Template',
sendMsg: 'Send Message',
system: 'System',
menuPermission: 'Menu & Permission',
menuPermissionBase: 'Base Permission',
menuPermissionAdd: 'Add Menu Permission',
menuPermissionEdit: 'Edit Menu Permission',
menuPermissionDelete: 'Delete Menu Permission',
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
account: 'Account',
accountBase: 'Base Permission',
accountAdd: 'Add Account',
accountEdit: 'Edit Account',
accountDelete: 'Delete Account',
accountEnableDisable: 'Enable/Disable Account',
accountRoleAllocation: 'Role Allocation',
role: 'Role',
roleBase: 'Base Permission',
roleAdd: 'Add Role',
roleEdit: 'Edit Role',
roleDelete: 'Delete Role',
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
sysConf: 'System Config',
sysConfBase: 'Base Permission',
sysConfSave: 'Save System Config',
opLog: 'Operation Log',
opLogBase: 'Base Permission',
noPagePermission: 'No Page Permission',
authcertShowciphertext: 'Show Ciphertext',
},
home: {
personalInfo: 'Personal Information',
welcomeMsg: `Hello, {name}, no matter how bad life gets, it doesn't prevent me from getting better!`,

View File

@@ -1,3 +1,5 @@
import { exportExcel } from '@/common/utils/export';
export default {
db: {
// db instance
@@ -16,6 +18,7 @@ export default {
dbFilterPlaceholder: 'DB name: Input filterable',
sqlRecord: 'SQL records',
dump: 'Export',
dbDumpFail: 'DB export failed',
dumpContent: 'Export Content',
structure: 'Structure',
data: 'Data',
@@ -55,6 +58,8 @@ export default {
execSuccess: 'Successful execution',
execFail: 'Execution failure',
sqlScriptRun: 'Run SQL Script',
sqlScriptRunSuccess: 'SQL script executed successfully',
sqlScriptRunFail: 'SQL script execution failed',
saveSql: 'Save SQL',
execInfo: 'Execution info',
result: 'Result',
@@ -62,7 +67,7 @@ export default {
resultSet: 'Result Set',
tableDataEmptyTextTips:
'tips: Single table query at the beginning of select * or click the default query data of the table name, double-click the data online modification',
noSelctRunSqlMsg: 'Select the sql you want to execute',
noSelectRunSqlMsg: 'Select the sql you want to execute or move the cursor near the sql you want to execute',
enterExecRemarkTips: 'Please enter remark',
execRemarkPlaceholder: 'Enter the remark to execute the sql',
currentSqlTabIsRunning: 'The current result set tab is being executed, please use the new TAB to execute',
@@ -96,6 +101,7 @@ export default {
cancelFiexd: 'Cancel Fixed',
formView: 'Form View',
genJson: 'Generating JSON',
exportExcel: 'Export Excel',
exportCsv: 'Export CSV',
exportSql: 'Export SQL',
onlySelectOneData: 'Only one row can be selected',
@@ -163,6 +169,7 @@ export default {
transfer2Db: 'Transfer to DB',
transfer2File: 'Transfer to File',
fileSaveDays: 'File retention days',
fileType: 'File Type',
transferStrategy: 'Transfer Strategy',
day: 'Day',
transferFull: 'Full',

View File

@@ -0,0 +1,83 @@
export default {
docker: {
containerConf: 'Container Config',
addr: 'Address',
addrTips: 'eg: unix:///var/run/docker.sock 、tcp://192.168.1.1',
container: 'Container',
containerName: 'Container Name',
running: 'Running',
stopped: 'Stopped',
name: 'Container Name',
ip: 'IP Address',
status: 'Status',
stats: 'Stats',
memory: 'Memory',
stop: 'Stop',
stopContainerConfirm: 'Are you sure to stop container [{name}] ?',
removeContainerConfirm: 'Are you sure to remove container [{name}] ?',
restart: 'Restart',
createContainer: 'Create Container',
mount: 'Mount',
hostDir: 'Host Directory',
containerDir: 'Container Directory',
permission: 'Permission',
rw: 'RW',
ro: 'RO',
port: 'Port',
image: 'Image',
tag: 'Tag',
size: 'Size',
used: 'Used',
unUsed: 'UnUsed',
imageName: 'Image Name',
log: 'Log',
lines: 'Lines',
follow: 'Follow',
stopImageConfirm: 'Are you sure to stop image [{name}] ?',
export: 'Export',
imageUploading: 'Image uploading, please wait...',
imageTips: 'Support manual input and select',
forcePull: 'Force Pull Image',
hostPortPlaceholder: '80',
forcePullTips: 'Ignore the server existing image, pull again',
server: 'Server',
protocol: 'Protocol',
networkMode: 'Network Mode',
consoleTerminal: 'Console Terminal',
otherOption: 'Other Option',
tty: 'tty',
openStdin: 'stdin (-i)',
privileged: 'Privileged',
restartPolicy: 'Restart Policy',
noRestart: 'No Restart',
alwaysRestart: 'Always Restart',
onFailure: 'On Failure',
unlessStopped: 'Unless Stopped',
cpuShare: 'CPU Share',
cpuShareTips: 'The default container share is 1024 cpus, and increasing it will give the current container more CPU time',
cpuQuota: 'CPU Quota',
cpuLimitTips: 'A CPU limit of 0 turns off the limit',
cpuCanUseTips: 'The maximum available is {cpuTotal} cores',
core: 'Core',
memoryLimit: 'Memory Limit',
memoryLimitTips: 'A memory limit of 0 turns off the limit',
shmSize: 'Shm Size',
memoryCanUseTips: 'Maximum available {memTotal}',
tagTips: `One in a row, for example:
tag1=value1
tag2=value2`,
envParam: 'Env Param',
envParamTips: `One in a row, for example:
env1=value1
env2=value2`,
device: 'Device',
driver: 'Driver',
driverTips: 'Device drivers to be used by the container, e.g. : nvidia, etc',
count: 'Count',
capabilitie: 'Capabilitie',
deviceId: 'Device ID',
capabilitiePlaceholder: 'eg: gpu',
},
};

View File

@@ -1,6 +1,7 @@
export default {
es: {
keywordPlaceholder: 'host / name / code',
protocol: 'Protocol',
port: 'Port',
size: 'size',
docs: 'docs',
@@ -16,11 +17,11 @@ export default {
connSuccess: 'be connected successfully',
shouldTestConn: 'please test connection first',
instance: 'ES Instance',
instanceSave: 'Save Instance',
instanceDel: 'Delete Instance',
operation: 'Data Operation',
dataSave: 'Data Save',
dataDel: 'Data Del',
instanceSave: 'ES-Save Instance',
instanceDel: 'Es-Delete Instance',
operation: 'Es-Data Operation',
dataSave: 'Es-Data Save',
dataDel: 'Es-Data Del',
indexName: 'Index Name',
requireIndexName: 'Index Name Is Required',
indexDetail: 'Index Detail',
@@ -59,7 +60,6 @@ export default {
docJsonError: 'Document JSON Format Error',
sortParams: 'Sort',
otherParams: 'Other',
previewParams: 'Preview',
closeIndexConfirm: 'This operation will close index [{name}]. Do you want to continue?',
openIndexConfirm: 'This operation will open index [{name}]. Do you want to continue?',
clearCacheConfirm: 'This operation will clear index [{name}] cache. Do you want to continue?',

View File

@@ -113,5 +113,9 @@ export default {
taskBeginTime: 'Start Time',
flowAudit: 'Process Audit',
notify: 'Notify',
aitask: 'AI Task',
aiAuditRule: 'Audit Rule',
aiAuditRuleTip: 'Please input the audit rule',
},
};

View File

@@ -23,6 +23,7 @@ export default {
openTerminal: 'Open Terminal',
newTabOpenTerminal: 'Open Terminal(New TAB)',
fileManage: 'File Manage',
fileTabPrefix: 'File-',
scriptManage: 'Script Manage',
machineState: 'Machine State',
remoteFileDesktopManage: 'Remote desktop file management', // Remote desktop file management
@@ -65,7 +66,7 @@ export default {
processName: 'Process Name',
selectSortType: 'Please select a sort type',
selectProcessNum: 'Please select the number of processes',
cpuDesc: 'CUP descending',
cpuDesc: 'CPU descending',
memDesc: 'Memory descending',
virtualMemory: 'Virtual Memory',
fixedMemory: 'Fixed Memory',
@@ -87,6 +88,8 @@ export default {
scriptResultEnumRealTime: 'Real-time',
scriptTypeEnumPrivate: 'Private',
scriptTypeEnumPublic: 'Public',
category: 'Category',
categoryTips: 'support input new category and selection',
// security
cmdConfig: 'Command Config',
@@ -136,5 +139,7 @@ export default {
fileTooLargeTips: 'The file is too large, please download and use it',
uploadSuccess: 'Upload successfully',
fileExceedsSysConf: 'The uploaded file exceeds the system configuration [{uploadMaxFileSize}]',
fileUploadSuccess: 'Machine file upload successful',
fileUploadFail: 'Machine file upload failed',
},
};

View File

@@ -0,0 +1,154 @@
export default {
menu: {
index: 'Home',
personalCenter: 'Personal Center',
myResource: 'Resource',
tag: 'Resource',
tagTree: 'Resource Tree',
tagSave: 'Save Tag',
tagDelete: 'Delete Tag',
authorization: 'Authorization',
authorizationBase: 'Base Permission',
authorizationSave: 'Save Authorization',
authorizationDelete: 'Delete Authorization',
team: 'Team',
teamSave: 'Save Team',
teamDelete: 'Delete Team',
teamMemberAdd: 'Add Member',
teamMemberDelete: 'Delete Member',
teamTagSave: 'Save Team Tag',
machine: 'Machine',
machineOp: 'Machine Operation',
machineOpBase: 'Base Permission',
machineList: 'Machine List',
machineBase: 'Base Permission',
machineCreate: 'Create Machine',
machineEdit: 'Edit Machine',
machineDelete: 'Delete Machine',
machineTerminal: 'Machine Terminal',
machineFileConf: 'File',
machineFileConfCreate: 'File-Add Config',
machineFileConfDelete: 'File-Delete Config',
machineFileCreate: 'File-Create',
machineFileDelete: 'File-Delete',
machineFileWrite: 'File-Write',
machineFileUpload: 'File-Upload',
machineScript: 'Script',
machineScriptSave: 'Script-Save',
machineScriptDelete: 'Script-Delete',
machineScriptRun: 'Script-Run',
machineKillprocess: 'Kill Process',
machineCronJob: 'Cron Job',
machineCronJobSvae: 'Cron Job-Save',
machineCronJobDelete: 'Cron Job-Delete',
machineSecurityConfig: 'Security Config',
machineSecurityCmdSvae: 'Cmd Config-Save',
machineSecurityCmdDelete: 'Cmd Config-Delete',
db: 'Database',
dbms: 'DBMS',
dbDataOp: 'Data Operation',
dbDataOpBase: 'DB-Base Permission',
dbDataOpSqlScriptRun: 'DB-SQL Script Run',
dbDataExport: 'DB-Data Export',
dbInstance: 'DB Instance',
dbInstanceBase: 'Base Permission',
dbInstanceSave: 'Save Instance',
dbInstanceDelete: 'Delete Instance',
dbBase: 'Db Base Permission',
dbSave: 'Save Db',
dbDelete: 'Delete Db',
dbDataSync: 'Data Sync',
dbDataSyncBase: 'Base Permission',
dbDataSyncSave: 'Save Sync Task',
dbDataSyncDelete: 'Delete Sync Task',
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
dbDataSyncLog: 'Sync Log',
dbTransfer: 'DB Transfer',
dbTransferBase: 'Base Permission',
dbTransferSave: 'Save Transfer Task',
dbTransferDelete: 'Delete Transfer Task',
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
dbTransferRun: 'Run Transfer Task',
dbTransferRunLog: 'Transfer Log',
dbTransferFileShow: 'ransfer File-Show',
dbTransferFileDelete: 'Transfer File-Delete',
dbTransferFileDownload: 'Transfer File-Download',
dbTransferFileRun: 'Transfer File-Run',
redis: 'Redis',
redisSave: 'Save Redis',
redisDel: 'Delete Redis',
redisDataOp: 'Redis - Data Operation',
redisDataOpBase: 'Redis - Base Permission',
redisDataOpSave: 'Redis - Save Data',
redisDataOpDelete: 'Redis - Delete Data',
redisManage: 'Redis Manage',
redisManageBase: 'Redis - Base Permission',
mongo: 'Mongo',
mongoSave: 'Save Mongo',
mongoDel: 'Delete Mongo',
mongoDataOp: 'Mongo - Data Operation',
mongoDataOpBase: 'Mongo - Base Permission',
mongoDataOpSave: 'Mongo - Save Data',
mongoDataOpDelete: 'Mongo - Delete Data',
mongoManage: 'Mongo Manage',
mongoManageBase: 'Mongo - Base Permission',
container: 'Container',
containerSave: 'Save Container',
containerDel: 'Delete Container',
flow: 'Flow',
myTask: 'My Task',
myFlow: 'My Flow',
flowProcDef: 'Process Define',
flowProcDefSave: 'Save Process Define',
flowProcDefDelete: 'Delete Process Define',
msgManage: 'Message',
channel: 'Message Channel',
msgChannelBase: 'Base Permission',
saveMsgChannel: 'Save Message Channel',
delMsgChannel: 'Delete Message Channel',
msgTmpl: 'Message Template',
msgTmplBase: 'Base Permission',
saveMsgTmpl: 'Save Message Template',
delMsgTmpl: 'Delete Message Template',
sendMsg: 'Send Message',
system: 'System',
menuPermission: 'Menu & Permission',
menuPermissionBase: 'Base Permission',
menuPermissionAdd: 'Add Menu Permission',
menuPermissionEdit: 'Edit Menu Permission',
menuPermissionDelete: 'Delete Menu Permission',
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
account: 'Account',
accountBase: 'Base Permission',
accountAdd: 'Add Account',
accountEdit: 'Edit Account',
accountDelete: 'Delete Account',
accountEnableDisable: 'Enable/Disable Account',
accountRoleAllocation: 'Role Allocation',
role: 'Role',
roleBase: 'Base Permission',
roleAdd: 'Add Role',
roleEdit: 'Edit Role',
roleDelete: 'Delete Role',
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
sysConf: 'System Config',
sysConfBase: 'Base Permission',
sysConfSave: 'Save System Config',
opLog: 'Operation Log',
opLogBase: 'Base Permission',
noPagePermission: 'No Page Permission',
authcertShowciphertext: 'Show Ciphertext',
aiAssistant: 'AI Assistant',
},
};

View File

@@ -0,0 +1,253 @@
export default {
milvus: {
createResourceGroup: 'Create Resource Group',
createUser: 'Create User',
createRole: 'Create Role',
dataOperation: 'Data Operation',
resourceGroupName: 'Resource Group Name',
resourceGroupNamePlaceholder: 'Please enter resource group name',
resourceGroupDetail: 'Resource Group Detail',
descriptionPlaceholder: 'Please enter description',
collectionDetail: 'Collection Detail',
createPartition: 'Create Partition',
createCollection: 'Create Collection',
collectionNamePlaceholder: 'Please enter collection name',
description: 'Description',
shardsNum: 'Shards Number',
consistencyLevel: 'Consistency Level',
createDatabase: 'Create Database',
databaseName: 'Database Name',
databaseNamePlaceholder: 'Please enter database name',
partitionName: 'Partition Name',
partitionNamePlaceholder: 'Please enter partition name',
createIndex: 'Create Index',
fieldName: 'Field Name',
fieldNamePlaceholder: 'Please enter field name',
indexName: 'Index Name',
indexNamePlaceholder: 'Please enter index name',
indexType: 'Index Type',
indexTypeMem: 'Mem Index',
indexTypeDisk: 'Disk Index',
indexTypeGpu: 'GPU Index',
metricType: 'Metric Type',
roleName: 'Role Name',
roleNamePlaceholder: 'Please enter role name',
grantPrivilege: 'Grant',
host: 'Host',
database: 'Database',
code: 'Code',
loadStatus: 'Load Status',
loaded: 'Loaded',
unloaded: 'Unloaded',
detail: 'Detail',
load: 'Load',
release: 'Release',
confirmDeleteCollection: 'Are you sure you want to delete collection {name}?',
confirmDeleteDatabase: 'Are you sure you want to delete database {name}?',
confirmDeletePartition: 'Are you sure you want to delete partition {name}?',
confirmDeleteIndex: 'Are you sure you want to delete index {name}?',
confirmDeleteRole: 'Are you sure you want to delete role {name}?',
confirmDeleteUser: 'Are you sure you want to delete user {name}?',
confirmDeleteResourceGroup: 'Are you sure you want to delete resource group {name}?',
partitionExists: 'Partition exists',
partitionNotExists: 'Partition does not exist',
createdSuccess: 'Created successfully',
updatedSuccess: 'Updated successfully',
deletedSuccess: 'Deleted successfully',
loadedSuccess: 'Loaded successfully',
releasedSuccess: 'Released successfully',
connSuccess: 'Connected successfully',
savedSuccess: 'Saved successfully',
querySuccess: 'Query successful, total {count} records',
searchSuccess: 'Search successful, total {count} results',
insertSuccess: 'Inserted successfully',
insertCount: 'Insert count: {count}',
versionInfo: 'Version Information',
healthStatus: 'Health Status',
healthy: 'Healthy',
unhealthy: 'Unhealthy',
notFetched: 'Not fetched',
notChecked: 'Not checked',
systemInfo: 'System Information',
healthDetail: 'Health Details',
dbId: 'DB ID',
dbName: 'Database Name',
createTime: 'Create Time',
apply: 'Apply',
checkHealth: 'Check Health',
getVersion: 'Get Version',
config: 'Config',
databaseProperties: 'Database Properties',
timezone: 'Timezone',
timezonePlaceholder: 'Please select timezone',
dataQuery: 'Data Query',
vectorSearch: 'Vector Search',
dataInsert: 'Data Insert',
collectionNameLabel: 'Collection Name',
queryExpr: 'Query Expression',
queryExprPlaceholder: 'e.g., id > 0',
outputFields: 'Output Fields',
outputFieldsPlaceholder: 'Comma separated, e.g., id,name,age',
query: 'Query',
queryResults: 'Query Results',
vectorField: 'Vector Field',
vectorFieldPlaceholder: 'Vector field name',
vectorData: 'Vector Data',
vectorDataPlaceholder: 'Please enter vector data, JSON array format, e.g., [[0.1, 0.2, 0.3, ...]]',
topK: 'Top K',
search: 'Search',
searchResults: 'Search Results',
similarityScore: 'Similarity Score',
otherFields: 'Other Fields',
vector: 'Vector',
inputData: 'Data',
inputDataPlaceholder: 'Please enter JSON array',
insert: 'Insert',
insertResult: 'Insert Result',
objectType: 'Object Type',
objectName: 'Object Name',
objectNamePlaceholder: 'Object name, e.g., Collection name',
privilege: 'Privilege',
pleaseSelect: 'Please select',
pleaseInput: 'Please enter',
connAddress: 'Please enter connection address, format: host:port',
dbNamePlaceholder: 'Please enter database name, default is default',
checkExists: 'Check Exists',
databaseManagement: 'Database Management',
collectionManagement: 'Collection Management',
partitionManagement: 'Partition Management',
userPermission: 'User Permission',
roleManagement: 'Role Management',
resourceGroup: 'Resource Group',
fields: 'Fields',
field: 'Field',
properties: 'Properties',
dataType: 'Data Type',
dim: 'Dimension',
maxLength: 'Max Length',
elementType: 'Element Type',
maxCapacity: 'Max Capacity',
attributes: 'Attributes',
primaryKey: 'Primary Key',
autoID: 'Auto ID',
nullable: 'Nullable',
partitionKey: 'Partition Key',
clusteringKey: 'Clustering Key',
mmap: 'Field Mmap',
defaultValue: 'Default Value',
defaultValuePlaceholder: 'Please enter default value',
index: 'Index',
noIndexSupport: 'This field type does not support indexing',
noIndexCreated: 'No index created',
deleteIndex: 'Delete index',
currentIndex: 'Current index',
primaryKeyOnlyInt64OrVarChar: 'PrimaryKey only Int64 or VarChar',
selectFieldToConfig: 'Please select a field to configure',
cannotDeletePrimaryKey: 'Cannot delete auto-generated primary key field',
addFieldRequired: 'Please add at least one field',
primaryKeyRequired: 'Please set a primary key field',
vectorFieldIndexRequired: 'All vector fields must have an index. Please add index for the following fields first: {fields}',
editCollection: 'Edit Collection',
copyCollection: 'Copy Collection',
readonly: 'Readonly',
noIndex: 'No Index',
indexes: 'indexes',
dynamicField: 'Dynamic Field',
enableDynamicField: 'Enable Dynamic Field',
disableDynamicField: 'Disable Dynamic Field',
dynamicFieldHint: 'Dynamic field allows storing JSON data not defined in the Schema',
dynamicFieldIndexes: 'Dynamic Field Indexes',
addIndex: 'Add Index',
jsonPath: 'JSON Path',
jsonPathPlaceholder: 'e.g., $meta["key_name"]',
jsonPathTip: 'JSON path expression to specify the JSON field part to index. Example: json["field_name"] or json["nested"]["field"]',
jsonCastType: 'JSON Cast Type',
jsonCastTypeTip: 'Convert JSON value to data type for indexing. Options: bool, double, varchar, array_bool, array_double, array_varchar',
jsonCastFunction: 'JSON Cast Function',
jsonCastFunctionPlaceholder: 'e.g., STRING_TO_DOUBLE',
jsonCastFunctionTip:
'Function to convert JSON value to specified data type. Options: STRING_TO_DOUBLE (convert string to numeric). Example: "99.99" -> 99.99',
dynamicFieldNameTip:
'Field name for dynamic field index. Specify the dynamic field to index. Required for dynamic field index. Example: "dynamic_field_name"',
reset: 'Reset',
allPartitions: 'All',
importFile: 'Import File',
insertSampleData: 'Insert Sample Data',
clearData: 'Clear Data',
viewData: 'View Data',
export: 'Export',
noData: 'No Data',
selectCollectionFirst: 'Please select a Collection first',
queryExprRequired: 'Please enter query expression',
confirmInsertSample: 'Are you sure you want to insert sample data?',
confirmClearData: 'Are you sure you want to clear all data? This action cannot be undone.',
noPrimaryKey: 'Primary key field not found, cannot delete data',
paginationInfo: 'Total {total} records, page {current} / {pages}',
pageSize10: '10/page',
pageSize20: '20/page',
pageSize50: '50/page',
pageSize100: '100/page',
prevPage: 'Previous',
nextPage: 'Next',
jumpTo: 'Jump to',
editRole: 'Edit Role',
availableRoles: 'Available Roles',
selectedRoles: 'Assigned Roles',
authorized: 'Authorized',
privilegeGroupType: 'Privilege Group Type',
clusterPrivilege: 'Cluster Privilege',
databasePrivilege: 'Database Privilege',
collectionPrivilege: 'Collection Privilege',
customPrivilegeGroup: 'Custom Privilege Group',
privilegeGroupManagement: 'Privilege Groups',
addPrivilegeGroup: 'Privilege Group',
editPrivilegeGroup: 'Edit Privilege Group',
deletePrivilegeGroup: 'Delete Privilege Group',
privilegeGroupName: 'Group Name',
builtinPrivilegeGroup: 'Built-in Privilege Group',
customPrivilegeGroupTip: 'Only custom privilege groups can be edited. Built-in groups are read-only.',
selectPrivileges: 'Select Privileges',
confirmDeletePrivilegeGroup: 'Are you sure to delete privilege group [{name}]? This action cannot be undone.',
privilegeGroupSaveSuccess: 'Privilege group saved successfully',
privilegeGroupDeleteSuccess: 'Privilege group deleted successfully',
selectAll: 'Select All',
privileges: 'Privileges',
// System Info
databaseCount: 'Database Count',
collectionCount: 'Collection Count',
total: 'Total',
capacity: 'Capacity',
nodeCount: 'Node Count',
allNormal: 'All Normal',
issuesFound: 'issues found',
unknown: 'Unknown',
getVersionFailed: 'Failed to get version',
checkHealthFailed: 'Failed to check health',
systemConfig: 'System Configuration',
availableNodes: 'Available Nodes',
nodeConfig: 'Node Config',
requestNodeNum: 'Request Node Num',
limitNodeNum: 'Limit Node Num',
nodeDetails: 'Node Details',
nodeId: 'Node ID',
nodeAddress: 'Node Address',
nodeHostname: 'Hostname',
loadedCollections: 'Loaded Collections',
replica: 'replica',
// Alias Management
aliases: 'Aliases',
addAlias: 'Add Alias',
aliasName: 'Alias Name',
aliasPlaceholder: 'Please enter alias name',
confirmDeleteAlias: 'Are you sure you want to delete alias {name}?',
addedAliasSuccess: 'Alias added successfully',
deletedAliasSuccess: 'Alias deleted successfully',
// Load Status Confirmation
loading: 'Loading',
confirmLoadCollection: 'Are you sure you want to load collection {name}?',
confirmReleaseCollection: 'You are attempting to release {name} with data. Please note that the data will no longer be available for search.',
},
};

View File

@@ -0,0 +1,89 @@
export default {
mq: {
kafka: {
hosts: 'Hosts',
username: 'Username',
sasl_mechanism: 'SASL Mechanism',
sasl_mechanism_placeholder: 'please select SASL Mechanism',
key: 'Key',
keywordPlaceholder: 'ip/name',
nodeManage: 'Node Management',
topicManage: 'Topic Management',
produceMessage: 'Produce Message',
consumeMessage: 'Consume Message',
nodes: 'Nodes',
config: 'Config',
nodeId: 'Node ID',
addr: 'Addr',
rack: 'Rack',
viewConfig: 'View Config',
brokerConfig: 'Broker Config',
topicConfig: 'topic Config',
selectTopic: 'select topic',
selectTopicPlaceholder: 'select topic',
selectTopicWarning: 'Please select topic',
selectBrokerViewConfig: 'Please select a broker to view config',
configName: 'Config Name',
configValue: 'Config Value',
configSource: 'Config Source',
configSensitive: 'Sensitive',
searchTopic: 'Enter topic name',
createTopic: 'Create Topic',
createPartitions: 'Create Partitions',
topicName: 'Topic Name',
topicNamePlaceholder: 'Please enter topic name',
partitions: 'Partitions',
topicPartitions: 'Topic Partitions',
replicationFactor: 'Replication Factor',
topicStatus: 'Status',
topicIsInternal: 'IsInternal',
viewPartitions: 'View Partitions',
delete: 'Delete',
messageKey: 'Message Key',
messageKeyPlaceholder: 'Optional: Enter message key',
partition: 'Partition',
partitionPlaceholder: 'Optional: Specify partition number, -1 for auto',
messageBody: 'Message Body',
messageHeaders: 'Message Headers',
addHeader: 'Add Header',
headerKey: 'Header Key',
headerValue: 'Header Value',
sendTimes: 'Send Times',
compression: 'Compression',
compressionPlaceholder: 'Please select compression',
sendMessage: 'Send Message',
messageNumber: 'Number',
consumerGroup: 'Consumer Group',
consumerGroupPlaceholder: 'Enter consumer group, empty for auto generate',
consumerOnlyTip: 'Note: Only effective when Group is first consumed, subsequent changes are invalid',
pullTimeout: 'Pull Timeout (s)',
decompression: 'Decompression',
decompressionPlaceholder: 'Please select decompression',
decode: 'Decode',
decodePlaceholder: 'Please select decode',
isolationLevel: 'Isolation Level',
isolationLevelPlaceholder: 'Please select isolation level',
readUncommitted: 'Read Uncommitted',
readCommitted: 'Read Committed',
commitOffset: 'Commit Offset',
loadOffsets: 'Load Offsets',
defaultConsumePosition: 'Default Consume Position',
earliest: 'Earliest',
latest: 'Latest',
defaultConsumeStartTime: 'Default Consume Start Time',
selectDateTime: 'Select date time',
offset: 'Offset',
timestamp: 'Timestamp',
headers: 'Headers',
messageDetail: 'Message Detail',
groupId: 'Group ID',
coordinator: 'Coordinator',
state: 'State',
protocolType: 'Protocol Type',
searchGroup: 'Enter group name',
selectGroupPlaceholder: 'select group',
Members: 'Members',
partitionsFeatureComingSoon: 'Partitions feature coming soon',
},
},
};

View File

@@ -25,8 +25,8 @@ export default {
success: 'Success',
menuCodeTips: `The menu type is the access path (if the menu path does not begin with '/', the access address will automatically concatenate the parent menu path), otherwise it is the unique code of the resource`,
menuCodePlaceholder: `A menu that does not begin with '/' will automatically concatenate the parent menu path`,
routerNameTips: 'For component caching to work, match the vue component name, such as ResourceLis',
componentPathTips: 'Access path components, such as: ` system/resource/ResourceList `, default in ` views ` directory',
routerNameTips:
'For component caching to work, the key for route.ts in the frontend module should match the vue component name, such as ResourceList',
isCacheTips: `If yes is selected, it will be 'keepalive' cached (reentering the page without refreshing the page and requesting data again), and needs the route name to match the vue component name`,
isHideTips:
'Select Hide and the route will not appear in the menu bar, but it will still be accessible. Disabled will not be able to access and operate',
@@ -190,6 +190,13 @@ export default {
loginFailCountPlaceholder: 'Disable login after n failed login attempts',
loginFainMin: 'Prohibited login time',
loginFailMinPlaceholder: 'After a specified number of login failures, re-login is prohibited within m minutes',
aiModelConf: 'AI Model Config',
aiModel: 'Model',
aiModelPlaceholder: 'protocol/model name, such as openai/gpt-3.5-turbo',
aiBaseUrl: 'Base URL',
aiBaseUrlPlaceholder: 'Please enter the model request URL',
aiApiKey: 'API Key',
},
syslog: {
operator: 'Operator',

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