27 Commits

Author SHA1 Message Date
68757d25a7 release: v3.2.0 2024-08-05 20:46:13 +08:00
5fdfada11d chore: 路由切换时检测前端版本更新(原为定时器检测) 2024-08-04 23:25:28 +08:00
ff405d12ab fix: 修复任务日志日期查询部分错误 2024-08-04 22:46:25 +08:00
f54caed4da fix: 优化子路由设置 2024-08-04 14:31:29 +08:00
秋帆
0a596f3fdc fix: 日志在小屏显示问题 2024-08-03 22:25:13 +08:00
秋帆
edadea91ed chore: 修复日志滚动问题 2024-08-03 20:38:30 +08:00
秋帆
d7fc693650 chore: 优化任务日志详情 2024-08-03 20:09:06 +08:00
秋帆
271865b5da Merge branch 'dev' of https://gitee.com/continew/continew-admin-ui into dev 2024-08-03 17:25:02 +08:00
秋帆
90693cb25d feat: 新增单页面通知公告编辑与查看 2024-08-03 17:24:54 +08:00
ee6a6e437d revert: 回退用户管理部门树组件(此树查询不应该校验功能权限) 2024-08-01 22:52:45 +08:00
KAI
99fa5709ee feat: 新增支持 KKFileView 文件预览功能,需要自行部署文件预览服务器 2024-08-01 01:03:02 +00:00
kils
881c1ee1e4 feat: 系统配置-基础配置 logo 及 favicon 改为 base64 存储 2024-07-31 02:21:16 +00:00
bad6e30e41 fix: 修复文件管理没有文件时控制台报错
Closes #IAFDB0
2024-07-24 22:41:22 +08:00
5c09011077 ci: 修复 Action Copy 错误 2024-07-24 21:10:44 +08:00
50aa79125a docs: 更新项目源码链接 2024-07-24 21:10:32 +08:00
b15437537b style: 移除滚动条样式 2024-07-22 21:05:17 +08:00
805ae65556 chore: 优化部分命名 2024-07-19 00:03:46 +08:00
ed085c92bd chore: 优化部分代码格式 2024-07-18 22:55:53 +08:00
段新月
ce297c0904 fix: 修复表格固定操作列滚动时的错位样式问题 2024-07-18 10:13:41 +00:00
KAI
e8c1d4b69b feat: 新增任务调度模块
SnailJob(灵活,可靠和快速的分布式任务重试和分布式任务调度平台)
2024-07-18 07:22:09 +00:00
137f3a9684 !12 解决代码生成页面丢失目录层级问题
Merge pull request !12 from carrypan/dev
2024-07-09 11:16:36 +00:00
carrypan
fe086830dd fix: 解决代码生成页面丢失目录层级问题 2024-07-09 18:25:40 +08:00
327c28d830 fix: 仅生产环境检查版本更新 2024-07-03 23:18:47 +08:00
d959e77bf7 Merge branch 'dev' of https://github.com/continew-org/continew-admin-ui into dev 2024-06-19 17:07:47 +08:00
kils
f72b4b8d56 feat: 新增用户批量导入功能 (#23) 2024-06-19 16:51:51 +08:00
秋帆
3364cb1858 fix: 修复通知公告重叠问题 2024-06-19 10:21:51 +08:00
92e773e621 fix: 修复部分路由错误 2024-06-17 21:16:49 +08:00
69 changed files with 3270 additions and 19985 deletions

View File

@@ -1,10 +1,11 @@
# 环境变量 (命名必须以 VITE_ 开头)
# 接口前缀
VITE_API_PREFIX = '/api'
# 接口地址
VITE_API_BASE_URL = 'http://localhost:8000'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:8000'
# 地址前缀
@@ -12,3 +13,9 @@ VITE_BASE = '/'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false
# 是否开启KKFileView
FILE_OPEN_PREVIEW = true
# KKFileView服务器地址
FILE_VIEW_SERVER_URL = 'http://192.168.122.209:8012'

View File

@@ -8,4 +8,10 @@ VITE_API_BASE_URL = 'https://api.continew.top'
VITE_API_WS_URL = 'wss://api.continew.top'
# 地址前缀
VITE_BASE = '/'
VITE_BASE = '/'
# 是否开启KKFileView
FILE_OPEN_PREVIEW = false
# KKFileView服务器地址
FILE_VIEW_SERVER_URL = 'http://localhost:8012'

View File

@@ -14,3 +14,9 @@ VITE_BASE = '/test'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = true
# 是否开启KKFileView
FILE_OPEN_PREVIEW = false
# KKFileView服务器地址
FILE_VIEW_SERVER_URL = 'http://localhost:8012'

View File

@@ -34,14 +34,15 @@ jobs:
run: pnpm build
# 6、拷贝到服务器
- name: Copy
uses: garygrossgarten/github-action-scp@release
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USERNAME }}
password: ${{ secrets.SERVER_PASSWORD }}
local: ./dist
remote: /docker/continew-admin/tmp
source: ./dist/*
target: /docker/continew-admin/tmp
strip_components: 1
# 7、重启 Nginx
- name: Restart
uses: appleboy/ssh-action@master
@@ -53,4 +54,4 @@ jobs:
script: |
rm -rf /docker/continew-admin/html/*
mv /docker/continew-admin/tmp/* /docker/continew-admin/html
docker restart nginx
docker restart nginx

BIN
.idea/icon.png generated

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,93 +1,120 @@
## [v3.1.0](https://github.com/Charles7c/continew-admin-ui/compare/v3.0.1...v3.1.0) (2024-06-16)
## [v3.2.0](https://github.com/continew-org/continew-admin-ui/compare/v3.1.0...v3.2.0) (2024-08-05)
### ✨ 新特性
* 系统配置新增安全设置功能 ([395a564](https://github.com/Charles7c/continew-admin-ui/commit/395a5642afbe3bac8b6b3f161949264a874033ba))
* useTable 支持 “无分页” 列表 ([1421412](https://github.com/Charles7c/continew-admin-ui/commit/1421412d678c926868b06ae8adeba292f390d3b1))
* 图片文件支持缩略图 (GitHub#17) ([c82dc90](https://github.com/Charles7c/continew-admin-ui/commit/c82dc9083bf7dbb9cccdd7c4daff6fe743eb9a0c))
* 在线用户增加最后活跃时间显示 ([fff4de5](https://github.com/Charles7c/continew-admin-ui/commit/fff4de56f30d3e3f777bd45b2f77be61bba3a555)) ([4eef0db](https://github.com/Charles7c/continew-admin-ui/commit/4eef0db9f93cb73e10113c8f69ad547f502db621))
* 新增行为验证码,行为验证码重新上线 (Gitee#7) ([778b3c6](https://github.com/Charles7c/continew-admin-ui/commit/778b3c677fee14071d49355980936b52d16a7313))
* 新增消息中心 ([fdd4b9a](https://github.com/Charles7c/continew-admin-ui/commit/fdd4b9a4dfcb600e8455c5c402fc6f818b6f1507))
* 新增 WebSocket 消息通知 (GitHub#20) ([adc6f64](https://github.com/Charles7c/continew-admin-ui/commit/adc6f643b3ba481313b3f23e876eb4836d8753b4)) ([56b1fdd](https://github.com/Charles7c/continew-admin-ui/commit/56b1fdd75521b08334b25e2d03f7cbcfe2014360)) ([c104ba5](https://github.com/Charles7c/continew-admin-ui/commit/c104ba5445f1c990b08ec5fd3a8cf1d783d65c76))
* 新增邮件配置 (Gitee#8) ([1ebfd11](https://github.com/Charles7c/continew-admin-ui/commit/1ebfd115eb4f488a7a9464415ce061f9ad36eca0)) ([45cbabf](https://github.com/Charles7c/continew-admin-ui/commit/45cbabf54503210305f7e74382fa7c4d702c359c)) ([66f89b4](https://github.com/Charles7c/continew-admin-ui/commit/66f89b44d897b7e6874b9882e8708cadf5ab60aa)) ([6e520a3](https://github.com/Charles7c/continew-admin-ui/commit/6e520a30720c418b7484f37c1736f189613e83ce))
* 文件管理增加复制文件 URL 按钮 ([5c6d311](https://github.com/Charles7c/continew-admin-ui/commit/5c6d3119eb4aab0f679aaeadcead7f96f6f1ea22))
* 新增用户批量导入功能 (GitHub#23) ([f72b4b8](https://github.com/continew-org/continew-admin-ui/commit/f72b4b8d563acd6d2829018be0d079a835911f18))
* 新增任务调度模块 SnailJob灵活可靠和快速的分布式任务重试和分布式任务调度平台 (Gitee#13) ([e8c1d4b](https://github.com/continew-org/continew-admin-ui/commit/e8c1d4b69b10a53f4adfaf8a2fd4b8280de965c7)) ([d7fc693](https://github.com/continew-org/continew-admin-ui/commit/d7fc693650259d8ad50aaf69504b991343f4694b)) ([edadea9](https://github.com/continew-org/continew-admin-ui/commit/edadea91edc74a7f95b67e7401aa7efb439f6ffd)) ([0a596f3](https://github.com/continew-org/continew-admin-ui/commit/0a596f3fdccc9fcecdb3d550889cb006450b30a3)) ([ff405d1](https://github.com/continew-org/continew-admin-ui/commit/ff405d12ab441f64c29b986fccb91caf727a5811))
* 系统配置-基础配置 logo 及 favicon 改为 base64 存储 (Gitee#16) ([881c1ee](https://github.com/continew-org/continew-admin-ui/commit/881c1ee1e41805d5728648b1a72e50480199216b))
* 新增支持 KKFileView 文件预览功能,需要自行部署文件预览服务器 (Gitee#17) ([99fa570](https://github.com/continew-org/continew-admin-ui/commit/99fa5709ee03a6f368c8297a7306c02872adfcb2))
* 新增单页面通知公告编辑与查看 ([90693cb](https://github.com/continew-org/continew-admin-ui/commit/90693cb25d061af9d15b4579cf82db80a38cfc40))
### 💎 功能优化
- 优化部分代码格式 ([ed085c9](https://github.com/continew-org/continew-admin-ui/commit/ed085c92bdbf61bae6334ceaee4a3b7e5c605065))
- 优化部分命名 ([805ae65](https://github.com/continew-org/continew-admin-ui/commit/805ae65556e7969cd0a1ac0ddee24b9a2c0be0ff))
- 移除滚动条样式 ([b154375](https://github.com/continew-org/continew-admin-ui/commit/b15437537b0b8948e6ede22830852cd3eb778e84))
- 回退用户管理部门树组件(此树查询不应该校验功能权限) ([ee6a6e4](https://github.com/continew-org/continew-admin-ui/commit/ee6a6e437d8f0806137ab49252c8d6f34337d3cd))
- 优化子路由设置 ([f54caed](https://github.com/continew-org/continew-admin-ui/commit/f54caed4da38dc329c52e3e07419fca31f56bee7))
- 路由切换时检测前端版本更新(原为定时器检测) ([5fdfada](https://github.com/continew-org/continew-admin-ui/commit/5fdfada11d6813ae2728797e0c5ef81387c39c6d))
### 🐛 问题修复
- 修复部分路由错误 ([92e773e](https://github.com/continew-org/continew-admin-ui/commit/92e773e621657946aab3a9149208139d98cac996))
- 修复通知公告重叠问题 ([3364cb1](https://github.com/continew-org/continew-admin-ui/commit/3364cb185855541246a93f8663efe197597df170))
- 解决代码生成页面丢失目录层级问题 (Gitee#12) ([fe08683](https://github.com/continew-org/continew-admin-ui/commit/fe086830dd6a50a0bbf7d1d59563b85a3bfa401c))
- 修复表格固定操作列滚动时的错位样式问题 (Gitee#14) ([ce297c0](https://github.com/continew-org/continew-admin-ui/commit/ce297c0904f00ef6f93a9772b149f817a91a3f2a))
- 修复文件管理没有文件时控制台报错 ([bad6e30](https://github.com/continew-org/continew-admin-ui/commit/bad6e30e4133507cd6e44de9f525c25d3ebc1adb))
## [v3.1.0](https://github.com/continew-org/continew-admin-ui/compare/v3.0.1...v3.1.0) (2024-06-16)
### ✨ 新特性
* 系统配置新增安全设置功能 ([395a564](https://github.com/continew-org/continew-admin-ui/commit/395a5642afbe3bac8b6b3f161949264a874033ba))
* useTable 支持 “无分页” 列表 ([1421412](https://github.com/continew-org/continew-admin-ui/commit/1421412d678c926868b06ae8adeba292f390d3b1))
* 图片文件支持缩略图 (GitHub#17) ([c82dc90](https://github.com/continew-org/continew-admin-ui/commit/c82dc9083bf7dbb9cccdd7c4daff6fe743eb9a0c))
* 在线用户增加最后活跃时间显示 ([fff4de5](https://github.com/continew-org/continew-admin-ui/commit/fff4de56f30d3e3f777bd45b2f77be61bba3a555)) ([4eef0db](https://github.com/continew-org/continew-admin-ui/commit/4eef0db9f93cb73e10113c8f69ad547f502db621))
* 新增行为验证码,行为验证码重新上线 (Gitee#7) ([778b3c6](https://github.com/continew-org/continew-admin-ui/commit/778b3c677fee14071d49355980936b52d16a7313))
* 新增消息中心 ([fdd4b9a](https://github.com/continew-org/continew-admin-ui/commit/fdd4b9a4dfcb600e8455c5c402fc6f818b6f1507))
* 新增 WebSocket 消息通知 (GitHub#20) ([adc6f64](https://github.com/continew-org/continew-admin-ui/commit/adc6f643b3ba481313b3f23e876eb4836d8753b4)) ([56b1fdd](https://github.com/continew-org/continew-admin-ui/commit/56b1fdd75521b08334b25e2d03f7cbcfe2014360)) ([c104ba5](https://github.com/continew-org/continew-admin-ui/commit/c104ba5445f1c990b08ec5fd3a8cf1d783d65c76))
* 新增邮件配置 (Gitee#8) ([1ebfd11](https://github.com/continew-org/continew-admin-ui/commit/1ebfd115eb4f488a7a9464415ce061f9ad36eca0)) ([45cbabf](https://github.com/continew-org/continew-admin-ui/commit/45cbabf54503210305f7e74382fa7c4d702c359c)) ([66f89b4](https://github.com/continew-org/continew-admin-ui/commit/66f89b44d897b7e6874b9882e8708cadf5ab60aa)) ([6e520a3](https://github.com/continew-org/continew-admin-ui/commit/6e520a30720c418b7484f37c1736f189613e83ce))
* 文件管理增加复制文件 URL 按钮 ([5c6d311](https://github.com/continew-org/continew-admin-ui/commit/5c6d3119eb4aab0f679aaeadcead7f96f6f1ea22))
* 新增版权条显示配置 ([0f3d927](https://gitee.com/continew/continew-admin-ui/commit/0f3d927f9894e296e5dde83feb1738206c44b5b1)) ([d7e29e2](https://gitee.com/continew/continew-admin-ui/commit/d7e29e238ee31301807275be1147824295995650))
* 新增密码过期修改页面逻辑 ([921d9c6](https://github.com/charles7c/continew-admin-ui/commit/921d9c63e955711473e1c911f59da4711cdc1197))
* 新增前端简略版本更新提示 ([03d05e1](https://github.com/charles7c/continew-admin-ui/commit/03d05e1821a0360afa724d86ce34a51aedb9c07e))
* 新增密码过期修改页面逻辑 ([921d9c6](https://github.com/continew-org/continew-admin-ui/commit/921d9c63e955711473e1c911f59da4711cdc1197))
* 新增前端简略版本更新提示 ([03d05e1](https://github.com/continew-org/continew-admin-ui/commit/03d05e1821a0360afa724d86ce34a51aedb9c07e))
### 💎 功能优化
- 路由多级缓存调整为扁平化方案 ([5f3dd93](https://github.com/Charles7c/continew-admin-ui/commit/5f3dd93376ed62c803d6e26965f43812c86abee8))
- 优化 ESLint 配置并更正问题代码eslint src --fix ([5d9fedc](https://github.com/Charles7c/continew-admin-ui/commit/5d9fedc35406e00d88d8921ffe04b99a7c49cb8e))
- 代码生成预览调整为以文件树结构形式显示 (Gitee#5) ([c9198b3](https://github.com/Charles7c/continew-admin-ui/commit/c9198b315c25cb3e8fd7f769b543e98e131f878c))
- 优化公告和字典项响应式窗口效果 ([4c2f36f](https://github.com/Charles7c/continew-admin-ui/commit/4c2f36fe6b5254a613cabd686501e891cd8c7d1c))
- 优化个人中心部分代码 ([eb11cae](https://github.com/Charles7c/continew-admin-ui/commit/eb11cae635ff4a0661603509cec4e85a462f5a63))
- 更换 ESLint 配置为 @antfu/eslint-config ([bfc8e42](https://github.com/Charles7c/continew-admin-ui/commit/bfc8e42bad6777243fdca9bf37b0d95a92c75159))
- 代码生成预览样式调整 (Gitee#6) ([fe656af](https://github.com/Charles7c/continew-admin-ui/commit/fe656af1aa1afbc290cf6a6121347106adf5df60)) ([cc0840e](https://github.com/Charles7c/continew-admin-ui/commit/cc0840e2ae7f25f25432c6a01781ac1a8112eea7))
- 启动项目时,控制台增加应用信息打印 ([52e7682](https://github.com/Charles7c/continew-admin-ui/commit/52e7682a4786ae6e3fae49dcbd8ee473f30d2cb5))
- 优化部分弹框响应式效果 ([c1c5f7f](https://github.com/Charles7c/continew-admin-ui/commit/c1c5f7f632827286982fdc0b9235cb115298e541))
- 优化文件管理部分显示效果 ([7a2c66e](https://github.com/Charles7c/continew-admin-ui/commit/7a2c66e6463eb50d8c7bee0dcd21c396fe642ceb))
- 优化重置路由实现 ([7c1106e](https://github.com/Charles7c/continew-admin-ui/commit/7c1106e8c26d3dc3c2ecee93f9f98bc56a171720))
- 优化 copy 组件使用 ([c369b88](https://github.com/Charles7c/continew-admin-ui/commit/c369b88185c85bb7782383617fd6debf1f6c16d9)) ([a8b5d97](https://github.com/Charles7c/continew-admin-ui/commit/a8b5d97bfa0ed256205284deb7364bf50e18927a))
- 优化用户角色名称展示 ([d4b9057](https://github.com/Charles7c/continew-admin-ui/commit/d4b9057554f7bbe58d45429529d7279182100616))
- 优化删除弹窗按钮样式 ([c2806c4](https://github.com/Charles7c/continew-admin-ui/commit/c2806c469695adbe3ef1950957a33d48059c6cb6))
- 优化表格页面样式及表格纵向滚动条 ([861f620](https://github.com/Charles7c/continew-admin-ui/commit/861f6203cc0ebcdd7087434c9d8bafccf4360812))
- 重构字典管理(左树右表) ([a175120](https://github.com/Charles7c/continew-admin-ui/commit/a175120d699f5da099e7f027a6c5f0fba26705d8)) ([aac5899](https://github.com/Charles7c/continew-admin-ui/commit/aac5899fe0a01fe0e91ffc1904c94ac9ecaa4885))
- 重构用户管理部门树,支持部门管理 ([ca05fab](https://github.com/Charles7c/continew-admin-ui/commit/ca05fabdec277965057f7901317edefca74cb258)) ([1be08f1](https://github.com/Charles7c/continew-admin-ui/commit/1be08f10010654dc675d67b792f1fc870df5961e)) ([f8ded4b](https://github.com/Charles7c/continew-admin-ui/commit/f8ded4b491a22369d43ff3e76f75c771130c4f1c))
- 优化表格列表显示 ([ed7be3e](https://github.com/Charles7c/continew-admin-ui/commit/ed7be3ef25a91d66bcd33bae6176ecb81c85ec44))
- 优化文件管理分页 ([00da9ac](https://github.com/Charles7c/continew-admin-ui/commit/00da9acdd09e4f2f8233ec22ae37408f9a027674))
- 优化系统配置加载与切换问题 ([605ac4d](https://github.com/Charles7c/continew-admin-ui/commit/605ac4d0865e2b7471583f3e0b5a14993bf25104))
- 优化全局 loading 及 empty 配置 ([7e329fc](https://github.com/Charles7c/continew-admin-ui/commit/7e329fcffacc58cb626b3b7a71a53d8decc170f7))
- 适配系统参数 API 新的使用方式 ([1909b6e](https://github.com/Charles7c/continew-admin-ui/commit/1909b6e907f8d8dd00d8e59eff8c2125914cad3f))
- 存储管理S3存储配置填充默认域名 (GitHub#21) ([5897bde](https://github.com/Charles7c/continew-admin-ui/commit/5897bde0c45dd61a94ac9bcf85b55f12a7fe5133))
- 优化个人中心部分默认显示效果 ([f2206b7](https://github.com/Charles7c/continew-admin-ui/commit/f2206b78012d594010bc6cee47a95a9ebab1ad1b))
- 调整对话框默认可拖拽,表格默认可调整列宽 ([5581d3f](https://github.com/Charles7c/continew-admin-ui/commit/5581d3fd8910997d61ca6e281cec50caef264ca3))
- 目录下仅有一个菜单时平铺展示 ([dc4ae0f](https://github.com/Charles7c/continew-admin-ui/commit/dc4ae0fb34a940030f4fc1841ede3557ccccb58c))
- 路由多级缓存调整为扁平化方案 ([5f3dd93](https://github.com/continew-org/continew-admin-ui/commit/5f3dd93376ed62c803d6e26965f43812c86abee8))
- 优化 ESLint 配置并更正问题代码eslint src --fix ([5d9fedc](https://github.com/continew-org/continew-admin-ui/commit/5d9fedc35406e00d88d8921ffe04b99a7c49cb8e))
- 代码生成预览调整为以文件树结构形式显示 (Gitee#5) ([c9198b3](https://github.com/continew-org/continew-admin-ui/commit/c9198b315c25cb3e8fd7f769b543e98e131f878c))
- 优化公告和字典项响应式窗口效果 ([4c2f36f](https://github.com/continew-org/continew-admin-ui/commit/4c2f36fe6b5254a613cabd686501e891cd8c7d1c))
- 优化个人中心部分代码 ([eb11cae](https://github.com/continew-org/continew-admin-ui/commit/eb11cae635ff4a0661603509cec4e85a462f5a63))
- 更换 ESLint 配置为 @antfu/eslint-config ([bfc8e42](https://github.com/continew-org/continew-admin-ui/commit/bfc8e42bad6777243fdca9bf37b0d95a92c75159))
- 代码生成预览样式调整 (Gitee#6) ([fe656af](https://github.com/continew-org/continew-admin-ui/commit/fe656af1aa1afbc290cf6a6121347106adf5df60)) ([cc0840e](https://github.com/continew-org/continew-admin-ui/commit/cc0840e2ae7f25f25432c6a01781ac1a8112eea7))
- 启动项目时,控制台增加应用信息打印 ([52e7682](https://github.com/continew-org/continew-admin-ui/commit/52e7682a4786ae6e3fae49dcbd8ee473f30d2cb5))
- 优化部分弹框响应式效果 ([c1c5f7f](https://github.com/continew-org/continew-admin-ui/commit/c1c5f7f632827286982fdc0b9235cb115298e541))
- 优化文件管理部分显示效果 ([7a2c66e](https://github.com/continew-org/continew-admin-ui/commit/7a2c66e6463eb50d8c7bee0dcd21c396fe642ceb))
- 优化重置路由实现 ([7c1106e](https://github.com/continew-org/continew-admin-ui/commit/7c1106e8c26d3dc3c2ecee93f9f98bc56a171720))
- 优化 copy 组件使用 ([c369b88](https://github.com/continew-org/continew-admin-ui/commit/c369b88185c85bb7782383617fd6debf1f6c16d9)) ([a8b5d97](https://github.com/continew-org/continew-admin-ui/commit/a8b5d97bfa0ed256205284deb7364bf50e18927a))
- 优化用户角色名称展示 ([d4b9057](https://github.com/continew-org/continew-admin-ui/commit/d4b9057554f7bbe58d45429529d7279182100616))
- 优化删除弹窗按钮样式 ([c2806c4](https://github.com/continew-org/continew-admin-ui/commit/c2806c469695adbe3ef1950957a33d48059c6cb6))
- 优化表格页面样式及表格纵向滚动条 ([861f620](https://github.com/continew-org/continew-admin-ui/commit/861f6203cc0ebcdd7087434c9d8bafccf4360812))
- 重构字典管理(左树右表) ([a175120](https://github.com/continew-org/continew-admin-ui/commit/a175120d699f5da099e7f027a6c5f0fba26705d8)) ([aac5899](https://github.com/continew-org/continew-admin-ui/commit/aac5899fe0a01fe0e91ffc1904c94ac9ecaa4885))
- 重构用户管理部门树,支持部门管理 ([ca05fab](https://github.com/continew-org/continew-admin-ui/commit/ca05fabdec277965057f7901317edefca74cb258)) ([1be08f1](https://github.com/continew-org/continew-admin-ui/commit/1be08f10010654dc675d67b792f1fc870df5961e)) ([f8ded4b](https://github.com/continew-org/continew-admin-ui/commit/f8ded4b491a22369d43ff3e76f75c771130c4f1c))
- 优化表格列表显示 ([ed7be3e](https://github.com/continew-org/continew-admin-ui/commit/ed7be3ef25a91d66bcd33bae6176ecb81c85ec44))
- 优化文件管理分页 ([00da9ac](https://github.com/continew-org/continew-admin-ui/commit/00da9acdd09e4f2f8233ec22ae37408f9a027674))
- 优化系统配置加载与切换问题 ([605ac4d](https://github.com/continew-org/continew-admin-ui/commit/605ac4d0865e2b7471583f3e0b5a14993bf25104))
- 优化全局 loading 及 empty 配置 ([7e329fc](https://github.com/continew-org/continew-admin-ui/commit/7e329fcffacc58cb626b3b7a71a53d8decc170f7))
- 适配系统参数 API 新的使用方式 ([1909b6e](https://github.com/continew-org/continew-admin-ui/commit/1909b6e907f8d8dd00d8e59eff8c2125914cad3f))
- 存储管理S3存储配置填充默认域名 (GitHub#21) ([5897bde](https://github.com/continew-org/continew-admin-ui/commit/5897bde0c45dd61a94ac9bcf85b55f12a7fe5133))
- 优化个人中心部分默认显示效果 ([f2206b7](https://github.com/continew-org/continew-admin-ui/commit/f2206b78012d594010bc6cee47a95a9ebab1ad1b))
- 调整对话框默认可拖拽,表格默认可调整列宽 ([5581d3f](https://github.com/continew-org/continew-admin-ui/commit/5581d3fd8910997d61ca6e281cec50caef264ca3))
- 目录下仅有一个菜单时平铺展示 ([dc4ae0f](https://github.com/continew-org/continew-admin-ui/commit/dc4ae0fb34a940030f4fc1841ede3557ccccb58c))
### 🐛 问题修复
- 修复用户列表禁用列错误 ([1e5a50c](https://github.com/Charles7c/continew-admin-ui/commit/1e5a50c37bc8dbc18d917e523b0a215a510f57db))
- 修复菜单管理列表滚动问题 ([5101dd1](https://github.com/Charles7c/continew-admin-ui/commit/5101dd12d9769d8927afb40619fb68d696c22a82))
- 修复打包部署后或 preview 模式下,布局切换及页签显示异常 ([68d8f0f](https://github.com/Charles7c/continew-admin-ui/commit/68d8f0f5b36be162a0c64d500d845388c239c4a7))
- 修复文件管理图表加载错误 ([d1af509](https://github.com/Charles7c/continew-admin-ui/commit/d1af509a1aaa7d1a6982f3dcdadb7202b71b9475))
- 字典编码不允许修改 ([0a6cd6e](https://github.com/Charles7c/continew-admin-ui/commit/0a6cd6ef989309a450a810852cbd74e35ed29b6a))
- 修复字典重复请求问题 ([6705027](https://github.com/Charles7c/continew-admin-ui/commit/6705027273e098cde57792743c3a0bdacb559449)) ([30222b0](https://github.com/Charles7c/continew-admin-ui/commit/30222b08ab6539552f3679f4cb9442f477f4df55))
- 代码生成配置表单校验错误自动跳转回错误 tab ([a015d9f](https://github.com/Charles7c/continew-admin-ui/commit/a015d9f843cb72aeb99674a271914044a4e00794))
- 修复文件管理左侧‘全部’查询问题 ([d6c5b89](https://github.com/Charles7c/continew-admin-ui/commit/d6c5b8988c84d6d33474d51162bad12973b86c91))
- 修复导出报 400 的问题 ([377a1ff](https://github.com/Charles7c/continew-admin-ui/commit/377a1ff1b74fa357545c3298e5b9c480b0f3f0d3))
- 修复用户管理排序参数错误 ([bcbe106](https://github.com/Charles7c/continew-admin-ui/commit/bcbe10660fbfdab5a7c7b17c9353aba3adc12638))
- 修复初始值使用错误 ([fd55ad4](https://github.com/Charles7c/continew-admin-ui/commit/fd55ad422888f74ea2deda679172db0cff923c9f))
- 修复第三方登录错误 ([a775b86](https://github.com/Charles7c/continew-admin-ui/commit/a775b86e2e0973a16e6b9a1945acbd904773b876))
- 修复验证码长度限制错误 ([8702be4](https://github.com/Charles7c/continew-admin-ui/commit/8702be45ed64dde39f443c2e5570fd2474821e6b))
- 修复用户列表禁用列错误 ([1e5a50c](https://github.com/continew-org/continew-admin-ui/commit/1e5a50c37bc8dbc18d917e523b0a215a510f57db))
- 修复菜单管理列表滚动问题 ([5101dd1](https://github.com/continew-org/continew-admin-ui/commit/5101dd12d9769d8927afb40619fb68d696c22a82))
- 修复打包部署后或 preview 模式下,布局切换及页签显示异常 ([68d8f0f](https://github.com/continew-org/continew-admin-ui/commit/68d8f0f5b36be162a0c64d500d845388c239c4a7))
- 修复文件管理图表加载错误 ([d1af509](https://github.com/continew-org/continew-admin-ui/commit/d1af509a1aaa7d1a6982f3dcdadb7202b71b9475))
- 字典编码不允许修改 ([0a6cd6e](https://github.com/continew-org/continew-admin-ui/commit/0a6cd6ef989309a450a810852cbd74e35ed29b6a))
- 修复字典重复请求问题 ([6705027](https://github.com/continew-org/continew-admin-ui/commit/6705027273e098cde57792743c3a0bdacb559449)) ([30222b0](https://github.com/continew-org/continew-admin-ui/commit/30222b08ab6539552f3679f4cb9442f477f4df55))
- 代码生成配置表单校验错误自动跳转回错误 tab ([a015d9f](https://github.com/continew-org/continew-admin-ui/commit/a015d9f843cb72aeb99674a271914044a4e00794))
- 修复文件管理左侧‘全部’查询问题 ([d6c5b89](https://github.com/continew-org/continew-admin-ui/commit/d6c5b8988c84d6d33474d51162bad12973b86c91))
- 修复导出报 400 的问题 ([377a1ff](https://github.com/continew-org/continew-admin-ui/commit/377a1ff1b74fa357545c3298e5b9c480b0f3f0d3))
- 修复用户管理排序参数错误 ([bcbe106](https://github.com/continew-org/continew-admin-ui/commit/bcbe10660fbfdab5a7c7b17c9353aba3adc12638))
- 修复初始值使用错误 ([fd55ad4](https://github.com/continew-org/continew-admin-ui/commit/fd55ad422888f74ea2deda679172db0cff923c9f))
- 修复第三方登录错误 ([a775b86](https://github.com/continew-org/continew-admin-ui/commit/a775b86e2e0973a16e6b9a1945acbd904773b876))
- 修复验证码长度限制错误 ([8702be4](https://github.com/continew-org/continew-admin-ui/commit/8702be45ed64dde39f443c2e5570fd2474821e6b))
## [v3.0.1](https://github.com/Charles7c/continew-admin-ui/compare/v3.0.0...v3.0.1) (2024-05-03)
## [v3.0.1](https://github.com/continew-org/continew-admin-ui/compare/v3.0.0...v3.0.1) (2024-05-03)
### ✨ 新特性
* 新增表格全屏、尺寸工具 ([b8a84a3](https://github.com/Charles7c/continew-admin-ui/commit/b8a84a3a0890d4f6d0e39ecbe50c4f645bd0f106))
* 新增验证码超时显示效果,超时后显示已过期请刷新 (GitHub#14) ([f99c8f1](https://github.com/Charles7c/continew-admin-ui/commit/f99c8f1b5a521f987b2822352f976fb0b1dc93b3))
* 文件管理增加资源统计,统计总存储量、各类型文件存储占用 (GitHub#15) ([c70d1ad](https://github.com/Charles7c/continew-admin-ui/commit/c70d1adbf922d28853bf4e6cf8cc4e14ad5b0ac7))
* 新增表格全屏、尺寸工具 ([b8a84a3](https://github.com/continew-org/continew-admin-ui/commit/b8a84a3a0890d4f6d0e39ecbe50c4f645bd0f106))
* 新增验证码超时显示效果,超时后显示已过期请刷新 (GitHub#14) ([f99c8f1](https://github.com/continew-org/continew-admin-ui/commit/f99c8f1b5a521f987b2822352f976fb0b1dc93b3))
* 文件管理增加资源统计,统计总存储量、各类型文件存储占用 (GitHub#15) ([c70d1ad](https://github.com/continew-org/continew-admin-ui/commit/c70d1adbf922d28853bf4e6cf8cc4e14ad5b0ac7))
### 💎 功能优化
- 统一性别约束/统一上级部门为必填 ([5264cf2](https://github.com/Charles7c/continew-admin-ui/commit/5264cf226fa3acd1398d9309e6a97d4d45b64850))
- 一级部门不能修改上级部门 ([b2a1658](https://github.com/Charles7c/continew-admin-ui/commit/b2a1658e3730078cf2fbeb3032c23c0922544594))
- 优化根据选中部门查询用户的点击效果 ([ca25285](https://github.com/Charles7c/continew-admin-ui/commit/ca252852373840b000c1f65ab925d18335a71fcb)) ([99c37d7](https://github.com/Charles7c/continew-admin-ui/commit/99c37d7de4a245836f89c29cf4b638032efae31f))
- 登录页面H5 端排版更换 ([05ab89d](https://github.com/Charles7c/continew-admin-ui/commit/05ab89d03fe6401994698ad9ecdeb8540ec49553))
- 优化 queryForm 的 Query 类型使用 ([5b71369](https://github.com/Charles7c/continew-admin-ui/commit/5b713692516db586f2d401a163192c62a963137a))
- 统一性别约束/统一上级部门为必填 ([5264cf2](https://github.com/continew-org/continew-admin-ui/commit/5264cf226fa3acd1398d9309e6a97d4d45b64850))
- 一级部门不能修改上级部门 ([b2a1658](https://github.com/continew-org/continew-admin-ui/commit/b2a1658e3730078cf2fbeb3032c23c0922544594))
- 优化根据选中部门查询用户的点击效果 ([ca25285](https://github.com/continew-org/continew-admin-ui/commit/ca252852373840b000c1f65ab925d18335a71fcb)) ([99c37d7](https://github.com/continew-org/continew-admin-ui/commit/99c37d7de4a245836f89c29cf4b638032efae31f))
- 登录页面H5 端排版更换 ([05ab89d](https://github.com/continew-org/continew-admin-ui/commit/05ab89d03fe6401994698ad9ecdeb8540ec49553))
- 优化 queryForm 的 Query 类型使用 ([5b71369](https://github.com/continew-org/continew-admin-ui/commit/5b713692516db586f2d401a163192c62a963137a))
### 🐛 问题修复
- 修复 Markdown 样式加载错误,改为全局统一加载 (GitHub#9) ([64648d0](https://github.com/Charles7c/continew-admin-ui/commit/64648d0c1d897d6e426199e7924ede9dfb40e8b8))
- 修复由于文件组件名称错误导致的侧边栏筛选功能失效 ([81dbea8](https://github.com/Charles7c/continew-admin-ui/commit/81dbea879377054e3646c2d07b51c3352501bbce))
- 修复文件管理数据不刷新和批量操作选中问题 (GitHub#13) ([724f60e](https://github.com/Charles7c/continew-admin-ui/commit/724f60eaf6b076cfb165ca0b1028c461146495ad))
- 修复文件重命名时不能回显原值的问题 ([3dfa97e](https://github.com/Charles7c/continew-admin-ui/commit/3dfa97e785acb42edd3798117f7e8eea326b4b64))
- 修复修改公告时保存按钮点击无效的问题 ([c0a5c2d](https://github.com/Charles7c/continew-admin-ui/commit/c0a5c2dffe50905b8610fbd066b8eecd5a4cbe89))
- 修复账号管理、安全设置路由处理错误 ([c0c5ba8](https://github.com/Charles7c/continew-admin-ui/commit/c0c5ba8efdab009e7e38ad9a8f68a655aba28718))
- 修复首页卡片显示问题 ([39465dc](https://github.com/Charles7c/continew-admin-ui/commit/39465dcaa38c9d79c820583a1dd82978e5588dec))
- 修复 H5 下登录页面错位显示 ([9d570a8](https://github.com/Charles7c/continew-admin-ui/commit/9d570a808ce1a15a1513eac0e9ec355d683febef))
- 修复 Markdown 样式加载错误,改为全局统一加载 (GitHub#9) ([64648d0](https://github.com/continew-org/continew-admin-ui/commit/64648d0c1d897d6e426199e7924ede9dfb40e8b8))
- 修复由于文件组件名称错误导致的侧边栏筛选功能失效 ([81dbea8](https://github.com/continew-org/continew-admin-ui/commit/81dbea879377054e3646c2d07b51c3352501bbce))
- 修复文件管理数据不刷新和批量操作选中问题 (GitHub#13) ([724f60e](https://github.com/continew-org/continew-admin-ui/commit/724f60eaf6b076cfb165ca0b1028c461146495ad))
- 修复文件重命名时不能回显原值的问题 ([3dfa97e](https://github.com/continew-org/continew-admin-ui/commit/3dfa97e785acb42edd3798117f7e8eea326b4b64))
- 修复修改公告时保存按钮点击无效的问题 ([c0a5c2d](https://github.com/continew-org/continew-admin-ui/commit/c0a5c2dffe50905b8610fbd066b8eecd5a4cbe89))
- 修复账号管理、安全设置路由处理错误 ([c0c5ba8](https://github.com/continew-org/continew-admin-ui/commit/c0c5ba8efdab009e7e38ad9a8f68a655aba28718))
- 修复首页卡片显示问题 ([39465dc](https://github.com/continew-org/continew-admin-ui/commit/39465dcaa38c9d79c820583a1dd82978e5588dec))
- 修复 H5 下登录页面错位显示 ([9d570a8](https://github.com/continew-org/continew-admin-ui/commit/9d570a808ce1a15a1513eac0e9ec355d683febef))
## v3.0.0 (2024-04-27)

View File

@@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License" />
</a>
<a href="https://github.com/Charles7c/continew-admin-ui" target="_blank">
<img src="https://img.shields.io/badge/RELEASE-v3.1.0-%23ff3f59.svg" alt="Release" />
<img src="https://img.shields.io/badge/RELEASE-v3.2.0-%23ff3f59.svg" alt="Release" />
</a>
<a href="https://github.com/Charles7c/continew-admin" target="_blank">
<img src="https://img.shields.io/github/stars/Charles7c/continew-admin?style=social" alt="GitHub stars" />
@@ -43,11 +43,11 @@ ContiNew AdminContinue New Admin持续迭代优化的前后端分离中后
## 项目源码
| | Gitee码云 | GitHub |
|----------|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| 前端 | [continew/continew-admin-ui](https://gitee.com/continew/continew-admin-ui) | [charles7c/continew-admin-ui](https://github.com/Charles7c/continew-admin-ui) |
| 后端 | [continew/continew-admin](https://gitee.com/continew/continew-admin) | [charles7c/continew-admin](https://github.com/Charles7c/continew-admin) |
| 前端v2.5 | [continew/continew-admin-ui-arco](https://gitee.com/continew/continew-admin-ui-arco) | [charles7c/continew-admin-ui-arco](https://github.com/Charles7c/continew-admin-ui-arco) |
| | 前端 | 后端 |
| :------ | :----------------------------------------------------------- | :----------------------------------------------------------- |
| Gitee | [continew/continew-admin-ui](https://gitee.com/continew/continew-admin-ui) | [continew/continew-admin](https://gitee.com/continew/continew-admin) |
| GitCode | [continew/continew-admin-ui](https://gitcode.com/continew/continew-admin-ui) | [continew/continew-admin](https://gitcode.com/continew/continew-admin) |
| GitHub | [continew-org/continew-admin-ui](https://github.com/continew-org/continew-admin-ui) | [continew-org/continew-admin](https://github.com/continew-org/continew-admin) |
## 项目起源

View File

@@ -11,7 +11,7 @@ export default function appInfo(): Plugin {
// eslint-disable-next-line no-console
console.log(
boxen(
`${bold(green(`${bgGreen('ContiNew Admin v3.1.0-SNAPSHOT')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('持续迭代优化的前后端分离中后台管理系统框架。')}`,
`${bold(green(`${bgGreen('ContiNew Admin v3.2.0')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('持续迭代优化的前后端分离中后台管理系统框架。')}`,
{
padding: 1,
margin: 1,

19500
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "continew-admin-ui",
"type": "module",
"version": "3.1.0",
"version": "3.2.0",
"private": "true",
"scripts": {
"dev": "vite --host",
@@ -24,6 +24,7 @@
"@vueuse/core": "^10.5.0",
"@wangeditor/editor": "^5.1.1",
"@wangeditor/editor-for-vue": "^5.1.12",
"aieditor": "^1.0.13",
"animate.css": "^4.1.1",
"axios": "^0.27.2",
"codemirror": "^6.0.1",
@@ -50,7 +51,7 @@
"vue-draggable-plus": "^0.3.5",
"vue-echarts": "^6.5.5",
"vue-json-pretty": "^2.4.0",
"vue-router": "^4.2.2",
"vue-router": "^4.3.3",
"xe-utils": "^3.5.7",
"xgplayer": "^2.31.6"
},
@@ -82,7 +83,7 @@
"vue-tsc": "^2.0.6"
},
"simple-git-hooks": {
"pre-commit": "npm lint-staged"
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"

880
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ export * from './common'
export * from './monitor'
export * from './system'
export * from './tool'
export * from './schedule'
export * from './area/type'
export * from './auth/type'
@@ -11,3 +12,4 @@ export * from './common/type'
export * from './monitor/type'
export * from './system/type'
export * from './tool/type'
export * from './schedule/type'

View File

@@ -0,0 +1,2 @@
export * from '../schedule/job'
export * from '../schedule/log'

39
src/apis/schedule/job.ts Normal file
View File

@@ -0,0 +1,39 @@
import type * as Schedule from './type'
import http from '@/utils/http'
const BASE_URL = '/schedule/job'
/** @desc 查询任务组列表 */
export function listGroup() {
return http.get(`${BASE_URL}/group`)
}
/** @desc 查询任务列表 */
export function listJob(query: Schedule.JobPageQuery) {
return http.get<PageRes<Schedule.JobResp[]>>(`${BASE_URL}`, query)
}
/** @desc 新增任务 */
export function addJob(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 修改任务 */
export function updateJob(data: any, id: number) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 修改任务状态 */
export function updateJobStatus(data: any, id: number) {
return http.patch(`${BASE_URL}/${id}/status`, data)
}
/** @desc 删除任务 */
export function deleteJob(id: number) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 执行任务 */
export function triggerJob(id: number) {
return http.post(`${BASE_URL}/trigger/${id}`)
}

34
src/apis/schedule/log.ts Normal file
View File

@@ -0,0 +1,34 @@
import type * as Schedule from './type'
import http from '@/utils/http'
const BASE_URL = '/schedule/log'
/** @desc 查询任务日志列表 */
export function listJobLog(query: Schedule.JobLogPageQuery) {
return http.get<PageRes<Schedule.JobLogResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询任务日志详情 */
export function getJobLogDetail(id: number) {
return http.get<boolean>(`${BASE_URL}/${id}`)
}
/** @desc 停止任务 */
export function stopJob(id: number) {
return http.post(`${BASE_URL}/stop/${id}`)
}
/** @desc 重试任务 */
export function retryJob(id: number) {
return http.post(`${BASE_URL}/retry/${id}`)
}
/** @desc 查询任务实例列表 */
export function listJobInstance(query: Schedule.JobInstanceQuery) {
return http.get<Schedule.JobInstanceResp[]>(`${BASE_URL}/instance`, query)
}
/** @desc 查询任务实例日志列表 */
export function listJobInstanceLog(query: Schedule.JobInstanceLogQuery) {
return http.get<Schedule.JobInstanceLogResp>(`${BASE_URL}/instance/log`, query)
}

85
src/apis/schedule/type.ts Normal file
View File

@@ -0,0 +1,85 @@
/** 任务类型 */
export interface JobResp {
id: number
groupName: string
jobName: string
description?: string
triggerType: number
triggerInterval: string | number
executorType: number
taskType: number
executorInfo: string
argsStr?: string
argsType?: string
routeKey: number
blockStrategy: number
executorTimeout: number
maxRetryTimes: number
retryInterval: number
parallelNum: number
jobStatus: number
nextTriggerAt?: Date
createDt?: Date
updateDt?: Date
}
export interface JobQuery {
groupName: string
jobName?: string
jobStatus?: number
}
export interface JobPageQuery extends JobQuery, PageQuery {}
/** 任务日志类型 */
export interface JobLogResp {
id: number
groupName: string
jobName: string
jobId: number
taskBatchStatus: number
operationReason: number
executorType: number
executorInfo: string
executionAt: string
createDt: string
}
export interface JobLogQuery {
jobId?: number
groupName?: string
jobName?: string
taskBatchStatus?: number
datetimeRange?: Array<string>
}
export interface JobLogPageQuery extends JobLogQuery, PageQuery {}
/** 任务实例类型 */
export interface JobInstanceResp {
id: number
groupName: string
jobId: number
taskBatchId: number
taskStatus: number
retryCount: number
resultMessage: string
clientInfo: string
}
export interface JobInstanceQuery {
jobId?: string | number
taskBatchId?: number | string
}
/** 任务实例日志类型 */
export interface JobInstanceLogResp {
id: number
message: any[]
finished: number
fromIndex: number
nextStartId: number
}
export interface JobInstanceLogQuery {
taskBatchId: number
jobId: number
taskId: number
startId: number
fromIndex: number
size: number
}

View File

@@ -25,6 +25,15 @@ export type UserDetailResp = UserResp & {
pwdResetTime?: string
}
export interface UserImportResp {
importKey: string
totalRows: number
validRows: number
duplicateUserRows: number
duplicateEmailRows: number
duplicatePhoneRows: number
}
export interface UserQuery {
description?: string
status?: number

View File

@@ -30,10 +30,25 @@ export function deleteUser(ids: string | Array<string>) {
/** @desc 导出用户 */
export function exportUser(query: System.UserQuery) {
return http.download<any>(`${BASE_URL}/export`, query)
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 重置密码 */
export function resetUserPwd(data: any, id: string) {
return http.patch(`${BASE_URL}/${id}/password`, data)
}
/** @desc 下载用户导入模板 */
export function downloadImportUserTemplate() {
return http.download(`${BASE_URL}/downloadImportUserTemplate`)
}
/** @desc 解析用户导入数据 */
export function parseImportUser(data: FormData) {
return http.post(`${BASE_URL}/parseImportUser`, data)
}
/** @desc 导入用户 */
export function importUser(data: any) {
return http.post(`${BASE_URL}/import`, data)
}

View File

@@ -12,6 +12,7 @@ export interface TableQuery {
tableName?: string
}
export interface TablePageQuery extends PageQuery, TableQuery {}
export interface FieldConfigResp {
tableName: string
columnName: string

View File

@@ -6,6 +6,7 @@
:shortcuts="shortcuts"
shortcuts-position="left"
style="height: 32px"
:allow-clear="allowClear"
/>
</template>
@@ -27,6 +28,10 @@ defineProps({
placeholder: {
type: Array as PropType<string[]>,
default: (): string[] => ['开始时间', '结束时间']
},
allowClear: {
type: Boolean,
default: true
}
})

View File

@@ -71,7 +71,7 @@
<div class="gi-table__body" :class="`gi-table__body-pagination-${attrs['page-position']}`">
<div class="gi-table__container">
<a-table ref="tableRef" :stripe="stripe" :size="size" column-resizable :bordered="{ cell: isBordered }"
v-bind="{ ...attrs, columns: _columns }" :scrollbar="false" :pagination="false">
v-bind="{ ...attrs, columns: _columns }" :scrollbar="true" :pagination="false">
<template v-for="key in Object.keys(slots)" :key="key" #[key]="scoped">
<slot :key="key" :name="key" v-bind="scoped"></slot>
</template>

View File

@@ -2,7 +2,7 @@
<div v-if="isDesktop" class="asider" :class="{ 'app-menu-dark': appStore.menuDark }"
:style="appStore.menuDark ? appStore.themeCSSVar : undefined">
<Logo :collapsed="appStore.menuCollapse"></Logo>
<a-layout-sider class="menu" collapsible breakpoint="xl" hide-trigger width="auto"
<a-layout-sider class="menu" collapsible breakpoint="xl" hide-trigger style="width: auto;"
:collapsed="appStore.menuCollapse" @collapse="handleCollapse">
<a-scrollbar outer-class="h-full" style="height: 100%; overflow: auto">
<Menu></Menu>

View File

@@ -4,75 +4,12 @@
</template>
<script setup lang="ts">
import { Button, Notification, Space } from '@arco-design/web-vue'
import LayoutDefault from './LayoutDefault.vue'
import LayoutMix from './LayoutMix.vue'
import { useAppStore } from '@/stores'
defineOptions({ name: 'Layout' })
const appStore = useAppStore()
let versionTag: string | null = null // 版本标识
let timer: NodeJS.Timeout | undefined // 定时器
// 更新
const onUpdateSystem = (id: string) => {
Notification.remove(id)
window.location.reload()
}
// 关闭更新弹窗
const onCloseUpdateSystem = (id: string) => {
Notification.remove(id)
}
// 提示用户更新弹窗
const handleNotification = () => {
const id = `updateModel`
Notification.info({
id,
title: '新版本更新',
content: '当前系统检测到有新的版本,请及时更新',
duration: 0,
closable: true,
position: 'bottomRight',
footer: () => {
return h(Space, {}, () => [h(Button, { type: 'primary', onClick: () => onUpdateSystem(id) }, '更新'), h(Button, { type: 'secondary', onClick: () => onCloseUpdateSystem(id) }, '关闭')])
}
})
}
/**
* 获取首页的 ETag 或 Last-Modified 值,作为当前版本标识
* @returns {Promise<string|null>} 返回 ETag 或 Last-Modified 值
*/
const getVersionTag = async () => {
const response = await fetch('/', {
cache: 'no-cache'
})
return response.headers.get('etag') || response.headers.get('last-modified')
}
/**
* 比较当前的 ETag 或 Last-Modified 值与最新获取的值
*/
const compareTag = async () => {
const newVersionTag = await getVersionTag()
if (versionTag === null) {
versionTag = newVersionTag
} else if (versionTag !== newVersionTag) {
// 如果 ETag 或 Last-Modified 发生变化,则认为有更新
// 清除定时器
clearInterval(timer)
// 提示用户更新
handleNotification()
}
}
onMounted(() => {
// 每60秒检查一次是否有新的 ETag 或 Last-Modified 值
timer = setInterval(compareTag, 6000)
})
onUnmounted(() => {
// 清除定时器
clearInterval(timer)
})
</script>
<style lang="scss" scoped></style>

View File

@@ -35,7 +35,6 @@ export const constantRoutes: RouteRecordRaw[] = [
},
{
path: '/',
name: 'Home',
component: Layout,
redirect: '/home',
meta: { hidden: false },

View File

@@ -1,9 +1,61 @@
import { Message } from '@arco-design/web-vue'
import { Button, Message, Notification, Space } from '@arco-design/web-vue'
import router from '@/router'
import { useRouteStore, useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'
// 版本更新
let versionTag: string | null = null // 版本标识
// 更新
const onUpdateSystem = (id: string) => {
Notification.remove(id)
window.location.reload()
}
// 关闭更新弹窗
const onCloseUpdateSystem = (id: string) => {
Notification.remove(id)
}
// 提示用户更新弹窗
const handleNotification = () => {
const id = `updateModel`
Notification.info({
id,
title: '新版本更新',
content: '当前系统检测到有新的版本,请及时更新',
duration: 0,
closable: true,
position: 'bottomRight',
footer: () => {
return h(Space, {}, () => [h(Button, { type: 'primary', onClick: () => onUpdateSystem(id) }, '更新'), h(Button, { type: 'secondary', onClick: () => onCloseUpdateSystem(id) }, '关闭')])
}
})
}
/**
* 获取首页的 ETag 或 Last-Modified 值,作为当前版本标识
* @returns {Promise<string|null>} 返回 ETag 或 Last-Modified 值
*/
const getVersionTag = async () => {
const response = await fetch('/', {
cache: 'no-cache'
})
return response.headers.get('etag') || response.headers.get('last-modified')
}
/**
* 比较当前的 ETag 或 Last-Modified 值与最新获取的值
*/
const compareTag = async () => {
const newVersionTag = await getVersionTag()
if (versionTag === null) {
versionTag = newVersionTag
} else if (versionTag !== newVersionTag) {
// 如果 ETag 或 Last-Modified 发生变化,则认为有更新
// 提示用户更新
handleNotification()
}
}
/** 免登录白名单 */
const whiteList = ['/login', '/social/callback', '/pwdExpired']
@@ -59,4 +111,10 @@ router.beforeEach(async (to, from, next) => {
next('/login')
}
}
// 生产环境开启检测版本更新
const isProd = import.meta.env.PROD
if (isProd) {
await compareTag()
}
})

View File

@@ -45,10 +45,16 @@ const transformComponentView = (component: string) => {
const formatAsyncRoutes = (menus: RouteItem[]) => {
if (!menus.length) return []
menus.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0)) // 排序
const pathMap = new Map()
const routes = mapTree(menus, (item) => {
pathMap.set(item.id, item.path)
if (item.children && item.children.length) {
item.children.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0)) // 排序
}
// 部分子菜单,例如:通知公告新增、查看详情,需要选中其父菜单
if (item.parentId && item.type === 2 && item.permission) {
item.activeMenu = pathMap.get(item.parentId)
}
return {
path: item.path,
name: item.name ?? transformPathToName(item.path),
@@ -58,7 +64,9 @@ const formatAsyncRoutes = (menus: RouteItem[]) => {
title: item.title,
hidden: item.isHidden,
keepAlive: item.isCache,
icon: item.icon
icon: item.icon,
showInTabs: item.showInTabs,
activeMenu: item.activeMenu
}
}
})

View File

@@ -1,6 +1,5 @@
/* 全局样式 */
@import './var.scss';
// 通用外边距
.gi_margin {
margin: $margin;
@@ -228,7 +227,25 @@
max-height: 100%;
overflow: hidden;
}
.detail{
height: 100%;
display: flex;
flex-direction: column;
&_header{
background: var(--color-bg-1);
}
&_content{
position: relative;
flex: 1;
// height: 100%;
overflow: auto;
display: flex;
flex-direction: column;
padding: $padding;
margin: $margin;
background: var(--color-bg-1);
}
}
.gi_card_title {
.arco-card-header-title::before {
content: '';

View File

@@ -4,9 +4,6 @@
// 全局类名样式
@import './global.scss';
// 自定义原生滚动条样式
@import './scrollbar-reset.scss';
// 自定义 nprogress 插件进度条颜色
@import './nprogress.scss';

View File

@@ -1,22 +0,0 @@
/* 美化滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
width: 6px;
border-radius: 6px;
background-color: var(--color-neutral-3);
}
::-webkit-scrollbar-thumb:hover {
width: 6px;
background-color: var(--color-neutral-4);
}
::-ms-scrollbar-thumb {
width: 6px;
border-radius: 6px;
background-color: var(--color-neutral-3);
}

View File

@@ -8,6 +8,13 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
CrontabDay: typeof import('./../components/CornTab/CrontabDay.vue')['default']
CrontabField: typeof import('./../components/CornTab/CrontabField.vue')['default']
CrontabGenerator: typeof import('./../components/CornTab/CrontabGenerator.vue')['default']
CrontabModel: typeof import('./../components/CornTab/CrontabModel.vue')['default']
CrontabMonth: typeof import('./../components/CornTab/CrontabMonth.vue')['default']
CrontabWeek: typeof import('./../components/CornTab/CrontabWeek.vue')['default']
CrontabYear: typeof import('./../components/CornTab/CrontabYear.vue')['default']
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']
GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default']
GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default']

2
src/types/env.d.ts vendored
View File

@@ -5,6 +5,8 @@ interface ImportMetaEnv {
readonly VITE_API_PREFIX: string
readonly VITE_API_BASE_URL: string
readonly VITE_BASE: string
readonly FILE_OPEN_PREVIEW: string
readonly FILE_VIEW_SERVER_URL: string
}
interface ImportMeta {

View File

@@ -22,7 +22,6 @@ export function downloadByUrl({
url: string
target?: '_self' | '_blank'
fileName?: string
isSameHost: boolean
}): Promise<boolean> {
// 是否同源
const isSameHost = new URL(url).host === location.host

View File

@@ -261,3 +261,21 @@ export const copyText = (text: any) => {
document.body.removeChild(textarea)
Message.success('复制成功')
}
/** @desc 文件的转换base64 */
export const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
if (reader.result) {
resolve(reader.result.toString())
} else {
reject(new Error('文件转base64失败'))
}
}
reader.onerror = (error) => {
reject(error)
}
reader.readAsDataURL(file)
})
}

View File

@@ -1,7 +1,7 @@
<template>
<a-card title="公告" :bordered="false" class="gi_card_title">
<template #extra>
<a-link href="/#/system/notice">更多</a-link>
<a-link href="/system/notice">更多</a-link>
</template>
<a-empty v-if="dataList.length === 0">暂无公告</a-empty>
<a-comment

View File

@@ -3,7 +3,7 @@
<WorkCard />
<a-alert>
全新版本 v3.0.0 已发布采用全新前端模板提供更可靠更舒适的前端开发体验点击查看
全新版本 v3.2.0 已发布新增定时任务支持重构公告新增点击查看
<span class="link" @click="open('https://continew.top/admin/other/changelog.html')">更新日志</span>
</a-alert>

View File

@@ -0,0 +1,344 @@
<template>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:modal-style="{ maxWidth: '700px' }"
:body-style="{ maxHeight: width >= 700 ? '76vh' : '100vh' }"
:width="width >= 700 ? '90%' : '100%'"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" :rules="rules" size="large" auto-label-width :layout="width >= 700 ? 'horizontal' : 'vertical'">
<fieldset>
<legend>基础配置</legend>
<a-row>
<a-col v-bind="colProps">
<a-form-item label="任务组" field="groupName">
<a-select v-model="form.groupName" placeholder="请选择任务组" :options="groupList" />
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="任务名称" field="jobName">
<a-input v-model.trim="form.jobName" placeholder="请输入任务名称" :max-length="64" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="描述" field="description">
<a-textarea
v-model.trim="form.description"
placeholder="请输入描述"
show-word-limit
:max-length="200"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
</fieldset>
<fieldset>
<legend>调度配置</legend>
<a-row>
<a-col v-bind="colProps">
<a-form-item label="触发类型" field="triggerType">
<a-select
v-model="form.triggerType"
placeholder="请选择触发类型"
:options="job_trigger_type_enum"
@change="triggerTypeChange"
/>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item :label="form.triggerType === 2 ? '间隔时长' : 'CRON表达式'" field="triggerInterval">
<a-input-number
v-if="form.triggerType === 2"
v-model="triggerIntervalNumber"
placeholder="请输入间隔时长"
:min="1"
>
<template #suffix>秒</template>
</a-input-number>
<a-input
v-else
v-model="form.triggerInterval"
placeholder="请输入CRON表达式"
/>
</a-form-item>
</a-col>
</a-row>
</fieldset>
<fieldset>
<legend>任务配置</legend>
<a-row>
<a-col v-bind="colProps">
<a-form-item label="任务类型" field="taskType">
<a-select v-model="form.taskType" :options="job_task_type_enum" placeholder="请选择任务类型" />
</a-form-item>
</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-form-item>
</a-col>
</a-row>
<a-form-item label="任务参数" field="argsStr">
<a-textarea
v-if="form.taskType !== 3"
v-model.trim="form.argsStr"
placeholder="请输入任务参数"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
<div v-else class="args-container">
<div v-for="(item, index) in args" :key="index" class="args-item">
<a-form-item hide-label :rules="[{ required: true, message: '请输入分片参数' }]">
<a-input v-model="item.value" :placeholder="`请输入分片参数 ${index + 1}`" />
</a-form-item>
<a-button status="danger" class="args-delete-button" @click="onDeleteArgs(index)">
<template #icon>
<icon-delete />
</template>
</a-button>
</div>
<a-button type="outline" class="add-button" style="width: 100%;" @click="onAddArgs">
<template #icon>
<icon-plus />
</template>
</a-button>
</div>
</a-form-item>
</fieldset>
<fieldset>
<legend>高级配置</legend>
<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-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-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">
<template #suffix>秒</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="最大重试次数" field="maxRetryTimes">
<a-input-number v-model="form.maxRetryTimes" placeholder="请输入最大重试次数" :min="0">
</a-input-number>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="重试间隔" field="retryInterval">
<a-input-number v-model.trim="form.retryInterval" placeholder="请输入重试间隔" :min="1">
<template #suffix>
</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="并行数" field="parallelNum">
<a-input-number v-model="form.parallelNum" placeholder="请输入并行数" :min="1" />
</a-form-item>
</a-col>
</a-row>
</fieldset>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { type ColProps, type FormInstance, Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addJob, listGroup, updateJob } from '@/apis'
import { useForm } from '@/hooks'
import { useDict } from '@/hooks/app'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const colProps: ColProps = { xs: 24, sm: 24, md: 12, lg: 12, xl: 12, xxl: 12 }
const { width } = useWindowSize()
const { job_trigger_type_enum, job_task_type_enum, job_route_strategy_enum, job_block_strategy_enum } = useDict(
'job_trigger_type_enum',
'job_task_type_enum',
'job_route_strategy_enum',
'job_block_strategy_enum'
)
const dataId = ref()
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改任务' : '新增任务'))
const formRef = ref<FormInstance>()
const rules: FormInstance['rules'] = {
groupName: [{ required: true, message: '请选择任务组' }],
jobName: [{ required: true, message: '请输入任务名称' }],
triggerType: [{ required: true, message: '请选择触发类型' }],
triggerInterval: [{ required: true, message: '请输入间隔时长' }],
taskType: [{ required: true, message: '请选择任务类型' }],
executorInfo: [{ required: true, message: '请输入执行器名称' }],
routeKey: [{ required: true, message: '请选择路由策略' }],
blockStrategy: [{ required: true, message: '请选择阻塞策略' }],
executorTimeout: [{ required: true, message: '请输入超时时间' }],
maxRetryTimes: [{ required: true, message: '请输入最大重试次数' }],
retryInterval: [{ required: true, message: '请输入重试间隔' }],
parallelNum: [{ required: true, message: '请输入并行数' }]
}
const { form, resetForm } = useForm({
triggerType: 2,
triggerInterval: 60,
taskType: 1,
routeKey: 4,
blockStrategy: 1,
executorTimeout: 60,
maxRetryTimes: 3,
retryInterval: 1,
parallelNum: 1
})
const args = ref<any[]>([])
// 重置
const reset = () => {
formRef.value?.resetFields()
args.value = [{ value: '' }]
resetForm()
}
const groupList = ref()
// 查询任务组列表
const getGroupList = async () => {
const { data } = await listGroup()
groupList.value = data?.map((item: string) => ({
label: item,
value: item
}))
}
const visible = ref(false)
// 新增
const onAdd = () => {
reset()
getGroupList()
dataId.value = undefined
visible.value = true
}
// 修改
const onUpdate = async (record: any) => {
await getGroupList()
reset()
dataId.value = record.id
Object.assign(form, record)
// 切片任务,解析 argsStr 并赋值给 args
if (form.taskType === 3 && form.argsStr) {
try {
const parsedArgs = JSON.parse(form.argsStr)
args.value = parsedArgs.map((arg: any) => ({ value: arg }))
} catch (error: any) {
Message.error(error)
}
}
visible.value = true
}
// 保存
const save = async () => {
try {
// 切片任务,将参数转换为 JSON 数组
if (form.taskType === 3) {
form.argsStr = JSON.stringify(args.value.map((arg) => arg.value))
}
const isInvalid = await formRef.value?.validate()
if (isInvalid) return false
if (isUpdate.value) {
await updateJob(form, dataId.value)
Message.success('修改成功')
} else {
await addJob(form)
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 触发类型切换
const triggerTypeChange = () => {
switch (form.triggerType) {
case 2:
form.triggerInterval = 60
break
case 3:
form.triggerInterval = ''
break
}
}
// 间隔时长
const triggerIntervalNumber = computed({
get() {
return Number(form.triggerInterval)
},
set(newValue) {
form.triggerInterval = newValue.toString()
}
})
// 新增切片参数
const onAddArgs = () => {
args.value.push({ value: '' })
}
// 删除切片参数
const onDeleteArgs = (index) => {
args.value.splice(index, 1)
}
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.args-container {
display: flex;
flex-direction: column;
width: 100%;
}
.args-item {
display: flex;
align-items: center;
button {
margin-bottom: 20px;
}
}
.args-item > *:not(:last-child) {
margin-right: 10px;
}
.add-button {
align-self: flex-start;
width: 100px;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<a-drawer v-model:visible="visible" title="任务详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID" :span="2">
<a-typography-paragraph copyable>{{ dataDetail?.id }}</a-typography-paragraph>
</a-descriptions-item>
<a-descriptions-item label="任务组">{{ dataDetail?.groupName }}</a-descriptions-item>
<a-descriptions-item label="任务名称">{{ dataDetail?.jobName }}</a-descriptions-item>
<a-descriptions-item label="触发类型">
<GiCellTag :value="dataDetail?.triggerType" :dict="job_trigger_type_enum" />
</a-descriptions-item>
<a-descriptions-item v-if="dataDetail?.triggerType === 1" label="CRON">{{ dataDetail?.triggerInterval }}</a-descriptions-item>
<a-descriptions-item v-else-if="dataDetail?.triggerType === 2" label="间隔时长">{{ dataDetail?.triggerInterval }} </a-descriptions-item>
<a-descriptions-item label="任务类型">
<GiCellTag :value="dataDetail?.taskType" :dict="job_task_type_enum" />
</a-descriptions-item>
<a-descriptions-item label="执行器名称">{{ dataDetail?.executorInfo }}</a-descriptions-item>
<a-descriptions-item label="任务参数">{{ dataDetail?.argsStr }}</a-descriptions-item>
<a-descriptions-item label="路由策略">
<GiCellTag :value="dataDetail?.routeKey" :dict="job_route_strategy_enum" />
</a-descriptions-item>
<a-descriptions-item label="阻塞策略">
<GiCellTag :value="dataDetail?.blockStrategy" :dict="job_block_strategy_enum" />
</a-descriptions-item>
<a-descriptions-item label="超时时间">{{ dataDetail?.executorTimeout }} </a-descriptions-item>
<a-descriptions-item label="最大重试次数">{{ dataDetail?.maxRetryTimes }}</a-descriptions-item>
<a-descriptions-item label="重试间隔">{{ dataDetail?.retryInterval }} </a-descriptions-item>
<a-descriptions-item label="并行数">{{ dataDetail?.parallelNum }}</a-descriptions-item>
<a-descriptions-item label="任务状态">
<GiCellTag :value="dataDetail?.jobStatus" :dict="job_status_enum" />
</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">{{ dataDetail?.description }}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import type { JobResp } from '@/apis'
import { useDict } from '@/hooks/app'
const { width } = useWindowSize()
const { job_status_enum, job_trigger_type_enum, job_task_type_enum, job_route_strategy_enum, job_block_strategy_enum } = useDict(
'job_status_enum',
'job_trigger_type_enum',
'job_task_type_enum',
'job_route_strategy_enum',
'job_block_strategy_enum'
)
const visible = ref(false)
const dataDetail = ref<JobResp>()
// 详情
const onDetail = (record: JobResp) => {
dataDetail.value = record
visible.value = true
}
defineExpose({ onDetail })
</script>

View File

@@ -0,0 +1,195 @@
<template>
<div class="table-page">
<GiTable
title="任务管理"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@refresh="search"
>
<template #custom-left>
<a-select
v-model="queryForm.groupName"
placeholder="请选择任务组"
:options="groupList"
style="width: 200px"
@change="search"
/>
<a-input v-model="queryForm.jobName" placeholder="请输入任务名称" allow-clear @change="search" />
<a-select v-model="queryForm.jobStatus" placeholder="请选择任务状态" :options="job_status_enum" allow-clear style="width: 150px" @change="search" />
<a-button @click="reset">重置</a-button>
</template>
<template #custom-right>
<a-button v-permission="['schedule:job:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<span>新增</span>
</a-button>
</template>
<template #jobName="{ record }">
<a-link @click="onDetail(record)">{{ record.jobName }}</a-link>
</template>
<template #triggerType="{ record }">
<GiCellTag :value="record.triggerType" :dict="job_trigger_type_enum" />:&nbsp;
<span v-if="record.triggerType === 2">{{ record.triggerInterval }} </span>
<span v-else>{{ record.triggerInterval }}</span>
</template>
<template #taskType="{ record }">
<GiCellTag :value="record.taskType" :dict="job_task_type_enum" />
{{ record.executorInfo }}
</template>
<template #jobStatus="{ record }">
<a-switch
v-model="record.jobStatus"
:checked-value="1"
:unchecked-value="0"
:disabled="!has.hasPerm('tool:job:update')"
@change="onUpdateStatus(record)"
/>
</template>
<template #action="{ record }">
<a-space>
<a-link @click="onLog(record)">日志</a-link>
<a-popconfirm content="是否确定立即执行一次任务?" type="warning" @ok="onTrigger(record)">
<a-link v-permission="['schedule:job:trigger']">执行</a-link>
</a-popconfirm>
<a-link v-permission="['schedule:job:update']" @click="onUpdate(record)">修改</a-link>
<a-link v-permission="['schedule:job:delete']" status="danger" @click="onDelete(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<JobAddModal ref="JobAddModalRef" @save-success="reset" />
<JobDetailDrawer ref="JobDetailDrawerRef" />
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useRouter } from 'vue-router'
import JobAddModal from './JobAddModal.vue'
import JobDetailDrawer from './JobDetailDrawer.vue'
import { type JobQuery, type JobResp, deleteJob, listGroup, listJob, triggerJob, updateJobStatus } from '@/apis'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
import has from '@/utils/has'
defineOptions({ name: 'ScheduleJob' })
const { job_status_enum, job_trigger_type_enum, job_task_type_enum } = useDict('job_status_enum', 'job_trigger_type_enum', 'job_task_type_enum')
const queryForm = reactive<JobQuery>({
groupName: ''
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete
} = useTable((page) => listJob({ ...queryForm, ...page }), { immediate: false })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize)
},
{ title: '任务名称', dataIndex: 'jobName', slotName: 'jobName', width: 100, ellipsis: true, tooltip: true },
{ title: '调度类型', dataIndex: 'triggerType', slotName: 'triggerType', width: 130 },
{ title: '任务类型', dataIndex: 'taskType', slotName: 'taskType', width: 130, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'jobStatus', width: 60, align: 'center', slotName: 'jobStatus' },
{ title: '描述', dataIndex: 'description', width: 130, ellipsis: true, tooltip: true },
{ title: '创建时间', dataIndex: 'createDt', width: 180 },
{ title: '修改时间', dataIndex: 'updateDt', width: 180, show: false },
{
title: '操作',
slotName: 'action',
width: 130,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['schedule:job:trigger', 'schedule:job:update', 'schedule:job:delete'])
}
]
const groupList = ref()
// 查询任务组列表
const getGroupList = async () => {
const { data } = await listGroup()
groupList.value = data?.map((item: string) => ({
label: item,
value: item
}))
queryForm.groupName = groupList.value[0].label
search()
}
// 重置
const reset = () => {
queryForm.jobName = undefined
queryForm.jobStatus = undefined
search()
}
// 删除
const onDelete = (record: JobResp) => {
return handleDelete(() => deleteJob(record.id), {
content: `是否确定删除任务 [${record.jobName}]`,
showModal: true
})
}
// 修改状态
const onUpdateStatus = (record: JobResp) => {
const msg = record.jobStatus === 1 ? '启用成功' : '禁用成功'
updateJobStatus({ jobStatus: record.jobStatus }, record.id)
.then(() => {
Message.success(msg)
}).catch(() => {
record.jobStatus = record.jobStatus === 1 ? 0 : 1
})
}
// 执行
const onTrigger = (record: JobResp) => {
triggerJob(record.id).then(() => {
Message.success('执行请求已下发')
})
}
const JobAddModalRef = ref<InstanceType<typeof JobAddModal>>()
// 新增
const onAdd = () => {
JobAddModalRef.value?.onAdd()
}
// 修改
const onUpdate = (record: JobResp) => {
JobAddModalRef.value?.onUpdate(record)
}
const JobDetailDrawerRef = ref<InstanceType<typeof JobDetailDrawer>>()
// 详情
const onDetail = (record: JobResp) => {
JobDetailDrawerRef.value?.onDetail(record)
}
const router = useRouter()
// 日志
const onLog = (record: JobResp) => {
router.push({ path: '/schedule/log', query: { jobId: record.id, jobName: record.jobName, groupName: record.groupName } })
}
onMounted(() => {
getGroupList()
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,188 @@
<template>
<a-modal v-model:visible="visible" title="任务日志详情" :bodyStyle="{ maxHeight: '80vh', overflow: 'auto' }"
:width="width >= 1500 ? 1500 : '100%'" :footer="false" @close="closed">
<div style="display: flex;">
<div style="padding: 10px 10px;">
<div class="job_list">
<div :class="`job_list_item ${item.id === activeId ? 'active' : ''}`" v-for="item in dataList" :key="item.id"
@click="onStartInfo(item)">
<div class="content">
<span class="title">{{ item.clientInfo.split('@')[1] }}</span>
<span class="status">
<a-tag bordered :color="statusList[item.taskStatus].color">{{ statusList[item.taskStatus].title
}}</a-tag>
</span>
</div>
</div>
</div>
</div>
<div class="code_view">
<GiCodeView :code-json="content" />
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type JobInstanceQuery, type JobInstanceResp, type JobLogResp, listJobInstance, listJobInstanceLog } from '@/apis'
import dayjs from "dayjs";
const { width } = useWindowSize()
const queryForm = reactive<JobInstanceQuery>({})
const dataList = ref<JobInstanceResp[]>([])
const loading = ref(false)
const activeId = ref<string | number>('')
const statusList = {
'1': {
title: '待处理',
color: 'gray',
isRun: false
},
'2': {
title: '运行中',
color: 'cyan',
isRun: true
},
'3': {
title: '成功',
color: 'green',
isRun: false
},
'4': {
title: '已失败',
color: 'red',
isRun: false
},
'5': {
title: '已停止',
color: 'purple',
isRun: false
},
'6': {
title: '已取消',
color: 'orange',
isRun: false
}
}
const visible = ref(false)
// 格式化日志
const formatLog = (log: any) => {
const date = new Date(Number.parseInt(log.time_stamp))
return `${dayjs(date).format('YYYY-MM-DD HH:mm:ss')} ${log.level} [${log.thread}] ${log.location} - ${log.message}`
}
const content = ref('')
const setIntervalNode = ref<NodeJS.Timeout>()
// 详情
const onDetail = (record: JobLogResp) => {
visible.value = true
// 更新 queryForm
queryForm.jobId = record.jobId
queryForm.taskBatchId = record.id
getInstanceList()
}
// 日志输出
const onLogDetail = async (record: JobInstanceResp) => {
activeId.value = record?.id
try {
// todo startId根据第一次查询 如果有返回!=0则需要在查一次
const res = await listJobInstanceLog({
taskBatchId: record.taskBatchId,
jobId: record.jobId,
taskId: record.id,
startId: 0,
fromIndex: 0,
size: 50
})
if (res.data?.finished) {
clearInterval(setIntervalNode.value)
}
content.value = res.data.message.map(formatLog).join('\n')
} catch (error) {
content.value = ''
}
}
const onStartInfo = (record: JobInstanceResp) => {
content.value = ''
clearInterval(setIntervalNode.value)
let isRun = statusList[record.taskStatus].isRun
if (isRun) {
setIntervalNode.value = setInterval(() => {
onLogDetail(record)
}, 1000)
} else {
onLogDetail(record)
}
}
// 查询列表数据
const getInstanceList = async (query: JobInstanceQuery = { ...queryForm }) => {
try {
loading.value = true
const res = await listJobInstance(query)
dataList.value = res.data
onStartInfo(dataList.value[0])
} finally {
loading.value = false
}
}
const closed = () => {
clearInterval(setIntervalNode.value)
}
onUnmounted(() => {
clearInterval(setIntervalNode.value)
})
defineExpose({ onDetail })
</script>
<style scoped lang="scss">
.job_list {
position: relative;
width: 100%;
border: 1px solid var(--color-neutral-3);
.job_list_item {
padding: 8px 10px;
cursor: pointer;
&:not(:last-child) {
border-bottom: 2px dashed var(--color-neutral-3);
}
&:hover {
background-color: var(--color-neutral-2);
}
.content {
display: flex;
justify-content: space-between;
justify-items: center;
.title {
margin-right: 20px;
}
}
}
.active {
border-bottom: 2px solid $color-theme !important;
background-color: var(--color-neutral-3) !important;
}
}
.code_view {
position: relative;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="table-page">
<GiTable
title="任务日志"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
@refresh="search"
>
<template #custom-left>
<a-select
v-model="queryForm.groupName"
placeholder="请选择任务组"
:options="groupList"
style="width: 200px"
@change="search"
/>
<a-input v-model="queryForm.jobName" placeholder="请输入任务名称" allow-clear @change="search" />
<a-select
v-model="queryForm.taskBatchStatus"
placeholder="请选择状态"
:options="job_execute_status_enum"
allow-clear
style="width: 150px"
@change="search"
/>
<DateRangePicker v-model="queryForm.datetimeRange" :allow-clear="false" @change="search" />
<a-button @click="reset">重置</a-button>
</template>
<template #taskBatchStatus="{ record }">
<GiCellTag :value="record.taskBatchStatus" :dict="job_execute_status_enum" />
</template>
<template #operationReason="{ record }">
<GiCellTag :value="record.operationReason" :dict="job_execute_reason_enum" />
</template>
<template #action="{ record }">
<a-space>
<a-link @click="onDetail(record)">详情</a-link>
<a-popconfirm content="是否确定停止本次执行?" type="warning" @ok="onStop(record)">
<a-link v-if="record.taskBatchStatus === 2" v-permission="['schedule:log:stop']" status="danger">停止</a-link>
</a-popconfirm>
<a-popconfirm content="是否确定重试本次执行?" type="warning" @ok="onRetry(record)">
<a-link
v-if="record.taskBatchStatus === 4 || record.taskBatchStatus === 5 || record.taskBatchStatus === 6"
v-permission="['schedule:log:retry']"
status="danger"
>
重试
</a-link>
</a-popconfirm>
</a-space>
</template>
</GiTable>
<JobLogDetailModal ref="JobLogDetailModalRef" />
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useRoute } from 'vue-router'
import dayjs from 'dayjs'
import JobLogDetailModal from './LogDetailModal.vue'
import { type JobLogQuery, type JobLogResp, listGroup, listJobLog, retryJob, stopJob } from '@/apis'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
import has from '@/utils/has'
defineOptions({ name: 'ScheduleLog' })
const { job_execute_reason_enum, job_execute_status_enum } = useDict('job_execute_reason_enum', 'job_execute_status_enum')
const queryForm = reactive<JobLogQuery>({
datetimeRange: [
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')
]
})
const {
tableData: dataList,
pagination,
loading,
search
} = useTable((page) => listJobLog({ ...queryForm, ...page }), { immediate: false })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize)
},
{ title: '任务组', dataIndex: 'groupName', width: 80, ellipsis: true, tooltip: true },
{ title: '任务名称', dataIndex: 'jobName', width: 80, ellipsis: true, tooltip: true },
{ title: '调度时间', dataIndex: 'createDt', width: 80 },
{ title: '执行状态', dataIndex: 'taskBatchStatus', slotName: 'taskBatchStatus', width: 50, align: 'center' },
{ title: '执行备注', dataIndex: 'operationReason', slotName: 'operationReason', width: 80, ellipsis: true, tooltip: true },
{ title: '执行时间', dataIndex: 'executionAt', width: 80 },
{
title: '操作',
slotName: 'action',
width: 60,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['schedule:log:stop', 'schedule:log:retry'])
}
]
const groupList = ref()
// 查询任务组列表
const getGroupList = async () => {
const { data } = await listGroup()
groupList.value = data?.map((item: string) => ({
label: item,
value: item
}))
}
// 重置
const reset = () => {
queryForm.taskBatchStatus = undefined
queryForm.datetimeRange = [
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')
]
search()
}
// 停止
const onStop = (record: JobLogResp) => {
stopJob(record.id).then(() => {
Message.success('停止成功')
})
}
// 重试
const onRetry = (record: JobLogResp) => {
retryJob(record.id).then(() => {
Message.success('重试成功')
})
}
const JobLogDetailModalRef = ref<InstanceType<typeof JobLogDetailModal>>()
// 查看日志详情
const onDetail = (record: JobLogResp) => {
JobLogDetailModalRef.value?.onDetail(record)
}
const route = useRoute()
onMounted(() => {
if (route.query) {
queryForm.jobId = route.query.jobId ? Number.parseInt(route.query.jobId as string, 10) : undefined
queryForm.groupName = route.query.groupName ? route.query.groupName : undefined
queryForm.jobName = route.query.jobName ? route.query.jobName : undefined
}
getGroupList()
search()
})
</script>
<style scoped lang="scss"></style>

