Compare commits

...

66 Commits

Author SHA1 Message Date
meilin.huang
9b7e569b3a feat: 机器终端支持文件&文件夹上传、支持选中文件下载 2026-05-14 21:29:13 +08:00
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
meilin.huang
d6280ea280 refactor: 使用泛型重构参数绑定等 2025-05-24 16:22:54 +08:00
meilin.huang
666b191b6c fix: some issue 2025-05-23 17:26:12 +08:00
meilin.huang
778cb7f4de reafctor: pool 2025-05-22 23:29:50 +08:00
zongyangleo
142bbd265d !134 feat: 新增支持es和连接池
* feat: 各连接,支持连接池
* feat:支持es
2025-05-21 04:42:30 +00:00
meilin.huang
f676ec9e7b feat: flow design & page query refactor 2025-05-20 21:04:47 +08:00
meilin.huang
44d379a016 otp: 样式优化 2025-04-26 17:37:09 +08:00
meilin.huang
2170509d92 refactor: code optimization 2025-04-23 20:36:32 +08:00
meilin.huang
798ab7d18b style: fix 2025-04-20 21:01:01 +08:00
meilin.huang
abd2b4bac0 refactor: 引入tailwind css & 后端部分非公共包位置调整 2025-04-18 22:07:37 +08:00
1163 changed files with 77374 additions and 416611 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

