mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2026-01-13 20:57:09 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be82815659 | ||
| 7ff0bfd846 | |||
|
|
d85ac20d7e | ||
| bbe36b4f9f | |||
| f0b24cc18a | |||
| 09775d69b9 | |||
| 4aeb795db0 | |||
| eddd7c5fc6 | |||
| c57a0a2195 | |||
| b1805dc41b | |||
| 26ff948a73 | |||
|
|
a3ce4b508a | ||
| 986c03e69f | |||
| 82cb66e112 | |||
|
|
eea9a93ae6 | ||
| a83d710b82 | |||
| cc7bb9cb70 | |||
|
|
8ddc1ff516 | ||
|
|
1b1cbb8627 |
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -15,13 +15,15 @@ body:
|
||||
options:
|
||||
- label: 重启项目和 IDE 后,仍然能够复现此问题
|
||||
required: true
|
||||
- label: 查阅过 [使用指南](https://continew.top/admin/frontend/structure.html) 和 [常见问题](https://continew.top/admin/faq.html) ,仍无解决方法
|
||||
- label: 尝试了最新 dev 分支代码(演示环境),仍有相同问题
|
||||
required: true
|
||||
- label: 根据报错信息(自行翻译英文)百度或 Google 后,仍无法解决
|
||||
required: true
|
||||
- label: 尝试了最新 dev 分支代码(演示环境),仍有相同问题
|
||||
- label: 查阅过 [使用指南](https://continew.top/docs/admin/frontend/structure.html) 和 [常见问题](https://continew.top/docs/admin/faq.html) ,仍无解决方法
|
||||
required: true
|
||||
- label: 搜索了项目 Issues,没有其他人提交过类似的 Bug(如果对应 Bug 尚未解决,您可以先订阅关注该 Issue,为了方便后来者查找问题解决方法,请避免创建重复的 Issue)
|
||||
- label: 搜索了 [吐槽广场](https://continew.top/docs/admin/issue-hub.html),没有其他人提交过类似的 Bug(如果对应 Bug 尚未解决,您可以先订阅关注该 Issue,为了方便后来者查找问题解决方法,请避免创建重复的 Issue)
|
||||
required: true
|
||||
- label: 问过 [DeepWiki](https://deepwiki.com/continew-org/continew-admin-ui) 及知名 AI 大模型,仍无解决方法
|
||||
required: true
|
||||
- label: 确认不是 gi-demo 前端模板相关的组件问题,例如:GiTable、GiForm、基础布局和配置等(如有此类组件相关的问题,请提交至 [gi-demo](https://gitee.com/lin0716/gi-demo) 或对应组件仓库)
|
||||
required: true
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -15,11 +15,9 @@ body:
|
||||
options:
|
||||
- label: 尝试了最新 dev 分支代码(演示环境),仍没有该功能
|
||||
required: true
|
||||
- label: 查阅过 [使用指南](https://continew.top/admin/frontend/structure.html) 和 [常见问题](https://continew.top/admin/faq.html) ,仍然认为很有必要
|
||||
- label: 查阅过 [使用指南](https://continew.top/docs/admin/frontend/structure.html) 和 [常见问题](https://continew.top/docs/admin/faq.html) ,仍然认为很有必要
|
||||
required: true
|
||||
- label: 查阅过 [需求墙](https://continew.top/admin/other/feature.html),仍没有该功能计划
|
||||
required: true
|
||||
- label: 搜索了项目 Issues,没有其他人提交过类似的 Feature(如果对应 Feature 尚未实现,您可以先订阅关注该 Issue,为了方便后来者查找问题解决方法,请避免创建重复的 Issue)
|
||||
- label: 搜索了 [吐槽广场](https://continew.top/docs/admin/issue-hub.html),没有其他人提交过类似的 Feature(如果对应 Feature 尚未实现,您可以先订阅关注该 Issue,为了方便后来者查找问题解决方法,请避免创建重复的 Issue)
|
||||
required: true
|
||||
- label: 确认不是基础组件类需求,例如:GiTable、GiForm、基础布局、纯前端组件(锁屏、引导)等(如有此类组件相关的需求,请提交至 [gi-demo](https://gitee.com/lin0716/gi-demo) 或对应组件仓库)
|
||||
required: true
|
||||
|
||||
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -40,9 +40,9 @@ jobs:
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
source: ./dist/*
|
||||
target: /tmp/html
|
||||
target: ${{ secrets.SERVER_TMP_PATH }}
|
||||
strip_components: 1
|
||||
# 7、重启 Nginx
|
||||
# 7、更新部署文件
|
||||
- name: Restart
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
@@ -52,5 +52,5 @@ jobs:
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
script: |
|
||||
rm -rf ${{ secrets.SERVER_PATH }}/*
|
||||
mv /tmp/html/* ${{ secrets.SERVER_PATH }}
|
||||
mv ${{ secrets.SERVER_TMP_PATH }}/* ${{ secrets.SERVER_PATH }}
|
||||
chmod -R 777 ${{ secrets.SERVER_PATH }}
|
||||
|
||||
106
README.md
106
README.md
@@ -1,7 +1,7 @@
|
||||
# ContiNew Admin UI
|
||||
|
||||
<a href="https://github.com/continew-org/continew-admin-ui" title="Release" target="_blank">
|
||||
<img src="https://img.shields.io/badge/RELEASE-v4.0.0-%23ff3f59.svg" alt="Release" />
|
||||
<img src="https://img.shields.io/badge/SNAPSHOT-v4.1.0-%23ff3f59.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" title="Vue" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Vue-3.5.4-%236CB52D.svg?logo=Vue.js" alt="Vue" />
|
||||
@@ -35,7 +35,7 @@
|
||||
<img src="https://gitcode.com/continew/continew-admin/star/badge.svg" alt="GitCode Stars" />
|
||||
</a>
|
||||
|
||||
📚 [在线文档](https://continew.top) | 🚀 [演示地址](https://continew.top/admin/guide/demo.html)
|
||||
📚 [在线文档](https://continew.top) | 🚀 [演示地址](https://continew.top/docs/admin/guide/demo.html) | 💬 [吐槽广场(你就是 Talk King!)](https://continew.top/docs/admin/issue-hub.html) | [](https://deepwiki.com/continew-org/continew-admin-ui)
|
||||
|
||||
## 简介
|
||||
|
||||
@@ -79,15 +79,17 @@ ContiNew Admin(Continue New Admin),页面现代美观,且专注设计与
|
||||
## 为什么选我们?
|
||||
|
||||
> [!TIP]
|
||||
> 更为完整的图文描述请查阅[《在线文档》](https://continew.top/admin/guide/why-choose-us.html)。
|
||||
> 更为完整的图文描述请查阅[《在线文档》](https://continew.top/docs/admin/guide/why-choose-us.html)。
|
||||
|
||||
**AI 编程纪元已经开启,基于 ContiNew 项目开发,让 AI 助手“学习”更优雅的代码规范,“写出”更优质的代码。**
|
||||
|
||||
1.**甄选技术栈:** ContiNew(Continue New) 项目致力于持续迭代优化,让技术不掉队。在技术选型时,进行深度广泛地调研,从流行度、成熟度和发展潜力等多方面甄选技术栈。
|
||||
**1.长期稳定:** 自 2022 年 12 月 8 日创建,2023 年 3 月 26 日发布 v1.0.0,截至今日,ContiNew Admin 已累计发布 25 个版本,ContiNew Starter 已累计发布 43 个版本。
|
||||
|
||||
2.**Starter 组件:** 从 v2.1.0 版本开始,抽取并封装后端基础组件及各框架集成配置到 ContiNew Starter 项目,且 **[已发布至 Maven 中央仓库](https://central.sonatype.com/search?q=continew-starter&namespace=top.continew)**,可在你的任意项目中直接引入所需依赖使用。即使你不用脚手架项目,难道能让你搭项目框架更快、更爽、更省力的 Starter 也要 Say No 吗?
|
||||
**2.甄选技术栈:** ContiNew(Continue New) 项目致力于持续迭代优化,确保技术栈紧跟时代。在技术选型时,我们进行了深度广泛的调研,从流行度、成熟度和发展潜力等多维度精心挑选技术栈。
|
||||
|
||||
3.**CRUD 套件:** 封装通用增删改查套件,适配后端各分层,几分钟即可提供一套 CRUD API,包括新增、修改、批量删除、查询详情、分页列表查询、全部列表查询、树型列表查询、导出到 Excel,甚至是字典列表(用于下拉选项场景),且 API 支持按实际所需开放或扩展。
|
||||
**3.Starter 组件:** 从 v2.1.0 版本开始,我们将后端基础组件及各框架集成配置抽取并封装到 ContiNew Starter 项目中,极大降低上手和升级难度。且 **[已发布至 Maven 中央仓库](https://central.sonatype.com/search?q=continew-starter&namespace=top.continew)**,你可以在任意项目中直接引入所需依赖使用。即使你不使用完整的中后台框架,这些能让你搭项目框架更快、更爽、更省力的 Starter 组件,难道不香吗?
|
||||
|
||||
**4.CRUD 套件:** 封装通用增删改查套件,适配后端各分层架构,几分钟即可提供一套完整的 CRUD API,包括新增、修改、批量删除、查询详情、分页列表、全部列表、树型列表、Excel 导出,甚至是字典列表(用于下拉选项场景)。所有 API 均可根据实际需求灵活开放或扩展。
|
||||
|
||||
```java
|
||||
@Tag(name = "部门管理 API")
|
||||
@@ -96,32 +98,43 @@ ContiNew Admin(Continue New Admin),页面现代美观,且专注设计与
|
||||
public class DeptController extends BaseController<DeptService, DeptResp, DeptDetailResp, DeptQuery, DeptReq> {}
|
||||
```
|
||||
|
||||
4.**代码生成器:** 提供代码生成器,已配套前、后端代码生成模板,数据表设计完之后,简单配置一下即可生成前、后端 80% 的代码,包含 CRUD API、权限控制、参数校验、接口文档等内容。如果业务不复杂,也可能就是 95% 的代码。
|
||||
**5.代码生成器:** 同步提供了代码生成器,配套前后端代码生成模板。数据表设计完成后,简单配置即可生成前后端 80% 的代码,包括 CRUD API、权限控制、参数校验、接口文档等内容。若业务不复杂,甚至能覆盖 95% 的代码量。
|
||||
|
||||
5.**改善开发体验:** 持续优化及适配能改善开发体验的组件。
|
||||
**6.提升开发体验:** 持续优化并适配各类能提升开发体验的组件。
|
||||
|
||||
- 适配 ContiNew Starter 各组件,针对 Spring 基础配置、通用解决方案以及流行框架进行了深度封装的 starter 集合,改善你在开发每个 Spring Boot Web 项目的体验。(时间日期及枚举参数自动转换、默认线程池、跨域、加密、脱敏、限流、幂等、License、日志、异常及响应通用解决方案等等,更多细节可查看 Starter 源码)
|
||||
- 适配 Crane4j 数据填充组件,减少因为一个用户名而产生的联表回填;
|
||||
- 适配 SpEL Validator 基于 SpEL 的 Java 参数校验,使用 SpEL 表达式,强化基础参数校验。例如:当其中一个字段为 xxx 时,另一个字段不能为空等等;
|
||||
- 适配 P6Spy SQL 性能分析组件,开发期间方便监控 SQL 执行;
|
||||
- 适配 TLog 链路追踪组件,方便在杂乱的日志文件中追踪你某次请求的日志记录;
|
||||
- 适配 JetCache 缓存框架(比 Spring Cache 更强大易用),通过注解声明即可快速实现方法级缓存,极大改善编码式缓存体验,且支持灵活的二级缓存配置、分布式自动刷新等能力;
|
||||
- 前端适配 Vue Devtools(Vue 官方提供的调试浏览器插件),极大提高 Vue 开发及调试效率
|
||||
- ContiNew Starter 组件集合:针对 Spring 基础配置、通用解决方案及流行框架进行深度封装,改善你开发每个 Spring Boot Web 项目的体验(包含时间日期及枚举参数自动转换、默认线程池、跨域、加密、脱敏、限流、幂等、License、日志、异常及响应通用解决方案等);
|
||||
- Crane4j 数据填充组件:减少因单个字段(如用户名)而产生的联表查询;
|
||||
- SpEL Validator:基于 SpEL 表达式的参数校验,强化复杂场景下的参数验证(如:当某字段为特定值时,另一字段不能为空);
|
||||
- P6Spy SQL 性能分析:开发期间可方便地监控 SQL 执行性能;
|
||||
- TLog 链路追踪:在繁杂的日志中快速定位某次请求的完整日志;
|
||||
- JetCache 缓存框架:通过注解即可实现方法级缓存,支持灵活的二级缓存配置和分布式自动刷新;
|
||||
|
||||
6.**Almost最佳后端规范:** 后端严格遵循阿里巴巴 Java 编码规范,注释覆盖率 > 45%,接口参数示例 100%,代码分层使用体验佳,变量、方法命名清晰统一,前端代码也使用严格的 ESLint、StyleLint 等检查。良好的设计,代码复用率极高!写代码时,让你有一种无需多写,理应如此的感觉。我是代码洁癖,我实际写的时候很清楚这到底是不是乱吹。
|
||||
**7.Almost 最佳后端规范:** 后端严格遵循阿里巴巴 Java 编码规范,注释覆盖率 > 45%,接口参数示例 100%。代码分层清晰,变量与方法命名统一规范,前端代码同样采用严格的 ESLint、StyleLint 等检查。优秀的设计带来极高的代码复用率!开发时,你会有一种“无需多写,理应如此”的流畅感。
|
||||
|
||||
7.**卓越工程:** 后端采用模块化工程结构,并适配了统一项目版本号、编译项目自动代码格式化等插件,提供了自定义打包部署结构配置(配置文件、三方依赖和主程序分离),提供全套环境及应用的 Docker Compose 部署脚本。为了减少您开发新项目时的改造耗时,项目品牌配置持续进行深度聚合,简单的配置和结构修改即可快速开始独属于你的新项目。我们还进行了全局 Lombok 配置,继承场景默认自动生效 @EqualsAndHashCode(callSuper = true)、@ToString(callSuper = true),不需要你手动添加了,并且主动禁用了部分 Lombok 注解,例如:@Val、@Log4j...,杜绝“又菜又爱玩”的 partner 滥用。
|
||||
**8.卓越工程化实践:** 后端采用模块化工程结构,集成了统一版本管理、编译时自动代码格式化等插件。提供自定义打包部署配置(配置文件、第三方依赖与主程序分离),以及全套环境和应用的 Docker Compose 部署脚本。
|
||||
|
||||
8.**业务脚手架:** 有颜有料,不止是说说而已,持续打磨 UI 设计与色彩主题。提供基于 RBAC 的权限控制、通用数据权限,包含丰富的通用业务功能:第三方登录,邮箱、短信(生产级炸弹漏洞处理方案),个人中心、用户管理、角色管理、部门管理、系统配置(基础站点配置、邮件配置、安全配置)、系统日志、消息中心、通知公告等,设计用心,逻辑合理闭环。
|
||||
为减少新项目开发的改造成本,我们持续深度聚合项目品牌配置,通过简单的配置和结构修改,即可快速启动你的专属项目。
|
||||
|
||||
由于篇幅有限,且项目正处于高速发展期,更多功能正在陆续上线(敬请关注仓库或群内动态)。另外像最基本的统一异常、错误处理,基础线程池等配置就不在此赘述,细节优化详情请 clone 代码查看。
|
||||
我们还进行了全局 Lombok 配置,继承场景默认自动应用 `@EqualsAndHashCode(callSuper = true)` 和 `@ToString(callSuper = true)`,无需手动添加。同时主动禁用了部分 Lombok 注解(如 `@Val`、`@Log4j` 等),避免“又菜又爱玩”的 partner 滥用。
|
||||
|
||||
**9.全能业务脚手架:** 支持 **SaaS 租户架构**,基于 RBAC 的权限控制与通用数据权限管理。精心设计的 UI 界面与色彩主题,兼具美观与实用性。内置丰富的通用业务解决方案:第三方登录、邮箱/短信服务(含生产级漏洞处理方案)、个人中心、用户管理、角色管理、组织管理、系统配置、系统日志、消息中心、通知公告等,逻辑闭环,开箱即用。
|
||||
|
||||
> 优秀的中后台框架不仅提供组件集成与配置,封装好用的工具,更应提供通用基础业务设计及解决方案,为初创团队减负。
|
||||
|
||||
**10.质量与安全并重:** 我们高度重视项目质量与安全,CI 已集成 Sonar、Codacy,代码提交即自动扫描质量问题。定期扫描 CVE 漏洞,及时解决潜在风险。封装了数据库字段加密、JSON 脱敏、XSS 过滤等工具,提供全方位的安全解决方案。
|
||||
|
||||
许多项目在开发或交付过程中需满足 Sonarqube 等质量指标,使用 ContiNew Admin 框架,让你从一开始就站在高质量的起点。
|
||||
|
||||
---
|
||||
|
||||
由于篇幅有限,且项目正处于高速发展期,更多功能正在持续开发中,敬请关注仓库或加入交流群了解最新动态。至于统一异常处理、错误处理、基础线程池配置(默认线程参数、线程上下文支持异步传递)等基础特性,这里不再赘述,更多细节优化欢迎克隆代码体验。
|
||||
> Talk is cheap, show the code.
|
||||
|
||||
## 系统功能
|
||||
|
||||
> [!TIP]
|
||||
> 更多功能和优化正在赶来💦,最新项目计划、进展请进群或关注 [需求墙](https://continew.top/admin/other/feature.html) 和 [更新日志](https://continew.top/admin/changelog/)。
|
||||
> 功能不会用?请查看 [功能手册](https://continew.top/admin/function/tenant/management.html)。
|
||||
> 更多功能和优化正在赶来💦,最新项目计划、进展请进群或查看 [吐槽广场](https://continew.top/docs/admin/issue-hub.html) 和 [更新日志](https://continew.top/docs/admin/changelog/)。
|
||||
> 功能不会用?请查看 [功能手册](https://continew.top/docs/admin/function/tenant/management.html)。
|
||||
|
||||
- 仪表盘:提供工作台、分析页,工作台提供功能快捷导航入口、最新公告、动态;分析页提供全面数据可视化能力
|
||||
- 个人中心:支持基础信息修改、密码修改、邮箱绑定、手机号绑定(并提供行为验证码、短信限流等安全处理)、第三方账号绑定/解绑、头像裁剪上传
|
||||
@@ -220,7 +233,7 @@ public class DeptController extends BaseController<DeptService, DeptResp, DeptDe
|
||||
## 快速开始
|
||||
|
||||
> **Note**
|
||||
> 更详细的流程,请查看在线文档[《快速开始》](https://continew.top/admin/guide/quick-start.html)。
|
||||
> 更详细的流程,请查看在线文档[《快速开始》](https://continew.top/docs/admin/guide/quick-start.html)。
|
||||
|
||||
```
|
||||
# 1.克隆本项目
|
||||
@@ -319,8 +332,6 @@ continew-admin-ui
|
||||
├─ vite.config.ts
|
||||
├─ .gitignore(Git 忽略文件相关配置文件)
|
||||
├─ .github(GitHub 相关配置目录,实际开发时直接删除)
|
||||
├─ .idea
|
||||
│ └─ icon.png(IDEA 项目图标,实际开发时直接删除)
|
||||
├─ .image(截图目录,实际开发时直接删除)
|
||||
├─ .vscode(VSCode 配置目录)
|
||||
├─ LICENSE(开源协议文件)
|
||||
@@ -328,43 +339,48 @@ continew-admin-ui
|
||||
└─ README.md(项目 README 文件,实际开发时替换为真实内容)
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
## 参与贡献
|
||||
|
||||
ContiNew Admin 致力于提供开箱即用,持续舒适的开发体验。作为一个开源项目,Creator 的初心是希望 ContiNew Admin 依托开源协作模式,提升技术透明度、放大集体智慧、共创优秀实践,源源不断地为企业级项目开发提供助力。
|
||||
ContiNew(Continue New)系列项目致力于通过持续迭代,为开发者提供舒适的开发体验。作为开源社区,我们的初衷是希望通过开源协作模式,提升技术透明度、放大集体智慧、共创优秀实践,源源不断地为企业级项目开发提供助力。
|
||||
|
||||
我们非常欢迎广大社区用户为 ContiNew Admin **贡献(开发,测试、文档、答疑等)** 或优化代码,欢迎各位感兴趣的小伙伴儿,[添加微信](https://continew.top/discussion.html) 讨论或认领任务。
|
||||
我们诚挚邀请广大社区用户为 ContiNew 项目贡献力量,包括但不限于 Issue 排查、测试验证、代码开发与重构等。每一份贡献,都是推动项目进步的重要力量(请查阅 [贡献指南](https://continew.top/about/contributing.html))。欢迎各位感兴趣的小伙伴儿,[添加微信](https://continew.top/discussion.html) 讨论或认领任务。
|
||||
|
||||
### 分支说明
|
||||
|
||||
ContiNew Admin 的分支目前分为下个大版本的开发分支和上个大版本的维护分支,PR 前请注意对应分支是否处于维护状态,版本支持情况请查看 [更新日志/版本支持](https://continew.top/admin/changelog/)。
|
||||
ContiNew 系列项目采用清晰的分支策略,确保开发与维护有序进行。提交 PR 前,请确认目标分支是否处于活跃维护状态,版本支持情况请查看 [更新日志#版本支持](https://continew.top/docs/admin/changelog/)。
|
||||
|
||||
| 分支 | 说明 |
|
||||
| ----- | ------------------------------------------------------------ |
|
||||
| dev | 开发分支,默认为下个大版本的 SNAPSHOT 版本,接受新功能或新功能优化 PR |
|
||||
| x.x.x | 维护分支,在 vx.x.x 版本维护期终止前(一般为下个大版本发布前),用于修复上个版本的 Bug,只接受已有功能修复,不接受新功能 PR |
|
||||
| dev | 开发分支,用于下个大版本的 SNAPSHOT 开发,接受新功能或功能优化 PR |
|
||||
| x.x.x | 维护分支,用于特定版本(如 vx.x.x)的 bug 修复,仅接受已有功能的修复 PR,不接受新功能 |
|
||||
|
||||
### 贡献代码
|
||||
### 流程步骤
|
||||
|
||||
如果您想提交新功能或优化现有代码,可以按照以下步骤操作:
|
||||
若您希望提交新功能或优化现有代码,请遵循以下步骤:
|
||||
|
||||
1. 首先,在 Gitee 或 GitHub 上将项目 fork 到您自己的仓库
|
||||
2. 然后,将 fork 过来的项目(即您的项目)克隆到本地
|
||||
3. 切换到当前仍在维护的分支(请务必充分了解分支使用说明,可进群联系维护者确认)
|
||||
4. 开始修改代码,修改完成后,将代码 commit 并 push 到您的远程仓库
|
||||
5. 在 Gitee 或 GitHub 上新建 pull request(pr),选择好源和目标,按模板要求填写说明信息后提交即可(多多参考 [已批准合并的 pr 记录](https://github.com/Charles7c/continew-admin-ui/pulls?q=is%3Apr+is%3Amerged),会大大增加批准合并率)
|
||||
6. 最后,耐心等待维护者合并您的请求即可
|
||||
|
||||
请记住,如果您有任何疑问或需要帮助,我们将随时提供支持。
|
||||
1. 在开源平台上将项目 fork 到您的个人仓库
|
||||
2. 将 fork 的项目克隆到本地开发环境
|
||||
3. 基于当前维护的分支(如 dev)创建新分支(如 feat/newFeature),请勿直接修改源分支(源分支仅做同步 ContiNew 最新代码用)
|
||||
4. 在新分支上进行代码修改,完成后提交并 push 到您的远程仓库
|
||||
5. 在开源平台上创建 pull request (PR),选择正确的源分支和目标分支,按模板填写说明信息(参考 [已合并的 PR](https://github.com/continew-org/continew-admin/pulls?q=is%3Apr+is%3Amerged) 可提高合并率)
|
||||
6. 提交 PR 后,系统会提示签署 CLA(贡献者协议)。请确保 commit 使用的邮箱与平台绑定邮箱一致(如果不一致,可以在本地通过 `git reset --soft HEAD~1` 回退,然后使用正确邮箱重新提交,最后 `git push -f` 即可,不需要重新创建 PR),然后使用该邮箱签署即可
|
||||
7. 耐心等待维护者审核并合并您的 PR(建议通过交流群进行快捷沟通)
|
||||
8. PR 合并后,下次贡献前请先同步最新代码,再重复步骤 3 开始
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 欢迎大家为 ContiNew Admin 贡献代码,我们非常感谢您的支持!为了更好地管理项目,维护者有一些要求:
|
||||
> 为了确保项目质量和协作效率,请注意以下事项:
|
||||
>
|
||||
> 1. 请确保代码、配置文件的结构和命名规范良好,完善的代码注释
|
||||
> 2. 在提交代码前,请按照 [Angular 提交规范](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) 编写 commit 的 message
|
||||
> 1. 代码和配置文件请参考已有风格,遵循清晰的结构与命名规范,提供完善的注释
|
||||
> 2. 提交时,请按照 [Angular 提交规范](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) 编写 commit message(参考已有风格)
|
||||
|
||||
## 反馈交流
|
||||
|
||||
欢迎各位小伙伴儿扫描下方二维码加入项目交流群,与项目维护团队及其他大佬用户实时交流讨论。
|
||||
感谢您对 ContiNew 开源项目的关注与支持!我们非常重视每一位用户的反馈和建议,这是推动项目不断进步的动力。 欢迎扫描下方二维码加入我们的官方交流群,与项目维护团队及其他大佬用户实时交流探讨。
|
||||
|
||||
- 与项目核心团队直接沟通,获取第一手项目动态
|
||||
- 解决使用过程中遇到的问题,分享经验心得
|
||||
- 参与功能讨论和需求收集,影响项目未来发展
|
||||
- 结识志同道合的技术爱好者,扩展人脉圈
|
||||
|
||||
<div align="left">
|
||||
<img src=".image/qrcode.jpg" alt="二维码" height="230px" />
|
||||
@@ -389,4 +405,4 @@ ContiNew Admin 的分支目前分为下个大版本的开发分支和上个大
|
||||
## License
|
||||
|
||||
- 遵循 <a href="https://github.com/Charles7c/continew-admin-ui/blob/dev/LICENSE" target="_blank">Apache-2.0</a> 开源许可协议
|
||||
- Copyright © 2022-present <a href="https://blog.charles7c.top" target="_blank">Charles7c</a>
|
||||
- Copyright © 2022-present <a href="https://charles7c.top" target="_blank">Charles7c</a>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function appInfo(): Plugin {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
boxen(
|
||||
`${bold(green(`${bgGreen('ContiNew Admin v4.0.0')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('常见问题:')}${underline('https://continew.top/admin/faq.html')}\n${cyan('更新日志:')}${underline('https://continew.top/admin/changelog/')}\n${cyan('持续迭代优化的,高质量多租户中后台管理系统框架')}`,
|
||||
`${bold(green(`${bgGreen('ContiNew Admin v4.1.0-SNAPSHOT')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('吐槽广场:')}${underline('https://continew.top/docs/admin/issue-hub.html')}\n${cyan('常见问题:')}${underline('https://continew.top/docs/admin/faq.html')}\n${cyan('更新日志:')}${underline('https://continew.top/docs/admin/changelog/')}\n${cyan('持续迭代优化的,高质量多租户中后台管理系统框架')}`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: 1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "continew-admin-ui",
|
||||
"type": "module",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0-SNAPSHOT",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"bootstrap": "pnpm install --registry=https://registry.npmmirror.com",
|
||||
@@ -14,6 +14,7 @@
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18n-jsautotranslate": "3.18.63",
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@arco-design/color": "^0.4.0",
|
||||
"@arco-themes/vue-gi-demo": "^0.0.51",
|
||||
@@ -44,6 +45,7 @@
|
||||
"pinia-plugin-persistedstate": "^3.1.0",
|
||||
"qs": "^6.11.2",
|
||||
"query-string": "^9.0.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"v-viewer": "^3.0.10",
|
||||
"viewerjs": "^1.11.6",
|
||||
"vite-plugin-vue-devtools": "^7.0.27",
|
||||
@@ -54,7 +56,7 @@
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-draggable-plus": "^0.3.5",
|
||||
"vue-echarts": "^6.5.5",
|
||||
"vue-json-pretty": "^2.4.0",
|
||||
"vue-json-viewer": "^3.0.4",
|
||||
"vue-router": "^4.3.3",
|
||||
"vue3-tree-org": "^4.2.2",
|
||||
"xe-utils": "^3.5.7",
|
||||
@@ -67,6 +69,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/query-string": "^6.3.0",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
|
||||
74
pnpm-lock.yaml
generated
74
pnpm-lock.yaml
generated
@@ -98,6 +98,9 @@ importers:
|
||||
query-string:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
spark-md5:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
v-viewer:
|
||||
specifier: ^3.0.10
|
||||
version: 3.0.13(viewerjs@1.11.6)(vue@3.5.12(typescript@5.0.4))
|
||||
@@ -128,9 +131,9 @@ importers:
|
||||
vue-echarts:
|
||||
specifier: ^6.5.5
|
||||
version: 6.7.2(@vue/runtime-core@3.5.12)(echarts@5.5.0)(vue@3.5.12(typescript@5.0.4))
|
||||
vue-json-pretty:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0(vue@3.5.12(typescript@5.0.4))
|
||||
vue-json-viewer:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(vue@3.5.12(typescript@5.0.4))
|
||||
vue-router:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(vue@3.5.12(typescript@5.0.4))
|
||||
@@ -162,6 +165,9 @@ importers:
|
||||
'@types/query-string':
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0
|
||||
'@types/spark-md5':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(terser@5.31.0))(vue@3.5.12(typescript@5.0.4))
|
||||
@@ -697,6 +703,7 @@ packages:
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
deprecated: Use @eslint/config-array instead
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||
@@ -704,6 +711,7 @@ packages:
|
||||
|
||||
'@humanwhocodes/object-schema@2.0.3':
|
||||
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@humanwhocodes/retry@0.3.0':
|
||||
resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==}
|
||||
@@ -813,55 +821,46 @@ packages:
|
||||
resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.17.2':
|
||||
resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.17.2':
|
||||
resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.17.2':
|
||||
resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.17.2':
|
||||
resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.17.2':
|
||||
resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.17.2':
|
||||
resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.17.2':
|
||||
resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.17.2':
|
||||
resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.17.2':
|
||||
resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==}
|
||||
@@ -1192,6 +1191,9 @@ packages:
|
||||
'@types/sortablejs@1.15.8':
|
||||
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
|
||||
|
||||
'@types/spark-md5@3.0.5':
|
||||
resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==}
|
||||
|
||||
'@types/svgo@2.6.4':
|
||||
resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==}
|
||||
|
||||
@@ -1748,6 +1750,9 @@ packages:
|
||||
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
clipboard@2.0.11:
|
||||
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1998,6 +2003,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
delegate@3.2.0:
|
||||
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2757,6 +2765,9 @@ packages:
|
||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
good-listener@1.2.2:
|
||||
resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
|
||||
|
||||
gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
|
||||
@@ -3960,6 +3971,9 @@ packages:
|
||||
scule@1.3.0:
|
||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||
|
||||
select@1.1.2:
|
||||
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
|
||||
|
||||
semver@5.7.2:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
@@ -4079,6 +4093,9 @@ packages:
|
||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||
deprecated: Please use @jridgewell/sourcemap-codec instead
|
||||
|
||||
spark-md5@3.0.2:
|
||||
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||
|
||||
spdx-correct@3.2.0:
|
||||
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
||||
|
||||
@@ -4249,6 +4266,9 @@ packages:
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
tiny-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
|
||||
|
||||
tippy.js@6.3.7:
|
||||
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
|
||||
|
||||
@@ -4614,11 +4634,10 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>=6.0.0'
|
||||
|
||||
vue-json-pretty@2.4.0:
|
||||
resolution: {integrity: sha512-e9bP41DYYIc2tWaB6KuwqFJq5odZ8/GkE6vHQuGcbPn37kGk4a3n1RNw3ZYeDrl66NWXgTlOfS+M6NKkowmkWw==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 5.0.0'}
|
||||
vue-json-viewer@3.0.4:
|
||||
resolution: {integrity: sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.0'
|
||||
vue: ^3.2.2
|
||||
|
||||
vue-router@4.3.3:
|
||||
resolution: {integrity: sha512-8Q+u+WP4N2SXY38FDcF2H1dUEbYVHVPtPCPZj/GTZx8RCbiB8AtJP9+YIxn4Vs0svMTNQcLIzka4GH7Utkx9xQ==}
|
||||
@@ -5758,6 +5777,8 @@ snapshots:
|
||||
|
||||
'@types/sortablejs@1.15.8': {}
|
||||
|
||||
'@types/spark-md5@3.0.5': {}
|
||||
|
||||
'@types/svgo@2.6.4':
|
||||
dependencies:
|
||||
'@types/node': 20.12.12
|
||||
@@ -6501,6 +6522,12 @@ snapshots:
|
||||
slice-ansi: 5.0.0
|
||||
string-width: 7.2.0
|
||||
|
||||
clipboard@2.0.11:
|
||||
dependencies:
|
||||
good-listener: 1.2.2
|
||||
select: 1.1.2
|
||||
tiny-emitter: 2.1.0
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -6744,6 +6771,8 @@ snapshots:
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
delegate@3.2.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
devlop@1.1.0:
|
||||
@@ -7653,6 +7682,10 @@ snapshots:
|
||||
merge2: 1.4.1
|
||||
slash: 3.0.0
|
||||
|
||||
good-listener@1.2.2:
|
||||
dependencies:
|
||||
delegate: 3.2.0
|
||||
|
||||
gopd@1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
@@ -8887,6 +8920,8 @@ snapshots:
|
||||
|
||||
scule@1.3.0: {}
|
||||
|
||||
select@1.1.2: {}
|
||||
|
||||
semver@5.7.2: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
@@ -9022,6 +9057,8 @@ snapshots:
|
||||
|
||||
sourcemap-codec@1.4.8: {}
|
||||
|
||||
spark-md5@3.0.2: {}
|
||||
|
||||
spdx-correct@3.2.0:
|
||||
dependencies:
|
||||
spdx-expression-parse: 3.0.1
|
||||
@@ -9203,6 +9240,8 @@ snapshots:
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
tiny-emitter@2.1.0: {}
|
||||
|
||||
tippy.js@6.3.7:
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
@@ -9615,8 +9654,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vue-json-pretty@2.4.0(vue@3.5.12(typescript@5.0.4)):
|
||||
vue-json-viewer@3.0.4(vue@3.5.12(typescript@5.0.4)):
|
||||
dependencies:
|
||||
clipboard: 2.0.11
|
||||
vue: 3.5.12(typescript@5.0.4)
|
||||
|
||||
vue-router@4.3.3(vue@3.5.12(typescript@5.0.4)):
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface UserInfo {
|
||||
registrationDate: string
|
||||
deptName: string
|
||||
roles: string[]
|
||||
roleNames: string[]
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
|
||||
32
src/apis/system/multipart-upload.ts
Normal file
32
src/apis/system/multipart-upload.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// 分片上传 API 封装
|
||||
import type * as T from './type'
|
||||
import http from '@/utils/http'
|
||||
|
||||
export type * from './type'
|
||||
|
||||
const BASE_URL = '/system/multipart-upload'
|
||||
|
||||
/** @desc 初始化分片上传 */
|
||||
export function initMultipartUpload(data: T.MultiPartUploadInitReq) {
|
||||
return http.post<T.MultiPartUploadInitResp>(`${BASE_URL}/init`, data)
|
||||
}
|
||||
|
||||
/** @desc 上传分片 */
|
||||
export function uploadPart(data: T.UploadPartReq, signal?: AbortSignal) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', data.file)
|
||||
formData.append('uploadId', data.uploadId)
|
||||
formData.append('partNumber', String(data.partNumber))
|
||||
formData.append('path', data.path)
|
||||
return http.post<T.UploadPartResp>(`${BASE_URL}/part`, formData, { signal })
|
||||
}
|
||||
|
||||
/** @desc 完成上传 */
|
||||
export function completeMultipartUpload(params: T.CompleteMultipartUploadReq) {
|
||||
return http.get<string>(`${BASE_URL}/complete/${params.uploadId}`)
|
||||
}
|
||||
|
||||
/** @desc 取消上传 */
|
||||
export function cancelUpload(params: T.CancelUploadParams) {
|
||||
return http.get<void>(`${BASE_URL}/cancel/${params.uploadId}`)
|
||||
}
|
||||
@@ -467,3 +467,56 @@ export interface MessageQuery {
|
||||
|
||||
export interface MessagePageQuery extends MessageQuery, PageQuery {
|
||||
}
|
||||
|
||||
/** 分片上传 - 初始化参数 */
|
||||
export interface MultiPartUploadInitReq {
|
||||
fileName: string
|
||||
fileSize: number
|
||||
fileMd5: string
|
||||
parentPath: string
|
||||
metaData: Record<string, string>
|
||||
}
|
||||
|
||||
/** 分片上传 - 初始化响应 */
|
||||
export interface MultiPartUploadInitResp {
|
||||
uploadId: string
|
||||
partSize: number
|
||||
path: string
|
||||
uploadedPartNumbers: number[]
|
||||
}
|
||||
|
||||
/** 分片上传 - 上传分片参数 */
|
||||
export interface UploadPartReq {
|
||||
uploadId: string
|
||||
partNumber: number
|
||||
file: Blob
|
||||
path: string
|
||||
}
|
||||
|
||||
/** 分片上传 - 上传分片响应 */
|
||||
export interface UploadPartResp {
|
||||
/** 分片编号 */
|
||||
partNumber: number
|
||||
/** 分片ETag */
|
||||
partETag: string
|
||||
/** 分片大小 */
|
||||
partSize: number
|
||||
/** 是否成功 */
|
||||
success: boolean
|
||||
/** 错误信息 */
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
/** 分片上传 - 完成上传参数 */
|
||||
export interface CompleteMultipartUploadReq {
|
||||
uploadId: string
|
||||
partETags: Array<{
|
||||
partNumber: number
|
||||
eTag: string
|
||||
}>
|
||||
}
|
||||
|
||||
/** 分片上传 - 取消上传参数 */
|
||||
export interface CancelUploadParams {
|
||||
uploadId: string
|
||||
}
|
||||
|
||||
@@ -199,9 +199,17 @@ const updateValue = (value: any, field: string) => {
|
||||
emit('update:modelValue', Object.assign(props.modelValue, { [field]: value }))
|
||||
}
|
||||
|
||||
/** 必填项 */
|
||||
const isRequired = (item: ColumnItem) => {
|
||||
if (typeof item.required === 'boolean') return item.required
|
||||
if (typeof item.required === 'function') {
|
||||
return item.required(props.modelValue)
|
||||
}
|
||||
}
|
||||
|
||||
/** 表单项校验规则 */
|
||||
const getFormItemRules = (item: ColumnItem) => {
|
||||
if (item.required) {
|
||||
if (isRequired(item)) {
|
||||
const defaultProps = getComponentBindProps(item)
|
||||
return [{ required: true, message: defaultProps.placeholder || `请输入${item.label}` }, ...(Array.isArray(item.rules) ? item.rules : [])]
|
||||
}
|
||||
|
||||
@@ -203,8 +203,8 @@ export interface ColumnItem<F = any> {
|
||||
props?: ColumnItemProps
|
||||
gridItemProps?: A.GridItemProps
|
||||
formItemProps?: Omit<A.FormItemInstance['$props'], 'label' | 'field'> // a-form-item的props
|
||||
required?: boolean // 是否必填
|
||||
rules?: A.FormItemInstance['$props']['rules'] // 表单校验规则
|
||||
required?: ColumnItemHide<F> // 是否必填
|
||||
hide?: ColumnItemHide<F> // 是否隐藏
|
||||
show?: ColumnItemShow<F> // 是否显示(优先级比hide高)
|
||||
disabled?: ColumnItemDisabled<F> // 是否禁用
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="json_pretty_container">
|
||||
<VueJsonPretty path="res" :data="JSONObject" :show-length="true" />
|
||||
<JsonViewer expand-depth="5" :value="JSONObject" :theme="currentThemeClass" sort />
|
||||
<icon-copy class="copy_icon" @click="onCopy(JSONObject)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import VueJsonPretty from 'vue-json-pretty'
|
||||
import 'vue-json-pretty/lib/styles.css'
|
||||
import JsonViewer from 'vue-json-viewer'
|
||||
import { useTheme } from '@arco-design/web-vue/es/watermark/hooks/use-theme'
|
||||
import { copyText } from '@/utils'
|
||||
|
||||
defineOptions({ name: 'JsonPretty', inheritAttrs: false })
|
||||
@@ -15,16 +15,19 @@ defineOptions({ name: 'JsonPretty', inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
json: string
|
||||
}>()
|
||||
|
||||
const JSONObject = computed(() => JSON.parse(props?.json))
|
||||
|
||||
// 拷贝
|
||||
const onCopy = (data: object) => {
|
||||
copyText(JSON.stringify(data))
|
||||
}
|
||||
// 监听主题变化
|
||||
const { theme } = useTheme()
|
||||
const currentThemeClass = computed(() => (theme.value === 'dark' ? 'vscode-dark' : 'vscode-light'))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "./json-them.scss";
|
||||
.json_pretty_container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
43
src/components/JsonPretty/json-them.scss
Normal file
43
src/components/JsonPretty/json-them.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.vscode-light {
|
||||
background: var(--color-bg-3,#ffffff);
|
||||
color: #1f2328;
|
||||
font-size: 14px;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
|
||||
:deep(.jv-key) { color: #0550ae; font-weight: 600; }
|
||||
:deep(.jv-string) { color: #0c5460; }
|
||||
:deep(.jv-number) { color: #116329; }
|
||||
:deep(.jv-boolean) { color: #c24038; font-weight: 600; }
|
||||
:deep(.jv-null) { color: #6f42c1; font-style: italic; }
|
||||
:deep(.jv-undefined) { color: #e36209; }
|
||||
:deep(.jv-function) { color: #795548; }
|
||||
:deep(.jv-button) { color: #0969da; }
|
||||
:deep(.jv-ellipsis) {
|
||||
color: #6a737d;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vscode-dark {
|
||||
background: var(--color-bg-3,#17171A);
|
||||
color: #d4d4d4;
|
||||
font-size: 14px;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
|
||||
:deep(.jv-key) { color: #9cdcfe; font-weight: 600; }
|
||||
:deep(.jv-string) { color: #ce9178; }
|
||||
:deep(.jv-number) { color: #b5cea8; }
|
||||
:deep(.jv-boolean) { color: #569cd6; font-weight: 600; }
|
||||
:deep(.jv-null) { color: #c586c0; font-style: italic; }
|
||||
:deep(.jv-undefined) { color: #e08331; }
|
||||
:deep(.jv-function) { color: #dcdcaa; }
|
||||
:deep(.jv-button) { color: #4fc1ff; }
|
||||
:deep(.jv-ellipsis) {
|
||||
color: #999;
|
||||
background-color: #333;
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
432
src/components/MultipartUpload/index.vue
Normal file
432
src/components/MultipartUpload/index.vue
Normal file
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<a-row :gutter="16" class="multipart-uploader-responsive-row">
|
||||
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
|
||||
<div
|
||||
class="multipart-uploader-table-flex"
|
||||
:class="{ dragover: isDragOver }"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<!-- 文件/文件夹选择和全局操作按钮 -->
|
||||
<div class="upload-select-area-flex">
|
||||
<div class="upload-btns-left">
|
||||
<a-button @click="triggerFileInput">选择文件</a-button>
|
||||
<a-button style="margin-left: 8px;" @click="triggerFolderInput">选择文件夹</a-button>
|
||||
<input ref="fileInput" type="file" multiple style="display: none" @change="onFileChange" />
|
||||
<input ref="folderInput" type="file" webkitdirectory directory style="display: none" @change="onFolderChange" />
|
||||
</div>
|
||||
<div class="upload-btns-right">
|
||||
<a-button type="primary" @click="startAllUpload">开始上传</a-button>
|
||||
<a-button style="margin-left: 8px;" status="danger" @click="clearAllTasks">清空</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px; color: #888; font-size: 13px;">
|
||||
支持拖拽文件到此区域上传(文件夹请使用"选择文件夹"按钮)
|
||||
<br />
|
||||
<small style="color: #999;">提示:拖拽上传时,所有文件将上传到根目录</small>
|
||||
</div>
|
||||
<!-- 表格区域 -->
|
||||
<div class="gi-table-flex-body">
|
||||
<div class="gi-table-flex-container">
|
||||
<a-table
|
||||
:data="fileTasks"
|
||||
:columns="columns"
|
||||
row-key="uid"
|
||||
:pagination="pagination"
|
||||
style="height: 100%; background: transparent;"
|
||||
>
|
||||
<template #progress="{ record }">
|
||||
<template v-if="md5CalculatingTaskUid === record.uid">
|
||||
<span style="color: #888;">正在计算MD5...</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-progress :percent="record.progress" :animation="true" size="large" />
|
||||
</template>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<div>
|
||||
<a-tag :color="statusColor(record.status)" size="small">{{ statusText(record.status) }}</a-tag>
|
||||
<div v-if="record.status === 'failed' && record.errorMessage" style="margin-top: 4px; font-size: 12px; color: #f56c6c;">
|
||||
{{ record.errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-tooltip v-if="record.status === 'waiting'" content="开始">
|
||||
<a-button size="mini" type="text" @click="startTask(record)"><IconPlayArrow /></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="record.status === 'uploading'" content="暂停">
|
||||
<a-button size="mini" type="text" @click="pauseTask(record)"><IconPause /></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="record.status === 'paused'" content="继续">
|
||||
<a-button size="mini" type="text" @click="resumeTask(record)"><IconPlayArrow /></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="record.status === 'failed'" content="重试">
|
||||
<a-button size="mini" type="text" @click="retryTask(record)"><IconRefresh /></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="取消">
|
||||
<a-button v-if="record.status !== 'completed' && record.status !== 'cancelled'" size="mini" type="text" @click="cancelTask(record)"><IconClose /></a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="删除">
|
||||
<a-button size="mini" type="text" status="danger" @click="removeTask(record)"><IconDelete /></a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { h, ref, resolveComponent } from 'vue'
|
||||
import { IconClose, IconDelete, IconPause, IconPlayArrow, IconRefresh } from '@arco-design/web-vue/es/icon'
|
||||
import { useMultipartUploader } from '@/hooks/modules/useMultipartUploader'
|
||||
import { getFilesFromDataTransferItems, isFileSystemAccessAPISupported } from '@/utils/drag-drop-file-util'
|
||||
|
||||
// 组件props定义
|
||||
const props = defineProps<{
|
||||
extraParams?: Record<string, any>
|
||||
maxConcurrentFiles?: number
|
||||
maxConcurrentChunks?: number
|
||||
maxUploadWorkers?: number
|
||||
rootPath?: string
|
||||
}>()
|
||||
// 文件/文件夹选择input引用
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const folderInput = ref<HTMLInputElement | null>(null)
|
||||
// 拖拽高亮状态
|
||||
const isDragOver = ref(false)
|
||||
const pagination = {
|
||||
pageSize: 10,
|
||||
showTotal: true,
|
||||
showJumper: true,
|
||||
position: ['bottomCenter'],
|
||||
}
|
||||
|
||||
// 文件大小格式化工具
|
||||
function formatFileSize(bytes: number) {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'fileName',
|
||||
ellipsis: true,
|
||||
render: ({ record }) => h(
|
||||
resolveComponent('a-tooltip'),
|
||||
{ content: record.fileName, placement: 'top' },
|
||||
() => h('span', record.fileName),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '文件目录',
|
||||
dataIndex: 'relativePath',
|
||||
ellipsis: true,
|
||||
render: ({ record }) => {
|
||||
// 显示完整路径
|
||||
const displayPath = record.parentPath
|
||||
|
||||
// 确保路径格式正确
|
||||
if (record.relativePath && record.relativePath !== '/') {
|
||||
// 对于文件夹上传,relativePath格式为:folderName/file.txt
|
||||
// 我们只需要显示parentPath,因为它已经包含了正确的路径
|
||||
const pathParts = record.relativePath.split('/')
|
||||
if (pathParts.length > 1) {
|
||||
// 如果是文件夹内的文件,只显示parentPath
|
||||
// parentPath已经是/test/upload这样的格式
|
||||
}
|
||||
}
|
||||
|
||||
return h(
|
||||
resolveComponent('a-tooltip'),
|
||||
{ content: displayPath, placement: 'top' },
|
||||
() => h('span', displayPath),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '文件类型',
|
||||
dataIndex: 'fileType',
|
||||
ellipsis: true,
|
||||
render: ({ record }) => h(
|
||||
resolveComponent('a-tooltip'),
|
||||
{ content: record.fileType, placement: 'top' },
|
||||
() => h('span', record.fileType),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
dataIndex: 'fileSize',
|
||||
ellipsis: true,
|
||||
render: ({ record }) => formatFileSize(record.fileSize),
|
||||
width: 120,
|
||||
},
|
||||
{ title: '进度', slotName: 'progress', width: 140 },
|
||||
{ title: '状态', slotName: 'status', width: 80 },
|
||||
{ title: '操作', slotName: 'actions', width: 150 },
|
||||
]
|
||||
|
||||
// 使用 useMultipartUploader composable
|
||||
const {
|
||||
fileTasks,
|
||||
uploadingCount: _uploadingCount,
|
||||
maxConcurrent: _maxConcurrent,
|
||||
maxChunkConcurrent: _maxChunkConcurrent,
|
||||
startAllUpload,
|
||||
addFiles,
|
||||
pauseTask,
|
||||
resumeTask,
|
||||
cancelTask,
|
||||
startTask,
|
||||
retryTask,
|
||||
clearAllTasks,
|
||||
removeTask,
|
||||
formatFileSize: _formatFileSize,
|
||||
md5CalculatingTaskUid,
|
||||
} = useMultipartUploader({
|
||||
maxConcurrentFiles: props.maxConcurrentFiles,
|
||||
maxConcurrentChunks: props.maxConcurrentChunks,
|
||||
maxUploadWorkers: props.maxUploadWorkers,
|
||||
rootPath: props.rootPath,
|
||||
})
|
||||
|
||||
// 触发文件选择
|
||||
function triggerFileInput() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
// 触发文件夹选择
|
||||
function triggerFolderInput() {
|
||||
folderInput.value?.click()
|
||||
}
|
||||
// 文件选择事件处理
|
||||
function onFileChange(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
// 移除 clearAllTasks(),改为追加模式
|
||||
// 普通文件上传路径 = rootPath
|
||||
addFiles(Array.from(files), props.rootPath || '', false)
|
||||
// 不要自动 startAllUpload()
|
||||
;(e.target as HTMLInputElement).value = ''
|
||||
}
|
||||
// 文件夹选择事件处理
|
||||
function onFolderChange(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
// 移除 clearAllTasks(),改为追加模式
|
||||
// 带目录文件上传路径 = rootPath
|
||||
// 文件夹上传时,webkitRelativePath会自动包含文件夹路径
|
||||
addFiles(Array.from(files), props.rootPath || '', true)
|
||||
// 不要自动 startAllUpload()
|
||||
;(e.target as HTMLInputElement).value = ''
|
||||
}
|
||||
// 拖拽进入区域
|
||||
function onDragOver(_e: DragEvent) {
|
||||
isDragOver.value = true
|
||||
}
|
||||
// 拖拽离开区域
|
||||
function onDragLeave(_e: DragEvent) {
|
||||
isDragOver.value = false
|
||||
}
|
||||
// 拖拽释放文件/文件夹
|
||||
async function onDrop(e: DragEvent) {
|
||||
isDragOver.value = false
|
||||
e.preventDefault()
|
||||
|
||||
let files: File[]
|
||||
if (isFileSystemAccessAPISupported()) {
|
||||
files = await getFilesFromDataTransferItems(e.dataTransfer!.items)
|
||||
addFiles(files, props.rootPath || '', true)
|
||||
} else {
|
||||
files = Array.from(e.dataTransfer?.files || [])
|
||||
// 验证文件的有效性
|
||||
const validFiles = files.filter((file) => {
|
||||
return !(!file || file.size === 0)
|
||||
})
|
||||
if (validFiles.length === 0) {
|
||||
return
|
||||
}
|
||||
// 检查是否有文件夹结构
|
||||
const hasFolder = validFiles.some((f) => {
|
||||
if ((f as any).webkitRelativePath) {
|
||||
return true
|
||||
}
|
||||
return f.name.includes('/') || f.name.includes('\\')
|
||||
})
|
||||
addFiles(validFiles, props.rootPath || '', hasFolder)
|
||||
}
|
||||
}
|
||||
// 状态文本映射
|
||||
function statusText(status: string) {
|
||||
switch (status) {
|
||||
case 'waiting': return '等待中'
|
||||
case 'uploading': return '上传中'
|
||||
case 'paused': return '已暂停'
|
||||
case 'completed': return '已完成'
|
||||
case 'failed': return '失败'
|
||||
case 'cancelled': return '已取消'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
// 状态颜色映射
|
||||
function statusColor(status: string) {
|
||||
switch (status) {
|
||||
case 'waiting': return '#909399'
|
||||
case 'uploading': return '#409EFF'
|
||||
case 'paused': return '#E6A23C'
|
||||
case 'completed': return '#67C23A'
|
||||
case 'failed': return '#F56C6C'
|
||||
case 'cancelled': return '#C0C4CC'
|
||||
default: return '#909399'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.multipart-uploader-table-flex {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #0000000d;
|
||||
border: 2px dashed #e5e6eb;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
min-width: 1000px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 700px;
|
||||
}
|
||||
.multipart-uploader-table-flex.dragover {
|
||||
border: 2px dashed #409eff;
|
||||
}
|
||||
.upload-select-area-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.upload-btns-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.upload-btns-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
.upload-select-area {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.gi-table-flex-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px #0001;
|
||||
padding: 8px 0 0 0;
|
||||
}
|
||||
.gi-table-flex-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 400px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
:deep(.arco-table) {
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
:deep(.arco-table-th) {
|
||||
min-width: 120px;
|
||||
font-weight: 500;
|
||||
}
|
||||
:deep(.arco-table-td) {
|
||||
max-width: 400px;
|
||||
min-width: 120px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.arco-table-td:nth-child(1)),
|
||||
:deep(.arco-table-th:nth-child(1)) {
|
||||
min-width: 200px;
|
||||
max-width: 350px;
|
||||
}
|
||||
:deep(.arco-table-td:nth-child(2)),
|
||||
:deep(.arco-table-th:nth-child(2)) {
|
||||
min-width: 180px;
|
||||
max-width: 300px;
|
||||
}
|
||||
:deep(.arco-table-td:last-child),
|
||||
:deep(.arco-table-th:last-child) {
|
||||
min-width: 160px;
|
||||
max-width: 200px;
|
||||
}
|
||||
:deep(.arco-table-element):has(tbody .arco-table-tr-empty) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.arco-table-pagination) {
|
||||
margin-top: auto !important;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.multipart-uploader-responsive-row {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.multipart-uploader-table-flex {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.multipart-uploader-table-flex {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 6px;
|
||||
}
|
||||
.gi-table-flex-body {
|
||||
min-height: 200px;
|
||||
height: 300px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.multipart-uploader-table-flex {
|
||||
min-width: 100vw;
|
||||
max-width: 100vw;
|
||||
border-radius: 0;
|
||||
padding: 2px;
|
||||
}
|
||||
.gi-table-flex-body {
|
||||
min-height: 120px;
|
||||
height: 180px;
|
||||
padding: 0;
|
||||
}
|
||||
.upload-select-area-flex {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.upload-btns-right {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,3 +7,4 @@ export * from './modules/useDevice'
|
||||
export * from './modules/useBreakpoint'
|
||||
export * from './modules/useDownload'
|
||||
export * from './modules/useResetReactive'
|
||||
export * from './modules/useMultipartUploader'
|
||||
|
||||
@@ -1,49 +1,58 @@
|
||||
import { Message, Notification } from '@arco-design/web-vue'
|
||||
|
||||
/**
|
||||
* @description 接收数据流生成 blob,创建链接,下载文件
|
||||
* @param {Function} api 导出表格的api方法 (必传)
|
||||
* @param {string} tempName 导出的文件名 (必传)
|
||||
* @param {object} params 导出的参数 (默认{})
|
||||
* @param {boolean} isNotify 是否有导出消息提示 (默认为 true)
|
||||
* @param {string} fileName 导出的文件名 (可选,例如:导出数据.xlsx,默认从响应头或时间戳生成)
|
||||
* @param {string} fileType 导出的文件格式 (默认为.xlsx)
|
||||
* @param {boolean} isNotify 是否显示导出提示消息 (默认为 false)
|
||||
* @returns {Promise<void>} 无返回值
|
||||
*/
|
||||
interface NavigatorWithMsSaveOrOpenBlob extends Navigator {
|
||||
msSaveOrOpenBlob: (blob: Blob, fileName: string) => void
|
||||
}
|
||||
export const useDownload = async (api: () => Promise<any>, isNotify = false, tempName = '', fileType = '.xlsx') => {
|
||||
export const useDownload = async (api: () => Promise<any>, fileName = '', fileType = '.xlsx', isNotify = false) => {
|
||||
try {
|
||||
const res = await api()
|
||||
if (res.headers['content-disposition']) {
|
||||
tempName = decodeURI(res.headers['content-disposition'].split(';')[1].split('=')[1])
|
||||
} else {
|
||||
tempName = tempName || new Date().getTime() + fileType
|
||||
if (!fileName) {
|
||||
if (res.headers['content-disposition']) {
|
||||
// 从响应头提取文件名
|
||||
fileName = decodeURI(res.headers['content-disposition'].split(';')[1].split('=')[1])
|
||||
} else {
|
||||
// 时间戳生成
|
||||
fileName = new Date().getTime() + fileType
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotify && !res?.code) {
|
||||
Notification.warning({
|
||||
title: '温馨提示',
|
||||
content: '如果数据庞大会导致下载缓慢哦,请您耐心等待!',
|
||||
})
|
||||
}
|
||||
if (res.status !== 200 || res.data == null || !(res.data instanceof Blob)) {
|
||||
Message.error('导出失败,请稍后再试!')
|
||||
// 验证响应数据
|
||||
if (res.status !== 200 || !res.data || !(res.data instanceof Blob)) {
|
||||
Message.error('导出失败:无效的响应数据')
|
||||
return
|
||||
}
|
||||
const blob = new Blob([res.data])
|
||||
// 兼容 edge 不支持 createObjectURL 方法
|
||||
if ('msSaveOrOpenBlob' in (navigator as unknown as NavigatorWithMsSaveOrOpenBlob)) {
|
||||
;(window.navigator as unknown as NavigatorWithMsSaveOrOpenBlob).msSaveOrOpenBlob(blob, tempName + fileType)
|
||||
// 兼容 IE/Edge 浏览器
|
||||
if ('msSaveOrOpenBlob' in navigator) {
|
||||
return (navigator as unknown as NavigatorWithMsSaveOrOpenBlob).msSaveOrOpenBlob(blob, fileName)
|
||||
}
|
||||
// 创建下载链接并触发下载
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const exportFile = document.createElement('a')
|
||||
exportFile.style.display = 'none'
|
||||
exportFile.download = tempName
|
||||
exportFile.href = blobUrl
|
||||
document.body.appendChild(exportFile)
|
||||
exportFile.click()
|
||||
// 去除下载对 url 的影响
|
||||
document.body.removeChild(exportFile)
|
||||
const link = document.createElement('a')
|
||||
link.style.display = 'none'
|
||||
link.download = fileName
|
||||
link.href = blobUrl
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
// 清理资源
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
} catch (error) {
|
||||
// console.log(error)
|
||||
const errorMsg = error instanceof Error ? error.message : '未知错误'
|
||||
Message.error(`下载失败: ${errorMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
791
src/hooks/modules/useMultipartUploader.ts
Normal file
791
src/hooks/modules/useMultipartUploader.ts
Normal file
@@ -0,0 +1,791 @@
|
||||
// 分片上传通用 hooks,支持多文件/多分片并发、暂停、恢复、取消、重试等
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { throttle } from 'lodash-es'
|
||||
import {
|
||||
cancelUpload,
|
||||
completeMultipartUpload,
|
||||
initMultipartUpload,
|
||||
uploadPart,
|
||||
} from '@/apis/system/multipart-upload'
|
||||
|
||||
// 文件上传任务对象类型
|
||||
export interface FileTask {
|
||||
uid: string // 唯一标识
|
||||
file: File // 文件对象
|
||||
relativePath: string // 相对路径(支持文件夹结构)
|
||||
parentPath: string // 文件夹根路径
|
||||
status: 'waiting' | 'uploading' | 'paused' | 'completed' | 'failed' | 'cancelled' // 状态
|
||||
progress: number // 上传进度(0-1)
|
||||
uploadedChunks: number[] // 已上传分片编号
|
||||
totalChunks: number // 总分片数
|
||||
chunkSize: number // 分片大小(由后端返回)
|
||||
fileName: string // 文件名
|
||||
fileType: string // 文件类型
|
||||
fileSize: number // 文件大小
|
||||
fileMd5?: string // 文件MD5
|
||||
uploadId?: string // 分片上传ID
|
||||
path?: string // 文件路径(由后端返回)
|
||||
partETags: Array<{ partNumber: number, eTag: string }> // 分片ETag列表
|
||||
errorMessage?: string // 错误信息
|
||||
abortController?: AbortController // 请求中断控制器
|
||||
_uploading?: boolean // 标记是否正在上传(内部控制)
|
||||
_pause?: () => void // 暂停方法
|
||||
_resume?: () => void // 继续方法
|
||||
_cancel?: () => void // 取消方法
|
||||
_retryCount?: Map<number, number> // 分片重试次数记录
|
||||
}
|
||||
|
||||
/**
|
||||
* useMultipartUploader - 通用分片上传 hooks
|
||||
* @param props.maxConcurrentFiles 最大同时上传文件数(全局并发)
|
||||
* @param props.maxConcurrentChunks 每个文件分片上传最大并发数
|
||||
* @param props.maxUploadWorkers 最大上传Worker数量(基于CPU核心数)
|
||||
* @returns 上传相关响应式状态与操作方法
|
||||
*/
|
||||
export function useMultipartUploader(props: {
|
||||
maxConcurrentFiles?: number
|
||||
maxConcurrentChunks?: number
|
||||
maxUploadWorkers?: number
|
||||
rootPath?: string
|
||||
}) {
|
||||
// 获取CPU核心数,用于控制Worker数量
|
||||
const getCpuCores = () => {
|
||||
if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) {
|
||||
return navigator.hardwareConcurrency
|
||||
}
|
||||
return 2 // 默认2个核心
|
||||
}
|
||||
|
||||
// 所有上传任务列表
|
||||
const fileTasks = ref<FileTask[]>([])
|
||||
// 当前正在上传的文件数量
|
||||
const uploadingCount = computed(() => fileTasks.value.filter((t) => t.status === 'uploading').length)
|
||||
// 最大并发上传文件数
|
||||
const maxConcurrent = computed(() => props.maxConcurrentFiles ?? getCpuCores())
|
||||
// 每个文件分片上传最大并发数
|
||||
const maxChunkConcurrent = computed(() => props.maxConcurrentChunks ?? getCpuCores())
|
||||
// 最大上传Worker数量
|
||||
const maxUploadWorkers = computed(() => props.maxUploadWorkers ?? getCpuCores() / 2)
|
||||
|
||||
// 本地队列管理
|
||||
const uploadQueue = ref<Array<{ task: FileTask, chunkNumber: number }>>([])
|
||||
const activeUploads = ref<Set<string>>(new Set()) // 正在上传的分片ID集合
|
||||
|
||||
const md5CalculatingTaskUid = ref<string | null>(null) // MD5计算中的任务ID
|
||||
const performanceStats = ref<{
|
||||
md5StartTime: number
|
||||
md5EndTime: number
|
||||
uploadStartTime: number
|
||||
uploadEndTime: number
|
||||
totalTime: number
|
||||
} | null>(null)
|
||||
|
||||
// MD5 Worker实例
|
||||
let md5Worker: Worker | null = null
|
||||
|
||||
/** 节流的进度更新函数 */
|
||||
const updateTaskProgress = throttle((task: FileTask, totalChunks: number) => {
|
||||
const currentFinishedChunks = task.uploadedChunks.length
|
||||
if (totalChunks > 0) {
|
||||
task.progress = Number(Math.min(currentFinishedChunks / totalChunks, 1).toFixed(2))
|
||||
} else {
|
||||
task.progress = 0
|
||||
}
|
||||
}, 150)
|
||||
|
||||
/**
|
||||
* 初始化MD5 Worker
|
||||
*/
|
||||
function initMd5Worker() {
|
||||
if (typeof Worker !== 'undefined' && !md5Worker) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[Hooks] 初始化MD5 Worker...')
|
||||
md5Worker = new Worker('/src/utils/md5-worker.ts', { type: 'module' })
|
||||
md5Worker.onmessage = function (e) {
|
||||
const { type, taskId, md5, error } = e.data
|
||||
|
||||
if (type === 'complete' && md5) {
|
||||
const task = fileTasks.value.find((t) => t.uid === taskId)
|
||||
if (task) {
|
||||
task.fileMd5 = md5
|
||||
md5CalculatingTaskUid.value = null
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] MD5计算完成: ${task.fileName}, MD5: ${md5}`)
|
||||
}
|
||||
} else if (type === 'error') {
|
||||
console.error('MD5计算失败:', error)
|
||||
md5CalculatingTaskUid.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文件MD5(使用Web Worker - 优化版本)
|
||||
*/
|
||||
function calcFileMd5(file: File, taskUid: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!md5Worker) {
|
||||
initMd5Worker()
|
||||
}
|
||||
|
||||
if (md5Worker) {
|
||||
md5CalculatingTaskUid.value = taskUid
|
||||
|
||||
// 记录MD5计算开始时间
|
||||
performanceStats.value = {
|
||||
md5StartTime: Date.now(),
|
||||
md5EndTime: 0,
|
||||
uploadStartTime: 0,
|
||||
uploadEndTime: 0,
|
||||
totalTime: 0,
|
||||
}
|
||||
|
||||
// 根据文件大小动态调整分块和分片大小
|
||||
const blockSize = file.size > 200 * 1024 * 1024 ? 50 * 1024 * 1024 : 25 * 1024 * 1024 // 50MB或25MB块
|
||||
const chunkSize = file.size > 100 * 1024 * 1024 ? 10 * 1024 * 1024 : 2 * 1024 * 1024 // 10MB或2MB分片
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 发送文件到Worker: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB, 块大小: ${(blockSize / 1024 / 1024).toFixed(2)}MB, 分片大小: ${(chunkSize / 1024 / 1024).toFixed(2)}MB`)
|
||||
|
||||
md5Worker.postMessage({
|
||||
file,
|
||||
taskId: taskUid,
|
||||
blockSize,
|
||||
chunkSize,
|
||||
})
|
||||
|
||||
// 监听完成事件
|
||||
const handleComplete = (e: MessageEvent) => {
|
||||
const { type, taskId, md5 } = e.data
|
||||
if (type === 'complete' && taskId === taskUid) {
|
||||
md5Worker?.removeEventListener('message', handleComplete)
|
||||
|
||||
// 记录MD5计算结束时间
|
||||
if (performanceStats.value) {
|
||||
performanceStats.value.md5EndTime = Date.now()
|
||||
const md5Time = performanceStats.value.md5EndTime - performanceStats.value.md5StartTime
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`MD5计算完成,耗时: ${md5Time}ms,文件大小: ${formatFileSize(file.size)}`)
|
||||
}
|
||||
|
||||
resolve(md5)
|
||||
}
|
||||
}
|
||||
|
||||
md5Worker.addEventListener('message', handleComplete)
|
||||
} else {
|
||||
reject(new Error('Web Worker not supported'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分片到上传队列
|
||||
*/
|
||||
function addChunkToQueue(task: FileTask, chunkNumber: number) {
|
||||
const chunkId = `${task.uid}-${chunkNumber}`
|
||||
if (!activeUploads.value.has(chunkId)) {
|
||||
uploadQueue.value.push({ task, chunkNumber })
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`添加分片到队列: ${task.fileName} - 分片${chunkNumber}`)
|
||||
processUploadQueue()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传队列 - 优化版本
|
||||
*/
|
||||
function processUploadQueue() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 处理上传队列,队列长度: ${uploadQueue.value.length}, 活跃上传数: ${activeUploads.value.size}`)
|
||||
// 智能队列处理:优先处理小文件的分片,避免大文件阻塞
|
||||
const sortedQueue = [...uploadQueue.value].sort((a, b) => {
|
||||
// 优先处理已完成更多分片的文件
|
||||
const aProgress = a.task.uploadedChunks.length / a.task.totalChunks
|
||||
const bProgress = b.task.uploadedChunks.length / b.task.totalChunks
|
||||
return bProgress - aProgress
|
||||
})
|
||||
|
||||
while (sortedQueue.length > 0 && activeUploads.value.size < maxUploadWorkers.value) {
|
||||
const { task, chunkNumber } = sortedQueue.shift()!
|
||||
const chunkId = `${task.uid}-${chunkNumber}`
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 检查分片: ${task.fileName} - 分片${chunkNumber}, 任务状态: ${task.status}`)
|
||||
if (task.status === 'uploading' && !activeUploads.value.has(chunkId)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 开始上传分片: ${task.fileName} - 分片${chunkNumber}`)
|
||||
activeUploads.value.add(chunkId)
|
||||
uploadChunk(task, chunkNumber)
|
||||
|
||||
// 从原始队列中移除已处理的项目
|
||||
const index = uploadQueue.value.findIndex((item) =>
|
||||
item.task.uid === task.uid && item.chunkNumber === chunkNumber,
|
||||
)
|
||||
if (index > -1) {
|
||||
uploadQueue.value.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 跳过分片: ${task.fileName} - 分片${chunkNumber}, 原因: 状态不是uploading或已在活跃上传中`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单个分片
|
||||
*/
|
||||
async function uploadChunk(task: FileTask, chunkNumber: number) {
|
||||
const chunkId = `${task.uid}-${chunkNumber}`
|
||||
|
||||
try {
|
||||
const start = (chunkNumber - 1) * task.chunkSize
|
||||
const end = Math.min(start + task.chunkSize, task.fileSize)
|
||||
const chunkBlob = task.file.slice(start, end)
|
||||
|
||||
// 创建 AbortController 用于中断请求
|
||||
if (!task.abortController) {
|
||||
task.abortController = new AbortController()
|
||||
}
|
||||
|
||||
const res = await uploadPart({
|
||||
uploadId: task.uploadId!,
|
||||
partNumber: chunkNumber,
|
||||
file: chunkBlob,
|
||||
path: task.path!,
|
||||
}, task.abortController.signal)
|
||||
|
||||
// 检查上传是否成功
|
||||
if (res.data && res.data.success) {
|
||||
// 保存ETag
|
||||
task.partETags.push({
|
||||
partNumber: chunkNumber,
|
||||
eTag: res.data.partETag,
|
||||
})
|
||||
|
||||
// 更新已上传分片列表
|
||||
if (!task.uploadedChunks.includes(chunkNumber)) {
|
||||
task.uploadedChunks.push(chunkNumber)
|
||||
}
|
||||
|
||||
updateTaskProgress(task, task.totalChunks)
|
||||
|
||||
// 检查是否所有分片都上传完成
|
||||
if (task.uploadedChunks.length >= task.totalChunks) {
|
||||
await completeMultipartUpload({
|
||||
uploadId: task.uploadId!,
|
||||
partETags: task.partETags,
|
||||
})
|
||||
task.status = 'completed'
|
||||
task.progress = 1
|
||||
startNextTasks()
|
||||
}
|
||||
} else {
|
||||
// 上传失败,抛出错误
|
||||
const errorMessage = res.data?.errorMessage || '分片上传失败'
|
||||
throw new Error(`分片${chunkNumber}上传失败: ${errorMessage}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// 检查是否是取消请求导致的错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`分片上传被取消: ${task.fileName} - 分片${chunkNumber}`)
|
||||
return
|
||||
}
|
||||
|
||||
console.error(`分片上传失败: ${task.fileName} - 分片${chunkNumber}`, error)
|
||||
|
||||
// 检查任务是否已经被取消或暂停
|
||||
if (task.status === 'cancelled' || task.status === 'paused') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`任务 ${task.fileName} 已被取消或暂停,跳过错误处理`)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是网络错误或服务器错误
|
||||
const isNetworkError = error instanceof TypeError
|
||||
|| (error as any)?.message?.includes('Network')
|
||||
|| (error as any)?.message?.includes('fetch')
|
||||
|
||||
const isServerError = (error as any)?.response?.status >= 500
|
||||
|| (error as any)?.response?.status === 429
|
||||
|
||||
if (isNetworkError || isServerError) {
|
||||
// 初始化重试计数器
|
||||
if (!task._retryCount) {
|
||||
task._retryCount = new Map()
|
||||
}
|
||||
|
||||
const currentRetryCount = task._retryCount.get(chunkNumber) || 0
|
||||
const maxRetries = 3 // 最大重试3次
|
||||
|
||||
if (currentRetryCount < maxRetries) {
|
||||
// 网络错误或服务器错误,将分片重新加入队列进行重试
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`分片 ${chunkNumber} 上传失败,第${currentRetryCount + 1}次重试: ${task.fileName}`)
|
||||
|
||||
// 更新重试次数
|
||||
task._retryCount.set(chunkNumber, currentRetryCount + 1)
|
||||
|
||||
// 延迟重试,避免立即重试
|
||||
setTimeout(() => {
|
||||
if (task.status === 'uploading' && !activeUploads.value.has(chunkId)) {
|
||||
addChunkToQueue(task, chunkNumber)
|
||||
}
|
||||
}, 2000 * (currentRetryCount + 1)) // 递增延迟:2秒、4秒、6秒
|
||||
} else {
|
||||
// 超过最大重试次数,标记任务失败
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`分片 ${chunkNumber} 重试次数超过限制,标记任务失败: ${task.fileName}`)
|
||||
task.status = 'failed'
|
||||
task.errorMessage = `分片 ${chunkNumber} 重试次数超过限制`
|
||||
|
||||
// 清理队列中该任务的所有分片
|
||||
uploadQueue.value = uploadQueue.value.filter((item) => item.task.uid !== task.uid)
|
||||
|
||||
// 清理正在上传的分片
|
||||
const activeChunkIds = Array.from(activeUploads.value).filter((id) => id.startsWith(task.uid))
|
||||
activeChunkIds.forEach((id) => activeUploads.value.delete(id))
|
||||
|
||||
// 启动下一个任务
|
||||
startNextTasks()
|
||||
}
|
||||
} else {
|
||||
// 其他错误(如认证错误、参数错误等),标记任务失败
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`任务 ${task.fileName} 遇到不可恢复的错误,标记为失败`)
|
||||
task.status = 'failed'
|
||||
task.errorMessage = (error as Error)?.message || '上传失败'
|
||||
|
||||
// 清理队列中该任务的所有分片
|
||||
uploadQueue.value = uploadQueue.value.filter((item) => item.task.uid !== task.uid)
|
||||
|
||||
// 清理正在上传的分片
|
||||
const activeChunkIds = Array.from(activeUploads.value).filter((id) => id.startsWith(task.uid))
|
||||
activeChunkIds.forEach((id) => activeUploads.value.delete(id))
|
||||
|
||||
// 启动下一个任务
|
||||
startNextTasks()
|
||||
}
|
||||
} finally {
|
||||
activeUploads.value.delete(chunkId)
|
||||
processUploadQueue() // 继续处理队列
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片上传核心逻辑,处理单个文件的分片上传、并发、暂停、恢复、取消等
|
||||
* @param task FileTask
|
||||
*/
|
||||
async function uploadFileTask(task: FileTask) {
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 开始上传任务: ${task.fileName}, 当前状态: ${task.status}`)
|
||||
// 1. 初始化分片上传,获取 uploadId
|
||||
if (!task.uploadId) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 任务 ${task.fileName} 没有 uploadId,准备调用 initMultipartUpload`)
|
||||
// 若没有MD5,先计算
|
||||
if (!task.fileMd5) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 任务 ${task.fileName} 没有 MD5,开始计算...`)
|
||||
task.fileMd5 = await calcFileMd5(task.file, task.uid)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 调用 initMultipartUpload: ${task.fileName}, MD5: ${task.fileMd5}, 路径: ${task.parentPath}`)
|
||||
|
||||
// 确保parentPath不是空字符串,如果是则使用"/"
|
||||
const parentPath = task.parentPath && task.parentPath !== '' ? task.parentPath : '/'
|
||||
|
||||
const res = await initMultipartUpload({
|
||||
fileName: task.fileName,
|
||||
fileSize: task.fileSize,
|
||||
fileMd5: task.fileMd5,
|
||||
parentPath,
|
||||
metaData: {
|
||||
contentType: task.fileType,
|
||||
originalName: task.fileName,
|
||||
},
|
||||
})
|
||||
|
||||
if (res && res.data) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] initMultipartUpload 成功: ${task.fileName}, uploadId: ${res.data.uploadId}`)
|
||||
task.uploadId = res.data.uploadId
|
||||
task.chunkSize = res.data.partSize
|
||||
task.path = res.data.path
|
||||
|
||||
// 处理断点续传:如果后端返回了已上传的分片编号
|
||||
if (res.data.uploadedPartNumbers && res.data.uploadedPartNumbers.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 发现已上传分片: ${task.fileName}, 已上传分片: ${res.data.uploadedPartNumbers.join(',')}`)
|
||||
// 将已上传的分片编号添加到任务中
|
||||
task.uploadedChunks = [...res.data.uploadedPartNumbers]
|
||||
|
||||
// 计算当前进度
|
||||
const totalChunks = Math.ceil(task.fileSize / task.chunkSize)
|
||||
updateTaskProgress(task, totalChunks)
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 断点续传进度: ${task.fileName}, 进度: ${(task.progress * 100).toFixed(1)}%`)
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] initMultipartUpload 失败: ${task.fileName}`)
|
||||
task.status = 'failed'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 计算总分片数
|
||||
const totalChunks = Math.ceil(task.fileSize / task.chunkSize)
|
||||
task.totalChunks = totalChunks
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 计算总分片数: ${task.fileName}, 总分片数: ${totalChunks}, 分片大小: ${task.chunkSize}`)
|
||||
|
||||
// 检查是否有断点续传的分片
|
||||
const hasResumeData = task.uploadedChunks.length > 0
|
||||
|
||||
if (!hasResumeData) {
|
||||
// 如果没有断点续传数据,重新初始化
|
||||
task.uploadedChunks = []
|
||||
task.partETags = []
|
||||
task.progress = 0
|
||||
} else {
|
||||
// 有断点续传数据,计算当前进度
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 发现断点续传数据: ${task.fileName}, 已上传分片: ${task.uploadedChunks.join(',')}`)
|
||||
updateTaskProgress(task, task.totalChunks)
|
||||
}
|
||||
|
||||
// 将所有未完成的分片添加到队列
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 开始添加分片到队列: ${task.fileName}`)
|
||||
for (let i = 1; i <= totalChunks; i++) {
|
||||
// 只添加未上传的分片
|
||||
if (!task.uploadedChunks.includes(i)) {
|
||||
addChunkToQueue(task, i)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 分片添加完成: ${task.fileName}, 队列长度: ${uploadQueue.value.length}`)
|
||||
|
||||
// 挂载暂停/取消控制方法到 task
|
||||
task._pause = () => {
|
||||
task.status = 'paused'
|
||||
// 暂停时清空队列中该任务的分片
|
||||
uploadQueue.value = uploadQueue.value.filter((item) => item.task.uid !== task.uid)
|
||||
// 清理正在上传的分片
|
||||
const activeChunkIds = Array.from(activeUploads.value).filter((id) => id.startsWith(task.uid))
|
||||
activeChunkIds.forEach((id) => activeUploads.value.delete(id))
|
||||
}
|
||||
|
||||
task._resume = () => {
|
||||
if (task.status === 'paused') {
|
||||
task.status = 'uploading'
|
||||
// 重新添加未完成的分片到队列
|
||||
for (let i = 1; i <= task.totalChunks; i++) {
|
||||
if (!task.uploadedChunks.includes(i)) {
|
||||
addChunkToQueue(task, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task._cancel = () => {
|
||||
task.status = 'cancelled'
|
||||
// 中断所有正在进行的请求
|
||||
if (task.abortController) {
|
||||
task.abortController.abort()
|
||||
}
|
||||
// 清空队列中该任务的分片
|
||||
uploadQueue.value = uploadQueue.value.filter((item) => item.task.uid !== task.uid)
|
||||
// 清理正在上传的分片
|
||||
const activeChunkIds = Array.from(activeUploads.value).filter((id) => id.startsWith(task.uid))
|
||||
activeChunkIds.forEach((id) => activeUploads.value.delete(id))
|
||||
if (task.uploadId) {
|
||||
cancelUpload({ uploadId: task.uploadId })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
task.status = 'failed'
|
||||
startNextTasks()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动下一个可用的上传任务(受最大并发数限制)
|
||||
*/
|
||||
function startNextTasks() {
|
||||
let available = maxConcurrent.value - uploadingCount.value
|
||||
for (const task of fileTasks.value) {
|
||||
if (available <= 0) break
|
||||
if ((task.status === 'waiting' || task.status === 'uploading') && !task._uploading) {
|
||||
task.status = 'uploading'
|
||||
task._uploading = true
|
||||
available--
|
||||
uploadFileTask(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部开始上传(将所有 waiting 状态任务置为 uploading 并启动并发上传)
|
||||
*/
|
||||
function startAllUpload() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[Hooks] 开始上传按钮被点击,准备启动所有等待中的任务')
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[Hooks] 当前任务列表:', fileTasks.value.map((t) => ({ name: t.fileName, status: t.status })))
|
||||
for (const task of fileTasks.value) {
|
||||
if (task.status === 'waiting' || task.status === 'paused') {
|
||||
task._uploading = false // 标记尚未调度
|
||||
// 如果是暂停状态,需要重新激活
|
||||
if (task.status === 'paused') {
|
||||
task.status = 'uploading'
|
||||
}
|
||||
}
|
||||
}
|
||||
startNextTasks()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文件/文件夹到上传队列
|
||||
* @param files File[]
|
||||
* @param parentPath 父目录
|
||||
* @param isFolder 是否为文件夹
|
||||
*/
|
||||
function addFiles(files: File[], parentPath: string, isFolder = false) {
|
||||
// 验证文件的有效性
|
||||
const validFiles = files.filter((file) => {
|
||||
if (!file) {
|
||||
return false
|
||||
}
|
||||
if (file.size === 0) {
|
||||
return false
|
||||
}
|
||||
if (!file.name || file.name.trim() === '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const file of validFiles) {
|
||||
const relativePath = (file as any).webkitRelativePath || '/'
|
||||
let parent = ''
|
||||
|
||||
// 调试:查看 webkitRelativePath 的实际内容
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('文件路径调试:', {
|
||||
fileName: file.name,
|
||||
webkitRelativePath: relativePath,
|
||||
isFolder,
|
||||
})
|
||||
|
||||
if (isFolder) {
|
||||
// 文件夹上传:如果有webkitRelativePath,则路径为 rootPath + webkitRelativePath
|
||||
// 如果没有webkitRelativePath,则路径为 rootPath
|
||||
if (relativePath && relativePath !== '/') {
|
||||
// 有webkitRelativePath的情况,例如:folder/file.txt
|
||||
parent = props.rootPath || parentPath || '/'
|
||||
// 确保路径格式正确,去除结尾的斜杠
|
||||
if (parent.length > 1 && parent.endsWith('/')) {
|
||||
parent = parent.slice(0, -1)
|
||||
}
|
||||
|
||||
// 从 webkitRelativePath 中提取文件夹路径(去掉文件名)
|
||||
const pathParts = relativePath.split('/')
|
||||
// 去掉最后一个部分(文件名),只保留文件夹路径
|
||||
pathParts.pop()
|
||||
const folderPath = pathParts.join('/') // 重新组合文件夹路径
|
||||
|
||||
// 组合路径:rootPath + 文件夹路径
|
||||
if (folderPath) {
|
||||
parent = `${parent}/${folderPath}`
|
||||
}
|
||||
} else {
|
||||
// 没有webkitRelativePath的情况,直接使用rootPath
|
||||
parent = props.rootPath || parentPath || '/'
|
||||
}
|
||||
} else {
|
||||
// 普通文件上传:直接使用rootPath
|
||||
parent = props.rootPath || parentPath || '/'
|
||||
}
|
||||
|
||||
// 去除 parentPath 结尾的 /
|
||||
if (parent.length > 1 && parent.endsWith('/')) {
|
||||
parent = parent.slice(0, -1)
|
||||
}
|
||||
|
||||
// 确保路径不以双斜杠开头
|
||||
if (parent.startsWith('//')) {
|
||||
parent = parent.substring(1)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('最终路径:', {
|
||||
fileName: file.name,
|
||||
parentPath: parent,
|
||||
relativePath,
|
||||
})
|
||||
|
||||
const task: FileTask = {
|
||||
uid: `${file.name}-${file.size}-${Date.now()}-${Math.random()}`,
|
||||
file,
|
||||
fileName: file.name,
|
||||
fileType: file.type,
|
||||
fileSize: file.size,
|
||||
relativePath,
|
||||
parentPath: parent,
|
||||
status: 'waiting',
|
||||
progress: 0,
|
||||
uploadedChunks: [],
|
||||
totalChunks: 0,
|
||||
chunkSize: 0, // 初始化时设为0,后续由后端返回
|
||||
fileMd5: '',
|
||||
path: '', // 初始化时设为空,后续由后端返回
|
||||
partETags: [],
|
||||
errorMessage: '', // 初始化错误信息
|
||||
abortController: new AbortController(), // 初始化请求中断控制器
|
||||
_retryCount: new Map(), // 初始化重试计数器
|
||||
}
|
||||
|
||||
// 立即开始计算MD5,但不自动开始上传
|
||||
calcFileMd5(file, task.uid).then((md5) => {
|
||||
task.fileMd5 = md5
|
||||
}).catch((_error) => {
|
||||
task.status = 'failed'
|
||||
})
|
||||
|
||||
fileTasks.value.push(task)
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停单个任务
|
||||
function pauseTask(task: FileTask) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`暂停任务: ${task.fileName}`)
|
||||
task._pause?.()
|
||||
}
|
||||
|
||||
// 恢复单个任务
|
||||
function resumeTask(task: FileTask) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Hooks] 继续任务: ${task.fileName}, 当前状态: ${task.status}`)
|
||||
task._resume?.()
|
||||
if (task.status === 'paused') {
|
||||
task._uploading = false
|
||||
startNextTasks()
|
||||
}
|
||||
}
|
||||
|
||||
// 取消单个任务
|
||||
function cancelTask(task: FileTask) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`取消任务: ${task.fileName}`)
|
||||
|
||||
// 中断所有正在进行的请求
|
||||
if (task.abortController) {
|
||||
task.abortController.abort()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`已中断任务 ${task.fileName} 的所有请求`)
|
||||
}
|
||||
|
||||
task._cancel?.()
|
||||
}
|
||||
|
||||
// 启动单个任务
|
||||
function startTask(task: FileTask) {
|
||||
if (task.status === 'waiting') {
|
||||
task.status = 'uploading'
|
||||
task._uploading = false
|
||||
startNextTasks()
|
||||
}
|
||||
}
|
||||
|
||||
// 失败重试单个任务
|
||||
function retryTask(task: FileTask) {
|
||||
if (task.status === 'failed') {
|
||||
task.status = 'uploading'
|
||||
task.progress = 0
|
||||
// 重试时保留已上传的分片信息,支持断点续传
|
||||
// task.uploadedChunks = [] // 注释掉,保留断点续传数据
|
||||
// task.partETags = [] // 注释掉,保留断点续传数据
|
||||
task._uploading = false
|
||||
task._retryCount = new Map() // 重置重试计数器
|
||||
task.errorMessage = '' // 清除错误信息
|
||||
task.abortController = new AbortController() // 重新创建请求中断控制器
|
||||
startNextTasks()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有上传任务
|
||||
function clearAllTasks() {
|
||||
fileTasks.value = []
|
||||
}
|
||||
|
||||
// 删除单个任务
|
||||
function removeTask(task: FileTask) {
|
||||
// 先取消任务
|
||||
if (task.status === 'uploading' || task.status === 'waiting' || task.status === 'paused') {
|
||||
task._cancel?.()
|
||||
}
|
||||
// 清理队列中的分片
|
||||
uploadQueue.value = uploadQueue.value.filter((item) => item.task.uid !== task.uid)
|
||||
// 清理正在上传的分片
|
||||
const activeChunkIds = Array.from(activeUploads.value).filter((id) => id.startsWith(task.uid))
|
||||
activeChunkIds.forEach((id) => activeUploads.value.delete(id))
|
||||
// 从任务列表中移除
|
||||
fileTasks.value = fileTasks.value.filter((t) => t.uid !== task.uid)
|
||||
}
|
||||
|
||||
// 文件大小格式化工具
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
// 组件销毁时,终止所有上传任务和Worker
|
||||
onUnmounted(() => {
|
||||
fileTasks.value.forEach((task) => {
|
||||
if (task.status === 'uploading' || task.status === 'waiting' || task.status === 'paused') {
|
||||
pauseTask(task)
|
||||
}
|
||||
})
|
||||
|
||||
if (md5Worker) {
|
||||
md5Worker.terminate()
|
||||
md5Worker = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
fileTasks,
|
||||
uploadingCount,
|
||||
maxConcurrent,
|
||||
maxChunkConcurrent,
|
||||
uploadFileTask,
|
||||
startNextTasks,
|
||||
startAllUpload,
|
||||
addFiles,
|
||||
pauseTask,
|
||||
resumeTask,
|
||||
cancelTask,
|
||||
startTask,
|
||||
retryTask,
|
||||
clearAllTasks,
|
||||
removeTask,
|
||||
formatFileSize,
|
||||
md5CalculatingTaskUid,
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ import directives from './directives'
|
||||
// 状态管理
|
||||
import pinia from '@/stores'
|
||||
|
||||
// 多语言切换
|
||||
import {translateJsVueUseModel} from './utils/translate' // 导入translate插件
|
||||
|
||||
// 对特定组件进行默认配置
|
||||
Card.props.bordered = false
|
||||
|
||||
@@ -39,5 +42,6 @@ app.use(pinia)
|
||||
app.use(ArcoVue)
|
||||
app.use(ArcoVueIcon)
|
||||
app.use(directives)
|
||||
//app.use(translateJsVueUseModel) //去掉注释,即可启用多语言切换能力。 具体多语言切换的设置,位于 /src/utils/translate.ts
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -33,6 +33,7 @@ const storeSetup = () => {
|
||||
registrationDate: '',
|
||||
deptName: '',
|
||||
roles: [],
|
||||
roleNames: [],
|
||||
permissions: [],
|
||||
})
|
||||
const nickname = computed(() => userInfo.nickname)
|
||||
|
||||
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -52,6 +52,7 @@ declare module 'vue' {
|
||||
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
|
||||
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
|
||||
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']
|
||||
MultipartUpload: typeof import('./../components/MultipartUpload/index.vue')['default']
|
||||
ParentView: typeof import('./../components/ParentView/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
49
src/utils/drag-drop-file-util.ts
Normal file
49
src/utils/drag-drop-file-util.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 递归读取 DataTransferItemList 中的所有文件(支持文件夹结构)
|
||||
* 自动为每个 File 对象添加 webkitRelativePath 属性
|
||||
* 仅在支持 File System Access API 的浏览器下有效
|
||||
*/
|
||||
export async function getFilesFromDataTransferItems(items: DataTransferItemList): Promise<File[]> {
|
||||
const files: File[] = []
|
||||
|
||||
async function traverse(handle: FileSystemHandle, path = ''): Promise<void> {
|
||||
if (handle.kind === 'file') {
|
||||
const file = await (handle as FileSystemFileHandle).getFile()
|
||||
// 创建新的 File 对象,包含相对路径信息
|
||||
const fileWithPath = new File([file], file.name, {
|
||||
type: file.type,
|
||||
lastModified: file.lastModified,
|
||||
})
|
||||
// 使用 Object.defineProperty 添加 webkitRelativePath
|
||||
Object.defineProperty(fileWithPath, 'webkitRelativePath', {
|
||||
value: path + file.name,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
})
|
||||
files.push(fileWithPath)
|
||||
} else if (handle.kind === 'directory') {
|
||||
for await (const [name, childHandle] of (handle as any).entries()) {
|
||||
await traverse(childHandle, `${path}${name}/`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of Array.from(items)) {
|
||||
if (item.kind === 'file' && 'getAsFileSystemHandle' in item) {
|
||||
const handle = await (item as any).getAsFileSystemHandle()
|
||||
if (handle) {
|
||||
await traverse(handle, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前浏览器是否支持 File System Access API 拖拽文件夹
|
||||
*/
|
||||
export function isFileSystemAccessAPISupported(): boolean {
|
||||
return typeof window !== 'undefined' && 'getAsFileSystemHandle' in DataTransferItem.prototype
|
||||
}
|
||||
149
src/utils/md5-worker.ts
Normal file
149
src/utils/md5-worker.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import SparkMD5 from 'spark-md5'
|
||||
|
||||
// 确保在 Web Worker 环境中运行
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
// 监听来自主线程的消息
|
||||
globalThis.addEventListener('message', (event) => {
|
||||
const { file, taskId, blockSize, chunkSize } = event.data
|
||||
|
||||
if (file && taskId && blockSize && chunkSize) {
|
||||
calculateFileMd5Optimized(file, taskId, blockSize, chunkSize)
|
||||
} else {
|
||||
globalThis.postMessage({
|
||||
type: 'error',
|
||||
taskId: taskId || 'unknown',
|
||||
error: 'Missing required parameters: file, taskId, blockSize, chunkSize',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function calculateFileMd5Optimized(file: File, taskId: string, blockSize: number, chunkSize: number) {
|
||||
const totalSize = file.size
|
||||
const blocks = Math.ceil(totalSize / blockSize)
|
||||
const blockHashes: string[] = Array.from({ length: blocks })
|
||||
let processedBytes = 0
|
||||
let processedBlocks = 0
|
||||
|
||||
const maxConcurrency = Math.max(2, navigator.hardwareConcurrency || 2)
|
||||
let activeWorkers = 0
|
||||
let nextBlockIndex = 0
|
||||
|
||||
function processBlock(blockIndex: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const start = blockIndex * blockSize
|
||||
const end = Math.min(start + blockSize, totalSize)
|
||||
const block = file.slice(start, end)
|
||||
|
||||
const spark = new SparkMD5.ArrayBuffer()
|
||||
const chunks = Math.ceil(block.size / chunkSize)
|
||||
let currentChunk = 0
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (e: ProgressEvent<FileReader>) {
|
||||
try {
|
||||
if (e.target?.result) {
|
||||
spark.append(e.target.result as ArrayBuffer)
|
||||
processedBytes += (e.target.result as ArrayBuffer).byteLength
|
||||
|
||||
globalThis.postMessage({
|
||||
type: 'progress',
|
||||
taskId,
|
||||
progress: processedBytes / totalSize,
|
||||
processedBytes,
|
||||
totalSize,
|
||||
})
|
||||
|
||||
currentChunk++
|
||||
if (currentChunk < chunks) {
|
||||
loadNextChunk()
|
||||
} else {
|
||||
blockHashes[blockIndex] = spark.end()
|
||||
processedBlocks++
|
||||
resolve()
|
||||
}
|
||||
} else {
|
||||
reject(new Error('FileReader result is null'))
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = function (e: ProgressEvent<FileReader>) {
|
||||
console.error(`[Worker] 文件读取错误:`, e)
|
||||
globalThis.postMessage({
|
||||
type: 'error',
|
||||
taskId,
|
||||
error: e,
|
||||
})
|
||||
reject(new Error('FileReader error'))
|
||||
}
|
||||
|
||||
function loadNextChunk() {
|
||||
try {
|
||||
const chunkStart = currentChunk * chunkSize
|
||||
const chunkEnd = Math.min(chunkStart + chunkSize, block.size)
|
||||
const blob = block.slice(chunkStart, chunkEnd)
|
||||
reader.readAsArrayBuffer(blob)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
loadNextChunk()
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function scheduleBlocks() {
|
||||
while (activeWorkers < maxConcurrency && nextBlockIndex < blocks) {
|
||||
const currentIndex = nextBlockIndex++
|
||||
activeWorkers++
|
||||
processBlock(currentIndex).then(() => {
|
||||
activeWorkers--
|
||||
if (processedBlocks >= blocks) {
|
||||
// 所有块完成,计算最终 MD5
|
||||
try {
|
||||
const finalSpark = new SparkMD5.ArrayBuffer()
|
||||
blockHashes.forEach((hash) => {
|
||||
const hashBuffer = new TextEncoder().encode(hash)
|
||||
finalSpark.append(hashBuffer)
|
||||
})
|
||||
const finalMd5 = finalSpark.end()
|
||||
|
||||
globalThis.postMessage({
|
||||
type: 'complete',
|
||||
taskId,
|
||||
md5: finalMd5,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`[Worker] 计算最终 MD5 时出错:`, error)
|
||||
globalThis.postMessage({
|
||||
type: 'error',
|
||||
taskId,
|
||||
error,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 继续调度
|
||||
scheduleBlocks()
|
||||
}
|
||||
}).catch((error) => {
|
||||
activeWorkers--
|
||||
console.error(`[Worker] 处理块时出错:`, error)
|
||||
globalThis.postMessage({
|
||||
type: 'error',
|
||||
taskId,
|
||||
error,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 启动调度器
|
||||
scheduleBlocks()
|
||||
}
|
||||
54
src/utils/translate.ts
Normal file
54
src/utils/translate.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {translateJsVueUseModel, translate} from './translateVue3TS' // 导入 translate 的 VUE3 的 ts 插件
|
||||
|
||||
/*
|
||||
|
||||
translate.js AI 多语言切换模块的自定义配置。
|
||||
如果不想启用,你可以通过以下方式中的任何一种进行禁用
|
||||
1. 直接将所有配置全部注释掉
|
||||
2. 将 /src/main.ts 中的这一行 app.use(translateJsVueUseModel) 注释掉即可。
|
||||
|
||||
*/
|
||||
|
||||
//打印包含具体执行时间的debug日志
|
||||
//translate.time.use = true;
|
||||
//window.translate.time.printTime = 100;
|
||||
|
||||
// 针对翻译动作的性能监控 https://translate.zvo.cn/549733.html
|
||||
//translate.time.execute.start();
|
||||
|
||||
// 设置当前切换所支持的语言 http://translate.zvo.cn/4056.html
|
||||
window.translate.selectLanguageTag.languages = 'chinese_simplified,english,korean,latin,french,russian';
|
||||
|
||||
// 设置本地语种(当前网页的语种) ,如果你网页语种很多,比如国际化论坛,哪个国家发言的都有,那这里你可以不用设置,交给 translate.js 自动去识别当前网页语种 http://translate.zvo.cn/4066.html
|
||||
window.translate.language.setLocal('chinese_simplified');
|
||||
|
||||
// 本地语种也进行强制翻译 http://translate.zvo.cn/289574.html
|
||||
//translate.language.translateLocal = true;
|
||||
|
||||
// 翻译时忽略指定的文字不翻译 http://translate.zvo.cn/283381.html
|
||||
translate.ignore.text.push('ContiNew Admin');
|
||||
|
||||
// 网页打开时自动隐藏文字,翻译完成后显示译文 http://translate.zvo.cn/549731.html
|
||||
// 注意,如果不启用本多语言切换能力,这个要注释掉,不然你网页的文本是会被隐藏的
|
||||
//window.translate.visual.webPageLoadTranslateBeforeHiddenText({inHeadTip: false});
|
||||
|
||||
// 启用翻译中的遮罩层 http://translate.zvo.cn/407105.html
|
||||
window.translate.progress.api.startUITip();
|
||||
|
||||
// 设置采用开源免费的 client.edge 无服务端翻译服务通道,无需任何注册接入即可直接使用 http://translate.zvo.cn/4081.html
|
||||
translate.service.use('client.edge');
|
||||
|
||||
// 网页ajax请求触发自动翻译 http://translate.zvo.cn/4086.html
|
||||
translate.request.listener.start();
|
||||
|
||||
// 开启页面元素动态监控,js改变的内容也会被翻译,参考文档: http://translate.zvo.cn/4067.html
|
||||
translate.listener.start();
|
||||
|
||||
// 元素的内容整体翻译能力配置 ,提高翻译的语义 http://translate.zvo.cn/4078.html
|
||||
translate.whole.enableAll();
|
||||
|
||||
//触发翻译执行,有关这个的说明可参考 http://translate.zvo.cn/547814.html
|
||||
translate.execute();
|
||||
|
||||
//导出,其中translateJsVueUseModel为vue插件,translate为js函数
|
||||
export { translateJsVueUseModel, translate };
|
||||
91
src/utils/translateVue3TS.ts
Normal file
91
src/utils/translateVue3TS.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { nextTick } from 'vue';
|
||||
import translate from 'i18n-jsautotranslate'
|
||||
|
||||
/*
|
||||
|
||||
因为这个文件没什么需要用户单独设置的,只是整体对vue的适配,所以这个文件后续调好了会放到 npm 上,当前因为下面DOM渲染完毕触发的问题没有精准触发,所以暂时先放到这里进行方便优化调试
|
||||
|
||||
|
||||
*/
|
||||
|
||||
var originalTrasnalteLog = translate.log;
|
||||
translate.log = function(obj){
|
||||
if(typeof(obj) === 'string' && obj.indexOf('- translate.js -') !== -1){
|
||||
//不显示 translate.js 的说明
|
||||
}else{
|
||||
originalTrasnalteLog(obj);
|
||||
}
|
||||
}
|
||||
|
||||
//vue3框架的一些单独设置
|
||||
translate.vue3 = {
|
||||
/*
|
||||
是否有 translate.execute() 代码的触发
|
||||
有则是true,没有则是false
|
||||
false则不会再dom渲染完后自动进行翻译,自然也不会显示 select 选择语言
|
||||
*/
|
||||
isExecute: false,
|
||||
}
|
||||
//如果网页上有 translate.execute() 代码的触发,那么就设置 isExecute 为 true
|
||||
translate.lifecycle.execute.trigger.push(function(data){
|
||||
if(data.executeTriggerNumber === 1){
|
||||
translate.vue3.isExecute = true;
|
||||
translate.time.log('打开页面后,第一次触发 translate.execute() - 设置 translate.vue3.isExecute = true;');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
translate.time.log('设置vue3初始化配置 - translate.vue3');
|
||||
|
||||
//将 translate 参数挂载到 window 上,方便在全局调用
|
||||
if(typeof(window.translate) === 'undefined'){
|
||||
window.translate = translate;
|
||||
}
|
||||
translate.time.log('将 translate 参数挂载到 window 上,方便在全局调用');
|
||||
|
||||
const translateJsVueUseModel = {
|
||||
install(app) {
|
||||
// 直接监听应用挂载完成
|
||||
const originalMount = app.mount;
|
||||
app.mount = function(...args) {
|
||||
const root = originalMount.apply(this, args);
|
||||
// 应用挂载完成后执行
|
||||
// 使用双重nextTick确保DOM完全稳定后再执行翻译
|
||||
// 第一个nextTick确保初始DOM渲染完成
|
||||
nextTick(() => {
|
||||
// 第二个nextTick确保可能的异步更新也完成
|
||||
nextTick(() => {
|
||||
|
||||
/*
|
||||
|
||||
这里有问题,应该是vue的DOM渲染完毕后触发,但是实际打断点测试,DOM还没有渲染完就触发了,这里还需要跟踪优化
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
if(translate.vue3.isExecute){
|
||||
translate.time.log('组件渲染完成,触发 translate.execute();');
|
||||
|
||||
//对vue3的某些第三方组件进行容错处理
|
||||
translate.faultTolerance.documentCreateTextNode.use(); //对VUE的某些组件频繁渲染dom进行容错
|
||||
translate.time.log('对vue3的某些第三方组件进行容错处理 - translate.faultTolerance.documentCreateTextNode.use();');
|
||||
|
||||
translate.execute();
|
||||
setTimeout(() => {
|
||||
translate.execute();
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
translate.execute();
|
||||
}, 2000);
|
||||
}else{
|
||||
translate.time.log('组件渲染完成,但未发现translate.execute();存在,不进行翻译');
|
||||
}
|
||||
});
|
||||
});
|
||||
return root;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//export default translateJsVueUseModel;
|
||||
export { translateJsVueUseModel, translate };
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<GiIframe src="https://continew.top/admin/changelog/"></GiIframe>
|
||||
<GiIframe src="https://continew.top/docs/admin/changelog/"></GiIframe>
|
||||
</template>
|
||||
|
||||
<script lang='ts' setup>
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
const links = [
|
||||
{ text: '项目简介', url: 'https://continew.top/admin/guide/introduction.html' },
|
||||
{ text: '快速开始', url: 'https://continew.top/admin/guide/quick-start.html' },
|
||||
{ text: '常见问题', url: 'https://continew.top/admin/faq.html' },
|
||||
{ text: '更新日志', url: 'https://continew.top/admin/changelog/' },
|
||||
{ text: '项目简介', url: 'https://continew.top/docs/admin/guide/introduction.html' },
|
||||
{ text: '快速开始', url: 'https://continew.top/docs/admin/guide/quick-start.html' },
|
||||
{ text: '常见问题', url: 'https://continew.top/docs/admin/faq.html' },
|
||||
{ text: '更新日志', url: 'https://continew.top/docs/admin/changelog/' },
|
||||
{ text: '贡献指南', url: 'https://continew.top/about/contributing.html' },
|
||||
{ text: '赞助支持 💖', url: 'https://continew.top/sponsor/' },
|
||||
]
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<span>{{ title }}</span>
|
||||
</h3>
|
||||
|
||||
<!-- 多语言切换的 Select 下拉选择 -->
|
||||
<LanguageSelect/>
|
||||
|
||||
<a-row align="stretch" class="login-box">
|
||||
<a-col :xs="0" :sm="12" :md="13">
|
||||
<div class="login-left">
|
||||
@@ -100,6 +103,7 @@ import { useAppStore } from '@/stores'
|
||||
import { useTenantStore } from '@/stores/modules/tenant'
|
||||
import { useDevice } from '@/hooks'
|
||||
import { getTenantIdByDomain, getTenantStatus } from '@/apis'
|
||||
import LanguageSelect from 'i18n-jsautotranslate/ArcoDesign/Vue3/LanguageSelect.vue';
|
||||
|
||||
defineOptions({ name: 'Login' })
|
||||
|
||||
@@ -542,5 +546,13 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//多语言切换
|
||||
.LanguageSelect {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 80px;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible" title="日志详情" :width="720" :footer="false">
|
||||
<a-drawer v-model:visible="visible" title="日志详情" :width="width >= 720 ? 720 : '100%'" :footer="false">
|
||||
<a-descriptions title="基本信息" :column="2" size="large" class="general-description">
|
||||
<a-descriptions-item label="日志 ID">{{ dataDetail?.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="Trace ID"><a-typography-paragraph :copyable="!!dataDetail?.traceId">{{ dataDetail?.traceId }}</a-typography-paragraph></a-descriptions-item>
|
||||
@@ -68,8 +68,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from '@vueuse/core/index'
|
||||
import { type LogDetailResp, getLog as getDetail } from '@/apis/monitor'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const dataId = ref('')
|
||||
const dataDetail = ref<LogDetailResp>()
|
||||
const visible = ref(false)
|
||||
@@ -97,6 +100,5 @@ defineExpose({ onOpen })
|
||||
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 5px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -89,7 +89,7 @@ const columns: TableInstance['columns'] = [
|
||||
{ title: '所属模块', dataIndex: 'module', align: 'center', ellipsis: true, tooltip: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
// dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
align: 'center',
|
||||
filterable: {
|
||||
|
||||
@@ -56,39 +56,40 @@
|
||||
<a-space>
|
||||
<a-link v-permission="['open:app:get']" title="详情" @click="onDetail(record)">详情</a-link>
|
||||
<a-link v-permission="['open:app:update']" title="修改" @click="onUpdate(record)">修改</a-link>
|
||||
<a-link
|
||||
v-permission="['open:app:delete']"
|
||||
status="danger"
|
||||
:disabled="record.disabled"
|
||||
:title="record.disabled ? '禁止删除' : '删除'"
|
||||
@click="onDelete(record)"
|
||||
>
|
||||
删除
|
||||
</a-link>
|
||||
<a-dropdown>
|
||||
<a-button v-if="has.hasPermOr(['open:app:resetSecret'])" type="text" size="mini" title="更多">
|
||||
<a-button v-if="has.hasPermOr(['open:app:resetSecret', 'open:app:delete'])" type="text" size="mini" title="更多">
|
||||
<template #icon>
|
||||
<icon-more :size="16" />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption v-permission="['open:app:resetSecret']" title="重置密钥" @click="onResetSecret(record)">重置密钥</a-doption>
|
||||
<a-doption v-permission="['open:app:delete']">
|
||||
<a-link
|
||||
status="danger"
|
||||
:disabled="record.disabled"
|
||||
:title="record.disabled ? '禁止删除' : '删除'"
|
||||
@click="onDelete(record)"
|
||||
>
|
||||
删除
|
||||
</a-link>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<AppAddModal ref="AppAddModalRef" @save-success="search" />
|
||||
<AppDetailDrawer ref="AppDetailDrawerRef" />
|
||||
<AddModal ref="AddModalRef" @save-success="search" />
|
||||
<DetailDrawer ref="DetailDrawerRef" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import AppAddModal from './AppAddModal.vue'
|
||||
import AppDetailDrawer from './AppDetailDrawer.vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import DetailDrawer from './DetailDrawer.vue'
|
||||
import {
|
||||
type AppQuery,
|
||||
type AppResp,
|
||||
@@ -137,14 +138,14 @@ const columns: TableInstance['columns'] = [
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
slotName: 'action',
|
||||
width: 190,
|
||||
width: 160,
|
||||
align: 'center',
|
||||
fixed: !isMobile() ? 'right' : undefined,
|
||||
show: has.hasPermOr([
|
||||
'open:app:get',
|
||||
'open:app:update',
|
||||
'open:app:delete',
|
||||
'open:app:resetSecret',
|
||||
'open:app:delete',
|
||||
]),
|
||||
},
|
||||
]
|
||||
@@ -200,21 +201,21 @@ const onResetSecret = async (record: AppResp) => {
|
||||
})
|
||||
}
|
||||
|
||||
const AppAddModalRef = ref<InstanceType<typeof AppAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
AppAddModalRef.value?.onAdd()
|
||||
AddModalRef.value?.onAdd()
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: AppResp) => {
|
||||
AppAddModalRef.value?.onUpdate(record.id)
|
||||
AddModalRef.value?.onUpdate(record.id)
|
||||
}
|
||||
|
||||
const AppDetailDrawerRef = ref<InstanceType<typeof AppDetailDrawer>>()
|
||||
const DetailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
// 详情
|
||||
const onDetail = (record: AppResp) => {
|
||||
AppDetailDrawerRef.value?.onOpen(record.id)
|
||||
DetailDrawerRef.value?.onOpen(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item label="任务名称" field="jobName">
|
||||
<a-input v-model.trim="form.jobName" placeholder="请输入任务名称" :max-length="64" show-word-limit />
|
||||
<a-input v-model="form.jobName" placeholder="请输入任务名称" :max-length="64" show-word-limit />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model.trim="form.description"
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
@@ -99,14 +99,14 @@
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item label="执行器名称" field="executorInfo">
|
||||
<a-input v-model.trim="form.executorInfo" placeholder="请输入执行器名称" :max-length="255" />
|
||||
<a-input v-model="form.executorInfo" placeholder="请输入执行器名称" :max-length="255" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="任务参数" field="argsStr">
|
||||
<a-textarea
|
||||
v-if="form.taskType !== 3"
|
||||
v-model.trim="form.argsStr"
|
||||
v-model="form.argsStr"
|
||||
placeholder="请输入任务参数"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
@@ -134,17 +134,17 @@
|
||||
<a-row>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item label="路由策略" field="routeKey">
|
||||
<a-select v-model.trim="form.routeKey" placeholder="请选择路由策略" :options="job_route_strategy_enum" />
|
||||
<a-select v-model="form.routeKey" placeholder="请选择路由策略" :options="job_route_strategy_enum" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item label="阻塞策略" field="blockStrategy">
|
||||
<a-select v-model.trim="form.blockStrategy" placeholder="请选择阻塞策略" :options="job_block_strategy_enum" />
|
||||
<a-select v-model="form.blockStrategy" placeholder="请选择阻塞策略" :options="job_block_strategy_enum" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item label="超时时间" field="executorTimeout">
|
||||
<a-input-number v-model.trim="form.executorTimeout" placeholder="请输入超时时间" :min="1">
|
||||
<a-input-number v-model="form.executorTimeout" placeholder="请输入超时时间" :min="1">
|
||||
<template #suffix>秒</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
@@ -157,7 +157,7 @@
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item label="重试间隔" field="retryInterval">
|
||||
<a-input-number v-model.trim="form.retryInterval" placeholder="请输入重试间隔" :min="1">
|
||||
<a-input-number v-model="form.retryInterval" placeholder="请输入重试间隔" :min="1">
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
@@ -62,18 +62,29 @@
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-link v-permission="['schedule:log:list']" title="日志" @click="onLog(record)">日志</a-link>
|
||||
<a-popconfirm content="是否确定立即执行一次任务?" type="warning" @ok="onTrigger(record)">
|
||||
<a-link v-permission="['schedule:job:trigger']" title="执行">执行</a-link>
|
||||
</a-popconfirm>
|
||||
<a-link v-permission="['schedule:job:update']" title="修改" @click="onUpdate(record)">修改</a-link>
|
||||
<a-link v-permission="['schedule:job:delete']" status="danger" title="删除" @click="onDelete(record)">删除</a-link>
|
||||
<a-dropdown>
|
||||
<a-button v-if="has.hasPermOr(['schedule:log:list', 'schedule:job:delete'])" type="text" size="mini" title="更多">
|
||||
<template #icon>
|
||||
<icon-more :size="16" />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption v-permission="['schedule:log:list']" title="查看日志" @click="onLog(record)">查看日志</a-doption>
|
||||
<a-doption v-permission="['schedule:job:delete']">
|
||||
<a-link status="danger" title="删除" @click="onDelete(record)">删除</a-link>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<JobAddModal ref="JobAddModalRef" @save-success="reset" />
|
||||
<JobDetailDrawer ref="JobDetailDrawerRef" />
|
||||
<AddModal ref="AddModalRef" @save-success="reset" />
|
||||
<DetailDrawer ref="DetailDrawerRef" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
@@ -81,8 +92,8 @@
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import JobAddModal from './JobAddModal.vue'
|
||||
import JobDetailDrawer from './JobDetailDrawer.vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import DetailDrawer from './DetailDrawer.vue'
|
||||
import { type JobQuery, type JobResp, deleteJob, listGroup, listJob, triggerJob, updateJobStatus } from '@/apis/schedule'
|
||||
import { useTable } from '@/hooks'
|
||||
import { useDict } from '@/hooks/app'
|
||||
@@ -121,13 +132,13 @@ const columns: TableInstance['columns'] = [
|
||||
{
|
||||
title: '操作',
|
||||
slotName: 'action',
|
||||
width: 200,
|
||||
width: 160,
|
||||
align: 'center',
|
||||
fixed: !isMobile() ? 'right' : undefined,
|
||||
show: has.hasPermOr([
|
||||
'schedule:log:list',
|
||||
'schedule:job:trigger',
|
||||
'schedule:job:update',
|
||||
'schedule:log:list',
|
||||
'schedule:job:delete',
|
||||
]),
|
||||
},
|
||||
@@ -178,21 +189,21 @@ const onTrigger = (record: JobResp) => {
|
||||
})
|
||||
}
|
||||
|
||||
const JobAddModalRef = ref<InstanceType<typeof JobAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
JobAddModalRef.value?.onAdd()
|
||||
AddModalRef.value?.onAdd()
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: JobResp) => {
|
||||
JobAddModalRef.value?.onUpdate(record)
|
||||
AddModalRef.value?.onUpdate(record)
|
||||
}
|
||||
|
||||
const JobDetailDrawerRef = ref<InstanceType<typeof JobDetailDrawer>>()
|
||||
const DetailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
// 详情
|
||||
const onDetail = (record: JobResp) => {
|
||||
JobDetailDrawerRef.value?.onOpen(record)
|
||||
DetailDrawerRef.value?.onOpen(record)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -53,16 +53,16 @@
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<ClientAddModal ref="ClientAddModalRef" @save-success="search" />
|
||||
<ClientDetailDrawer ref="ClientDetailDrawerRef" />
|
||||
<AddModal ref="AddModalRef" @save-success="search" />
|
||||
<DetailDrawer ref="DetailDrawerRef" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import type { LabelValue } from '@arco-design/web-vue/es/tree-select/interface'
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import ClientAddModal from './ClientAddModal.vue'
|
||||
import ClientDetailDrawer from './ClientDetailDrawer.vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import DetailDrawer from './DetailDrawer.vue'
|
||||
import { type ClientQuery, type ClientResp, deleteClient, listClient } from '@/apis/system/client'
|
||||
import { DisEnableStatusList } from '@/constant/common'
|
||||
import { useTable } from '@/hooks'
|
||||
@@ -186,21 +186,21 @@ const onDelete = (record: ClientResp) => {
|
||||
})
|
||||
}
|
||||
|
||||
const ClientAddModalRef = ref<InstanceType<typeof ClientAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
ClientAddModalRef.value?.onAdd()
|
||||
AddModalRef.value?.onAdd()
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: ClientResp) => {
|
||||
ClientAddModalRef.value?.onUpdate(record.id)
|
||||
AddModalRef.value?.onUpdate(record.id)
|
||||
}
|
||||
|
||||
const ClientDetailDrawerRef = ref<InstanceType<typeof ClientDetailDrawer>>()
|
||||
const DetailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
// 详情
|
||||
const onDetail = (record: ClientResp) => {
|
||||
ClientDetailDrawerRef.value?.onOpen(record.id)
|
||||
DetailDrawerRef.value?.onOpen(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:help="mailConfig.MAIL_PROTOCOL.description"
|
||||
hide-asterisk
|
||||
>
|
||||
<a-select v-model.trim="form.MAIL_PROTOCOL">
|
||||
<a-select v-model="form.MAIL_PROTOCOL">
|
||||
<a-option label="SMTP" value="smtp" />
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
@@ -28,7 +28,7 @@
|
||||
:help="mailConfig.MAIL_HOST.description"
|
||||
hide-asterisk
|
||||
>
|
||||
<a-input v-model.trim="form.MAIL_HOST" />
|
||||
<a-input v-model="form.MAIL_HOST" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="MAIL_PORT"
|
||||
@@ -44,7 +44,7 @@
|
||||
:help="mailConfig.MAIL_USERNAME.description"
|
||||
hide-asterisk
|
||||
>
|
||||
<a-input v-model.trim="form.MAIL_USERNAME" />
|
||||
<a-input v-model="form.MAIL_USERNAME" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="MAIL_PASSWORD"
|
||||
@@ -52,7 +52,7 @@
|
||||
:help="mailConfig.MAIL_PASSWORD.description"
|
||||
hide-asterisk
|
||||
>
|
||||
<a-input-password v-model.trim="form.MAIL_PASSWORD" />
|
||||
<a-input-password v-model="form.MAIL_PASSWORD" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="MAIL_SSL_ENABLED"
|
||||
|
||||
@@ -74,20 +74,20 @@
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item class="input-item" field="SITE_TITLE" :label="siteConfig.SITE_TITLE.name" :help="siteConfig.SITE_TITLE.description">
|
||||
<a-input v-model.trim="form.SITE_TITLE" placeholder="请输入系统名称" :max-length="18" show-word-limit />
|
||||
<a-input v-model="form.SITE_TITLE" placeholder="请输入系统名称" :max-length="18" show-word-limit />
|
||||
</a-form-item>
|
||||
<a-form-item class="input-item" field="SITE_DESCRIPTION" :label="siteConfig.SITE_DESCRIPTION.name" :help="siteConfig.SITE_DESCRIPTION.description">
|
||||
<a-textarea
|
||||
v-model.trim="form.SITE_DESCRIPTION"
|
||||
v-model="form.SITE_DESCRIPTION"
|
||||
placeholder="请输入系统描述"
|
||||
:auto-size="{ minRows: 1, maxRows: 3 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item class="input-item" field="SITE_COPYRIGHT" :label="siteConfig.SITE_COPYRIGHT.name" :help="siteConfig.SITE_COPYRIGHT.description">
|
||||
<a-input v-model.trim="form.SITE_COPYRIGHT" placeholder="请输入版权声明" />
|
||||
<a-input v-model="form.SITE_COPYRIGHT" placeholder="请输入版权声明" />
|
||||
</a-form-item>
|
||||
<a-form-item field="SITE_BEIAN" :label="siteConfig.SITE_BEIAN.name" :help="siteConfig.SITE_BEIAN.description">
|
||||
<a-input v-model.trim="form.SITE_BEIAN" placeholder="请输入备案号" :max-length="30" show-word-limit />
|
||||
<a-input v-model="form.SITE_BEIAN" placeholder="请输入备案号" :max-length="30" show-word-limit />
|
||||
</a-form-item>
|
||||
<a-space style="margin-top: 16px">
|
||||
<a-button v-if="!isUpdate" v-permission="['system:siteConfig:update']" type="primary" @click="onUpdate">
|
||||
|
||||
@@ -71,14 +71,14 @@
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<SmsConfigAddModal ref="SmsConfigAddModalRef" @save-success="search" />
|
||||
<AddModal ref="AddModalRef" @save-success="search" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import SmsConfigAddModal from './SmsConfigAddModal.vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import {
|
||||
type SmsConfigQuery,
|
||||
type SmsConfigResp,
|
||||
@@ -208,15 +208,15 @@ const onSetDefault = (record: SmsConfigResp) => {
|
||||
})
|
||||
}
|
||||
|
||||
const SmsConfigAddModalRef = ref<InstanceType<typeof SmsConfigAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
SmsConfigAddModalRef.value?.onAdd()
|
||||
AddModalRef.value?.onAdd()
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: SmsConfigResp) => {
|
||||
SmsConfigAddModalRef.value?.onUpdate(record.id)
|
||||
AddModalRef.value?.onUpdate(record.id)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
@before-ok="save"
|
||||
@close="reset"
|
||||
>
|
||||
<GiForm ref="formRef" v-model="form" :columns="columns" />
|
||||
<GiForm ref="formRef" v-model="form" :columns="columns">
|
||||
<template #secretKey>
|
||||
<a-input
|
||||
v-model="form.secretKey"
|
||||
:placeholder="isUpdate ? '保持 Secret Key 为空将不更改' : '请输入 Secret Key'"
|
||||
/>
|
||||
</template>
|
||||
</GiForm>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
@@ -77,7 +84,7 @@ const columns: ColumnItem[] = reactive([
|
||||
field: 'secretKey',
|
||||
type: 'input',
|
||||
span: 24,
|
||||
required: true,
|
||||
required: () => !isUpdate.value,
|
||||
show: () => form.type === 2,
|
||||
},
|
||||
{
|
||||
@@ -165,7 +172,7 @@ const save = async () => {
|
||||
if (isUpdate.value) {
|
||||
await updateStorage({
|
||||
...form,
|
||||
secretKey: form.type === 2 && !form.secretKey.includes('*') ? encryptByRsa(form.secretKey) || '' : null,
|
||||
secretKey: form.type === 2 && form.secretKey ? encryptByRsa(form.secretKey) || '' : null,
|
||||
}, dataId.value)
|
||||
Message.success('修改成功')
|
||||
} else {
|
||||
@@ -14,11 +14,11 @@
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<StorageAddModal ref="StorageAddModalRef" @save-success="search" />
|
||||
<AddModal ref="AddModalRef" @save-success="search" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import StorageAddModal from '../StorageAddModal.vue'
|
||||
import AddModal from '../AddModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
@@ -35,10 +35,10 @@ const search = () => {
|
||||
emit('save-success')
|
||||
}
|
||||
|
||||
const StorageAddModalRef = ref<InstanceType<typeof StorageAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
StorageAddModalRef.value?.onAdd(props.type)
|
||||
AddModalRef.value?.onAdd(props.type)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -72,12 +72,12 @@
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<StorageAddModal ref="StorageAddModalRef" @save-success="search" />
|
||||
<AddModal ref="AddModalRef" @save-success="search" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import StorageAddModal from '../StorageAddModal.vue'
|
||||
import AddModal from '../AddModal.vue'
|
||||
import has from '@/utils/has'
|
||||
import { type StorageResp, deleteStorage, setDefaultStorage, updateStorageStatus } from '@/apis/system'
|
||||
import { useDict } from '@/hooks/app'
|
||||
@@ -182,11 +182,11 @@ const onDelete = (record: StorageResp) => {
|
||||
})
|
||||
}
|
||||
|
||||
const StorageAddModalRef = ref<InstanceType<typeof StorageAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: StorageResp) => {
|
||||
StorageAddModalRef.value?.onUpdate(record.id)
|
||||
AddModalRef.value?.onUpdate(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</a-dropdown>
|
||||
</a-card>
|
||||
</div>
|
||||
<DeptAddModal ref="DeptAddModalRef" :depts="dataList" @save-success="search" />
|
||||
<AddModal ref="AddModalRef" :depts="dataList" @save-success="search" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
import 'vue3-tree-org/lib/vue3-tree-org.css'
|
||||
import { Vue3TreeOrg } from 'vue3-tree-org'
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import DeptAddModal from './DeptAddModal.vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import { type DeptQuery, type DeptResp, deleteDept, exportDept, listDept } from '@/apis/system/dept'
|
||||
import type GiTable from '@/components/GiTable/index.vue'
|
||||
import { useDownload, useTable } from '@/hooks'
|
||||
@@ -196,17 +196,17 @@ const onExport = () => {
|
||||
useDownload(() => exportDept(queryForm))
|
||||
}
|
||||
|
||||
const DeptAddModalRef = ref<InstanceType<typeof DeptAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = (parentId?: string) => {
|
||||
DeptAddModalRef.value?.onAdd(parentId)
|
||||
AddModalRef.value?.onAdd(parentId)
|
||||
}
|
||||
const handleAdd = (record: DeptResp) => {
|
||||
onAdd(record.id)
|
||||
}
|
||||
// 修改
|
||||
const onUpdate = (record: DeptResp) => {
|
||||
DeptAddModalRef.value?.onUpdate(record.id)
|
||||
AddModalRef.value?.onUpdate(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,16 +12,35 @@
|
||||
<a-row justify="space-between" class="file-main__search">
|
||||
<!-- 左侧区域 -->
|
||||
<a-space wrap>
|
||||
<a-upload v-permission="['system:file:upload']" :show-file-list="false" :custom-request="handleUpload">
|
||||
<template #upload-button>
|
||||
<a-button type="primary" shape="round">
|
||||
<template #icon>
|
||||
<icon-upload />
|
||||
<!-- 上传文件按钮改为下拉菜单,包含普通上传和分片上传 -->
|
||||
<a-dropdown trigger="click">
|
||||
<a-button type="primary" shape="round">
|
||||
<icon-upload />
|
||||
上传文件
|
||||
</a-button>
|
||||
<template #content>
|
||||
<!-- 普通上传 -->
|
||||
<a-upload v-permission="['system:file:upload']" :show-file-list="false" :custom-request="handleUpload" style="display: block;">
|
||||
<template #upload-button>
|
||||
<a-button type="text" style="width: 100%; text-align: left;">
|
||||
普通上传
|
||||
</a-button>
|
||||
</template>
|
||||
<template #default>上传</template>
|
||||
</a-upload>
|
||||
<!-- 分片上传 -->
|
||||
<a-button type="text" style="width: 100%; text-align: left;" @click="visible = true">
|
||||
分片上传
|
||||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-dropdown>
|
||||
<a-modal v-model:visible="visible" title="分片上传" :width="width > 1350 ? 1350 : '100%'" :footer="false" @close="search">
|
||||
<MultipartUpload
|
||||
v-if="visible"
|
||||
:root-path="queryForm.parentPath"
|
||||
:chunk-size="5 * 1024 * 1024"
|
||||
:max-concurrent-files="3"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<a-input-group>
|
||||
<a-input v-model="queryForm.originalName" :placeholder="queryForm.type && queryForm.type !== '0' ? '请输入名称' : '在当前目录下搜索名称'" allow-clear style="width: 200px" />
|
||||
@@ -101,6 +120,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Message, Modal, type RequestOption } from '@arco-design/web-vue'
|
||||
import { api as viewerApi } from 'v-viewer'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import {
|
||||
openFileDetailModal,
|
||||
openFileRenameModal,
|
||||
@@ -122,7 +142,7 @@ const FilePreview = defineAsyncComponent(() => import('@/components/FilePreview/
|
||||
const FileList = defineAsyncComponent(() => import('./FileList.vue'))
|
||||
const route = useRoute()
|
||||
const { mode, selectedFileIds, toggleMode, addSelectedFileItem } = useFileManage()
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const queryForm = reactive<FileQuery>({
|
||||
originalName: undefined,
|
||||
parentPath: (!route.query.type || route.query.type?.toString() === '0') ? '/' : undefined,
|
||||
@@ -248,7 +268,6 @@ const handleMulDelete = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 上传
|
||||
const handleUpload = (options: RequestOption) => {
|
||||
const controller = new AbortController()
|
||||
@@ -276,6 +295,8 @@ const handleUpload = (options: RequestOption) => {
|
||||
}
|
||||
}
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
if (!to.query.type) return
|
||||
if (to.query.type === '0' || !to.query.type) {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<a-row>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item label="菜单标题" field="title">
|
||||
<a-input v-model.trim="form.title" placeholder="请输入菜单标题" :max-length="30" show-word-limit allow-clear />
|
||||
<a-input v-model="form.title" placeholder="请输入菜单标题" :max-length="30" show-word-limit allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
@@ -40,22 +40,22 @@
|
||||
<GiIconSelector v-model="form.icon" />
|
||||
</a-form-item>
|
||||
<a-form-item v-else label="权限标识" field="permission">
|
||||
<a-input v-model.trim="form.permission" placeholder="system:user:add" :max-length="100" show-word-limit allow-clear />
|
||||
<a-input v-model="form.permission" placeholder="system:user:add" :max-length="100" show-word-limit allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item v-if="[1, 2].includes(form.type)" label="路由地址" field="path">
|
||||
<a-input v-model.trim="form.path" placeholder="请输入路由地址" allow-clear />
|
||||
<a-input v-model="form.path" placeholder="请输入路由地址" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item v-if="form.type === 1 || (form.type === 2 && !form.isExternal)" label="重定向" field="redirect">
|
||||
<a-input v-model.trim="form.redirect" placeholder="请输入重定向地址" allow-clear />
|
||||
<a-input v-model="form.redirect" placeholder="请输入重定向地址" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.type === 2 && form.isExternal" label="组件路径" field="component">
|
||||
<a-input v-model.trim="form.component" placeholder="请输入组件路径" allow-clear />
|
||||
<a-input v-model="form.component" placeholder="请输入组件路径" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -69,7 +69,7 @@
|
||||
<a-row>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item v-if="form.type === 1 || (form.type === 2 && !form.isExternal)" label="组件名称" field="name">
|
||||
<a-input v-model.trim="form.name" placeholder="请输入组件名称" :max-length="50" show-word-limit allow-clear />
|
||||
<a-input v-model="form.name" placeholder="请输入组件名称" :max-length="50" show-word-limit allow-clear />
|
||||
<template #extra>
|
||||
<div v-if="componentName">
|
||||
<span>建议组件名称:</span>
|
||||
@@ -80,7 +80,7 @@
|
||||
</a-col>
|
||||
<a-col v-bind="colProps">
|
||||
<a-form-item v-if="form.type === 2" label="权限标识" field="permission">
|
||||
<a-input v-model.trim="form.permission" placeholder="system:user:add" :max-length="100" show-word-limit allow-clear />
|
||||
<a-input v-model="form.permission" placeholder="system:user:add" :max-length="100" show-word-limit allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -90,14 +90,14 @@
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<MenuAddModal ref="MenuAddModalRef" :menus="dataList" @save-success="search" />
|
||||
<AddModal ref="AddModalRef" :menus="dataList" @save-success="search" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import MenuAddModal from './MenuAddModal.vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import { type MenuResp, clearMenuCache, deleteMenu, listMenu } from '@/apis/system/menu'
|
||||
import type GiTable from '@/components/GiTable/index.vue'
|
||||
import { useTable } from '@/hooks'
|
||||
@@ -209,15 +209,15 @@ const onExpanded = () => {
|
||||
tableRef.value?.tableRef?.expandAll(isExpanded.value)
|
||||
}
|
||||
|
||||
const MenuAddModalRef = ref<InstanceType<typeof MenuAddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = (parentId?: string) => {
|
||||
MenuAddModalRef.value?.onAdd(parentId)
|
||||
AddModalRef.value?.onAdd(parentId)
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: MenuResp) => {
|
||||
MenuAddModalRef.value?.onUpdate(record.id)
|
||||
AddModalRef.value?.onUpdate(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -72,13 +72,13 @@
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<NoticeDetailDrawer ref="NoticeDetailDrawerRef" />
|
||||
<DetailDrawer ref="DetailDrawerRef" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import NoticeDetailDrawer from './NoticeDetailDrawer.vue'
|
||||
import DetailDrawer from './DetailDrawer.vue'
|
||||
import { type NoticeQuery, type NoticeResp, deleteNotice, listNotice } from '@/apis/system'
|
||||
import { useTable } from '@/hooks'
|
||||
import { useDict } from '@/hooks/app'
|
||||
@@ -153,10 +153,10 @@ const onUpdate = (record: NoticeResp) => {
|
||||
router.push({ path: '/system/notice/add', query: { id: record.id, type: 'update' } })
|
||||
}
|
||||
|
||||
const NoticeDetailDrawerRef = ref<InstanceType<typeof NoticeDetailDrawer>>()
|
||||
const DetailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
// 详情
|
||||
const onDetail = (record: NoticeResp) => {
|
||||
NoticeDetailDrawerRef.value?.onOpen(record.id)
|
||||
DetailDrawerRef.value?.onOpen(record.id)
|
||||
}
|
||||
|
||||
// 查看
|
||||
|
||||
@@ -12,17 +12,17 @@
|
||||
<fieldset>
|
||||
<legend>基础信息</legend>
|
||||
<a-form-item label="名称" field="name">
|
||||
<a-input v-model.trim="form.name" placeholder="请输入名称" max-length="30" show-word-limit />
|
||||
<a-input v-model="form.name" placeholder="请输入名称" max-length="30" show-word-limit />
|
||||
</a-form-item>
|
||||
<a-form-item label="编码" field="code">
|
||||
<a-input v-model.trim="form.code" placeholder="请输入编码" max-length="30" show-word-limit :disabled="isUpdate" />
|
||||
<a-input v-model="form.code" placeholder="请输入编码" max-length="30" show-word-limit :disabled="isUpdate" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" field="sort">
|
||||
<a-input-number v-model="form.sort" placeholder="请输入排序" :min="1" mode="button" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model.trim="form.description"
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
@@ -34,7 +34,7 @@
|
||||
<legend>数据权限</legend>
|
||||
<a-form-item hide-label field="dataScope">
|
||||
<a-select
|
||||
v-model.trim="form.dataScope"
|
||||
v-model="form.dataScope"
|
||||
:options="data_scope_enum"
|
||||
placeholder="请选择数据权限"
|
||||
:disabled="form.isSystem"
|
||||
@@ -57,13 +57,13 @@
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<RoleAssignModal ref="RoleAssignModalRef" @save-success="search" />
|
||||
<AssignModal ref="AssignModalRef" @save-success="search" />
|
||||
</template>
|
||||
|
||||
<script lang='tsx' setup>
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import RoleAssignModal from '../RoleAssignModal.vue'
|
||||
import AssignModal from '../AssignModal.vue'
|
||||
import { useResetReactive, useTable } from '@/hooks'
|
||||
import { type RoleUserQuery, type RoleUserResp, listRoleUser, unassignFromUsers } from '@/apis/system/role'
|
||||
import { isMobile } from '@/utils'
|
||||
@@ -158,10 +158,10 @@ const onDelete = (record: RoleUserResp) => {
|
||||
})
|
||||
}
|
||||
|
||||
const RoleAssignModalRef = ref<InstanceType<typeof RoleAssignModal>>()
|
||||
const AssignModalRef = ref<InstanceType<typeof AssignModal>>()
|
||||
// 分配
|
||||
const onAssign = () => {
|
||||
RoleAssignModalRef.value?.onOpen(props.roleId)
|
||||
AssignModalRef.value?.onOpen(props.roleId)
|
||||
}
|
||||
|
||||
// 监听 roleId 的变化
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RoleAddDrawer ref="RoleAddDrawerRef" @save-success="getTreeData" />
|
||||
<AddDrawer ref="AddDrawerRef" @save-success="getTreeData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import type { TreeNodeData } from '@arco-design/web-vue'
|
||||
import { mapTree } from 'xe-utils'
|
||||
import RoleAddDrawer from '../RoleAddDrawer.vue'
|
||||
import AddDrawer from '../AddDrawer.vue'
|
||||
import RightMenu from './RightMenu.vue'
|
||||
import { type RoleResp, deleteRole, listRole } from '@/apis/system/role'
|
||||
import has from '@/utils/has'
|
||||
@@ -112,16 +112,16 @@ const treeData = computed(() => {
|
||||
return search(searchKey.value.toLowerCase())
|
||||
})
|
||||
|
||||
const RoleAddDrawerRef = ref<InstanceType<typeof RoleAddDrawer>>()
|
||||
const AddDrawerRef = ref<InstanceType<typeof AddDrawer>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
RoleAddDrawerRef.value?.onAdd()
|
||||
AddDrawerRef.value?.onAdd()
|
||||
}
|
||||
|
||||
// 点击菜单项
|
||||
const onMenuItemClick = (mode: string, node: RoleResp) => {
|
||||
if (mode === 'update') {
|
||||
RoleAddDrawerRef.value?.onUpdate(node.id)
|
||||
AddDrawerRef.value?.onUpdate(node.id)
|
||||
} else if (mode === 'delete') {
|
||||
Modal.warning({
|
||||
title: '提示',
|
||||
|
||||
@@ -53,46 +53,47 @@
|
||||
<a-space>
|
||||
<a-link v-permission="['system:user:get']" title="详情" @click="onDetail(record)">详情</a-link>
|
||||
<a-link v-permission="['system:user:update']" title="修改" @click="onUpdate(record)">修改</a-link>
|
||||
<a-link
|
||||
v-permission="['system:user:delete']"
|
||||
status="danger"
|
||||
:disabled="record.isSystem"
|
||||
:title="record.isSystem ? '系统内置数据不能删除' : '删除'"
|
||||
@click="onDelete(record)"
|
||||
>
|
||||
删除
|
||||
</a-link>
|
||||
<a-dropdown>
|
||||
<a-button v-if="has.hasPermOr(['system:user:resetPwd', 'system:user:updateRole'])" type="text" size="mini" title="更多">
|
||||
<a-button v-if="has.hasPermOr(['system:user:resetPwd', 'system:user:updateRole', 'system:user:delete'])" type="text" size="mini" title="更多">
|
||||
<template #icon>
|
||||
<icon-more :size="16" />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption v-permission="['system:user:resetPwd']" title="重置密码" @click="onResetPwd(record)">重置密码</a-doption>
|
||||
<a-doption v-permission="['system:user:updateRole']" title="分配角色" @click="onUpdateRole(record)">分配角色</a-doption>
|
||||
<a-doption v-permission="['system:user:updateRole']" :disabled="record.isSystem" title="分配角色" @click="onUpdateRole(record)">分配角色</a-doption>
|
||||
<a-doption v-permission="['system:user:delete']">
|
||||
<a-link
|
||||
status="danger"
|
||||
:disabled="record.isSystem"
|
||||
:title="record.isSystem ? '系统内置数据不能删除' : '删除'"
|
||||
@click="onDelete(record)"
|
||||
>
|
||||
删除
|
||||
</a-link>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<UserAddDrawer ref="UserAddDrawerRef" @save-success="search" />
|
||||
<UserImportDrawer ref="UserImportDrawerRef" @save-success="search" />
|
||||
<UserDetailDrawer ref="UserDetailDrawerRef" />
|
||||
<UserResetPwdModal ref="UserResetPwdModalRef" />
|
||||
<UserUpdateRoleModal ref="UserUpdateRoleModalRef" @save-success="search" />
|
||||
<AddDrawer ref="AddDrawerRef" @save-success="search" />
|
||||
<ImportDrawer ref="ImportDrawerRef" @save-success="search" />
|
||||
<DetailDrawer ref="DetailDrawerRef" />
|
||||
<PwdResetModal ref="PwdResetModalRef" />
|
||||
<RoleUpdateModal ref="RoleUpdateModalRef" @save-success="search" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import DeptTree from './dept/index.vue'
|
||||
import UserAddDrawer from './UserAddDrawer.vue'
|
||||
import UserImportDrawer from './UserImportDrawer.vue'
|
||||
import UserDetailDrawer from './UserDetailDrawer.vue'
|
||||
import UserResetPwdModal from './UserResetPwdModal.vue'
|
||||
import UserUpdateRoleModal from './UserUpdateRoleModal.vue'
|
||||
import AddDrawer from './AddDrawer.vue'
|
||||
import ImportDrawer from './ImportDrawer.vue'
|
||||
import DetailDrawer from './DetailDrawer.vue'
|
||||
import PwdResetModal from './PwdResetModal.vue'
|
||||
import RoleUpdateModal from './RoleUpdateModal.vue'
|
||||
import { type UserResp, deleteUser, exportUser, listUser } from '@/apis/system/user'
|
||||
import { DisEnableStatusList } from '@/constant/common'
|
||||
import { useDownload, useResetReactive, useTable } from '@/hooks'
|
||||
@@ -174,15 +175,15 @@ const columns: TableInstance['columns'] = [
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
slotName: 'action',
|
||||
width: 190,
|
||||
width: 160,
|
||||
align: 'center',
|
||||
fixed: !isMobile() ? 'right' : undefined,
|
||||
show: has.hasPermOr([
|
||||
'system:user:get',
|
||||
'system:user:update',
|
||||
'system:user:delete',
|
||||
'system:user:resetPwd',
|
||||
'system:user:updateRole',
|
||||
'system:user:delete',
|
||||
]),
|
||||
},
|
||||
]
|
||||
@@ -212,39 +213,39 @@ const handleSelectDept = (keys: Array<any>) => {
|
||||
search()
|
||||
}
|
||||
|
||||
const UserImportDrawerRef = ref<InstanceType<typeof UserImportDrawer>>()
|
||||
const ImportDrawerRef = ref<InstanceType<typeof ImportDrawer>>()
|
||||
// 导入
|
||||
const onImport = () => {
|
||||
UserImportDrawerRef.value?.onOpen()
|
||||
ImportDrawerRef.value?.onOpen()
|
||||
}
|
||||
|
||||
const UserAddDrawerRef = ref<InstanceType<typeof UserAddDrawer>>()
|
||||
const AddDrawerRef = ref<InstanceType<typeof AddDrawer>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
UserAddDrawerRef.value?.onAdd()
|
||||
AddDrawerRef.value?.onAdd()
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: UserResp) => {
|
||||
UserAddDrawerRef.value?.onUpdate(record.id)
|
||||
AddDrawerRef.value?.onUpdate(record.id)
|
||||
}
|
||||
|
||||
const UserDetailDrawerRef = ref<InstanceType<typeof UserDetailDrawer>>()
|
||||
const DetailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
// 详情
|
||||
const onDetail = (record: UserResp) => {
|
||||
UserDetailDrawerRef.value?.onOpen(record.id)
|
||||
DetailDrawerRef.value?.onOpen(record.id)
|
||||
}
|
||||
|
||||
const UserResetPwdModalRef = ref<InstanceType<typeof UserResetPwdModal>>()
|
||||
const PwdResetModalRef = ref<InstanceType<typeof PwdResetModal>>()
|
||||
// 重置密码
|
||||
const onResetPwd = (record: UserResp) => {
|
||||
UserResetPwdModalRef.value?.onOpen(record.id)
|
||||
PwdResetModalRef.value?.onOpen(record.id)
|
||||
}
|
||||
|
||||
const UserUpdateRoleModalRef = ref<InstanceType<typeof UserUpdateRoleModal>>()
|
||||
const RoleUpdateModalRef = ref<InstanceType<typeof RoleUpdateModal>>()
|
||||
// 分配角色
|
||||
const onUpdateRole = (record: UserResp) => {
|
||||
UserUpdateRoleModalRef.value?.onOpen(record.id)
|
||||
RoleUpdateModalRef.value?.onOpen(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
>
|
||||
<a-form ref="formRef" :model="form" :rules="rules" auto-label-width size="large">
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model.trim="form.name" placeholder="请输入名称" />
|
||||
<a-input v-model="form.name" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" field="sort">
|
||||
<a-input-number v-model="form.sort" placeholder="请输入排序" :min="1" mode="button" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model.trim="form.description"
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
</template>
|
||||
</a-upload>
|
||||
<div class="name">
|
||||
<span style="margin-right: 10px">{{ userInfo.nickname }}</span>
|
||||
<span style="margin-right: 10px">
|
||||
{{ userInfo.nickname }}
|
||||
<icon-man v-if="userInfo.gender === 1" style="color: #19bbf1" />
|
||||
<icon-woman v-else-if="userInfo.gender === 2" style="color: #fa7fa9" />
|
||||
</span>
|
||||
<icon-edit :size="16" class="btn" @click="onUpdate" />
|
||||
</div>
|
||||
<div class="id">
|
||||
@@ -30,8 +34,6 @@
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-user /><span style="margin-left: 5px">用户名</span></template>
|
||||
{{ userInfo.username }}
|
||||
<icon-man v-if="userInfo.gender === 1" style="color: #19bbf1" />
|
||||
<icon-woman v-else-if="userInfo.gender === 2" style="color: #fa7fa9" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-phone /><span style="margin-left: 5px">手机</span></template>
|
||||
@@ -47,7 +49,7 @@
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-user-group /><span style="margin-left: 5px">角色</span></template>
|
||||
{{ userInfo.roles.join(',') }}
|
||||
{{ userInfo.roleNames.join(' · ') }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user