View File

@@ -51,7 +51,7 @@ const { form, resetForm } = useForm({
rePassword: ''
})
const columns: Columns = [
const columns: Columns = reactive([
{
label: '手机号',
field: 'phone',
@@ -137,7 +137,7 @@ const columns: Columns = [
return verifyType.value !== 'password'
}
}
]
])
const VerifyRef = ref<InstanceType<any>>()
const captchaType = ref('blockPuzzle')

View File

@@ -29,7 +29,7 @@ const options: Options = {
btns: { hide: true }
}
const columns: Columns = [
const columns: Columns = reactive([
{
label: '昵称',
field: 'nickname',
@@ -47,7 +47,7 @@ const columns: Columns = [
],
rules: [{ required: true, message: '请选择性别' }]
}
]
])
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)

View File

@@ -106,11 +106,11 @@ import {
type SiteConfig,
listOption,
resetOptionValue,
updateOption,
uploadFile
updateOption
} from '@/apis'
import { useAppStore } from '@/stores'
import { useForm } from '@/hooks'
import { fileToBase64 } from '@/utils'
defineOptions({ name: 'BasicSetting' })
@@ -212,15 +212,16 @@ const onResetValue = () => {
// 上传 favicon
const handleUploadFavicon = (options: RequestOption) => {
const controller = new AbortController()
; (async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
;(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
uploadFile(formData)
if (!fileItem.file) {
return
}
fileToBase64(fileItem.file).then()
.then((res) => {
onSuccess(res)
form.SITE_FAVICON = res.data.url
onSuccess()
form.SITE_FAVICON = res
Message.success('上传成功')
})
.catch((error) => {
@@ -242,15 +243,16 @@ const handleChangeFavicon = (_: any, currentFile: any) => {
// 上传 Logo
const handleUploadLogo = (options: RequestOption) => {
const controller = new AbortController()
; (async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
;(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
uploadFile(formData)
if (!fileItem.file) {
return
}
fileToBase64(fileItem.file).then()
.then((res) => {
onSuccess(res)
form.SITE_LOGO = res.data.url
onSuccess()
form.SITE_LOGO = res
Message.success('上传成功')
})
.catch((error) => {

View File

@@ -38,7 +38,7 @@ const options: Options = {
btns: { hide: true }
}
const columns: Columns = [
const columns: Columns = reactive([
{
label: '上级部门',
field: 'parentId',
@@ -99,7 +99,7 @@ const columns: Columns = [
uncheckedText: '禁用'
}
}
]
])
const { form, resetForm } = useForm({
sort: 999,

View File

@@ -131,9 +131,9 @@ const reset = () => {
}
// 删除
const onDelete = (item: DeptResp) => {
return handleDelete(() => deleteDept(item.id), {
content: `是否确定删除 [${item.name}]`,
const onDelete = (record: DeptResp) => {
return handleDelete(() => deleteDept(record.id), {
content: `是否确定删除 [${record.name}]`,
showModal: true
})
}
@@ -150,8 +150,8 @@ const onAdd = (parentId?: string) => {
}
// 修改
const onUpdate = (item: DeptResp) => {
DeptAddModalRef.value?.onUpdate(item.id)
const onUpdate = (record: DeptResp) => {
DeptAddModalRef.value?.onUpdate(record.id)
}
</script>

View File

@@ -43,7 +43,7 @@ const options: Options = {
btns: { hide: true }
}
const columns: Columns = [
const columns: Columns = reactive([
{ label: '标签', field: 'label', type: 'input', rules: [{ required: true, message: '请输入标签' }] },
{ label: '值', field: 'value', type: 'input', rules: [{ required: true, message: '请输入值' }] },
{ label: '标签颜色', field: 'color', type: 'input' },
@@ -77,7 +77,7 @@ const columns: Columns = [
uncheckedText: '禁用'
}
}
]
])
const { form, resetForm } = useForm({
color: 'blue',

View File

@@ -127,8 +127,8 @@ const reset = () => {
}
// 删除
const onDelete = (item: DictItemResp) => {
return handleDelete(() => deleteDictItem(item.id), { content: `是否确定删除 [${item.label}]`, showModal: true })
const onDelete = (record: DictItemResp) => {
return handleDelete(() => deleteDictItem(record.id), { content: `是否确定删除 [${record.label}]`, showModal: true })
}
// 根据选中字典查询
@@ -144,8 +144,8 @@ const onAdd = () => {
}
// 修改
const onUpdate = (item: DictItemResp) => {
DictItemAddModalRef.value?.onUpdate(item.id)
const onUpdate = (record: DictItemResp) => {
DictItemAddModalRef.value?.onUpdate(record.id)
}
</script>

View File

@@ -34,7 +34,7 @@ const options: Options = {
btns: { hide: true }
}
const columns: Columns = [
const columns: Columns = reactive([
{ label: '名称', field: 'name', type: 'input', rules: [{ required: true, message: '请输入名称' }] },
{ label: '编码', field: 'code', type: 'input', disabled: () => isUpdate.value, rules: [{ required: true, message: '请输入编码' }] },
{
@@ -46,7 +46,7 @@ const columns: Columns = [
autoSize: { minRows: 3, maxRows: 5 }
}
}
]
])
const { form, resetForm } = useForm({})

View File

@@ -91,7 +91,7 @@ const getStatisticsData = async () => {
unit: formatSize[1],
data: []
}
resData.data.forEach((fs: FileStatisticsResp) => {
resData.data?.forEach((fs: FileStatisticsResp) => {
const matchedItem = FileTypeList.find((item) => item.value === fs.type)
chartData.value.unshift({
name: matchedItem ? matchedItem.name : '',

View File

@@ -0,0 +1,101 @@
<template>
<div>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
width="90%"
draggable
>
<div class="modal-content">
<div class="modal-header">
<!-- <a-button type="primary" @click="onPrintFile"> -->
<!-- <template #icon> -->
<!-- <icon-printer /> -->
<!-- </template> -->
<!-- <template #default> -->
<!-- 打印 -->
<!-- </template> -->
<!-- </a-button> -->
<a-button type="primary" status="success" @click="onDownloadFile">
<template #icon>
<icon-download />
</template>
下载
</a-button>
</div>
<div class="iframe-container">
<iframe :src="previewUrl" />
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FileItem } from '@/apis'
import { encodeByBase64 } from '@/utils/encrypt'
const emit = defineEmits(['download'])
const visible = ref(false)
const title = ref('文件预览')
const fileObject = ref<FileItem>()
const isLoading = ref(false)
const error = ref('')
const previewUrl = ref('')
// 显示弹窗
function show(fileItem: FileItem) {
fileObject.value = fileItem
visible.value = true
title.value = `${fileItem.name}.${fileItem.extension}`
isLoading.value = true
error.value = ''
previewUrl.value = `${import.meta.env.FILE_VIEW_SERVER_URL}/onlinePreview?url=${encodeURIComponent(encodeByBase64(fileItem.url))}`
}
// 打印文件
// const onPrintFile = () => {
// }
// 下载文件
const onDownloadFile = () => {
emit('download', fileObject.value)
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.modal-content {
display: flex;
flex-direction: column;
height: 80vh;
}
.modal-header {
display: flex;
justify-content: flex-end;
padding: 10px;
background: #f5f5f5;
border-bottom: 1px solid #e8e8e8;
}
.iframe-container {
overflow: hidden;
flex: 1;
height: calc(80vh - 50px);
display: flex;
align-items: center;
justify-content: center;
}
iframe {
width: 100%;
height: 100%;
}
</style>

View File

@@ -17,7 +17,8 @@
</a-dropdown>
<a-input-group>
<a-input v-model="queryForm.name" placeholder="请输入文件名" allow-clear style="width: 200px" @change="search" />
<a-input v-model="queryForm.name" placeholder="请输入文件名" allow-clear style="width: 200px"
@change="search" />
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
@@ -30,7 +31,7 @@
<!-- 右侧区域 -->
<a-space wrap>
<a-button v-if="isBatchMode" :disabled="!selectedFileIds.length" type="primary" status="danger"
@click="handleMulDelete">
@click="handleMulDelete">
<template #icon>
<icon-delete />
</template>
@@ -57,16 +58,17 @@
<!-- 文件列表-宫格模式 -->
<a-spin id="fileMain" class="file-main__list" :loading="loading">
<FileGrid v-show="fileList.length && mode === 'grid'" :data="fileList" :is-batch-mode="isBatchMode"
:selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile"
@right-menu-click="handleRightMenuClick"></FileGrid>
:selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile"
@right-menu-click="handleRightMenuClick"></FileGrid>
<!-- 文件列表-列表模式 -->
<FileList v-show="fileList.length && mode === 'list'" :data="fileList" :is-batch-mode="isBatchMode"
:selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile"
@right-menu-click="handleRightMenuClick"></FileList>
:selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile"
@right-menu-click="handleRightMenuClick"></FileList>
<a-empty v-if="!fileList.length" />
</a-spin>
<FilePreview ref="filePreviewRef" @download="args => onDownload(args)" />
<div class="pagination">
<a-pagination v-bind="pagination" />
</div>
@@ -89,6 +91,7 @@ import { type FileItem, type FileQuery, deleteFile, listFile, uploadFile } from
import { ImageTypes } from '@/constant/file'
import 'viewerjs/dist/viewer.css'
import { downloadByUrl } from '@/utils/downloadFile'
import FilePreview from '@/views/system/file/main/FileMain/FilePreview.vue'
const FileList = defineAsyncComponent(() => import('./FileList.vue'))
const route = useRoute()
@@ -110,29 +113,43 @@ const {
pagination,
search
} = useTable((page) => listFile({ ...queryForm, ...page }), { immediate: false, paginationOption })
const filePreviewRef = ref()
// 点击文件
const handleClickFile = (item: FileItem) => {
if (ImageTypes.includes(item.extension)) {
if (item.url) {
const imgList: string[] = fileList.value.filter((i) => ImageTypes.includes(i.extension)).map((a) => a.url || '')
const index = imgList.findIndex((i) => i === item.url)
if (imgList.length) {
viewerApi({
options: {
initialViewIndex: index
},
images: imgList
})
if (JSON.parse(import.meta.env.FILE_OPEN_PREVIEW)) {
filePreviewRef.value.show(item)
} else {
if (ImageTypes.includes(item.extension)) {
if (item.url) {
const imgList: string[] = fileList.value.filter((i) => ImageTypes.includes(i.extension)).map((a) => a.url || '')
const index = imgList.findIndex((i) => i === item.url)
if (imgList.length) {
viewerApi({
options: {
initialViewIndex: index
},
images: imgList
})
}
}
}
if (item.extension === 'mp4') {
previewFileVideoModal(item)
}
if (item.extension === 'mp3') {
previewFileAudioModal(item)
}
}
if (item.extension === 'mp4') {
previewFileVideoModal(item)
}
if (item.extension === 'mp3') {
previewFileAudioModal(item)
}
}
// 下载文件
const onDownload = async (fileInfo: FileItem) => {
const res = await downloadByUrl({
url: fileInfo.url,
target: '_self',
fileName: `${fileInfo.name}.${fileInfo.extension}`
})
res ? Message.success('下载成功') : Message.error('下载失败')
search()
}
// 右键菜单
@@ -154,13 +171,7 @@ const handleRightMenuClick = async (mode: string, fileInfo: FileItem) => {
} else if (mode === 'detail') {
openFileDetailModal(fileInfo)
} else if (mode === 'download') {
const res = await downloadByUrl({
url: fileInfo.url,
target: '_self',
fileName: `${fileInfo.name}.${fileInfo.extension}`
})
res ? Message.success('下载成功') : Message.error('下载失败')
search()
await onDownload(fileInfo)
}
}
@@ -186,7 +197,7 @@ const handleMulDelete = () => {
// 上传
const handleUpload = (options: RequestOption) => {
const controller = new AbortController()
; (async function requestWrap() {
;(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()

View File

@@ -140,9 +140,9 @@ const reset = () => {
}
// 删除
const onDelete = (item: MenuResp) => {
return handleDelete(() => deleteMenu(item.id), {
content: `是否确定删除 [${item.title}]`,
const onDelete = (record: MenuResp) => {
return handleDelete(() => deleteMenu(record.id), {
content: `是否确定删除 [${record.title}]`,
showModal: true
})
}
@@ -162,8 +162,8 @@ const onAdd = (parentId?: string) => {
}
// 修改
const onUpdate = (item: MenuResp) => {
MenuAddModalRef.value?.onUpdate(item.id)
const onUpdate = (record: MenuResp) => {
MenuAddModalRef.value?.onUpdate(record.id)
}
</script>

View File

@@ -0,0 +1,123 @@
<!-- 未完善 -->
<template>
<div ref="divRef" class="container">
<div class="aie-container">
<div class="aie-header-panel" style="display: none;">
<div class="aie-container-header"></div>
</div>
<div class="aie-main">
<div class="aie-container-panel">
<div class="aie-container-main"></div>
</div>
</div>
<div class="aie-container-footer" style="display: none;"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { AiEditor, type AiEditorOptions } from 'aieditor'
import 'aieditor/dist/style.css'
import { useAppStore } from '@/stores'
defineOptions({ name: 'AiEditor' })
const props = defineProps<{
modelValue: string
options?: AiEditorOptions
}>()
const aieditor = ref<AiEditor | null>(null)
const appStore = useAppStore()
const divRef = ref<any>()
const editorConfig = reactive<AiEditorOptions>({
element: '',
theme: appStore.theme,
placeholder: '请输入内容',
content: '',
editable: false
})
const init = () => {
aieditor.value?.destroy()
aieditor.value = new AiEditor(editorConfig)
}
watch(() => props.modelValue, (value) => {
if (value !== aieditor.value?.getHtml()) {
editorConfig.content = value
init()
}
})
watch(() => appStore.theme, (value) => {
editorConfig.theme = value
init()
})
// 挂载阶段
onMounted(() => {
editorConfig.element = divRef.value
init()
})
// 销毁阶段
onUnmounted(() => {
aieditor.value?.destroy()
})
</script>
<style lang="scss" scoped>
.container {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.aie-header-panel {
position: sticky;
// top: 51px;
z-index: 1;
}
.aie-header-panel aie-header>div {
align-items: center;
justify-content: center;
padding: 10px 0;
}
.aie-container {
border: none !important;
}
.aie-container-panel {
width: calc(100% - 2rem - 2px);
max-width: 826.77px;
margin: 0rem auto;
border: 1px solid var(--color-border-1);
background-color: var() rgba($color: var(--color-bg-1), $alpha: 1.0);
height: 100%;
padding: 1rem;
z-index: 99;
overflow: auto;
box-sizing: border-box;
}
.aie-main {
position: relative;
overflow: hidden;
flex: 1;
box-sizing: border-box;
padding: 1rem 0px;
background-color: var(--color-bg-1);
}
.aie-directory {
position: absolute;
top: 30px;
left: 10px;
width: 260px;
z-index: 0;
}
.aie-title1 {
font-size: 14px;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,209 @@
<!-- 未完善 -->
<template>
<div ref="divRef" class="container">
<div class="aie-container">
<div class="aie-header-panel">
<div class="aie-container-header"></div>
</div>
<div class="aie-main">
<div class="aie-directory-content">
<div class="aie-directory">
<h5>目录</h5>
<div id="outline">
</div>
</div>
</div>
<div class="aie-container-panel">
<div class="aie-container-main"></div>
</div>
</div>
<div class="aie-container-footer"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { AiEditor, type AiEditorOptions } from 'aieditor'
import 'aieditor/dist/style.css'
import { useAppStore } from '@/stores'
defineOptions({ name: 'AiEditor' })
const props = defineProps<{
modelValue: string
options?: AiEditorOptions
}>()
const emit = defineEmits<(e: 'update:modelValue', value: string) => void>()
const appStore = useAppStore()
const divRef = ref<any>()
const aieditor = ref<AiEditor | null>(null)
const updateOutLine = (editor: AiEditor) => {
const outlineContainer = document.querySelector('#outline')
while (outlineContainer?.firstChild) {
outlineContainer.removeChild(outlineContainer.firstChild)
}
const outlines = editor.getOutline()
for (const outline of outlines) {
const child = document.createElement('div')
child.classList.add(`aie-title${outline.level}`)
child.style.marginLeft = `${14 * (outline.level - 1)}px`
child.style.padding = `4px 0`
child.innerHTML = `<a href="#${outline.id}">${outline.text}</a>`
child.addEventListener('click', (e) => {
e.preventDefault()
const el = editor.innerEditor.view.dom.querySelector(`#${outline.id}`) as HTMLElement
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' })
setTimeout(() => {
editor.focusPos(outline.pos + outline.size - 1)
}, 1000)
})
outlineContainer?.appendChild(child)
}
}
const editorConfig = reactive<AiEditorOptions>({
element: '',
theme: appStore.theme,
placeholder: '请输入内容',
content: '',
draggable: false,
onChange: (editor: AiEditor) => {
emit('update:modelValue', editor.getHtml())
updateOutLine(editor)
},
onCreated: (editor: AiEditor) => {
updateOutLine(editor)
}
})
watch(() => props.modelValue, (value) => {
if (value !== aieditor.value?.getHtml()) {
aieditor.value?.destroy()
editorConfig.content = value
aieditor.value = new AiEditor(editorConfig)
}
})
const init = () => {
editorConfig.element = divRef.value
aieditor.value = new AiEditor(editorConfig)
}
// 挂载阶段
onMounted(() => {
init()
})
// 销毁阶段
onUnmounted(() => {
aieditor.value?.destroy()
})
</script>
<style lang="scss" scoped>
.container {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.aie-header-panel {
position: sticky;
// top: 51px;
z-index: 1;
}
.aie-header-panel aie-header>div {
align-items: center;
justify-content: center;
padding: 10px 0;
}
.aie-container {
border: none !important;
}
.aie-container-panel {
width: calc(100% - 2rem - 2px);
max-width: 826.77px;
margin: 0rem auto;
border: 1px solid var(--color-border-1);
background-color: var() rgba($color: var(--color-bg-1), $alpha: 1.0);
height: 100%;
padding: 1rem;
z-index: 99;
overflow: auto;
box-sizing: border-box;
color: black;
}
.aie-main {
position: relative;
overflow: hidden;
flex: 1;
box-sizing: border-box;
padding: 1rem 0px;
background-color: var(--color-bg-2);
}
.aie-directory {
position: absolute;
top: 30px;
left: 10px;
width: 260px;
z-index: 0;
}
.aie-directory h5 {
// color: #000000c4;
font-size: 16px;
text-indent: 4px;
line-height: 32px;
}
.aie-directory a {
height: 30px;
font-size: 14px;
// color: #000000a3;
text-indent: 4px;
line-height: 30px;
text-decoration: none;
width: 100%;
display: inline-block;
margin: 0;
padding: 0;
white-space: nowrap;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
}
.aie-directory a:hover {
cursor: pointer;
// background-color: #334d660f;
border-radius: 4px;
}
.aie-title1 {
font-size: 14px;
font-weight: 500;
}
#outline {
text-indent: 2rem;
}
.aie-directory-content {
position: sticky;
top: 0px
}
@media screen and (max-width: 1280px) {
.aie-directory {
display: none;
}
}
@media screen and (max-width: 1400px) {
.aie-directory {
width: 200px;
}
}
</style>

View File

@@ -6,7 +6,7 @@
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['title']"
@@ -67,6 +67,7 @@ defineOptions({ name: 'SystemNotice' })
const { notice_type, notice_status_enum } = useDict('notice_type', 'notice_status_enum')
const router = useRouter()
const queryForm = reactive<NoticeQuery>({
sort: ['createTime,desc']
})
@@ -111,9 +112,9 @@ const reset = () => {
}
// 删除
const onDelete = (item: NoticeResp) => {
return handleDelete(() => deleteNotice(item.id), {
content: `是否确定删除公告 [${item.title}]`,
const onDelete = (record: NoticeResp) => {
return handleDelete(() => deleteNotice(record.id), {
content: `是否确定删除公告 [${record.title}]`,
showModal: true
})
}
@@ -121,18 +122,21 @@ const onDelete = (item: NoticeResp) => {
const NoticeAddModalRef = ref<InstanceType<typeof NoticeAddModal>>()
// 新增
const onAdd = () => {
NoticeAddModalRef.value?.onAdd()
// NoticeAddModalRef.value?.onAdd()
router.push({ path: '/system/notice/add' })
}
// 修改
const onUpdate = (item: NoticeResp) => {
NoticeAddModalRef.value?.onUpdate(item.id)
const onUpdate = (record: NoticeResp) => {
// NoticeAddModalRef.value?.onUpdate(record.id)
router.push({ path: '/system/notice/add', query: { id: record.id, type: 'edit' } })
}
const NoticeDetailModalRef = ref<InstanceType<typeof NoticeDetailModal>>()
// 详情
const onDetail = (item: NoticeResp) => {
NoticeDetailModalRef.value?.onDetail(item.id)
const onDetail = (record: NoticeResp) => {
// NoticeDetailModalRef.value?.onDetail(record.id)
router.push({ path: '/system/notice/detail', query: { id: record.id } })
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<div ref="containerRef" class="detail">
<div class="detail_header">
<a-affix :target="(containerRef as HTMLElement)">
<a-page-header title="通知公告" :subtitle="type === 'edit' ? '修改' : '新增'" @back="onBack">
<template #extra>
<a-button type="primary" @click="onReleased">{{ type === 'edit' ? '修改' : '发布' }}</a-button>
</template>
</a-page-header>
</a-affix>
</div>
<div class="detail_content" style="display: flex; flex-direction: column;">
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
<div style="flex: 1;">
<AiEditor v-model="form.content" />
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { Message } from '@arco-design/web-vue'
import AiEditor from '../components/edit/index.vue'
import { useTabsStore } from '@/stores'
import { type Columns, GiForm, type Options } from '@/components/GiForm'
import { addNotice, getNotice, updateNotice } from '@/apis'
import { useForm } from '@/hooks'
import { useDict } from '@/hooks/app'
const { notice_type } = useDict('notice_type')
const containerRef = ref<HTMLElement | null>()
const tabsStore = useTabsStore()
const route = useRoute()
const formRef = ref<InstanceType<typeof GiForm>>()
const { id, type } = route.query
const { form, resetForm } = useForm({
title: '',
type: '',
effectiveTime: '',
terminateTime: '',
content: ''
})
const options: Options = {
form: {},
col: { xs: 24, sm: 24, md: 12, lg: 12, xl: 12, xxl: 12 },
btns: { hide: true }
}
const columns: Columns = reactive([
{
label: '标题',
field: 'title',
type: 'input',
rules: [{ required: true, message: '请输入标题' }]
},
{
label: '类型',
field: 'type',
type: 'select',
options: notice_type,
rules: [{ required: true, message: '请输入类型' }]
},
{
label: '生效时间',
field: 'effectiveTime',
type: 'date-picker',
props: {
showTime: true
}
},
{
label: '终止时间',
field: 'terminateTime',
type: 'date-picker',
props: {
showTime: true
}
}
])
// 修改
const onUpdate = async (id: string) => {
resetForm()
const res = await getNotice(id)
Object.assign(form, res.data)
}
const onBack = () => {
tabsStore.closeCurrent(route.path)
}
const onReleased = async () => {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
try {
if (type === 'edit') {
await updateNotice(form, id as string)
Message.success('修改成功')
} else {
await addNotice(form)
Message.success('新增成功')
}
onBack()
return true
} catch (error) {
console.error(error)
return false
}
}
onMounted(() => {
// 当id存在与type为edit时执行修改操作
if (id && type === 'edit') {
onUpdate(id as string)
}
})
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,78 @@
<template>
<div ref="containerRef" class="detail">
<div class="detail_header">
<a-affix :target="(containerRef as HTMLElement)">
<a-page-header title="通知公告" subtitle="查看" @back="onBack">
</a-page-header>
</a-affix>
</div>
<div class="detail_content" style="display: flex; flex-direction: column;">
<h1 class="title">{{ form?.title }}</h1>
<div class="info">
<a-space>
<span>
<icon-user class="icon" />
<span class="label">发布人</span>
<span>{{ form?.createUserString }}</span>
</span>
<a-divider direction="vertical" />
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ form?.effectiveTime ? form?.effectiveTime : form?.createTime
}}</span>
</span>
</a-space>
</div>
<div style="flex: 1;">
<AiEditor v-model="form.content" />
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import AiEditor from '../components/detail/index.vue'
import { useTabsStore } from '@/stores'
import { getNotice } from '@/apis'
import { useForm } from '@/hooks'
const containerRef = ref<HTMLElement | null>()
const tabsStore = useTabsStore()
const route = useRoute()
const { id } = route.query
const { form, resetForm } = useForm({
title: '',
createUserString: '',
effectiveTime: '',
createTime: '',
content: ''
})
// 修改
const onDetail = async (id: string) => {
resetForm()
const res = await getNotice(id)
Object.assign(form, res.data)
}
const onBack = () => {
tabsStore.closeCurrent(route.path)
}
onMounted(() => {
onDetail(id as string)
})
</script>
<style scoped lang="scss">
.detail_content {
.title {
text-align: center;
}
.info {
text-align: right;
padding: 20px;
}
}
</style>

View File

@@ -50,13 +50,13 @@
</template>
</GiTable>
<RoleAddModal ref="RoleAddModalRef" @save-success="search" />
<RoleAddDrawer ref="RoleAddDrawerRef" @save-success="search" />
<RoleDetailDrawer ref="RoleDetailDrawerRef" />
</div>
</template>
<script setup lang="ts">
import RoleAddModal from './RoleAddModal.vue'
import RoleAddDrawer from './RoleAddDrawer.vue'
import RoleDetailDrawer from './RoleDetailDrawer.vue'
import { type RoleQuery, type RoleResp, deleteRole, listRole } from '@/apis'
import type { TableInstanceColumns } from '@/components/GiTable/type'
@@ -115,25 +115,25 @@ const reset = () => {
}
// 删除
const onDelete = (item: RoleResp) => {
return handleDelete(() => deleteRole(item.id), { content: `是否确定删除 [${item.name}]`, showModal: true })
const onDelete = (record: RoleResp) => {
return handleDelete(() => deleteRole(record.id), { content: `是否确定删除 [${record.name}]`, showModal: true })
}
const RoleAddModalRef = ref<InstanceType<typeof RoleAddModal>>()
const RoleAddDrawerRef = ref<InstanceType<typeof RoleAddDrawer>>()
// 新增
const onAdd = () => {
RoleAddModalRef.value?.onAdd()
RoleAddDrawerRef.value?.onAdd()
}
// 修改
const onUpdate = (item: RoleResp) => {
RoleAddModalRef.value?.onUpdate(item.id)
const onUpdate = (record: RoleResp) => {
RoleAddDrawerRef.value?.onUpdate(record.id)
}
const RoleDetailDrawerRef = ref<InstanceType<typeof RoleDetailDrawer>>()
// 详情
const onDetail = (item: RoleResp) => {
RoleDetailDrawerRef.value?.onDetail(item.id)
const onDetail = (record: RoleResp) => {
RoleDetailDrawerRef.value?.onDetail(record.id)
}
</script>

View File

@@ -62,12 +62,12 @@
</template>
</GiTable>
<StorageAddModal ref="StorageAddModalRef" @save-success="search" />
<StorageAddDrawer ref="StorageAddDrawerRef" @save-success="search" />
</div>
</template>
<script setup lang="ts">
import StorageAddModal from './StorageAddModal.vue'
import StorageAddDrawer from './StorageAddDrawer.vue'
import { type StorageQuery, type StorageResp, deleteStorage, listStorage } from '@/apis'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
@@ -130,19 +130,19 @@ const reset = () => {
}
// 删除
const onDelete = (item: StorageResp) => {
return handleDelete(() => deleteStorage(item.id), { content: `是否确定删除存储 [${item.name}]`, showModal: true })
const onDelete = (record: StorageResp) => {
return handleDelete(() => deleteStorage(record.id), { content: `是否确定删除存储 [${record.name}]`, showModal: true })
}
const StorageAddModalRef = ref<InstanceType<typeof StorageAddModal>>()
const StorageAddDrawerRef = ref<InstanceType<typeof StorageAddDrawer>>()
// 新增
const onAdd = () => {
StorageAddModalRef.value?.onAdd()
StorageAddDrawerRef.value?.onAdd()
}
// 修改
const onUpdate = (item: StorageResp) => {
StorageAddModalRef.value?.onUpdate(item.id)
const onUpdate = (record: StorageResp) => {
StorageAddDrawerRef.value?.onUpdate(record.id)
}
</script>

View File

@@ -0,0 +1,196 @@
<template>
<a-drawer
v-model:visible="visible"
title="导入用户"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
ok-text="确认导入"
cancel-text="取消导入"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" size="large" auto-label-width>
<a-alert v-if="!form.disabled" :show-icon="false" style="margin-bottom: 15px">
数据导入请严格按照模板填写格式要求和新增一致
<template #action>
<a-button size="small" type="primary" @click="downloadTemplate">下载模板</a-button>
</template>
</a-alert>
<fieldset>
<legend>1.上传解析文件</legend>
<div class="file-box">
<a-upload draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-butto="false"
:show-cancel-button="false" tip="仅支持xls、xlsx格式"
:file-list="uploadFile"
accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/>
</div>
<div v-if="dataResult.importKey">
<div class="file-box">
<a-space size="large">
<a-statistic title="总计行数" :value="dataResult.totalRows" />
<a-statistic title="正常行数" :value="dataResult.validRows" />
</a-space>
</div>
<div class="file-box">
<a-space size="large">
<a-statistic title="已存在用户" :value="dataResult.duplicateUserRows" />
<a-statistic title="已存在邮箱" :value="dataResult.duplicateEmailRows" />
<a-statistic title="已存在手机" :value="dataResult.duplicatePhoneRows" />
</a-space>
</div>
</div>
</fieldset>
<fieldset>
<legend>2.导入策略</legend>
<a-form-item label="用户已存在" field="duplicateUser">
<a-radio-group v-model="form.duplicateUser" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="3">停止导入</a-radio>
<a-radio :value="2">修改数据</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="邮箱已存在" field="duplicateEmail">
<a-radio-group v-model="form.duplicateEmail" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="3">停止导入</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="手机已存在" field="duplicatePhone">
<a-radio-group v-model="form.duplicatePhone" type="button">
<a-radio :value="1">跳过该行</a-radio>
<a-radio :value="3">停止导入</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="默认状态" field="defaultStatus">
<a-switch
v-model="form.defaultStatus"
:checked-value="1"
:unchecked-value="2"
checked-text="启用"
unchecked-text="禁用"
type="round"
/>
</a-form-item>
</fieldset>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { type UserImportResp, downloadImportUserTemplate, importUser, parseImportUser } from '@/apis'
import { useDownload, useForm } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const formRef = ref<FormInstance>()
const uploadFile = ref([])
const { form, resetForm } = useForm({
errorPolicy: 1,
duplicateUser: 1,
duplicateEmail: 1,
duplicatePhone: 1,
defaultStatus: 1
})
const dataResult = ref<UserImportResp>({
importKey: '',
totalRows: 0,
validRows: 0,
duplicateUserRows: 0,
duplicateEmailRows: 0,
duplicatePhoneRows: 0
})
// 重置
const reset = () => {
formRef.value?.resetFields()
dataResult.value.importKey = ''
uploadFile.value = []
resetForm()
}
const visible = ref(false)
const onImport = () => {
reset()
visible.value = true
}
// 下载模板
const downloadTemplate = () => {
useDownload(() => downloadImportUserTemplate())
}
// 上传解析导入数据
const handleUpload = (options: RequestOption) => {
const controller = new AbortController();
(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
try {
const res = await parseImportUser(formData)
dataResult.value = res.data
Message.success('上传解析成功')
onSuccess(res)
} catch (error) {
onError(error)
}
})()
return {
abort() {
controller.abort()
}
}
}
// 执行导入
const save = async () => {
try {
if (!dataResult.value.importKey) {
return false
}
form.importKey = dataResult.value.importKey
const res = await importUser(form)
Message.success(`导入成功,新增${res.data.insertRows},修改${res.data.updateRows}`)
emit('save-success')
return true
} catch (error) {
return false
}
}
defineExpose({ onImport })
</script>
<style lang="scss" scoped>
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.file-box {
margin-bottom: 20px;
margin-left: 10px;
}
</style>

View File

@@ -33,9 +33,9 @@ const options: Options = {
btns: { hide: true }
}
const columns: Columns = [
const columns: Columns = reactive([
{ label: '密码', field: 'newPassword', type: 'input-password', rules: [{ required: true, message: '请输入密码' }] }
]
])
const { form, resetForm } = useForm({})

View File

@@ -1,67 +0,0 @@
<template>
<a-menu class="right-menu">
<a-menu-item @click="onClick('add')">
<template #icon><icon-plus-circle :size="16" :stroke-width="3" /></template>
<span>新增</span>
</a-menu-item>
<a-menu-item v-permission="['system:dept:update']" @click="onClick('update')">
<template #icon><icon-edit :size="16" :stroke-width="3" /></template>
<span>修改</span>
</a-menu-item>
<a-menu-item v-permission="['system:dept:delete']" :title="data.isSystem ? '系统内置数据不能删除' : undefined" :disabled="data.isSystem" @click="onClick('delete')">
<template #icon><icon-delete :size="16" :stroke-width="3" /></template>
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<script lang="ts" setup>
import type { DeptResp } from '@/apis'
interface Props {
data: DeptResp
}
const props = withDefaults(defineProps<Props>(), {})
const emit = defineEmits<{
(e: 'on-menu-item-click', mode: string, data: DeptResp): void
}>()
// 点击菜单项
const onClick = (mode: string) => {
emit('on-menu-item-click', mode, props.data)
}
</script>
<style lang="scss" scoped>
:deep(.arco-menu-inner) {
padding: 4px;
.arco-menu-item {
height: 34px;
&:not(.arco-menu-selected) {
color: $color-text-1;
}
&:last-child {
margin-bottom: 0;
}
}
}
.right-menu {
width: 120px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 4px;
border: 1px solid var(--color-border-2);
box-sizing: border-box;
.arrow-icon {
margin-right: 0;
}
}
</style>

View File

@@ -9,51 +9,27 @@
<div class="left-tree__tree">
<a-tree
ref="treeRef"
:data="(treeData as unknown as TreeNodeData[])"
:field-names="{ key: 'id' }"
:data="deptList"
show-line
block-node
default-expand-all
:selected-keys="selectedKeys"
@select="select"
>
<template #title="node">
<a-trigger
v-model:popup-visible="node.popupVisible"
trigger="contextMenu"
align-point
animation-name="slide-dynamic-origin"
auto-fit-transform-origin
position="bl"
scroll-to-close
>
<a-tooltip v-if="node.description" :content="node.description" background-color="rgb(var(--primary-6))" position="right">
<div @contextmenu="onContextmenu(node)">{{ node.name }}</div>
</a-tooltip>
<div v-else @contextmenu="onContextmenu(node)">{{ node.name }}</div>
<template #content>
<RightMenu
v-if="has.hasPermOr(['system:dept:update', 'system:dept:delete'])"
:data="node"
@on-menu-item-click="onMenuItemClick"
/>
</template>
</a-trigger>
<template #switcher-icon="node, { isLeaf }">
<IconCaretDown v-if="!isLeaf" />
<IconIdcard v-else />
</template>
</a-tree>
</div>
</div>
</div>
<DeptAddModal ref="DeptAddModalRef" @save-success="getTreeData" />
</template>
<script setup lang="tsx">
import type { Message, Modal, TreeInstance, TreeNodeData } from '@arco-design/web-vue'
import { mapTree } from 'xe-utils'
import DeptAddModal from '../../dept/DeptAddModal.vue'
import RightMenu from './RightMenu.vue'
import { type DeptQuery, type DeptResp, deleteDept, listDept } from '@/apis'
import has from '@/utils/has'
import type { TreeInstance } from '@arco-design/web-vue'
import { ref } from 'vue'
import { useDept } from '@/hooks/app'
interface Props {
placeholder?: string
@@ -71,92 +47,25 @@ const select = (keys: Array<any>) => {
emit('node-click', keys)
}
const queryForm = reactive<DeptQuery>({
sort: ['parentId,asc', 'sort,asc', 'createTime,desc']
})
interface TreeItem extends DeptResp {
popupVisible: boolean
}
const treeRef = ref<TreeInstance>()
const treeData = ref<TreeItem[]>([])
const loading = ref(false)
// 查询树列表
const getTreeData = async (query: DeptQuery = { ...queryForm }) => {
try {
loading.value = true
const { data } = await listDept(query)
treeData.value = mapTree(data, (i) => ({
...i,
popupVisible: false,
switcherIcon: (node: any) => {
if (!node.isLeaf) {
return <icon-caret-down />
}
return <icon-idcard />
}
}))
await nextTick(() => {
const { deptList, getDeptList } = useDept({
onSuccess: () => {
nextTick(() => {
treeRef.value?.expandAll(true)
select([data[0].id])
select([deptList.value[0]?.key])
})
} finally {
loading.value = false
}
}
})
// 树查询
const inputValue = ref('')
watch(inputValue, (val) => {
queryForm.description = val
getTreeData()
getDeptList(val)
})
// 保存当前右键的节点
const contextmenuNode = ref<TreeItem | null>(null)
const onContextmenu = (node: TreeItem) => {
contextmenuNode.value = node
}
// 关闭右键菜单弹框
const closeRightMenuPopup = () => {
if (contextmenuNode.value?.popupVisible) {
contextmenuNode.value.popupVisible = false
}
}
const DeptAddModalRef = ref<InstanceType<typeof DeptAddModal>>()
// 右键菜单项点击
const onMenuItemClick = (mode: string, node: DeptResp) => {
closeRightMenuPopup()
if (mode === 'add') {
DeptAddModalRef.value?.onAdd(node.id)
} else if (mode === 'update') {
DeptAddModalRef.value?.onUpdate(node.id)
} else if (mode === 'delete') {
Modal.warning({
title: '提示',
content: `是否确定删除 [${node.name}]`,
hideCancel: false,
okButtonProps: { status: 'danger' },
onBeforeOk: async () => {
try {
const res = await deleteDept(node.id)
if (res.success) {
Message.success('删除成功')
getTreeData()
}
return res.success
} catch (error) {
return false
}
}
})
}
}
onMounted(() => {
getTreeData()
getDeptList()
})
</script>

View File

@@ -13,21 +13,31 @@
</a-col>
<a-col :xs="24" :sm="16" :md="17" :lg="18" :xl="19" :xxl="20" flex="1" class="h-full ov-hidden">
<GiTable row-key="id" :data="dataList" :columns="columns" :loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
:disabled-column-keys="['username']" @refresh="search">
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
:disabled-column-keys="['username']" @refresh="search">
<template #custom-left>
<a-input v-model="queryForm.description" placeholder="请输入关键词" allow-clear @change="search">
<template #prefix><icon-search /></template>
<template #prefix>
<icon-search />
</template>
</a-input>
<a-select v-model="queryForm.status" :options="DisEnableStatusList" placeholder="请选择状态" allow-clear
style="width: 150px" @change="search" />
style="width: 150px" @change="search" />
<a-button @click="reset">重置</a-button>
</template>
<template #custom-right>
<a-button v-permission="['system:user:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #icon>
<icon-plus />
</template>
<span>新增</span>
</a-button>
<a-button v-permission="['system:user:import']" @click="onImport">
<template #icon>
<icon-upload />
</template>
<span>导入</span>
</a-button>
<a-tooltip content="导出">
<a-button v-permission="['system:user:export']" class="gi_hover_btn-border" @click="onExport">
<template #icon>
@@ -38,7 +48,7 @@
</template>
<template #username="{ record }">
<GiCellAvatar :avatar="getAvatar(record.avatar, record.gender)" :name="record.username" is-link
@click="onDetail(record)" />
@click="onDetail(record)" />
</template>
<template #gender="{ record }">
<GiCellGender :gender="record.gender" />
@@ -57,7 +67,8 @@
<a-space>
<a-link v-permission="['system:user:update']" @click="onUpdate(record)">修改</a-link>
<a-link v-permission="['system:user:delete']" status="danger"
:title="record.isSystem ? '系统内置数据不能删除' : '删除'" :disabled="record.disabled" @click="onDelete(record)">
:title="record.isSystem ? '系统内置数据不能删除' : '删除'" :disabled="record.disabled"
@click="onDelete(record)">
删除
</a-link>
<a-dropdown>
@@ -72,7 +83,8 @@
</a-col>
</a-row>
<UserAddModal ref="UserAddModalRef" @save-success="search" />
<UserAddDrawer ref="UserAddDrawerRef" @save-success="search" />
<UserImportDrawer ref="UserImportDrawerRef" @save-success="search" />
<UserDetailDrawer ref="UserDetailDrawerRef" />
<UserResetPwdModal ref="UserResetPwdModalRef" />
</div>
@@ -80,7 +92,8 @@
<script setup lang="ts">
import DeptTree from './dept/index.vue'
import UserAddModal from './UserAddModal.vue'
import UserAddDrawer from './UserAddDrawer.vue'
import UserImportDrawer from './UserImportDrawer.vue'
import UserDetailDrawer from './UserDetailDrawer.vue'
import UserResetPwdModal from './UserResetPwdModal.vue'
import { type UserQuery, type UserResp, deleteUser, exportUser, listUser } from '@/apis'
@@ -152,9 +165,9 @@ const reset = () => {
}
// 删除
const onDelete = (item: UserResp) => {
return handleDelete(() => deleteUser(item.id), {
content: `是否确定删除 [${item.nickname}(${item.username})]`,
const onDelete = (record: UserResp) => {
return handleDelete(() => deleteUser(record.id), {
content: `是否确定删除 [${record.nickname}(${record.username})]`,
showModal: true
})
}
@@ -170,27 +183,33 @@ const handleSelectDept = (keys: Array<any>) => {
search()
}
const UserAddModalRef = ref<InstanceType<typeof UserAddModal>>()
const UserAddDrawerRef = ref<InstanceType<typeof UserAddDrawer>>()
// 新增
const onAdd = () => {
UserAddModalRef.value?.onAdd()
UserAddDrawerRef.value?.onAdd()
}
const UserImportDrawerRef = ref<InstanceType<typeof UserImportDrawer>>()
// 导入
const onImport = () => {
UserImportDrawerRef.value?.onImport()
}
// 修改
const onUpdate = (item: UserResp) => {
UserAddModalRef.value?.onUpdate(item.id)
const onUpdate = (record: UserResp) => {
UserAddDrawerRef.value?.onUpdate(record.id)
}
const UserDetailDrawerRef = ref<InstanceType<typeof UserDetailDrawer>>()
// 详情
const onDetail = (item: UserResp) => {
UserDetailDrawerRef.value?.onDetail(item.id)
const onDetail = (record: UserResp) => {
UserDetailDrawerRef.value?.onDetail(record.id)
}
const UserResetPwdModalRef = ref<InstanceType<typeof UserResetPwdModal>>()
// 重置密码
const onResetPwd = (item: UserResp) => {
UserResetPwdModalRef.value?.onReset(item.id)
const onResetPwd = (record: UserResp) => {
UserResetPwdModalRef.value?.onReset(record.id)
}
</script>

View File

@@ -95,7 +95,7 @@ const mergeDir = (parent: TreeNodeData) => {
mergeDir(child)
}
}
return ''
return parent.title
}
const pushDir = (children: TreeNodeData[] | undefined, treeNode: TreeNodeData) => {

View File

@@ -60,6 +60,8 @@ export default defineConfig(({ command, mode }) => {
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
}
},
// 以 envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中。
envPrefix: ['VITE', 'FILE']
}
})