@@ -7,20 +7,34 @@ ARG MAYFLY_GO_VERSION
ARG MAYFLY_GO_DIR_NAME=mayfly-go-linux-${TARGETARCH}
ARG MAYFLY_GO_URL=https://gitee.com/dromara/mayfly-go/releases/download/${MAYFLY_GO_VERSION}/${MAYFLY_GO_DIR_NAME}.zip
RUN wget -cO mayfly-go.zip ${MAYFLY_GO_URL} && \
RUN apk add --no-cache wget unzip && \
wget -cO mayfly-go.zip ${MAYFLY_GO_URL} && \
unzip mayfly-go.zip && \
mv ${MAYFLY_GO_DIR_NAME}/* /opt
cp -r ${MAYFLY_GO_DIR_NAME}/. /opt/ && \
rm -rf mayfly-go.zip ${MAYFLY_GO_DIR_NAME}
FROM ${BASEIMAGES}
ARG TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY --from=builder /opt/mayfly-go /usr/local/bin/mayfly-go
# 安装必要的运行时依赖并创建非root用户
RUN apk add --no-cache ca-certificates tzdata && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone && \
addgroup -g 1000 mayfly && \
adduser -u 1000 -G mayfly -s /bin/sh -D mayfly
# 复制构建产物
COPY --from=builder /opt/ /mayfly-go/
# 设置工作目录和权限
WORKDIR /mayfly-go
RUN chown -R mayfly:mayfly /mayfly-go
# 切换到非root用户
USER mayfly
EXPOSE 18888
CMD ["mayfly-go"]
CMD ["./mayfly-go"]

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

@@ -15,11 +15,14 @@
<img src="https://img.shields.io/github/stars/dromara/mayfly-go.svg?style=social" alt="github star"/>
<img src="https://img.shields.io/github/forks/dromara/mayfly-go.svg?style=social" alt="github fork"/>
</a>
<a href="https://github.com/dromara/mayfly-go" target="_blank">
<img src="https://gitcode.com/dromara/mayfly-go/star/badge.svg" alt="github star"/>
</a>
<a href="https://hub.docker.com/r/mayflygo/mayfly-go/tags" target="_blank">
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.24%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
@@ -28,7 +31,7 @@
## 前言
Web版 **统一管理操作平台**集成了对Linux系统的全面操作支持包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis单机、哨兵、集群模式以及 MongoDB 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
Web **统一管理操作平台**,集成了对 Linux 系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite、ClickHouse 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis单机、哨兵、集群模式MongoDB、Elasticsearch、Kafka、Milvus 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
## 开发语言与主要框架
@@ -48,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 "屏幕截图")
@@ -106,7 +99,7 @@ http://go.mayfly.run
## 💌 支持作者
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 帮我点个 ⭐ Star这将是对我极大的鼓励与支持。
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 或 <a target="_blank" href="https://gitcode.com/dromara/mayfly-go">Gitcode</a> 帮我点个 ⭐ Star这将是对我极大的鼓励与支持。
> 喝杯咖啡 ☕️ 或者来杯奶茶 🧋,让作者更有精神,写出更棒的代码!

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 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/vue3-essential', '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-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2],
'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/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': 'error',
'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,58 +10,68 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^13.1.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.7",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.2",
"monaco-sql-languages": "^0.12.2",
"monaco-themes": "^0.4.4",
"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.4.10",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0"
"shiki": "^4.0.2",
"shiki-stream": "^0.1.4",
"sortablejs": "^1.15.7",
"sql-formatter": "^15.7.3",
"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": {
"@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.3",
"@vue/compiler-sfc": "^3.5.13",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^10.0.0",
"prettier": "^3.2.5",
"sass": "^1.86.3",
"typescript": "^5.8.2",
"vite": "^6.2.6",
"@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

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

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="h100">
<el-watermark
:zIndex="10000000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
:content="themeConfig.watermarkText"
class="h100"
>
<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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M96.426667 649.173333H712.96a137.173333 137.173333 0 0 0 0-274.346666H96.426667c-12.8 43.52-19.626667 89.514667-19.626667 137.173333s6.826667 93.696 19.626667 137.173333z" fill="#07A5DE" p-id="6101"></path><path d="M563.2 25.6A486.4 486.4 0 0 0 125.354667 299.946667H837.546667c52.096 0 97.450667-29.013333 120.661333-71.808A485.76 485.76 0 0 0 563.2 25.6z" fill="#EFBF19" p-id="6102"></path><path d="M942.421333 816.64a137.258667 137.258667 0 0 0-129.749333-92.586667H125.312A486.4 486.4 0 0 0 563.2 998.4c153.344 0 290.090667-70.954667 379.221333-181.76z" fill="#3EBEB1" p-id="6103"></path><path d="M506.197333 649.173333c12.8-43.52 19.626667-89.514667 19.626667-137.173333s-6.826667-93.696-19.626667-137.173333H96.469333c-12.8 43.52-19.626667 89.514667-19.626666 137.173333s6.826667 93.696 19.626666 137.173333h409.728z" fill="#231F20" p-id="6104"></path><path d="M477.269333 724.053333H125.354667a488.533333 488.533333 0 0 0 175.957333 197.888 488.533333 488.533333 0 0 0 175.957333-197.930666z" fill="#019B8F" p-id="6105"></path><path d="M301.312 102.058667a488.533333 488.533333 0 0 1 175.957333 197.930666H125.354667a488.533333 488.533333 0 0 1 175.957333-197.930666z" fill="#D8A22A" p-id="6106"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M465.664 679.168c105.301333 0.597333 172.970667 1.066667 202.922667 1.450667 20.48 0.256 36.181333 0.426667 47.274666 0.469333h3.84c45.824 0 84.096 8.533333 114.901334 25.258667 31.189333 16.938667 54.826667 42.368 70.826666 76.245333l1.152 2.517333a24.106667 24.106667 0 0 1-1.621333 3.413334c-46.336 67.84-101.034667 116.565333-164.096 146.346666-63.146667 29.824-134.613333 40.704-214.485333 32.469334-159.232-16.384-283.477333-106.24-372.352-269.994667a5.973333 5.973333 0 0 1 3.584-8.618667c13.653333-3.968 27.733333-6.528 41.941333-7.594666 91.306667-1.365333 170.538667-1.877333 238.165333-1.962667h27.946667z m44.885333 63.829333l-0.64 1.152c-3.754667 6.485333-9.386667 15.36-16.128 25.6l-2.645333 3.925334-1.578667 2.346666c-21.205333 31.445333-51.072 72.234667-70.784 94.464 64.853333 34.304 133.162667 45.44 227.157334 27.52 95.146667-18.090667 145.450667-52.565333 175.829333-114.090666-5.034667-10.581333-14.592-19.285333-31.488-27.733334-12.8-6.4-32.426667-11.050667-58.752-14.250666l-221.013333 1.066666z m-257.578666-5.546666l1.237333 1.536c21.504 26.112 67.712 72.277333 96.896 95.786666 15.146667-14.08 29.098667-29.397333 41.642667-45.824 13.952-18.261333 24.149333-32.64 35.370666-52.821333l-175.146666 1.322667z m471.296-360.874667c38.229333 5.077333 67.626667 18.944 88.448 41.301333 20.736 22.229333 33.024 52.992 36.565333 92.373334 3.626667 39.722667-5.76 71.808-27.733333 96.426666-20.906667 23.381333-53.461333 40.106667-97.877334 49.706667l-2.645333 0.597333-2.816 0.554667H144.725333a8.021333 8.021333 0 0 1-7.893333-6.485333 1545.173333 1545.173333 0 0 1-0.298667-1.578667c-12.373333-62.378667-18.517333-106.666667-18.517333-132.906667 0-38.570667 5.888-81.962667 17.706667-130.261333l1.066666-4.394667a7.082667 7.082667 0 0 1 6.826667-5.333333h580.650667zM197.546667 442.88l-0.853334 2.688c-7.509333 24.064-12.544 44.330667-12.117333 70.954667 0 30.293333 5.418667 54.272 13.653333 81.664h283.050667l0.341333-2.218667 0.469334-3.2c3.541333-24.448 4.010667-47.701333 4.010666-76.544 0-30.805333-1.066667-51.541333-6.4-75.264l-282.154666 1.92z m493.397333-3.029333l-131.797333 1.024 0.512 2.474666c4.48 22.357333 6.741333 43.861333 6.741333 73.216 0 30.421333-2.432 53.76-7.552 79.189334l134.826667-0.170667 1.962666-0.213333c28.16-2.901333 49.194667-7.210667 62.421334-23.04 11.52-13.866667 17.152-32.469333 17.152-55.765334 0-24.746667-6.272-42.624-19.456-54.826666-13.653333-12.714667-34.474667-19.2-61.994667-21.674667l-2.816-0.213333z m49.877333-342.784c63.104 29.824 117.845333 78.592 164.181334 146.346666l1.536 2.304a23.466667 23.466667 0 0 1-1.066667 3.669334c-16 33.92-39.594667 59.306667-70.784 76.245333-30.805333 16.768-69.12 25.258667-114.986667 25.258667-11.178667 0-28.16 0.213333-50.944 0.469333-45.098667 0.597333-112.597333 1.408-202.965333 1.493333h-14.122667c-70.613333 0-154.453333-0.512-251.733333-1.962666a207.061333 207.061333 0 0 1-42.24-7.594667 5.973333 5.973333 0 0 1-3.626667-8.618667C242.986667 170.922667 367.146667 81.066667 526.378667 64.682667c79.829333-8.277333 151.296 2.56 214.4 32.426666z m-102.101333 28.501333c-85.205333-15.36-143.957333-4.010667-213.717333 27.221333 11.648 13.312 26.410667 33.621333 40.874666 55.04l1.578667 2.389334 2.56 3.754666 3.498667 5.376 2.346666 3.584 1.237334 1.877334c18.688 28.757333 35.157333 56.746667 41.728 70.613333h213.674666l2.474667-0.298667 2.56-0.341333c21.290667-2.986667 38.144-10.794667 55.978667-19.754667 17.408-8.576 30.122667-18.304 40.106666-29.866666-49.493333-63.018667-108.586667-104.106667-194.901333-119.594667zM367.744 186.453333c-12.458667 10.069333-34.304 29.44-56.192 50.048l-1.706667 1.621334-3.328 3.157333-3.498666 3.328-1.877334 1.792-2.048 2.005333c-17.322667 16.64-33.578667 33.109333-44.501333 45.909334l179.797333-1.536-1.109333-1.877334a3067.264 3067.264 0 0 1-12.672-21.418666l-11.776-20.053334-2.474667-4.053333-2.56-4.266667-1.152-2.005333-1.237333-2.005333c-12.458667-20.693333-24.917333-40.405333-33.706667-50.645334z" fill="#2c2c2c" p-id="5739"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 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

@@ -1,10 +1,12 @@
import request from './request';
import { useApiFetch } from '@/hooks/useRequest';
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: RequestInit = {}) {
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);
await execute();
return data.value;
const res = await execute();
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

@@ -1,10 +1,12 @@
import { i18n } from '@/i18n';
import { ElMessage } from 'element-plus';
/**
* 不符合业务断言错误
*/
class AssertError extends Error {
constructor(message: string) {
ElMessage.error(message);
super(message);
// 错误类名
this.name = 'AssertError';
@@ -15,11 +17,11 @@ class AssertError extends Error {
* 断言表达式为true
*
* @param condition 条件表达式
* @param msg 错误消息
* @param msgOrI18nKey 错误消息 或者 i18n key
*/
export function isTrue(condition: boolean, msg: string) {
export function isTrue(condition: boolean, msgOrI18nKey: string) {
if (!condition) {
throw new AssertError(msg);
throw new AssertError(i18n.global.t(msgOrI18nKey));
}
}
@@ -30,7 +32,7 @@ export function isTrue(condition: boolean, msg: 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,12 +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, '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)' }),
};
// 标签关联的资源类型
@@ -24,11 +34,17 @@ export const TagResourceTypeEnum = {
Machine: ResourceTypeEnum.Machine,
DbInstance: ResourceTypeEnum.Db,
EsInstance: ResourceTypeEnum.Es,
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
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' }),
};
// 标签关联的资源类型路径
@@ -37,4 +53,33 @@ export const TagResourceTypePath = {
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
};
// 消息子类型
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.9.4',
};
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

@@ -1,34 +1,37 @@
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';
import { initMachineSysMsgs } from '@/components/sysmsg/machine';
import { initDbSysMsgs } from '@/components/sysmsg/db';
/**
* 初始化全局系统消息
*/
export function initSysMsgs() {
initMachineSysMsgs();
initDbSysMsgs();
}
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 +41,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 +59,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 +99,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 +109,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

@@ -0,0 +1,16 @@
/**
* 下载文件
* @param url 文件下载地址
*/
export function downloadFile(url: string) {
// 使用隐藏的 iframe 下载,避免页面闪烁
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
// 1秒后移除 iframe
setTimeout(() => {
document.body.removeChild(iframe);
}, 1000);
}

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 '';
}
@@ -30,6 +30,18 @@ export function formatByteSize(size: number, fixed = 2) {
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
}
export function formatDocSize(size: number, fixed = 2) {
if (size === 0) {
return '0';
}
const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
const base = 1000;
const exponent = Math.floor(Math.log(size) / Math.log(base));
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
}
/**
* 容量转为对应的字节大小,如 1KB转为 1024
* @param sizeString 1kb 1gb等
@@ -86,8 +98,8 @@ export function formatTime(time: number, unit: string = 's') {
let result = '';
const timeUnits = Object.entries(units).map(([unit, duration]) => {
const value = Math.floor(seconds / duration);
seconds %= duration;
const value = Math.floor(seconds / (duration as any));
seconds %= duration as any;
return { value, unit };
});
@@ -114,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,13 +1,14 @@
<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"
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
:key="Math.random()"
v-show="state.isShow && !allHide"
@contextmenu="headerContextmenuClick"
>
<ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList">
@@ -125,6 +126,10 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
};
const headerContextmenuClick = (event: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;

View File

@@ -37,9 +37,9 @@
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="7" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 7" class="w100" clearable v-model="checkboxList" multiple>
<div class="flex items-center w-full">
<el-radio v-model="radioValue" :label="7" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 7" class="!w-full" clearable v-model="checkboxList" multiple>
<el-option v-for="item in 31" :key="item" :value="`${item}`">{{ item }}</el-option>
</el-select>
</div>

View File

@@ -22,9 +22,9 @@
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
<div class="flex items-center w-full">
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
</el-select>
</div>

View File

@@ -22,9 +22,9 @@
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
<div class="flex items-center w-full">
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
</el-select>
</div>

View File

@@ -22,9 +22,9 @@
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
<div class="flex items-center w-full">
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
<el-option v-for="item in 12" :key="item" :value="`${item}`">{{ item }}</el-option>
</el-select>
</div>

View File

@@ -22,9 +22,9 @@
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
<div class="flex items-center w-full">
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
</el-select>
</div>

View File

@@ -32,9 +32,9 @@
</el-form-item> -->
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="6" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 6" class="w100" clearable v-model="checkboxList" multiple>
<div class="flex items-center w-full">
<el-radio v-model="radioValue" :label="6" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 6" class="!w-full" clearable v-model="checkboxList" multiple>
<el-option v-for="(item, index) of weekList" :label="item" :key="index" :value="`${index + 1}`">{{ $t(item) }}</el-option>
</el-select>
</div>

View File

@@ -26,9 +26,9 @@
</el-form-item>
<el-form-item>
<div class="flex-align-center w100">
<el-radio v-model="radioValue" :label="5" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 5" class="w100" clearable v-model="checkboxList" multiple>
<div class="flex items-center w-full">
<el-radio v-model="radioValue" :label="5" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
<el-select @click="radioValue = 5" class="!w-full" clearable v-model="checkboxList" multiple>
<el-option v-for="item in 9" :key="item" :value="`${item - 1 + fullYear}`" :label="item - 1 + fullYear" />
</el-select>
</div>

View File

@@ -1,10 +1,10 @@
<template>
<div class="dynamic-form-edit w100">
<el-table :data="formItems" stripe class="w100">
<div class="dynamic-form-edit !w-full">
<el-table :data="formItems" stripe class="!w-full">
<el-table-column prop="name" label="model" min-width="100px">
<template #header>
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addItem()"> </el-button>
<span class="ml10">model field</span>
<span class="ml-2">model field</span>
</template>
<template #default="scope">
<el-input v-model="scope.row['model']" :placeholder="$t('components.df.fieldModelPlaceholder')" clearable> </el-input>

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

@@ -1,5 +1,5 @@
<template>
<div class="icon-selector w100 h100">
<div class="icon-selector !w-full !h-full">
<el-input
v-model="state.fontIconSearch"
:placeholder="state.fontIconPlaceholder"
@@ -12,7 +12,7 @@
@blur="onIconBlur"
>
<template #prepend>
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14" />
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="!text-[14px]" />
</template>
</el-input>
<el-popover
@@ -25,7 +25,7 @@
virtual-triggering
>
<template #default>
<div class="ml5 mt5">{{ $t(title) }}</div>
<div class="ml-1 mt-1">{{ $t(title) }}</div>
<div class="icon-selector-warp">
<el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
<el-tab-pane lazy label="ele" name="ele">

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,15 +1,15 @@
<template>
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5); height: 100%">
<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-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
<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>
</template>
<script lang="ts" setup>
import { watch, toRefs, reactive, onMounted, onBeforeUnmount, useTemplateRef, Ref } from 'vue';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import * as monaco from 'monaco-editor';
// 相关语言
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js';
@@ -31,19 +31,12 @@ import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
// 提示
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
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';
@@ -97,7 +90,11 @@ const languageArr = [
},
{
value: 'html',
label: 'XML/HTML',
label: 'Html',
},
{
value: 'xml',
label: 'Xml',
},
{
value: 'python',
@@ -134,6 +131,7 @@ const defaultOptions = {
theme: 'SolarizedLight',
automaticLayout: true, //自适应宽高布局
foldingStrategy: 'indentation', //代码可分小段折叠
folding: true,
roundedSelection: false, // 禁用选择文本背景的圆角
matchBrackets: 'near',
linkedEditing: true,
@@ -149,7 +147,14 @@ const defaultOptions = {
minimap: {
enabled: false, // 不要小地图
},
};
renderLineHighlight: 'all',
selectOnLineNumbers: false,
readOnly: false,
scrollBeyondLastLine: false,
lineNumbers: 'on',
lineNumbersMinChars: 3,
fixedOverflowWidgets: true, // 使弹出层不被容器限制
} as editor.IStandaloneEditorConstructionOptions;
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
@@ -161,6 +166,9 @@ self.MonacoEnvironment = {
if (label === 'json') {
return new JsonWorker();
}
if (label === 'html') {
return new HtmlWorker();
}
return new EditorWorker();
},
};
@@ -219,14 +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;
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, Object.assign(defaultOptions, props.options as any));
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) => {
@@ -309,13 +321,16 @@ defineExpose({ getEditor, format, focus });
</script>
<style lang="scss">
.monaco-editor {
.monaco-editor-custom {
.code-mode-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 130px;
max-width: 100px;
}
border: 1px solid var(--el-border-color-light, #ebeef5);
width: 100%;
}
</style>

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,9 +1,8 @@
<template>
<div>
<transition name="el-zoom-in-top">
<div class="h-full flex flex-col flex-1 overflow-hidden">
<transition name="page-table-search-form">
<!-- 查询表单 -->
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search"
:reset="reset" :search-col="searchCol">
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
<!-- 遍历父组件传入的 solts 透传给子组件 -->
<template v-for="(_, key) in useSlots()" v-slot:[key]>
<slot :name="key"></slot>
@@ -11,83 +10,104 @@
</SearchForm>
</transition>
<div class="card">
<div class="table-main">
<!-- 表格头部 操作按钮 -->
<div class="table-header">
<div class="header-button-lf">
<slot name="tableHeader" />
</div>
<div v-if="toolButton" class="header-button-ri">
<slot name="toolButton">
<div class="tool-button">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="simple-search-form">
<el-dropdown v-if="searchItems?.length > 1">
<SvgIcon :size="16" name="CaretBottom" class="mr4 mt6 simple-search-form-btn" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="searchItem in searchItems"
:key="searchItem.prop" @click="changeSimpleFormItem(searchItem)">
{{ $t(searchItem.label) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="simple-search-form-label mt5">
<el-text truncated tag="b">{{ `${$t(nowSearchItem?.label)} : ` }}</el-text>
</div>
<el-form-item style="width: 200px" :key="nowSearchItem.prop">
<SearchFormItem @keyup.enter.native="searchFormItemKeyUpEnter"
v-if="!nowSearchItem.slot" :item="nowSearchItem"
v-model="queryForm[nowSearchItem.prop]" />
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else
:name="nowSearchItem.slot">
</slot>
</el-form-item>
</div>
<div>
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search"
circle @click="search" />
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button v-if="showToolButton('search') && searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'" circle
@click="isShowSearch = !isShowSearch" />
<el-popover placement="bottom" title="表格配置"
popper-style="max-height: 550px; overflow: auto; max-width: 450px" width="auto"
trigger="click">
<div v-for="(item, index) in tableColumns" :key="index">
<el-checkbox v-model="item.show" :label="item.label" :true-value="true"
:false-value="false" />
</div>
<template #reference>
<el-button icon="Operation" circle :size="props.size"></el-button>
</template>
</el-popover>
</div>
</div>
</slot>
</div>
<el-card class="h-full" body-class="h-full flex flex-col">
<!-- 表格头部 操作按钮 -->
<div class="flex justify-between">
<div>
<slot name="tableHeader" />
</div>
<el-table ref="tableRef" v-bind="$attrs" :max-height="tableMaxHeight"
@selection-change="handleSelectionChange" :data="tableData" highlight-current-row
v-loading="loading" :size="props.size as any" :border="border">
<slot v-if="toolButton" name="toolButton">
<div class="flex">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="flex">
<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>
<el-dropdown-item v-for="searchItem in searchItems" :key="searchItem.prop" @click="changeSimpleFormItem(searchItem)">
{{ $t(searchItem.label) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="text-right mr-1.5 mt-1">
<el-text truncated tag="b">{{ `${$t(nowSearchItem?.label)} : ` }}</el-text>
</div>
<el-form-item class="w-[200px]" :key="nowSearchItem.prop">
<SearchFormItem
@keyup.enter.native="searchFormItemKeyUpEnter"
v-if="!nowSearchItem.slot"
:item="nowSearchItem"
v-model="queryForm[nowSearchItem.prop]"
/>
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else :name="nowSearchItem.slot"> </slot>
</el-form-item>
</div>
<div class="ml-2">
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search" circle @click="search" />
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button
v-if="showToolButton('search') && props.searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
circle
@click="isShowSearch = !isShowSearch"
/>
<el-popover
placement="bottom"
title="表格配置"
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
width="auto"
trigger="click"
>
<div v-for="(item, index) in tableColumns" :key="index">
<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>
</template>
</el-popover>
</div>
</div>
</slot>
</div>
<div class="flex-1 overflow-auto">
<el-table
v-show="showTable"
ref="tableRef"
v-bind="$attrs"
height="100%"
@selection-change="handleSelectionChange"
:data="tableData"
highlight-current-row
v-loading="loading"
:size="props.size as any"
:border="border"
>
<el-table-column v-if="props.showSelection" :selectable="selectable" type="selection" width="40" />
<template v-for="(item, index) in tableColumns">
<el-table-column :key="index" v-if="item.show" :prop="item.prop" :label="$t(item.label)"
:fixed="item.fixed" :align="item.align" :show-overflow-tooltip="item.showOverflowTooltip"
:min-width="item.minWidth" :sortable="item.sortable || false" :type="item.type"
:width="item.width">
<el-table-column
:key="index"
v-if="item.show"
:prop="item.prop"
:label="$t(item.label)"
:fixed="item.fixed"
:align="item.align"
:show-overflow-tooltip="item.showOverflowTooltip"
:min-width="item.minWidth"
:sortable="item.sortable || false"
:type="item.type"
:width="item.width"
>
<!-- 插槽预留功能 -->
<template #default="scope" v-if="item.slot">
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
@@ -95,21 +115,29 @@
<!-- 枚举类型使用tab展示 -->
<template #default="scope" v-else-if="item.type == 'tag'">
<enum-tag :size="props.size" :enums="item.typeParam"
:value="item.getValueByData(scope.row)"></enum-tag>
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
</template>
<template #default="scope" v-else>
<!-- 配置了美化文本按钮以及文本内容大于指定长度则显示美化按钮 -->
<el-popover v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
effect="light" trigger="click" placement="top" width="600px">
<el-popover
v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
effect="light"
trigger="click"
placement="top"
width="600px"
>
<template #default>
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal"
type="textarea" />
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal" type="textarea" />
</template>
<template #reference>
<el-link @click="formatText(item.getValueByData(scope.row))" :underline="false"
type="success" icon="MagicStick" class="mr5"></el-link>
<el-link
@click="formatText(item.getValueByData(scope.row))"
underline="never"
type="success"
icon="MagicStick"
class="mr-1"
></el-link>
</template>
</el-popover>
@@ -120,38 +148,42 @@
</el-table>
</div>
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
<el-pagination :small="props.size == 'small'" @current-change="pageNumChange"
@size-change="pageSizeChange" style="text-align: right" layout="prev, pager, next, total, sizes"
:total="total" v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize"
:page-sizes="pageSizes" />
<el-row v-if="props.pageable" class="mt-4" type="flex" justify="end">
<el-pagination
:small="props.size == 'small'"
@current-change="pageNumChange"
@size-change="pageSizeChange"
layout="prev, pager, next, total, sizes"
:total="total"
v-model:current-page="queryForm.pageNum"
v-model:page-size="queryForm.pageSize"
:page-sizes="pageSizes"
/>
</el-row>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue } from 'vue';
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue, h } from 'vue';
import { TableColumn } from './index';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
import { useEventListener } from '@vueuse/core';
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 { ElTable } from 'element-plus';
import { ElInput, ElTable } from 'element-plus';
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
export interface PageTableProps {
size?: string;
pageApi?: Api; // 请求表格数据的 api
columns: TableColumn[]; // 列配置项 ==> 必传
columns: TableColumn[] | any[]; // 列配置项 ==> 必传
showSelection?: boolean;
selectable?: (row: any) => boolean; // 是否可选
pageable?: boolean;
@@ -208,6 +240,10 @@ const showToolButton = (key: 'setting' | 'search') => {
const nowSearchItem: Ref<SearchItem> = ref(null) as any;
// 是否已经计算列宽度
const isCalculatedWidth: Ref<boolean> = ref(false);
const showTable: Ref<boolean> = ref(false);
/**
* 改变当前的搜索项
* @param searchItem 当前点击的搜索项
@@ -239,24 +275,35 @@ const state = reactive({
pageSizes: [] as any, // 可选每页显示的数据量
// 输入框宽度
formatVal: '', // 格式化后的值
tableMaxHeight: '500px',
});
const { pageSizes, formatVal, tableMaxHeight } = toRefs(state);
const { pageSizes, formatVal } = toRefs(state);
watch(tableData, (newValue: any) => {
if (newValue && newValue.length > 0) {
props.columns.forEach((item) => {
if (item.autoWidth && item.show) {
item.autoCalculateMinWidth(tableData.value);
}
});
calculateTableColumnMinWidth();
// 需要计算完才能显示表格,否则会有表格闪烁的问题
if (!showTable.value) {
showTable.value = true;
}
});
watch(isShowSearch, () => {
calcuTableHeight();
});
/**
* 计算表格列宽
*/
const calculateTableColumnMinWidth = () => {
if (isCalculatedWidth.value || !tableData.value || tableData.value.length === 0) {
return;
}
// 计算表格列宽
props.columns.forEach((item) => {
if (item.autoWidth && item.show) {
item.autoCalculateMinWidth(tableData.value);
}
});
isCalculatedWidth.value = true;
};
watch(
() => props.data,
@@ -266,9 +313,6 @@ watch(
);
onMounted(async () => {
calcuTableHeight();
useEventListener(window, 'resize', calcuTableHeight);
if (props.searchItems.length > 0) {
nowSearchItem.value = props.searchItems[0];
}
@@ -292,11 +336,6 @@ onMounted(async () => {
}
});
const calcuTableHeight = () => {
const headerHeight = isShowSearch.value ? 330 : 250;
state.tableMaxHeight = window.innerHeight - headerHeight + 'px';
};
const searchFormItemKeyUpEnter = (event: any) => {
event.preventDefault();
search();
@@ -327,113 +366,21 @@ defineExpose({
});
</script>
<style scoped lang="scss">
.table-box,
.table-main {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
.page-table-search-form-enter-active {
transition: all 0.3s ease-out;
}
// 表格 header 样式
.table-header {
width: 100%;
.page-table-search-form-leave-active {
transition: all 0.3s ease-in;
}
.header-button-lf {
float: left;
}
.page-table-search-form-enter-from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
.header-button-ri {
float: right;
.tool-button {
display: flex;
justify-content: space-between;
}
.simple-search-form {
margin-right: 10px;
display: flex;
justify-content: space-between;
::v-deep(.el-form-item__content > *) {
width: 100% !important;
}
.simple-search-form-label {
text-align: right;
margin-right: 5px;
}
.simple-search-form-btn:hover {
color: var(--el-color-primary);
}
}
}
.el-button {
margin-bottom: 10px;
}
}
// el-table 表格样式
.el-table {
flex: 1;
// 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83
table {
width: 100%;
}
// .el-table__header th {
// height: 45px;
// font-size: 15px;
// font-weight: bold;
// color: var(--el-text-color-primary);
// background: var(--el-fill-color-light);
// }
// .el-table__row {
// height: 45px;
// font-size: 14px;
// .move {
// cursor: move;
// .el-icon {
// cursor: move;
// }
// }
// }
// 设置 el-table 中 header 文字不换行,并省略
.el-table__header .el-table__cell>.cell {
// white-space: nowrap;
white-space: wrap;
}
// 解决表格数据为空时样式不居中问题(仅在element-plus中)
// .el-table__empty-block {
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
// .table-empty {
// line-height: 30px;
// }
// }
// table 中 image 图片样式
.table-image {
width: 50px;
height: 50px;
border-radius: 50%;
}
}
::v-deep(.el-form-item__label) {
font-weight: bold;
}
.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';
@@ -107,7 +107,6 @@ const handleItemKeyupEnter = (item: SearchItem) => {
margin-bottom: 10px;
box-sizing: border-box;
overflow-x: hidden;
background-color: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 6px;

View File

@@ -38,7 +38,7 @@ export class TableColumn {
/**
* 插槽名,
*/
private slotName: string = '';
slotName: string = '';
showOverflowTooltip: boolean = true;
@@ -71,14 +71,14 @@ export class TableColumn {
formatFunc: Function;
/**
* 是否显示该列
* 是否显示该列,1显示 0不显示
*/
private show: boolean = true;
show: number = 1;
/**
* 是否展示美化按钮主要用于美化json文本等
*/
private isBeautify: boolean = false;
isBeautify: boolean = false;
constructor(prop: string, label: string) {
this.prop = prop;
@@ -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

@@ -1,12 +0,0 @@
export const buildProgressProps = (): any => {
return {
progress: {
title: {
type: String,
},
executedStatements: {
type: Number,
},
},
};
};

View File

@@ -7,9 +7,16 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive } from 'vue';
import { formatTime } from 'element-plus/es/components/countdown/src/utils';
import { buildProgressProps } from './progress-notify';
const props = defineProps(buildProgressProps());
const props = defineProps({
progress: {
type: Object,
default: () => ({
title: '',
executedStatements: 0,
}),
},
});
const state = reactive({
elapsedTime: '00:00:00',

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