184 Commits

Author SHA1 Message Date
dvadmin
3271f00f87 !100 正式发布v3.1.0版本
1.[新增] 文件选择器添加插槽功能
2.[新增] 初次登录必须修改密码功能
3.[新增] 下载中心模块和异步导出功能
4.[新增] 批量删除功能(示例代码在菜单管理里面)
5.[新增] 角色批量授权用户功能
6.[优化] 菜单排序逻辑
7.[优化] 优化列权限逻辑
8.[修复] 允许路由在框架外显示 bug
9.[修复] 用户管理crud无法显示用户头像问题
10.[修复] 字段筛选排除 file文件字段
11.[修复] 更新import_export.py已过滤空数据表
12.[修复] 日期期间条件过滤不包含截止日期当天数据的bug

后续规划
1.审批流插件正在测试中
2.报表插件规划中
3.新版文档规划中
2025-01-07 13:44:34 +00:00
李强
1fe0a89338 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	web/src/views/system/dept/components/DeptUserCom/crud.tsx
#	web/src/views/system/user/crud.tsx
2025-01-07 21:36:48 +08:00
dvadmin
58971a3781 !96 增加角色批量授权用户
Merge pull request !96 from 木子-李/20250103-role
2025-01-07 13:28:08 +00:00
dvadmin
502e1f4d27 !99 修复用户管理crud无法显示用户头像问题
Merge pull request !99 from lxy/cherry-pick-1736165358
2025-01-07 13:26:22 +00:00
阿辉
2015db53ab 用户管理密码设置优化 2025-01-07 17:38:01 +08:00
lxy
73edafb95f 修复没有设置头像的显示异常 2025-01-07 10:30:29 +08:00
lxy
e8f5edd9c3 同步部门管理页面 2025-01-07 09:59:39 +08:00
lxy
2d69633660 修复用户管理无法显示头像问题
(cherry picked commit from <gitee.com//lxy0722/django-vue3-admin/commit/4a01e55143967f0a01d3509804d3ba146724928a>
2025-01-06 12:09:18 +00:00
李小涛
b5c583ba7d feat(20250103-role): 增加角色批量授权用户 2025-01-03 14:41:30 +08:00
dvadmin
21be91a894 !86 【角色管理】-【权限配置】优化
1、优化角色权限分配逻辑
2、采用实时保存,提高速度
2024-12-29 09:32:08 +00:00
dvadmin
a527d367d9 !85 字段筛选排除文件字段 update backend/dvadmin/utils/filters.py.
Merge pull request !85 from lxy/N/A
2024-12-29 09:31:11 +00:00
dvadmin
eeda1bea4a !80 修复: 更新import_export.py以过滤空数据表
Merge pull request !80 from xwz1024/develop
2024-12-29 09:30:09 +00:00
dvadmin
2a0de4f0d3 !88 优化列权限逻辑
Merge pull request !88 from 木子-李/20241226-fieldpermission
2024-12-29 09:28:59 +00:00
李小涛
792a22e606 feat(20241226-fieldpermission): 优化列权限逻辑
1、优化后端合并权限代码
2024-12-26 17:37:08 +08:00
李小涛
6726d0167e feat(20241226-fieldpermission): 优化列权限逻辑
1、优化后端合并权限代码
2024-12-26 10:26:27 +08:00
李小涛
dddafa4826 feat(20241226-fieldpermission): 优化列权限逻辑
1、后端优化,当多个角色的时候,合并列权限配置
2、前端优化,有多级表头时,列权限设置无效的bug
2024-12-26 08:56:19 +08:00
李小涛
db27235f61 refactor(20241225-role): 角色权限分配优化
1、优化角色权限分配逻辑
2、采用实时保存,提高速度
2024-12-25 08:59:54 +08:00
李小涛
282ab9a6a1 refactor(20241225-role): 角色权限分配优化
1、优化角色权限分配逻辑
2、采用实时保存,提高速度
2024-12-25 08:59:25 +08:00
阿辉
15c87ddd26 修复字典管理新增值时is_value错误的bug 2024-12-17 10:58:13 +08:00
阿辉
a36dcfa1e5 下载中心优化 2024-12-16 15:39:13 +08:00
lxy
ba5c2ab490 字段筛选排除文件字段 update backend/dvadmin/utils/filters.py.
url参数过滤将导致报错

Signed-off-by: lxy <46486798@qq.com>
2024-12-06 07:26:22 +00:00
李强
584bf57344 Merge remote-tracking branch 'upstream/develop' 2024-12-05 14:56:41 +08:00
阿辉
0c38343aca 文件选择器添加插槽 2024-12-05 14:37:38 +08:00
阿辉
d8f41919ea 文件选择器添加插槽,修复在下拉框多选情况下数据不同步的bug 2024-12-05 11:56:15 +08:00
阿辉
81d2fac8b6 文件选择器网络链接功能优化 2024-12-04 17:13:20 +08:00
阿辉
69d36cf858 文件选择器网络链接功能优化 2024-12-04 17:11:28 +08:00
阿辉
2701cf9352 文件选择器删除功能控制 2024-12-04 16:03:34 +08:00
阿辉
84b5426932 文件选择器删除功能控制 2024-12-04 16:01:44 +08:00
阿辉
36a8471dd0 文件选择器的url和附件管理的url优化 2024-12-04 15:31:26 +08:00
阿辉
1d02f3e138 文件列表序列化器更新 2024-12-04 15:16:48 +08:00
阿辉
da0f084b0c 文件选择器优化 2024-12-03 17:24:55 +08:00
阿辉
1993fdd509 文件选择器优化 2024-12-03 16:55:09 +08:00
阿辉
9fb5e95a55 文件选择器添加网络资源获取功能 2024-12-03 16:40:01 +08:00
阿辉
706986e187 文件选择器样式优化 2024-12-03 14:36:52 +08:00
阿辉
e3291fe22b 文件选择器删除功能 2024-12-03 14:28:47 +08:00
阿辉
ec137c9f84 文件选择器tab切换相关功能完善 2024-12-03 13:53:06 +08:00
阿辉
1158bbb790 文件选择器内置文件筛选完善 2024-12-03 11:16:11 +08:00
阿辉
e880af6f1e 文件选择器添加系统内置文件筛选(功能未完善) 2024-12-02 19:53:04 +08:00
阿辉
547cc30818 附件管理添加预览 2024-12-02 15:06:35 +08:00
阿辉
d0f562b6ed 文件选择器只获取文件选择器上传的列表 2024-12-02 10:58:02 +08:00
阿辉
ad3a190e96 文件选择器的样式属性传递优化 2024-11-29 15:05:46 +08:00
阿辉
9cc071edcd 添加onSave,onClose和onClosed事件,优化数据保存逻辑:表单会在选择时实时变化但不点确定则不会进行实际数据修改(表单数据会复原)(实时变化是否修改待定) 2024-11-27 16:46:32 +08:00
阿辉
4177f84a62 文件选择器代码错误复原 2024-11-26 21:45:59 +08:00
阿辉
c1db1c21d0 样式优化 2024-11-23 18:40:24 +08:00
阿辉
422f86da22 优化template的结构,包一层,否则唯一标识不会生效 2024-11-23 15:55:43 +08:00
阿辉
dac7ea90ae 文件选择器给modal加class防止被样式污染 2024-11-23 15:35:15 +08:00
阿辉
8ff773af3b 文件选择器的删除按钮优化 2024-11-22 15:54:33 +08:00
阿辉
b6557de0ca 优化 2024-11-21 16:29:32 +08:00
阿辉
4a68bf2f2b 用户初次登录修改密码优化 2024-11-20 17:55:55 +08:00
阿辉
9c370169d3 文件选择器添加类,样式,禁用选项 2024-11-19 18:52:48 +08:00
阿辉
f62f0b440d 添加filelist的crud中对文件选择器的测试代码 2024-11-19 15:00:34 +08:00
阿辉
4a8f907c7f 文件选择器的video类型优化 2024-11-19 15:00:07 +08:00
阿辉
1301346772 文件选择器的audio类型优化 2024-11-19 14:55:43 +08:00
阿辉
012c5a9c9c 修复文件选择器的image表单类型显示错误的bug 2024-11-19 14:50:58 +08:00
阿辉
683b5164aa 修复文件选择器的属性valueKey为id时的选择显示错误的bug 2024-11-19 14:41:34 +08:00
阿辉
3eeea14b97 文件选择器支持audio的表单类型选项,优化部分样式 2024-11-19 14:29:23 +08:00
阿辉
7a9ef47a68 文件选择器支持video的表单类型选项,添加一些错误属性值判断,多选模式有限制 2024-11-19 14:19:48 +08:00
阿辉
495976726e clear优化 2024-11-19 13:48:01 +08:00
阿辉
50bcc4346f clear优化 2024-11-19 13:42:34 +08:00
阿辉
2f4a6e6b1f 文件选择器添加表单类型选项,目前支持selector和image类型 2024-11-19 12:39:11 +08:00
阿辉
836b645507 Merge branch 'develop' of https://e.coding.net/dvadmin-private/code/dvadmin3 into develop 2024-11-15 18:54:17 +08:00
阿辉
ed3e3f12e0 修复文件选择器只有1个tab时默认选中错误的bug 2024-11-15 18:54:15 +08:00
1638245306
7280cbbb68 Merge remote-tracking branch 'origin/develop' into develop 2024-11-15 10:47:38 +08:00
1638245306
848e4a62af refactor(system): 调整菜单排序逻辑
- 将菜单排序方式从 id 改为 sort 字段
- 优化了前端获取当前角色路由的逻辑,提高用户体验
2024-11-15 10:47:25 +08:00
阿辉
98413bf125 文件选择器优化 2024-11-11 18:30:07 +08:00
阿辉
21d61794bc 文件选择器优化 2024-11-10 19:06:36 +08:00
阿辉
72e046fd6d 优化 2024-11-08 23:30:25 +08:00
阿辉
4f85de3247 文件选择器 2024-11-08 23:20:17 +08:00
阿辉
6ad048b86a 附件model更新 2024-11-07 23:08:25 +08:00
阿辉
b9976cc2dd 附件管理的过滤 2024-11-07 23:08:11 +08:00
阿辉
7a453da303 修复头像选择器组件上传时url传值问题 2024-11-07 20:23:41 +08:00
1638245306
421e89823a 1.初次登录必须修改密码 2024-11-06 13:09:09 +08:00
1638245306
436a722ce8 Merge remote-tracking branch 'origin/develop' into develop 2024-11-06 01:39:28 +08:00
1638245306
3ea38a59b7 1.优化登录页面;
2.新增初次登录强制修改密码;
2024-11-06 01:39:20 +08:00
李强
665d675deb Merge remote-tracking branch 'origin/master' 2024-10-28 21:17:12 +08:00
李强
46a1854e24 Merge remote-tracking branch 'origin/develop' into develop 2024-10-28 21:13:07 +08:00
dvadmin-开发-李强
a3c4e02aa1 Accept Merge Request #25: (develop -> master)
Merge Request: 合并

Created By: @dvadmin-开发-李强
Accepted By: @dvadmin-开发-李强
URL: https://dvadmin-private.coding.net/p/code/d/dvadmin3/git/merge/25?initial=true
2024-10-28 21:11:05 +08:00
阿辉
199a75293d 下载中心模块和异步导出功能 2024-10-24 22:08:24 +08:00
1638245306
3f58c1cb7a Merge remote-tracking branch 'origin/develop' into develop 2024-09-25 23:32:42 +08:00
1638245306
2c8b51f463 修复:允许路由在框架外显示 2024-09-25 23:32:35 +08:00
李强
7a752b1803 系统配置支持redis缓存 2024-09-18 21:05:35 +08:00
zhulj
69d23c6f69 修复: 更新import_export.py以过滤空数据表 2024-09-18 15:48:49 +08:00
dvadmin
652ad1355a !71 update web/src/views/system/personal/index.vue.
Merge pull request !71 from lxy/N/A
2024-08-29 01:10:32 +00:00
dvadmin
c0bd8c42a7 !70 update backend/dvadmin/system/views/user.py.
Merge pull request !70 from lxy/N/A
2024-08-29 01:10:01 +00:00
dvadmin
3657878d25 !69 update backend/dvadmin/system/views/message_center.py.
Merge pull request !69 from star/N/A
2024-08-29 01:06:44 +00:00
dvadmin
0e5d343f9f !68 update backend/dvadmin/utils/models.py 处理级联删除
Merge pull request !68 from 老高/N/A
2024-08-29 01:05:57 +00:00
李强
95f7046e49 feat: 线上版本号优化 2024-08-29 09:04:32 +08:00
李强
233774a981 feat: 优化部门权限 2024-08-29 09:04:13 +08:00
李强
47b0ee7fe7 feat: 添加信号signals 2024-08-29 09:03:39 +08:00
李强
b9c6ab6fcd Merge remote-tracking branch 'origin/develop' into develop 2024-08-29 08:33:31 +08:00
李强
a042a65555 feat: 更新package.json依赖 2024-08-29 08:32:23 +08:00
dvadmin
4ca0edc104 !66 更改npm镜像源为npmmirror.com
Merge pull request !66 from 老高/N/A
2024-08-29 00:30:07 +00:00
dvadmin
9c7e8097db !72 修复日期期间条件过滤不包含截止日期当天数据的bug
Merge pull request !72 from 好奇宝宝/N/A
2024-08-29 00:24:46 +00:00
dvadmin
3b2fd573bc !73 修复登录次数没有保存问题 update backend/dvadmin/system/views/login.py.
Merge pull request !73 from lxy/N/A
2024-08-29 00:16:30 +00:00
dvadmin
83a6e41015 !74 解决深色模式显示异常问题
Merge pull request !74 from lxy/cherry-pick-1722331014
2024-08-29 00:15:07 +00:00
dvadmin
f12dcb3f1f !75 修复celery时区问题
Merge pull request !75 from 木子-李/N/A
2024-08-29 00:14:27 +00:00
dvadmin
c11cf378cb !77 dictionary的key改成可选参数避免提示报错
dictionary的key改成可选参数避免提示报错
2024-08-29 00:13:45 +00:00
dvadmin
0764383989 !79 增加批量删除
增加表格多选
运行跨页多选
显示多选数据
移除多选数据
增加批量删除
示例代码在菜单管理里面
2024-08-29 00:13:00 +00:00
李小涛
b6e05c997d feat(20240827_BatchDelete): 增加批量删除
1. 增加表格多选
2. 运行跨页多选
3. 显示多选数据
4. 移除多选数据
5. 增加批量删除
2024-08-27 17:31:29 +08:00
李强
2576de48ec requirements.txt 2024-08-19 09:01:02 +08:00
快点洗澡睡觉吧
787b7b61c9 dictionary的key改成可选参数避免提示报错
Signed-off-by: 快点洗澡睡觉吧 <16840517@qq.com>
2024-08-12 12:45:45 +00:00
木子-李
5cf2eef7ad 修复celery时区问题
Signed-off-by: 木子-李 <1537080775@qq.com>
2024-08-01 14:05:49 +00:00
lxy
1981567c59 fixed 92a17fa from https://gitee.com/lxy0722/django-vue3-admin/pulls/1
解决在深色模式下的背景色显示异常问题
2024-07-30 09:16:55 +00:00
lxy
3979628281 修复登录次数没有保存问题 update backend/dvadmin/system/views/login.py.
修复登录次数没有保存问题

Signed-off-by: lxy <46486798@qq.com>
2024-07-29 03:19:29 +00:00
好奇宝宝
9a8506448f 优化日期期间条件过滤,包含截止日期当前数据
因为创建日期是一个datetime数据类型,直接使用lte不会包含截止日的数据

Signed-off-by: 好奇宝宝 <11259906+haoqibb@user.noreply.gitee.com>
2024-07-25 01:56:55 +00:00
lxy
e7b63a61ba update web/src/views/system/personal/index.vue.
修改密码时,原密码显示明文

Signed-off-by: lxy <46486798@qq.com>
2024-07-25 00:43:46 +00:00
lxy
e106324c70 update backend/dvadmin/system/views/user.py.
在创建用户时, 设置非默认密码,修改密码时提示密码错误,创建用户时调用了多次md5进行存储

Signed-off-by: lxy <46486798@qq.com>
2024-07-25 00:33:24 +00:00
star
a42ccd0e18 update backend/dvadmin/system/views/message_center.py.
消息中心模板类型是按角色、公告、部门时不返回用户信息,系统用户数据量过多时接口查询很慢

Signed-off-by: star <it_gjl@sina.com>
2024-07-24 15:59:40 +00:00
老高
5fc1390598 update backend/dvadmin/utils/models.py 处理级联删除
当模型中指定了`on_delete=models.CASCADE`实现级联软删除关联对象的逻辑。对于一对一、一对多和多对多关系的对象,都会进行软删除处理。

Signed-off-by: 老高 <794071084@qq.com>
2024-07-15 06:59:44 +00:00
老高
1942f1af4e 更改npm镜像源为npmmirror.com
将npm安装命令中的registry从淘宝npm源改为npmmirror.com,原有npm源已经失效

Signed-off-by: 老高 <794071084@qq.com>
2024-07-10 02:22:37 +00:00
dvadmin
c8e235bed6 !63 update backend/dvadmin/utils/field_permission.py.
Merge pull request !63 from lxy/N/A
2024-07-10 00:15:56 +00:00
dvadmin
cf93402763 !62 update backend/dvadmin/utils/middleware.py.
Merge pull request !62 from lxy/N/A
2024-07-10 00:13:49 +00:00
lxy
3dcef90bbe update backend/dvadmin/utils/field_permission.py.
修复一个账号拥有多个角色权限,导致前端某些模块没处理好导致的无权限问题(地区管理、登录日志),合并权限,不再返回多个相同的字段名权限,相同字段名的权限True值保留

Signed-off-by: lxy <46486798@qq.com>
2024-07-09 03:56:45 +00:00
lxy
6f8bae8d5c update backend/dvadmin/utils/middleware.py.
当同一时刻进来多个请求且都没有完成响应时,operation_log_id会保留最后一个进来的ID,导致之前按进来的请求记录到同一个id上,导致日志记录丢失

Signed-off-by: lxy <46486798@qq.com>
2024-07-08 09:33:51 +00:00
dvadmin
888a6f1c63 !61 地区管理
Merge pull request !61 from 木子-李/20240705_area
2024-07-08 00:16:36 +00:00
李小涛
6d587fc1e2 feat(20240705_area): 地区管理
- 优化地区管理:增删改查
- 优化tableSelect组件:增加树形结构和懒加载
2024-07-05 17:24:36 +08:00
李小涛
630ec1e774 feat(20240705_tableSelector): 表格选择组件
- 增加树形结构懒加载
2024-07-05 10:24:07 +08:00
dvadmin
8a17d6f82b !58 update backend/dvadmin/utils/models.py.
Merge pull request !58 from 木子-李/N/A
2024-07-05 00:47:11 +00:00
dvadmin
326149195f !59 update web/src/utils/columnPermission.ts.
Merge pull request !59 from 木子-李/N/A
2024-07-05 00:46:42 +00:00
木子-李
71ca7370e2 update web/src/utils/columnPermission.ts.
Signed-off-by: 木子-李 <1537080775@qq.com>
2024-07-04 14:56:42 +00:00
木子-李
0dcf8ae794 update backend/dvadmin/utils/models.py.
跨models引用模型的时候,


Signed-off-by: 木子-李 <1537080775@qq.com>
2024-07-04 05:51:38 +00:00
dvadmin
9c5fe4f483 !57 update backend/dvadmin/system/views/file_list.py.
Merge pull request !57 from 木子-李/N/A
2024-07-03 14:45:17 +00:00
dvadmin
c9ff9d0716 !56 消息中心
Merge pull request !56 from 木子-李/20240703_mesCenter
2024-07-03 14:44:58 +00:00
木子-李
27c9eff716 update backend/dvadmin/system/views/file_list.py.
修复lssuess问题(https://gitee.com/huge-dream/django-vue3-admin/issues/IA8ROD)
- 附件管理无法打开文件和图片
- 富文本无法打开文件和图片

Signed-off-by: 木子-李 <1537080775@qq.com>
2024-07-03 09:21:24 +00:00
李小涛
de603df07c fix(20240703_mesCenter): 消息中心
- 优化了前端index和crud
- 修复“我的接收”在点击查看,不显示目标内容bug
2024-07-03 15:12:30 +08:00
dvadmin
6793a09b8b !55 update backend/dvadmin/system/views/menu_button.py.
Merge pull request !55 from 木子-李/N/A
2024-07-02 13:20:03 +00:00
dvadmin
f60f84964a !54 列权限管控
Merge pull request !54 from 木子-李/20240701_FieldPermission
2024-07-02 13:19:47 +00:00
木子-李
bba4472009 update backend/dvadmin/system/views/menu_button.py.
Signed-off-by: 木子-李 <1537080775@qq.com>
2024-07-01 10:11:11 +00:00
李小涛
438480b2f1 feat(20240701_FieldPermission): 列权限管控
- 地区管理:增加列权限管控
- 登录日志:增加列权限管控
2024-07-01 15:48:00 +08:00
dvadmin
3129c33adc !53 update backend/dvadmin/system/views/role_menu_button_permission.py.
Merge pull request !53 from 木子-李/N/A
2024-06-30 07:58:15 +00:00
dvadmin
ac0aaefe0f !51 update web/src/views/system/role/components/PermissionComNew/index.vue.
Merge pull request !51 from 木子-李/N/A
2024-06-30 07:57:37 +00:00
dvadmin
ef43dc4900 !50 update web/src/views/system/menu/components/MenuFormCom/index.vue.
Merge pull request !50 from 木子-李/N/A
2024-06-30 07:57:23 +00:00
木子-李
03f467abfa update backend/dvadmin/system/views/role_menu_button_permission.py.
修复列权限BUG

Signed-off-by: 木子-李 <1537080775@qq.com>
2024-06-30 06:46:26 +00:00
木子-李
68d31dc515 update web/src/views/system/role/components/PermissionComNew/index.vue.
修复按钮权限保存失败bug

Signed-off-by: 木子-李 <1537080775@qq.com>
2024-06-30 06:36:50 +00:00
木子-李
9215cfd105 update web/src/views/system/menu/components/MenuFormCom/index.vue.
XEUtils 包重复引用

Signed-off-by: 木子-李 <1537080775@qq.com>
2024-06-30 05:37:10 +00:00
dvadmin
9c765c67e1 !49 优化权限配置
Merge pull request !49 from 木子-李/20240629_role_menu
2024-06-29 14:48:50 +00:00
dvadmin
2fd2c76bdb !48 update web/src/views/system/menu/components/MenuFormCom/index.vue.
Merge pull request !48 from 木子-李/N/A
2024-06-29 14:48:31 +00:00
李小涛
798f9e8a7f feat(20240629_role_menu): 优化权限配置
- 重构权限配置前端页面布局
2024-06-29 22:43:33 +08:00
木子-李
1879d0d2fd update web/src/views/system/menu/components/MenuFormCom/index.vue.
优化在新增修改菜单的时候,父级菜单tree只显示目录

Signed-off-by: 木子-李 <1537080775@qq.com>
2024-06-29 06:49:54 +00:00
李小涛
e1d9f555c8 feat(20240629_role_menu): 优化菜单管理
- 优化在新增修改菜单的时候,父级菜单tree只显示目录
2024-06-29 14:46:37 +08:00
dvadmin
d3c2bbbb5b !47 优化权限配置
Merge pull request !47 from 木子-李/role_menu_20240628
2024-06-29 05:40:48 +00:00
李小涛
453d1e3875 feat(role_menu_20240628): 优化权限配置
- 优化保存菜单按钮错误bug
- 优化非管理员角色给其他角色分配列权限禁用逻辑
- 优化按钮自定义数据权限后端逻辑
2024-06-29 13:30:18 +08:00
李小涛
d03a40d04f feat(role_menu_20240628): 优化权限配置
- 修复非管理员角色给其他角色分配权限的bug
- 修复列权限禁用判断逻辑
- 修复自定义数据权限部门判断逻辑
2024-06-29 11:46:45 +08:00
dvadmin
f2f0e06cd6 !30 在核心标准抽象模型模型中设置插入和更新模型的方法
Merge pull request !30 from pbb/develop
2024-06-29 02:26:49 +00:00
dvadmin
ae74105b74 !46 优化权限配置
Merge pull request !46 from 木子-李/role_menu_20240628
2024-06-28 15:56:38 +00:00
李小涛
314d7d79c2 feat(role_menu_20240628): 优化权限配置,增强用户体验
- 清除后端打印
- 统一变量名
2024-06-28 23:46:26 +08:00
李小涛
c2b0c3f25b feat(role_menu_20240628): 优化权限配置,增强用户体验
- 修复无法保存菜单授权bug
- 列权限增加禁用状态的逻辑判断
2024-06-28 23:35:05 +08:00
李强
4a26e1476a feat: 清除默认账号密码 2024-06-28 21:06:24 +08:00
李强
735f17c2d8 Merge remote-tracking branch 'origin/develop' into develop 2024-06-28 21:04:46 +08:00
dvadmin
47bccac7f9 !44 优化权限配置
Merge pull request !44 from 木子-李/role_per_20240628
2024-06-28 13:03:51 +00:00
dvadmin
f7f35151ac !41 修复登录错误次数写入的BUG
Merge pull request !41 from 好奇宝宝/N/A
2024-06-28 12:58:21 +00:00
dvadmin
b49b1f0cc6 !40 增加邮箱和手机号登录支持
Merge pull request !40 from 好奇宝宝/develop
2024-06-28 12:57:17 +00:00
dvadmin
11ec6fb150 !37 添加copy,Import及Import按钮,修改拼接的URL地址为menu_obj.component_name
Merge pull request !37 from 北风南里/N/A
2024-06-28 12:56:00 +00:00
dvadmin
591360b879 !36 update docker_env/django/Dockerfile.
Merge pull request !36 from 木子-李/N/A
2024-06-28 12:54:58 +00:00
李小涛
8c7e8aee9f feat(role_per_20240628): 优化权限配置
1、点击权限小齿轮,自动带出默认值
2024-06-28 16:22:16 +08:00
李小涛
8554bf18f4 fix(role_per_20240628): 优化权限配置
1、修复第一次选择自定义数据权限无法设置的bug
2024-06-28 16:02:29 +08:00
李小涛
0779cc1a84 fix(role_per_20240628): 优化权限配置
1、修复第一次选择自定义数据权限无法设置的bug
2024-06-28 15:46:44 +08:00
李小涛
1b8a502d66 fix(role_per_20240628): 优化权限配置
1、修复第一次选择自定义数据权限无法设置的bug
2024-06-28 15:36:38 +08:00
李小涛
087d478094 feat(role_per_20240628): 优化权限配置
1、修复首次打开权限配置,按钮小齿轮数据权限不显示bug
2、优化自定义数据权限:在input里面显示当前配置用户已有的权限
3、优化自定义数据权限:在树形选择器中,禁用掉当前登录用户没有权限的部门
2024-06-28 15:19:57 +08:00
猿小天
82d0b19bc2 1.更新并优化的权限管理 2024-06-28 11:20:19 +08:00
好奇宝宝
5cb7ec500c update backend/dvadmin/system/views/login.py.
修复登录错误次数写入的BUG

Signed-off-by: 好奇宝宝 <11259906+haoqibb@user.noreply.gitee.com>
2024-06-28 02:18:39 +00:00
好奇宝宝
7a21f44eab 删除文件 web/public/version-build 2024-06-28 02:15:47 +00:00
好奇宝宝
9383508a85 增加邮箱和手机号登录支持 2024-06-28 10:13:11 +08:00
北风南里
b6a4be25f2 添加copy,Import及Import按钮,修改拼接的URL地址为menu_obj.component_name
原来URL地址是拼接前端的路由地址,这与实际后端地址不匹配

Signed-off-by: 北风南里 <wskaudh@qq.com>
2024-06-26 09:04:54 +00:00
木子-李
7234d2b3e9 update docker_env/django/Dockerfile.
Signed-off-by: 木子-李 <1537080775@qq.com>
2024-06-26 05:53:20 +00:00
dvadmin
14640be036 !35 django-cors-headers依赖升级
Merge pull request !35 from vFeng/fix-update_django-cors-headers
2024-06-26 02:58:55 +00:00
dvadmin
af60e9a0fa !33 增加nginx 不缓存index.html&优化前端写法
Merge pull request !33 from vFeng/develop
2024-06-26 02:58:33 +00:00
周继风
259c51b23c [fix]更新django-cors-headers版本至4.4.0, 现在4.3.0版本的CorsMiddleware中间件会导致SessionMiddleware中无法设置session 2024-06-26 00:00:14 +08:00
周继风
5b60f60b70 [fix]更新django-cors-headers版本至4.4.0, 现在4.3.0版本的CorsMiddleware中间件会导致SessionMiddleware中无法设置session 2024-06-25 23:53:54 +08:00
周继风
38ad2db7a7 [feat]增加nginx不缓存index.html配置,搭配前端版本自动升级功能使用(不加此配置可能会导致用户刷新后还是旧版本) 2024-06-25 10:17:36 +08:00
周继风
0529c2747a [fix]优化前端代码写法 2024-06-25 10:16:11 +08:00
dvadmin
fe5e44913d !31 增加生产环境下前端代码版本判断,当前端版本发生改变时自动更新前端代码至最新版本
Merge pull request !31 from vFeng/develop
2024-06-24 14:46:40 +00:00
周继风
d7edbde434 [feat]增加生产环境中前端代码更新版本后用户端自动升级为最新前端版本 2024-06-24 22:35:17 +08:00
李强
b6c013dad7 feat: 优化登录认证失败刷新页面 2024-06-24 07:22:05 +08:00
猿小天
452bc0a63a 修复BUG: 子角色授权问题 2024-06-23 10:54:40 +08:00
acjzdpbb
275d380fe0 feat: 在核心标准抽象模型模型中设置插入和更新模型的方法 2024-06-23 10:07:48 +08:00
猿小天
354d230c2a 功能变化: 优化个人中心修改密码后强制退出登录状态 2024-06-22 22:53:59 +08:00
猿小天
37a0167193 Merge remote-tracking branch 'origin/develop' into develop 2024-06-22 22:24:25 +08:00
猿小天
6f4f5a771e 功能变化:
1..优化部门管理相关接口;
2024-06-22 22:23:35 +08:00
李强
297c4df74f logo.icns 2024-06-22 21:20:19 +08:00
李强
b1b49aa0db Merge remote-tracking branch 'origin/develop' into develop 2024-06-22 19:51:52 +08:00
1638245306
b1f9faca0f Merge remote-tracking branch 'origin/develop' into develop 2024-06-22 16:10:49 +08:00
1638245306
05f659bc87 修复用户管理的接口 2024-06-22 16:10:42 +08:00
李强
218036c0ea dvadmin3-build 兼容支持 2024-06-22 11:45:20 +08:00
113 changed files with 4884 additions and 2542 deletions

View File

@@ -114,7 +114,7 @@ cd web
# 安装依赖
npm install yarn
yarn install --registry=https://registry.npm.taobao.org
yarn install --registry=https://registry.npmmirror.com
# 启动服务
yarn build

1
backend/.gitignore vendored
View File

@@ -98,4 +98,5 @@ media/
__pypackages__/
package-lock.json
gunicorn.pid
plugins/*
!plugins/__init__.py

View File

@@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

View File

@@ -15,7 +15,7 @@ else:
from celery import Celery
app = Celery(f"application")
app.config_from_object('django.conf:settings')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
platforms.C_FORCE_ROOT = True

View File

@@ -2,6 +2,10 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.db import connection
from django.core.cache import cache
from dvadmin.utils.validator import CustomValidationError
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
def is_tenants_mode():
@@ -68,6 +72,9 @@ def init_dictionary():
:return:
"""
try:
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -88,7 +95,9 @@ def init_system_config():
:return:
"""
try:
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -107,6 +116,9 @@ def refresh_dictionary():
刷新字典配置
:return:
"""
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -122,6 +134,9 @@ def refresh_system_config():
刷新系统配置
:return:
"""
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model
@@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
init_dictionary_data = cache.get(f"init_dictionary")
if not init_dictionary_data:
refresh_dictionary()
return cache.get(f"init_dictionary") or {}
if not settings.DICTIONARY_CONFIG:
refresh_dictionary()
if is_tenants_mode():
@@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
dictionary_config = cache.get(f"init_dictionary")
if not dictionary_config:
refresh_dictionary()
dictionary_config = cache.get(f"init_dictionary")
return dictionary_config.get(key)
dictionary_config = get_dictionary_config(schema_name)
return dictionary_config.get(key)
@@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
children = get_dictionary_values(key, schema_name) or []
for ele in children:
res = get_dictionary_values(key, schema_name) or []
for ele in res.get('children'):
if ele.get("value") == str(name):
return ele.get("label")
return ""
@@ -187,6 +213,11 @@ def get_system_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
init_dictionary_data = cache.get(f"init_system_config")
if not init_dictionary_data:
refresh_system_config()
return cache.get(f"init_system_config") or {}
if not settings.SYSTEM_CONFIG:
refresh_system_config()
if is_tenants_mode():
@@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None):
:param schema_name: 对应系统配置的租户schema_name值
:return:
"""
if dispatch_db_type == 'redis':
system_config = cache.get(f"init_system_config")
if not system_config:
refresh_system_config()
system_config = cache.get(f"init_system_config")
return system_config.get(key)
system_config = get_system_config(schema_name)
return system_config.get(key)
def get_system_config_values_to_dict(key, schema_name=None):
"""
获取系统配置数据并转换为字典 **仅限于数组类型系统配置
:param key: 对应系统配置的key值(字典编号)
:param schema_name: 对应系统配置的租户schema_name值
:return:
"""
values_dict = {}
config_values = get_system_config_values(key, schema_name)
if not isinstance(config_values, list):
raise CustomValidationError("该方式仅限于数组类型系统配置")
for ele in get_system_config_values(key, schema_name):
values_dict[ele.get('key')] = ele.get('value')
return values_dict
def get_system_config_label(key, name, schema_name=None):
"""
获取获取系统配置label值

View File

@@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = []
# ********** 一键导入插件配置开始 **********
# 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心
# from dvadmin_celery.settings import * # celery 异步任务
# from dvadmin3_celery.settings import * # celery 异步任务
# from dvadmin_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -167,19 +167,13 @@
"method": 0
},
{
"name": "查询所有",
"name": "获取所有部门",
"value": "dept:SearchAll",
"api": "/api/system/dept/all_dept/",
"method": 0
},
{
"name": "懒加载查询所有",
"value": "dept:LazySearchAll",
"api": "/api/system/dept/dept_lazy_tree/",
"method": 0
},
{
"name": "头信息",
"name": "部门顶部信息",
"value": "dept:HeaderInfo",
"api": "/api/system/dept/dept_info/",
"method": 0
@@ -678,6 +672,53 @@
"model": "ApiWhiteList"
}
]
},
{
"name": "下载中心",
"icon": "ele-Download",
"sort": 9,
"is_link": false,
"is_catalog": false,
"web_path": "/downloadCenter",
"component": "system/downloadCenter/index",
"component_name": "downloadCenter",
"status": true,
"cache": false,
"visible": true,
"parent": 277,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "Search",
"api": "/api/system/downloadCenter/",
"method": 0
},
{
"name": "详情",
"value": "Retrieve",
"api": "/api/system/downloadCenter/{id}/",
"method": 0
},
{
"name": "新增",
"value": "Create",
"api": "/api/system/downloadCenter/",
"method": 1
},
{
"name": "编辑",
"value": "Update",
"api": "/api/system/downloadCenter/{id}/",
"method": 2
},
{
"name": "删除",
"value": "Delete",
"api": "/api/system/downloadCenter/{id}/",
"method": 3
}
]
}
],
"menu_button": [],

View File

@@ -1,5 +1,7 @@
import hashlib
import os
from time import time
from pathlib import PurePosixPath
from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
@@ -71,6 +73,7 @@ class Users(CoreModel, AbstractUser):
help_text="关联部门",
)
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数")
objects = CustomUserManager()
def set_password(self, raw_password):
@@ -405,6 +408,18 @@ class FileList(CoreModel):
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
UPLOAD_METHOD_CHOIDES = (
(0, '默认上传'),
(1, '文件选择器上传'),
)
upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式')
FILE_TYPE_CHOIDES = (
(0, '图片'),
(1, '视频'),
(2, '音频'),
(3, '其他'),
)
file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型')
def save(self, *args, **kwargs):
if not self.md5sum: # file is new
@@ -595,3 +610,41 @@ class MessageCenterTargetUser(CoreModel):
db_table = table_prefix + "message_center_target_user"
verbose_name = "消息中心目标用户表"
verbose_name_plural = verbose_name
def media_file_name_downloadcenter(instance:'DownloadCenter', filename):
h = instance.md5sum
basename, ext = os.path.splitext(filename)
return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower())
class DownloadCenter(CoreModel):
TASK_STATUS_CHOICES = [
(0, '任务已创建'),
(1, '任务进行中'),
(2, '任务完成'),
(3, '任务失败'),
]
task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称")
task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载')
file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名")
url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True)
size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小")
md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5")
def save(self, *args, **kwargs):
if self.url:
if not self.md5sum: # file is new
md5 = hashlib.md5()
for chunk in self.url.chunks():
md5.update(chunk)
self.md5sum = md5.hexdigest()
if not self.size:
self.size = self.url.size
super(DownloadCenter, self).save(*args, **kwargs)
class Meta:
db_table = table_prefix + "download_center"
verbose_name = "下载中心"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)

View File

@@ -0,0 +1,12 @@
from django.dispatch import Signal
# 初始化信号
pre_init_complete = Signal()
detail_init_complete = Signal()
post_init_complete = Signal()
# 租户初始化信号
pre_tenants_init_complete = Signal()
detail_tenants_init_complete = Signal()
post_tenants_init_complete = Signal()
post_tenants_all_init_complete = Signal()
# 租户创建完成信号
tenants_create_complete = Signal()

View File

@@ -0,0 +1,107 @@
from hashlib import md5
from io import BytesIO
from datetime import datetime
from time import sleep
from openpyxl import Workbook
from openpyxl.worksheet.table import Table, TableStyleInfo
from openpyxl.utils import get_column_letter
from django.core.files.base import ContentFile
from application.celery import app
from dvadmin.system.models import DownloadCenter
def is_number(num):
try:
float(num)
return True
except ValueError:
pass
try:
import unicodedata
unicodedata.numeric(num)
return True
except (TypeError, ValueError):
pass
return False
def get_string_len(string):
"""
获取字符串最大长度
:param string:
:return:
"""
length = 4
if string is None:
return length
if is_number(string):
return length
for char in string:
length += 2.1 if ord(char) > 256 else 1
return round(length, 1) if length <= 50 else 50
@app.task
def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict):
instance = DownloadCenter.objects.get(pk=dcid)
instance.task_status = 1
instance.save()
sleep(2)
try:
wb = Workbook()
ws = wb.active
header_data = ["序号", *export_field_label.values()]
hidden_header = ["#", *export_field_label.keys()]
df_len_max = [get_string_len(ele) for ele in header_data]
row = get_column_letter(len(export_field_label) + 1)
column = 1
ws.append(header_data)
for index, results in enumerate(data):
results_list = []
for h_index, h_item in enumerate(hidden_header):
for key, val in results.items():
if key == h_item:
if val is None or val == "":
results_list.append("")
elif isinstance(val, datetime):
val = val.strftime("%Y-%m-%d %H:%M:%S")
results_list.append(val)
else:
results_list.append(val)
# 计算最大列宽度
result_column_width = get_string_len(val)
if h_index != 0 and result_column_width > df_len_max[h_index]:
df_len_max[h_index] = result_column_width
ws.append([index + 1, *results_list])
column += 1
#  更新列宽
for index, width in enumerate(df_len_max):
ws.column_dimensions[get_column_letter(index + 1)].width = width
tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器
style = TableStyleInfo(
name="TableStyleLight11",
showFirstColumn=True,
showLastColumn=True,
showRowStripes=True,
showColumnStripes=True,
)
tab.tableStyleInfo = style
ws.add_table(tab)
stream = BytesIO()
wb.save(stream)
stream.seek(0)
s = md5()
while True:
chunk = stream.read(1024)
if not chunk:
break
s.update(chunk)
stream.seek(0)
instance.md5sum = s.hexdigest()
instance.file_name = filename
instance.url.save(filename, ContentFile(stream.read()))
instance.task_status = 2
except Exception as e:
instance.task_status = 3
instance.description = str(e)[:250]
instance.save()

View File

@@ -18,6 +18,7 @@ from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermi
from dvadmin.system.views.system_config import SystemConfigViewSet
from dvadmin.system.views.user import UserViewSet
from dvadmin.system.views.menu_field import MenuFieldViewSet
from dvadmin.system.views.download_center import DownloadCenterViewSet
system_url = routers.SimpleRouter()
system_url.register(r'menu', MenuViewSet)
@@ -35,6 +36,8 @@ system_url.register(r'message_center', MessageCenterViewSet)
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
system_url.register(r'column', MenuFieldViewSet)
system_url.register(r'login_log', LoginLogViewSet)
system_url.register(r'download_center', DownloadCenterViewSet)
urlpatterns = [
@@ -44,8 +47,8 @@ urlpatterns = [
path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})),
path('system_config/get_table_data/<int:pk>/', SystemConfigViewSet.as_view({'get': 'get_table_data'})),
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
path('clause/privacy.html', PrivacyView.as_view()),
path('clause/terms_service.html', TermsServiceView.as_view()),

View File

@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
import pypinyin
from django.db.models import Q
from rest_framework import serializers
from dvadmin.system.models import Area
from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -14,13 +16,21 @@ class AreaSerializer(CustomModelSerializer):
"""
pcode_count = serializers.SerializerMethodField(read_only=True)
hasChild = serializers.SerializerMethodField()
pcode_info = serializers.SerializerMethodField()
def get_pcode_info(self, instance):
pcode = Area.objects.filter(code=instance.pcode_id).values("name", "code")
return pcode
def get_pcode_count(self, instance: Area):
return Area.objects.filter(pcode=instance).count()
def get_hasChild(self, instance):
hasChild = Area.objects.filter(pcode=instance.code)
if hasChild:
return True
return False
class Meta:
model = Area
fields = "__all__"
@@ -32,12 +42,24 @@ class AreaCreateUpdateSerializer(CustomModelSerializer):
地区管理 创建/更新时的列化器
"""
def to_internal_value(self, data):
pinyin = ''.join([''.join(i) for i in pypinyin.pinyin(data["name"], style=pypinyin.NORMAL)])
data["level"] = 1
data["pinyin"] = pinyin
data["initials"] = pinyin[0].upper() if pinyin else "#"
pcode = data["pcode"] if 'pcode' in data else None
if pcode:
pcode = Area.objects.get(pk=pcode)
data["pcode"] = pcode.code
data["level"] = pcode.level + 1
return super().to_internal_value(data)
class Meta:
model = Area
fields = '__all__'
class AreaViewSet(CustomModelViewSet):
class AreaViewSet(CustomModelViewSet, FieldPermissionMixin):
"""
地区管理接口
list:查询
@@ -48,21 +70,28 @@ class AreaViewSet(CustomModelViewSet):
"""
queryset = Area.objects.all()
serializer_class = AreaSerializer
create_serializer_class = AreaCreateUpdateSerializer
update_serializer_class = AreaCreateUpdateSerializer
extra_filter_class = []
def get_queryset(self):
def list(self, request, *args, **kwargs):
self.request.query_params._mutable = True
params = self.request.query_params
pcode = params.get('pcode', None)
page = params.get('page', None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
if params and pcode:
queryset = self.queryset.filter(enable=True, pcode=pcode)
else:
known_params = {'page', 'limit', 'pcode'}
# 使用集合操作检查是否有未知参数
other_params_exist = any(param not in known_params for param in params)
if other_params_exist:
queryset = self.queryset.filter(enable=True)
return queryset
else:
pcode = params.get('pcode', None)
params['limit'] = 999
if params and pcode:
queryset = self.queryset.filter(enable=True, pcode=pcode)
else:
queryset = self.queryset.filter(enable=True, level=1)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")

View File

@@ -10,6 +10,7 @@ from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users
from dvadmin.utils.filters import DataLevelPermissionsFilter
from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -124,33 +125,7 @@ class DeptViewSet(CustomModelViewSet):
data = serializer.data
return SuccessResponse(data=data)
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
def dept_lazy_tree(self, request, *args, **kwargs):
parent = self.request.query_params.get('parent')
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Dept.objects.values('id', 'name', 'parent')
else:
role_ids = request.user.role.values_list('id', flat=True)
data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
user_dept_id = request.user.dept.id
dept_list = [user_dept_id]
data_range_list = list(set(data_range))
for item in data_range_list:
if item in [0, 2]:
dept_list = [user_dept_id]
elif item == 1:
dept_list = Dept.recursion_all_dept(dept_id=user_dept_id)
elif item == 3:
dept_list = Dept.objects.values_list('id', flat=True)
elif item == 4:
dept_list = request.user.role.values_list('dept', flat=True)
else:
dept_list = []
queryset = Dept.objects.filter(id__in=dept_list).values('id', 'name', 'parent')
return DetailResponse(data=queryset, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def all_dept(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')

View File

@@ -0,0 +1,49 @@
from rest_framework import serializers
from django.conf import settings
from django_filters.rest_framework import FilterSet, CharFilter
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.system.models import DownloadCenter
class DownloadCenterSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
if self.request.query_params.get('prefix'):
if settings.ENVIRONMENT in ['local']:
prefix = 'http://127.0.0.1:8000'
elif settings.ENVIRONMENT in ['test']:
prefix = 'http://{host}/api'.format(host=self.request.get_host())
else:
prefix = 'https://{host}/api'.format(host=self.request.get_host())
return (f'{prefix}/media/{str(instance.url)}')
return f'media/{str(instance.url)}'
class Meta:
model = DownloadCenter
fields = "__all__"
read_only_fields = ["id"]
class DownloadCenterFilterSet(FilterSet):
task_name = CharFilter(field_name='task_name', lookup_expr='icontains')
file_name = CharFilter(field_name='file_name', lookup_expr='icontains')
class Meta:
model = DownloadCenter
fields = ['task_status', 'task_name', 'file_name']
class DownloadCenterViewSet(CustomModelViewSet):
queryset = DownloadCenter.objects.all()
serializer_class = DownloadCenterSerializer
filter_class = DownloadCenterFilterSet
permission_classes = []
extra_filter_class = []
def get_queryset(self):
if self.request.user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(creator=self.request.user)

View File

@@ -1,12 +1,15 @@
import hashlib
import mimetypes
import django_filters
from django.conf import settings
from django.db import connection
from rest_framework import serializers
from rest_framework.decorators import action
from application import dispatch
from dvadmin.system.models import FileList
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -15,7 +18,16 @@ class FileSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
# return 'media/' + str(instance.url)
if self.request.query_params.get('prefix'):
if settings.ENVIRONMENT in ['local']:
prefix = 'http://127.0.0.1:8000'
elif settings.ENVIRONMENT in ['test']:
prefix = 'http://{host}/api'.format(host=self.request.get_host())
else:
prefix = 'https://{host}/api'.format(host=self.request.get_host())
if instance.file_url:
return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}"
return (f'{prefix}/media/{str(instance.url)}')
return instance.file_url or (f'media/{str(instance.url)}')
class Meta:
@@ -35,6 +47,8 @@ class FileSerializer(CustomModelSerializer):
validated_data['md5sum'] = md5.hexdigest()
validated_data['engine'] = file_engine
validated_data['mime_type'] = file.content_type
ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None)
validated_data['file_type'] = 3 if ft is None else ft
if file_backup:
validated_data['url'] = file
if file_engine == 'oss':
@@ -64,6 +78,22 @@ class FileSerializer(CustomModelSerializer):
return super().create(validated_data)
class FileAllSerializer(CustomModelSerializer):
class Meta:
model = FileList
fields = ['id', 'name']
class FileFilter(django_filters.FilterSet):
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名")
mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型")
class Meta:
model = FileList
fields = ['name', 'mime_type', 'upload_method', 'file_type']
class FileViewSet(CustomModelViewSet):
"""
文件管理接口
@@ -75,5 +105,22 @@ class FileViewSet(CustomModelViewSet):
"""
queryset = FileList.objects.all()
serializer_class = FileSerializer
filter_fields = ['name', ]
permission_classes = []
filter_class = FileFilter
permission_classes = []
@action(methods=['GET'], detail=False)
def get_all(self, request):
data1 = self.get_serializer(self.get_queryset(), many=True).data
data2 = []
if dispatch.is_tenants_mode():
from django_tenants.utils import schema_context
with schema_context('public'):
data2 = self.get_serializer(FileList.objects.all(), many=True).data
return DetailResponse(data=data2+data1)
def list(self, request, *args, **kwargs):
if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode():
from django_tenants.utils import schema_context
with schema_context('public'):
return super().list(request, *args, **kwargs)
return super().list(request, *args, **kwargs)

View File

@@ -4,11 +4,15 @@ from datetime import datetime, timedelta
from captcha.views import CaptchaStore, captcha_image
from django.contrib import auth
from django.contrib.auth import login
from django.contrib.auth.hashers import check_password, make_password
from django.db.models import Q
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
@@ -83,22 +87,30 @@ class LoginSerializer(TokenObtainPairSerializer):
else:
self.image_code and self.image_code.delete()
raise CustomValidationError("图片验证码错误")
user = Users.objects.get(username=attrs['username'])
try:
user = Users.objects.get(
Q(username=attrs['username']) | Q(email=attrs['username']) | Q(mobile=attrs['username']))
except Users.DoesNotExist:
raise CustomValidationError("您登录的账号不存在")
except Users.MultipleObjectsReturned:
raise CustomValidationError("您登录的账号存在多个,请联系管理员检查登录账号唯一性")
if not user.is_active:
raise CustomValidationError("账号已被锁定,联系管理员解锁")
try:
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
attrs['username'] = user.username
data = super().validate(attrs)
data["username"] = self.user.username
data["name"] = self.user.name
data["userId"] = self.user.id
data["avatar"] = self.user.avatar
data['user_type'] = self.user.user_type
data['pwd_change_count'] = self.user.pwd_change_count
dept = getattr(self.user, 'dept', None)
if dept:
data['dept_info'] = {
'dept_id': dept.id,
'dept_name': dept.name,
}
role = getattr(self.user, 'role', None)
if role:
@@ -114,6 +126,7 @@ class LoginSerializer(TokenObtainPairSerializer):
user.login_error_count += 1
if user.login_error_count >= 5:
user.is_active = False
user.save()
raise CustomValidationError("账号已被锁定,联系管理员解锁")
user.save()
count = 5 - user.login_error_count

View File

@@ -7,6 +7,7 @@
@Remark: 按钮权限管理
"""
from dvadmin.system.models import LoginLog
from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -22,7 +23,7 @@ class LoginLogSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class LoginLogViewSet(CustomModelViewSet):
class LoginLogViewSet(CustomModelViewSet, FieldPermissionMixin):
"""
登录日志接口
list:查询
@@ -33,4 +34,4 @@ class LoginLogViewSet(CustomModelViewSet):
"""
queryset = LoginLog.objects.all()
serializer_class = LoginLogSerializer
extra_filter_class = []
# extra_filter_class = []

View File

@@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet):
"""用于前端获取当前角色的路由"""
user = request.user
if user.is_superuser:
queryset = self.queryset.filter(status=1)
queryset = self.queryset.filter(status=1).order_by("sort")
else:
role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
queryset = Menu.objects.filter(id__in=menu_list)
queryset = Menu.objects.filter(id__in=menu_list).order_by("sort")
serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")

View File

@@ -10,12 +10,14 @@ from django.db.models import F
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission, Menu
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class MenuButtonSerializer(CustomModelSerializer):
"""
菜单按钮-序列化器
@@ -92,17 +94,15 @@ class MenuButtonViewSet(CustomModelViewSet):
"""
menu_obj = Menu.objects.filter(id=request.data['menu']).first()
result_list = [
{'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api{menu_obj.web_path}/',
'method': 1},
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api{menu_obj.web_path}/{{id}}/',
'method': 3},
{'menu': menu_obj.id, 'name': '修改', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api{menu_obj.web_path}/{{id}}/',
'method': 2},
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api{menu_obj.web_path}/',
'method': 0},
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api{menu_obj.web_path}/{{id}}/',
'method': 0}]
{'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 3},
{'menu': menu_obj.id, 'name': '编辑', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 2},
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api/{menu_obj.component_name}/', 'method': 0},
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 0},
{'menu': menu_obj.id, 'name': '复制', 'value': f'{menu_obj.component_name}:Copy', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
{'menu': menu_obj.id, 'name': '导入', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api/{menu_obj.component_name}/import_data/', 'method': 1},
{'menu': menu_obj.id, 'name': '导出', 'value': f'{menu_obj.component_name}:Export', 'api': f'/api{menu_obj.component_name}/export_data/', 'method': 1},]
serializer = self.get_serializer(data=result_list, many=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return SuccessResponse(serializer.data, msg="批量创建成功")
return SuccessResponse(serializer.data, msg="批量创建成功")

View File

@@ -36,6 +36,8 @@ class MessageCenterSerializer(CustomModelSerializer):
return serializer.data
def get_user_info(self, instance, parsed_query):
if instance.target_type in (1,2,3):
return []
users = instance.target_user.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
@@ -80,6 +82,9 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
"""
目标用户序列化器-序列化器
"""
role_info = DynamicSerializerMethodField()
user_info = DynamicSerializerMethodField()
dept_info = DynamicSerializerMethodField()
is_read = serializers.SerializerMethodField()
def get_is_read(self, instance):
@@ -90,6 +95,44 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
return queryset.is_read
return False
def get_role_info(self, instance, parsed_query):
roles = instance.target_role.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
from dvadmin.system.views.role import RoleSerializer
serializer = RoleSerializer(
roles,
many=True,
parsed_query=parsed_query
)
return serializer.data
def get_user_info(self, instance, parsed_query):
if instance.target_type in (1,2,3):
return []
users = instance.target_user.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
from dvadmin.system.views.user import UserSerializer
serializer = UserSerializer(
users,
many=True,
parsed_query=parsed_query
)
return serializer.data
def get_dept_info(self, instance, parsed_query):
dept = instance.target_dept.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
from dvadmin.system.views.dept import DeptSerializer
serializer = DeptSerializer(
dept,
many=True,
parsed_query=parsed_query
)
return serializer.data
class Meta:
model = MessageCenter
fields = "__all__"

View File

@@ -26,6 +26,12 @@ class RoleSerializer(CustomModelSerializer):
"""
角色-序列化器
"""
users = serializers.SerializerMethodField()
@staticmethod
def get_users(instance):
users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name')
return users
class Meta:
model = Role
@@ -116,3 +122,23 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
create_serializer_class = RoleCreateUpdateSerializer
update_serializer_class = RoleCreateUpdateSerializer
search_fields = ['name', 'key']
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_users(self, request, pk):
"""
设置 角色-用户
:param request:
:return:
"""
data = request.data
direction = data.get('direction')
movedKeys = data.get('movedKeys')
role = Role.objects.get(pk=pk)
if direction == "left":
# left : 移除用户权限
role.users_set.remove(*movedKeys)
else:
# right : 添加用户权限
role.users_set.add(*movedKeys)
serializer = RoleSerializer(role)
return DetailResponse(data=serializer.data, msg="更新成功")

View File

@@ -6,22 +6,20 @@
@Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理
"""
from django.db.models import F, Subquery, OuterRef, Exists
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \
MenuField
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \
MenuField, FieldPermission
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
"""
菜单按钮-序列化
角色-菜单-按钮-权限 查询序列化
"""
class Meta:
@@ -32,7 +30,7 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化
角色-菜单-按钮-权限 创建/修改序列化
"""
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
@@ -43,63 +41,99 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class RoleButtonPermissionSerializer(CustomModelSerializer):
class RoleMenuSerializer(CustomModelSerializer):
"""
角色按钮权限
角色-菜单 序列化
"""
isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
data = self.request.data
return RoleMenuPermission.objects.filter(
menu_id=instance.id,
role_id=params.get('roleId', data.get('roleId')),
).exists()
class Meta:
model = Menu
fields = ["id", "name", "parent", "is_catalog", "isCheck"]
class RoleMenuButtonSerializer(CustomModelSerializer):
"""
角色-菜单-按钮 序列化
"""
isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
role_menu_btn_perm_id = serializers.SerializerMethodField()
dept = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
data = self.request.data
return RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
menu_button_id=instance.id,
role_id=params.get('roleId', data.get('roleId')),
).exists()
def get_data_range(self, instance):
params = self.request.query_params
obj = RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).first()
obj = self.get_role_menu_btn_prem(instance)
if obj is None:
return None
return obj.data_range
def get_role_menu_btn_perm_id(self, instance):
obj = self.get_role_menu_btn_prem(instance)
if obj is None:
return None
return obj.id
def get_dept(self, instance):
obj = self.get_role_menu_btn_prem(instance)
if obj is None:
return None
return obj.dept.all().values_list('id', flat=True)
def get_role_menu_btn_prem(self, instance):
params = self.request.query_params
data = self.request.data
obj = RoleMenuButtonPermission.objects.filter(
menu_button_id=instance.id,
role_id=params.get('roleId', data.get('roleId')),
).first()
return obj
class Meta:
model = MenuButton
fields = ['id', 'name', 'value', 'isCheck', 'data_range']
class RoleFieldPermissionSerializer(CustomModelSerializer):
class Meta:
model = FieldPermission
fields = "__all__"
fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept']
class RoleMenuFieldSerializer(CustomModelSerializer):
"""
角色-菜单-字段 序列化
"""
is_query = serializers.SerializerMethodField()
is_create = serializers.SerializerMethodField()
is_update = serializers.SerializerMethodField()
def get_is_query(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
if queryset:
return queryset.is_query
return False
def get_is_create(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
if queryset:
return queryset.is_create
return False
def get_is_update(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
if queryset:
return queryset.is_update
return False
@@ -109,54 +143,6 @@ class RoleMenuFieldSerializer(CustomModelSerializer):
fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update']
class RoleMenuSerializer(CustomModelSerializer):
menus = serializers.SerializerMethodField()
def get_menus(self, instance):
menu_list = Menu.objects.filter(parent=instance['id']).values('id', 'name')
serializer = RoleMenuPermissionSerializer(menu_list, many=True, request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id', 'name', 'menus']
class RoleMenuPermissionSerializer(CustomModelSerializer):
"""
菜单和按钮权限
"""
# name = serializers.SerializerMethodField()
isCheck = serializers.SerializerMethodField()
btns = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField()
# def get_name(self, instance):
# parent_list = Menu.get_all_parent(instance['id'])
# names = [d["name"] for d in parent_list]
# return "/".join(names)
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuPermission.objects.filter(
menu__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_btns(self, instance):
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
serializer = RoleButtonPermissionSerializer(btn_list, many=True, request=self.request)
return serializer.data
def get_columns(self, instance):
col_list = MenuField.objects.filter(menu=instance['id'])
serializer = RoleMenuFieldSerializer(col_list, many=True, request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id', 'name', 'isCheck', 'btns', 'columns']
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
"""
菜单按钮接口
@@ -173,204 +159,102 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
extra_filter_class = []
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_role_premission(self, request):
def get_role_menu(self, request):
"""
角色授权获取:
:param request: role
:return: menu,btns,columns
获取 角色-菜单
:param request:
:return:
"""
menu_queryset = Menu.objects.all()
serializer = RoleMenuSerializer(menu_queryset, many=True, request=request)
return DetailResponse(data=serializer.data)
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu(self, request):
"""
设置 角色-菜单
:param request:
:return:
"""
data = request.data
roleId = data.get('roleId')
menuId = data.get('menuId')
isCheck = data.get('isCheck')
if isCheck:
# 添加权限:创建关联记录
instance = RoleMenuPermission.objects.create(role_id=roleId, menu_id=menuId)
else:
# 删除权限:移除关联记录
RoleMenuPermission.objects.filter(role_id=roleId, menu_id=menuId).delete()
menu_instance = Menu.objects.get(id=menuId)
serializer = RoleMenuSerializer(menu_instance, request=request)
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_role_menu_btn_field(self, request):
"""
获取 角色-菜单-按钮-列字段
:param request:
:return:
"""
params = request.query_params
role = params.get('role', None)
if role is None:
return ErrorResponse(msg="未获取到角色信息")
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all()
else:
role_id = request.user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True)
queryset = Menu.objects.filter(status=1, is_catalog=True, id__in=menu_list).values('name', 'id').all()
serializer = RoleMenuSerializer(queryset, many=True, request=request)
data = serializer.data
return DetailResponse(data=data)
# data = []
# if is_superuser:
# queryset = Menu.objects.filter(status=1, is_catalog=False).values('name', 'id').all()
# else:
# role_id = request.user.role.values_list('id', flat=True)
# menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True)
# queryset = Menu.objects.filter(status=1, is_catalog=False, id__in=menu_list).values('name', 'id')
# for item in queryset:
# parent_list = Menu.get_all_parent(item['id'])
# names = [d["name"] for d in parent_list]
# completeName = "/".join(names)
# isCheck = RoleMenuPermission.objects.filter(
# menu__id=item['id'],
# role__id=role,
# ).exists()
# mbCheck = RoleMenuButtonPermission.objects.filter(
# menu_button=OuterRef("pk"),
# role__id=role,
# )
# btns = MenuButton.objects.filter(
# menu__id=item['id'],
# ).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',
# data_range=F('menu_button_permission__data_range'))
# dicts = {
# 'name': completeName,
# 'id': item['id'],
# 'isCheck': isCheck,
# 'btns': btns,
#
# }
# data.append(dicts)
# return DetailResponse(data=data)
menuId = params.get('menuId', None)
menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId)
menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request)
menu_field_queryset = MenuField.objects.filter(menu_id=menuId)
menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request)
return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data})
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_premission(self, request, pk):
def set_role_menu_field(self, request, pk):
"""
角色菜单和按钮及按钮范围授权:
:param request:
:param pk: role
:return:
设置 角色-菜单-列字段
"""
body = request.data
RoleMenuPermission.objects.filter(role=pk).delete()
RoleMenuButtonPermission.objects.filter(role=pk).delete()
for item in body:
for menu in item["menus"]:
if menu.get('isCheck'):
menu_parent = Menu.get_all_parent(menu.get('id'))
role_menu_permission_list = []
for d in menu_parent:
role_menu_permission_list.append(RoleMenuPermission(role_id=pk, menu_id=d["id"]))
RoleMenuPermission.objects.bulk_create(role_menu_permission_list)
# RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
for btn in menu.get('btns'):
if btn.get('isCheck'):
data_range = btn.get('data_range', 0) or 0
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),
data_range=data_range)
instance.dept.set(btn.get('dept', []))
for col in menu.get('columns'):
FieldPermission.objects.update_or_create(role_id=pk, field_id=col.get('id'),
defaults={
'is_query': col.get('is_query'),
'is_create': col.get('is_create'),
'is_update': col.get('is_update')
})
return DetailResponse(msg="授权成功")
data = request.data
for col in data:
FieldPermission.objects.update_or_create(
role_id=pk, field_id=col.get('id'),
defaults={
'is_create': col.get('is_create'),
'is_update': col.get('is_update'),
'is_query': col.get('is_query'),
})
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_menu_get_button(self, request):
"""
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
:param request:
:return:
"""
if params := request.query_params:
if menu_id := params.get('menu', None):
is_superuser = request.user.is_superuser
if is_superuser:
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
else:
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__menu=menu_id
).values(btn_id=F('menu_button__id'), name=F('menu_button__name'))
return DetailResponse(data=queryset)
return ErrorResponse(msg="参数错误")
return DetailResponse(data=[], msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def data_scope(self, request):
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu_btn(self, request):
"""
获取数据权限范围:角色授权页面使用
:param request:
:return:
设置 角色-菜单-按钮
"""
is_superuser = request.user.is_superuser
if is_superuser:
data = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
]
return DetailResponse(data=data)
data = request.data
isCheck = data.get('isCheck', None)
roleId = data.get('roleId', None)
btnId = data.get('btnId', None)
if isCheck:
# 添加权限:创建关联记录
RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId)
else:
data = []
role_list = request.user.role.values_list('id', flat=True)
if params := request.query_params:
if menu_button_id := params.get('menu_button', None):
role_queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__id=menu_button_id
).values_list('data_range', flat=True)
data_range_list = list(set(role_queryset))
for item in data_range_list:
if item == 0:
data = [{
"value": 0,
"label": '仅本人数据权限'
}]
elif item == 1:
data = [{
"value": 0,
"label": '仅本人数据权限'
}, {
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 2:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 3:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 3,
"label": '全部数据权限'
}, ]
elif item == 4:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}]
else:
data = []
return DetailResponse(data=data)
return ErrorResponse(msg="参数错误")
# 删除权限:移除关联记录
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()
menu_btn_instance = MenuButton.objects.get(id=btnId)
serializer = RoleMenuButtonSerializer(menu_btn_instance, request=request)
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu_btn_data_range(self, request):
"""
设置 角色-菜单-按钮-权限
"""
data = request.data
instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id'))
instance.data_range = data.get('data_range')
instance.dept.add(*data.get('dept'))
if not data.get('dept'):
instance.dept.clear()
instance.save()
serializer = RoleMenuButtonPermissionSerializer(instance, request=request)
return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_dept_all(self, request):
@@ -379,72 +263,20 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
:param request:
:return:
"""
params = request.query_params
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Dept.objects.values('id', 'name', 'parent')
else:
if not params:
return ErrorResponse(msg="参数错误")
menu_button = params.get('menu_button')
if menu_button is None:
return ErrorResponse(msg="参数错误")
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values(
dept_id=F('dept__id'),
name=F('dept__name'),
parent=F('dept__parent')
)
return DetailResponse(data=queryset)
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def menu_to_button(self, request):
"""
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
menu_id = params.get('menu', None)
if menu_id is None:
return ErrorResponse(msg="未获取到参数")
is_superuser = request.user.is_superuser
if is_superuser:
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
else:
if params:
# 当前登录用户的角色
role_list = request.user.role.values_list('id', flat=True)
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
return ErrorResponse(msg="未获取到参数")
menu_button_id = params.get('menu_button')
# 当前登录用户角色可以分配的自定义部门权限
dept_checked_disabled = RoleMenuButtonPermission.objects.filter(
role_id__in=role_list, menu_button_id=menu_button_id
).values_list('dept', flat=True)
dept_list = Dept.objects.values('id', 'name', 'parent')
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_menu(self, request):
"""
获取角色对应的按钮权限
:param request:
:return:
"""
params = request.query_params
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
return DetailResponse(data=queryset)
data = []
for dept in dept_list:
dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled
data.append(dept)
return DetailResponse(data=data)

View File

@@ -119,7 +119,6 @@ class UserUpdateSerializer(CustomModelSerializer):
"""
更改激活状态
"""
print(111, value)
if value:
self.initial_data["login_error_count"] = 0
return value
@@ -287,6 +286,7 @@ class UserViewSet(CustomModelViewSet):
"dept": user.dept_id,
"is_superuser": user.is_superuser,
"role": user.role.values_list('id', flat=True),
"pwd_change_count":user.pwd_change_count
}
if hasattr(connection, 'tenant'):
result['tenant_id'] = connection.tenant and connection.tenant.id
@@ -320,7 +320,6 @@ class UserViewSet(CustomModelViewSet):
"""密码修改"""
data = request.data
old_pwd = data.get("oldPassword")
print(old_pwd)
new_pwd = data.get("newPassword")
new_pwd2 = data.get("newPassword2")
if old_pwd is None or new_pwd is None or new_pwd2 is None:
@@ -331,13 +330,33 @@ class UserViewSet(CustomModelViewSet):
if not verify_password:
old_pwd_md5 = hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest()
verify_password = check_password(str(old_pwd_md5), request.user.password)
# 创建用户时、自定义密码无法修改问题
if not verify_password:
old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest()
verify_password = check_password(str(old_pwd_md5), request.user.password)
if verify_password:
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.password = make_password(new_pwd)
request.user.pwd_change_count += 1
request.user.save()
return DetailResponse(data=None, msg="修改成功")
else:
return ErrorResponse(msg="旧密码不正确")
@action(methods=["post"], detail=False, permission_classes=[IsAuthenticated])
def login_change_password(self, request, *args, **kwargs):
"""初次登录进行密码修改"""
data = request.data
new_pwd = data.get("password")
new_pwd2 = data.get("password_regain")
if new_pwd != new_pwd2:
return ErrorResponse(msg="两次密码不匹配")
else:
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.pwd_change_count += 1
request.user.save()
return DetailResponse(data=None, msg="修改成功")
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
def reset_to_default_password(self, request,pk):
"""恢复默认密码"""
@@ -407,9 +426,12 @@ class UserViewSet(CustomModelViewSet):
queryset = self.filter_queryset(self.get_queryset())
else:
queryset = self.filter_queryset(self.get_queryset())
# print(queryset.values('id','name','dept__id'))
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
# print(serializer.data)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")

View File

@@ -5,34 +5,39 @@ from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import FieldPermission, MenuField
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.models import get_custom_app_models
def merge_permission(data):
"""
合并权限
"""
result = {}
for item in data:
field_name = item.pop('field_name')
if field_name not in result:
result[field_name] = item
else:
for key, value in item.items():
result[field_name][key] = result[field_name][key] or value
return result
class FieldPermissionMixin:
@action(methods=['get'], detail=False,permission_classes=[IsAuthenticated])
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def field_permission(self, request):
"""
获取字段权限
"""
finded = False
for model in get_custom_app_models():
if model['object'] is self.serializer_class.Meta.model:
finded = True
break
if finded:
break
if finded is False:
return []
model = self.serializer_class.Meta.model.__name__
user = request.user
if user.is_superuser==1:
data = MenuField.objects.filter( model=model['model']).values('field_name')
for item in data:
item['is_create'] = True
item['is_query'] = True
item['is_update'] = True
# 创建一个默认字典来存储最终的结果
if user.is_superuser == 1:
data = MenuField.objects.filter(model=model).values('field_name')
result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data}
else:
roles = request.user.role.values_list('id', flat=True)
data= FieldPermission.objects.filter(
field__model=model['model'],role__in=roles
).values( 'is_create', 'is_query', 'is_update',field_name=F('field__field_name'))
return DetailResponse(data=data)
data = FieldPermission.objects.filter(
field__model=model, role__in=roles
).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name'))
result = merge_permission(data)
return DetailResponse(data=result)

View File

@@ -37,11 +37,11 @@ class CoreModelFilterBankend(BaseFilterBackend):
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q()
if create_datetime_after and create_datetime_before:
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=create_datetime_before)
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
elif create_datetime_after:
create_filter &= Q(create_datetime__gte=create_datetime_after)
elif create_datetime_before:
create_filter &= Q(create_datetime__lte=create_datetime_before)
create_filter &= Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
# 更新时间范围过滤条件
update_filter = Q()
@@ -340,7 +340,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
from timezone_field import TimeZoneField
# 不进行 过滤的model 类
if isinstance(field, (models.JSONField, TimeZoneField)):
if isinstance(field, (models.JSONField, TimeZoneField, models.FileField)):
continue
# warn if the field doesn't exist.
if field is None:

View File

@@ -86,4 +86,5 @@ def import_to_data(file_url, field_data, m2m_fields=None):
else:
array[key] = cell_value
tables.append(array)
return tables
data = [i for i in tables if len(i) != 0]
return data

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import datetime
from urllib.parse import quote
from django.db import transaction
@@ -11,8 +12,10 @@ from rest_framework.decorators import action
from rest_framework.request import Request
from dvadmin.utils.import_export import import_to_data
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.request_util import get_verbose_name
from dvadmin.system.tasks import async_export_data
from dvadmin.system.models import DownloadCenter
class ImportSerializerMixin:
@@ -301,6 +304,16 @@ class ExportSerializerMixin:
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
data = self.export_serializer_class(queryset, many=True, request=request).data
try:
async_export_data.delay(
data,
str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"),
DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务', dept_belong_id=request.user.dept_id).pk,
self.export_field_label
)
return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载")
except:
pass
# 导出excel 表
response = HttpResponse(content_type="application/msexcel")
response["Access-Control-Expose-Headers"] = f"Content-Disposition"

View File

@@ -32,6 +32,14 @@ class ApiLoggingMiddleware(MiddlewareMixin):
request.request_path = get_request_path(request)
def __handle_response(self, request, response):
# 判断有无log_id属性使用All记录时会出现此情况
if request.request_data.get('log_id', None) is None:
return
# 移除log_id不记录此ID
log_id = request.request_data.pop('log_id')
# request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性
body = getattr(request, 'request_data', {})
# 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数)
@@ -60,7 +68,7 @@ class ApiLoggingMiddleware(MiddlewareMixin):
'status': True if response.data.get('code') in [2000, ] else False,
'json_result': {"code": response.data.get('code'), "msg": response.data.get('msg')},
}
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=self.operation_log_id)
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=log_id)
if not operation_log.request_modular and settings.API_MODEL_MAP.get(request.request_path, None):
operation_log.request_modular = settings.API_MODEL_MAP[request.request_path]
operation_log.save()
@@ -71,7 +79,8 @@ class ApiLoggingMiddleware(MiddlewareMixin):
if self.methods == 'ALL' or request.method in self.methods:
log = OperationLog(request_modular=get_verbose_name(view_func.cls.queryset))
log.save()
self.operation_log_id = log.id
# self.operation_log_id = log.id
request.request_data['log_id'] = log.id
return

View File

@@ -6,13 +6,14 @@
@Created on: 2021/5/31 031 22:08
@Remark: 公共基础model类
"""
from datetime import datetime
from importlib import import_module
from django.apps import apps
from django.db import models
from django.conf import settings
from application import settings
from django.apps import apps
from django.conf import settings
from django.db import models
from rest_framework.request import Request
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
@@ -60,8 +61,24 @@ class SoftDeleteModel(models.Model):
"""
重写删除方法,直接开启软删除
"""
self.is_deleted = True
self.save(using=using)
if soft_delete:
self.is_deleted = True
self.save(using=using)
# 级联软删除关联对象
for related_object in self._meta.related_objects:
related_model = getattr(self, related_object.get_accessor_name())
# 处理一对多和多对多的关联对象
if related_object.one_to_many or related_object.many_to_many:
related_objects = related_model.all()
elif related_object.one_to_one:
related_objects = [related_model]
else:
continue
for obj in related_objects:
obj.delete(soft_delete=True)
else:
super().delete(using=using, *args, **kwargs)
class CoreModel(models.Model):
@@ -87,6 +104,111 @@ class CoreModel(models.Model):
verbose_name = '核心模型'
verbose_name_plural = verbose_name
def get_request_user(self, request: Request):
if getattr(request, "user", None):
return request.user
return None
def get_request_user_id(self, request: Request):
if getattr(request, "user", None):
return getattr(request.user, "id", None)
return None
def get_request_user_name(self, request: Request):
if getattr(request, "user", None):
return getattr(request.user, "name", None)
return None
def get_request_user_username(self, request: Request):
if getattr(request, "user", None):
return getattr(request.user, "username", None)
return None
def common_insert_data(self, request: Request):
data = {
'create_datetime': datetime.now(),
'creator': self.get_request_user(request)
}
return {**data, **self.common_update_data(request)}
def common_update_data(self, request: Request):
return {
'update_datetime': datetime.now(),
'modifier': self.get_request_user_username(request)
}
exclude_fields = [
'_state',
'pk',
'id',
'create_datetime',
'update_datetime',
'creator',
'creator_id',
'creator_pk',
'creator_name',
'modifier',
'modifier_id',
'modifier_pk',
'modifier_name',
'dept_belong_id',
]
def get_exclude_fields(self):
return self.exclude_fields
def get_all_fields(self):
return self._meta.fields
def get_all_fields_names(self):
return [field.name for field in self.get_all_fields()]
def get_need_fields_names(self):
return [field.name for field in self.get_all_fields() if field.name not in self.exclude_fields]
def to_data(self):
"""将模型转化为字典(去除不包含字段)(注意与to_dict_data区分)。
"""
res = {}
for field in self.get_need_fields_names():
field_value = getattr(self, field)
res[field] = field_value.id if (issubclass(field_value.__class__, CoreModel)) else field_value
return res
@property
def DATA(self):
return self.to_data()
def to_dict_data(self):
"""需要导出的字段去除不包含字段注意与to_data区分
"""
return {field: getattr(self, field) for field in self.get_need_fields_names()}
@property
def DICT_DATA(self):
return self.to_dict_data()
def insert(self, request):
"""插入模型
"""
assert self.pk is None, f'模型{self.__class__.__name__}还没有保存到数据中不能手动指定ID'
validated_data = {**self.common_insert_data(request), **self.DICT_DATA}
return self.__class__._default_manager.create(**validated_data)
def update(self, request, update_data: dict[str, any] = None):
"""更新模型
"""
assert isinstance(update_data, dict), 'update_data必须为字典'
validated_data = {**self.common_insert_data(request), **update_data}
for key, value in validated_data.items():
# 不允许修改id,pk,uuid字段
if key in ['id', 'pk', 'uuid']:
continue
if hasattr(self, key):
setattr(self, key, value)
self.save()
return self
def get_all_models_objects(model_name=None):
"""
@@ -97,16 +219,9 @@ def get_all_models_objects(model_name=None):
if not settings.ALL_MODELS_OBJECTS:
all_models = apps.get_models()
for item in list(all_models):
table = {
"tableName": item._meta.verbose_name,
"table": item.__name__,
"tableFields": []
}
table = {"tableName": item._meta.verbose_name, "table": item.__name__, "tableFields": []}
for field in item._meta.fields:
fields = {
"title": field.verbose_name,
"field": field.name
}
fields = {"title": field.verbose_name, "field": field.name}
table['tableFields'].append(fields)
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
if model_name:
@@ -117,25 +232,20 @@ def get_all_models_objects(model_name=None):
def get_model_from_app(app_name):
"""获取模型里的字段"""
model_module = import_module(app_name + '.models')
exclude_models = getattr(model_module, 'exclude_models', [])
filter_model = [
getattr(model_module, item) for item in dir(model_module)
if item != 'CoreModel' and issubclass(getattr(model_module, item).__class__, models.base.ModelBase)
value for key, value in model_module.__dict__.items()
if key != 'CoreModel'
and isinstance(value, type)
and issubclass(value, models.Model)
and key not in exclude_models
]
model_list = []
for model in filter_model:
if model.__name__ == 'AbstractUser':
continue
fields = [
{'title': field.verbose_name, 'name': field.name, 'object': field}
for field in model._meta.fields
]
model_list.append({
'app': app_name,
'verbose': model._meta.verbose_name,
'model': model.__name__,
'object': model,
'fields': fields
})
fields = [{'title': field.verbose_name, 'name': field.name, 'object': field} for field in model._meta.fields]
model_list.append({'app': app_name, 'verbose': model._meta.verbose_name, 'model': model.__name__, 'object': model, 'fields': fields})
return model_list

View File

@@ -44,6 +44,35 @@ class AnonymousUserPermission(BasePermission):
return True
class SuperuserPermission(BasePermission):
"""
超级管理员权限类
"""
def has_permission(self, request, view):
if isinstance(request.user, AnonymousUser):
return False
# 判断是否是超级管理员
if request.user.is_superuser:
return True
class AdminPermission(BasePermission):
"""
普通管理员权限类
"""
def has_permission(self, request, view):
if isinstance(request.user, AnonymousUser):
return False
# 判断是否是超级管理员
is_superuser = request.user.is_superuser
# 判断是否是管理员角色
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
return True
def ReUUID(api):
"""
将接口的uuid替换掉
@@ -81,8 +110,9 @@ class CustomPermission(BasePermission):
# ********#
if not hasattr(request.user, "role"):
return False
role_id_list = request.user.role.values_list('id',flat=True)
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
role_id_list = request.user.role.values_list('id', flat=True)
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(
permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
ApiList = [
str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')]

View File

@@ -26,7 +26,6 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
# 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
modifier_field_id = "modifier"
modifier_name = serializers.SerializerMethodField(read_only=True)
dept_belong_id = serializers.IntegerField(required=False, allow_null=True)
def get_modifier_name(self, instance):
if not hasattr(instance, "modifier"):
@@ -52,7 +51,7 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
)
update_datetime = serializers.DateTimeField(
format="%Y-%m-%d %H:%M:%S", required=False
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
)
def __init__(self, instance=None, data=empty, request=None, **kwargs):
@@ -71,11 +70,11 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
validated_data[self.creator_field_id] = self.request.user
if (
self.dept_belong_id_field_name in self.fields.fields
and validated_data.get(self.dept_belong_id_field_name, None) is None
self.dept_belong_id_field_name in self.fields.fields
and validated_data.get(self.dept_belong_id_field_name, None) is None
):
validated_data[self.dept_belong_id_field_name] = getattr(
self.request.user, "dept_id", None
self.request.user, "dept_id", validated_data.get(self.dept_belong_id_field_name, None)
)
return super().create(validated_data)

View File

@@ -9,5 +9,9 @@ from application.settings import LOGGING
if __name__ == '__main__':
multiprocessing.freeze_support()
uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=4,
workers = 4
if os.sys.platform.startswith('win'):
# Windows操作系统
workers = None
uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=workers,
log_config=LOGGING)

View File

@@ -1,32 +1,31 @@
Django==4.2.7
Django==4.2.14
django-comment-migrate==0.1.7
django-cors-headers==4.3.0
django-filter==23.3
django-cors-headers==4.4.0
django-filter==24.2
django-ranged-response==0.2.0
djangorestframework==3.14.0
django-restql==0.15.3
django-simple-captcha==0.5.20
django-timezone-field==6.0.1
djangorestframework-simplejwt==5.3.0
djangorestframework==3.15.2
django-restql==0.15.4
django-simple-captcha==0.6.0
django-timezone-field==7.0
djangorestframework-simplejwt==5.3.1
drf-yasg==1.21.7
mysqlclient==2.2.0
pypinyin==0.49.0
pypinyin==0.51.0
ua-parser==0.18.0
pyparsing==3.1.1
openpyxl==3.1.2
requests==2.31.0
typing-extensions==4.8.0
tzlocal==5.1
channels==3.0.5
channels-redis==4.1.0
pyparsing==3.1.2
openpyxl==3.1.5
requests==2.32.3
typing-extensions==4.12.2
tzlocal==5.2
channels==4.1.0
channels-redis==4.2.0
websockets==11.0.3
user-agents==2.2.0
six==1.16.0
whitenoise==6.6.0
whitenoise==6.7.0
psycopg2==2.9.9
uvicorn==0.23.2
gunicorn==21.2.0
gevent==23.9.1
Pillow==10.1.0
dvadmin-celery==1.0.5
pyinstaller==6.8.0
uvicorn==0.30.3
gunicorn==22.0.0
gevent==24.2.1
Pillow==10.4.0
pyinstaller==6.9.0

BIN
backend/static/logo.icns Normal file

Binary file not shown.

View File

@@ -6,4 +6,4 @@ RUN awk 'BEGIN { cmd="cp -i ./conf/env.example.py ./conf/env.py "; print "n" |
RUN sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.1'|g" ./conf/env.py
RUN sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.1'|g" ./conf/env.py
RUN python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt
CMD ["/backend/docker_start.sh"]
CMD ["sh","docker_start.sh"]

View File

@@ -7,6 +7,10 @@ server {
index index.html index.htm;
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
# 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况
if ($request_uri ~* "^/$|^/index.html|^/index.htm") {
add_header Cache-Control "no-store";
}
}
location ~ ^/api/ {

View File

@@ -11,6 +11,10 @@ server {
real_ip_header X-Forwarded-For;
root /usr/share/nginx/html;
index index.html index.php index.htm;
# 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况
if ($request_uri ~* "^/$|^/index.html|^/index.htm") {
add_header Cache-Control "no-store";
}
}
location /api/ {

2
web/.gitignore vendored
View File

@@ -21,3 +21,5 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
# 构建版本文件无需上传git
public/version-build

View File

@@ -1,6 +1,6 @@
{
"name": "django-vue3-admin",
"version": "3.0.3",
"version": "3.0.4",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT",
"scripts": {
@@ -10,32 +10,32 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.10",
"@fast-crud/fast-crud": "^1.20.1",
"@fast-crud/fast-extends": "^1.20.1",
"@fast-crud/ui-element": "^1.20.1",
"@fast-crud/ui-interface": "^1.20.1",
"@iconify/vue": "^4.1.1",
"@types/lodash": "^4.14.202",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@element-plus/icons-vue": "^2.3.1",
"@fast-crud/fast-crud": "^1.21.2",
"@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2",
"@iconify/vue": "^4.1.2",
"@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"autoprefixer": "^10.4.14",
"axios": "^1.2.1",
"countup.js": "^2.3.2",
"cropperjs": "^1.5.13",
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"countup.js": "^2.8.0",
"cropperjs": "^1.6.2",
"e-icon-picker": "2.1.1",
"echarts": "^5.4.1",
"echarts": "^5.5.1",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.5.5",
"element-plus": "^2.8.0",
"element-tree-line": "^0.2.1",
"font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",
"js-table2excel": "^1.0.3",
"js-cookie": "^3.0.5",
"js-table2excel": "^1.1.2",
"jsplumb": "^2.15.6",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.0.28",
"pinia-plugin-persist": "^1.0.0",
@@ -49,31 +49,31 @@
"tailwindcss": "^3.2.7",
"ts-md5": "^1.3.1",
"upgrade": "^1.1.0",
"vue": "^3.2.45",
"vue": "^3.4.38",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "^1.0.8",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vxe-table": "^4.4.1",
"xe-utils": "^3.5.7"
"vue-i18n": "^9.14.0",
"vue-router": "^4.4.3",
"vxe-table": "^4.6.18",
"xe-utils": "^3.5.30"
},
"devDependencies": {
"@types/node": "^18.11.13",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.0",
"@types/node": "^18.19.42",
"@types/nprogress": "^0.2.3",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.46.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/compiler-sfc": "^3.2.45",
"eslint": "^8.54.0",
"eslint-plugin-vue": "^9.8.0",
"@vitejs/plugin-vue": "^5.1.2",
"@vue/compiler-sfc": "^3.4.38",
"eslint": "^9.9.0",
"eslint-plugin-vue": "^9.27.0",
"prettier": "^2.8.1",
"sass": "^1.56.2",
"typescript": "^4.9.4",
"vite": "^4.0.0",
"vite": "^5.4.1",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-eslint-parser": "^9.1.0"
"vue-eslint-parser": "^9.4.3"
},
"browserslist": [
"> 1%",

BIN
web/src/assets/login-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View File

@@ -0,0 +1,84 @@
<template>
<div ref="itemRef" class="file-item" :title="data.name" @mouseenter="isShow = true" @mouseleave="isShow = false">
<div v-if="showTitle" class="file-name" :class="{ show: isShow }">{{ data.name }}</div>
<component :is="FileTypes[data.file_type].tag" v-bind="FileTypes[data.file_type].attr" />
<div v-if="props.showClose" class="file-del" :class="{ show: isShow }">
<el-icon :size="24" color="white" @click.stop="delFileHandle" style="cursor: pointer;">
<CircleClose style="mix-blend-mode: difference;" />
</el-icon>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { ref, defineProps, PropType, watch, onMounted, h } from 'vue';
import { successNotification } from '/@/utils/message';
import { getBaseURL } from '/@/utils/baseUrl';
const props = defineProps({
fileData: { type: Object as PropType<any>, required: true },
api: { type: Object as PropType<any>, required: true },
showTitle: { type: Boolean, default: true },
showClose: { type: Boolean, default: true },
});
const _OtherFileComponent = defineComponent({ template: '<el-icon><Files /></el-icon>' });
const FileTypes = [
{ tag: 'img', attr: { src: getBaseURL(props.fileData.url), draggable: false } },
{ tag: 'video', attr: { src: getBaseURL(props.fileData.url), controls: false, autoplay: true, muted: true, loop: true } },
{ tag: 'audio', attr: { src: getBaseURL(props.fileData.url), controls: true, autoplay: false, muted: false, loop: false, volume: 0 } },
{ tag: _OtherFileComponent, attr: { style: { fontSize: '2rem' } } },
];
const isShow = ref<boolean>(false);
const itemRef = ref<HTMLDivElement>();
const data = ref<any>(null);
const delFileHandle = () => props.api.DelObj(props.fileData.id).then(() => {
successNotification('删除成功');
emit('onDelFile');
});
watch(props.fileData, (nVal) => data.value = nVal, { immediate: true, deep: true });
const emit = defineEmits(['onDelFile']);
defineExpose({});
onMounted(() => { });
</script>
<style scoped>
.file-item {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-items: center;
}
.file-item>* {
width: 100% !important;
}
.file-name {
display: none;
position: absolute;
top: 0;
left: 0;
padding: 4px 12px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
word-break: break-all;
white-space: normal;
color: white;
background-color: rgba(0, 0, 0, .5);
overflow: hidden;
text-overflow: ellipsis;
}
.file-del {
display: none;
position: absolute;
left: 0;
bottom: 0;
justify-content: flex-end;
}
.show {
display: flex !important;
}
</style>

View File

@@ -0,0 +1,478 @@
<template>
<div style="width: 100%;" :class="props.class" :style="props.style">
<slot name="input" v-bind="{}">
<div v-if="props.showInput" style="width: 100%;" :class="props.inputClass" :style="props.inputStyle">
<el-select v-if="props.inputType === 'selector'" v-model="data" suffix-icon="arrow-down" clearable
:multiple="props.multiple" placeholder="请选择文件" @click="selectVisiable = true && !props.disabled"
:disabled="props.disabled" @clear="selectedInit" @remove-tag="selectedInit">
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
:label="item.name" />
</el-select>
<div v-if="props.inputType === 'image'" style="position: relative;" class="form-display"
@mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
<template #error>
<div></div>
</template>
</el-image>
<div v-show="!(!!data)"
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
<Close />
</el-icon>
</div>
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;"
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
<video :src="data" :controls="false" :autoplay="true" :muted="true" :loop="true"
:style="{ maxWidth: props.inputSize * 2 + 'px', maxHeight: props.inputSize + 'px', margin: '0 auto' }"></video>
<div v-show="!(!!data)"
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
<Close />
</el-icon>
</div>
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;"
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
<audio :src="data" :controls="!!data" :autoplay="false" :muted="true" :loop="true"
style="width: 100%; z-index: 1;"></audio>
<div v-show="!(!!data)"
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
<Close />
</el-icon>
</div>
</div>
</slot>
<el-dialog v-model="selectVisiable" :draggable="true" width="50%" :align-center="false" :append-to-body="true"
@open="if (listData.length === 0) listRequest();" @close="onClose" @closed="onClosed" modal-class="_overlay">
<template #header>
<span class="el-dialog__title">文件选择</span>
<el-divider style="margin: 0;" />
</template>
<div style="padding: 4px;">
<div style="width: 100%; display: flex; justify-content: space-between; gap: 12px;">
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
@tab-change="handleTabChange" v-if="!isSuperTenent">
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="0" label="图片" />
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="1" label="视频" />
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="2" label="音频" />
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="3" label="其他" />
</el-tabs>
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
@tab-change="handleTabChange" v-if="isTenentMode">
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="4" label="系统图片" />
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="5" label="系统视频" />
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="6" label="系统音频" />
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="7" label="系统其他" />
</el-tabs>
</div>
<el-row justify="space-between" class="headerBar">
<el-col :span="12">
<slot name="actionbar-left">
<el-input v-model="filterForm.name" :placeholder="`请输入${TypeLabel[tabsActived % 4]}名`"
prefix-icon="search" clearable @change="listRequest" />
<div>
<el-tag v-if="props.multiple" type="primary" effect="light">
一共选中&nbsp;{{ data?.length || 0 }}&nbsp;个文件
</el-tag>
</div>
</slot>
</el-col>
<el-col :span="12" style="width: 100%; display: flex; gap: 12px; justify-content: flex-end;">
<slot name="actionbar-right" v-bind="{}">
<el-button type="default" circle icon="refresh" @click="listRequest" />
<template v-if="tabsActived > 3 ? isSuperTenent : true">
<el-upload ref="uploadRef" :action="getBaseURL() + 'api/system/file/'" :multiple="false" :drag="false"
:data="{ upload_method: 1 }" :show-file-list="true" :accept="AcceptList[tabsActived % 4]"
:on-success="() => { listRequest(); listRequestAll(); uploadRef.clearFiles(); }"
v-if="props.showUploadButton">
<el-button type="primary" icon="plus">上传{{ TypeLabel[tabsActived % 4] }}</el-button>
</el-upload>
<el-button type="info" icon="link" @click="netVisiable = true" v-if="props.showNetButton">
网络{{ TypeLabel[tabsActived % 4] }}
</el-button>
</template>
</slot>
</el-col>
</el-row>
<div v-if="!listData.length">
<slot name="empty">
<el-empty description="无内容请上传" style="width: 100%; height: calc(50vh); margin-top: 24px; padding: 4px;" />
</slot>
</div>
<div ref="listContainerRef" class="listContainer" v-else>
<div v-for="item, index in listData" :key="index" @click="onItemClick($event)" :data-id="item[props.valueKey]"
:style="{ width: (props.itemSize || 100) + 'px', cursor: props.selectable ? 'pointer' : 'normal' }">
<slot name="item" :data="item">
<FileItem :fileData="item" :api="fileApi" :showClose="tabsActived < 4 || isSuperTenent"
@onDelFile="listRequest(); listRequestAll();" />
</slot>
</div>
</div>
<div class="listPaginator">
<el-pagination background size="small" layout="total, sizes, prev, pager, next" :total="pageForm.total"
v-model:page-size="pageForm.limit" :page-sizes="[10, 20, 30, 40, 50]" v-model:current-page="pageForm.page"
:hide-on-single-page="false" @change="handlePageChange" />
</div>
</div>
<!-- 只要在获取中就最大程度阻止关闭dialog -->
<el-dialog v-model="netVisiable" :draggable="false" width="50%" :align-center="false" :append-to-body="true"
:title="'网络' + TypeLabel[tabsActived % 4] + '上传'" @closed="netUrl = ''" :close-on-click-modal="!netLoading"
:close-on-press-escape="!netLoading" :show-close="!netLoading" modal-class="_overlay">
<el-form-item :label="TypeLabel[tabsActived % 4] + '链接'">
<el-input v-model="netUrl" placeholder="请输入网络连接" clearable @input="netChange">
<template #prepend>
<el-select v-model="netPrefix" style="width: 110px;">
<el-option v-for="item, index in ['HTTP://', 'HTTPS://']" :key="index" :label="item" :value="item" />
</el-select>
</template>
</el-input>
</el-form-item>
<template #footer>
<el-button v-if="!netLoading" type="default" @click="netVisiable = false">取消</el-button>
<el-button type="primary" @click="confirmNetUrl" :loading="netLoading">
{{ netLoading ? '网络文件获取中...' : '确定' }}
</el-button>
</template>
</el-dialog>
<template #footer v-if="props.showInput">
<el-button type="default" @click="onClose">取消</el-button>
<el-button type="primary" @click="onSave">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { useUi, UserPageQuery, AddReq, EditReq, DelReq } from '@fast-crud/fast-crud';
import { ref, reactive, defineProps, PropType, watch, onMounted, nextTick } from 'vue';
import { getBaseURL } from '/@/utils/baseUrl';
import { request } from '/@/utils/service';
import { SHOW } from './types';
import FileItem from './fileItem.vue';
import { pluginsAll } from '/@/views/plugins/index';
import { storeToRefs } from "pinia";
import { useUserInfo } from "/@/stores/userInfo";
import { errorNotification, successNotification } from '/@/utils/message';
const userInfos = storeToRefs(useUserInfo()).userInfos;
const isTenentMode = !!(pluginsAll && pluginsAll.length && pluginsAll.indexOf('dvadmin3-tenants-web') >= 0);
const isSuperTenent = (userInfos.value as any).schema_name === 'public';
const TypeLabel = ['图片', '视频', '音频', '文件']
const AcceptList = ['image/*', 'video/*', 'audio/*', ''];
const props = defineProps({
modelValue: {},
class: { type: Object as PropType<String | Object>, default: '' },
inputClass: { type: Object as PropType<String | Object>, default: '' },
style: { type: Object as PropType<Object | string>, default: {} },
inputStyle: { type: Object as PropType<Object | string>, default: {} },
disabled: { type: Boolean, default: false },
tabsType: { type: Object as PropType<'' | 'card' | 'border-card'>, default: '' },
itemSize: { type: Number, default: 100 },
// 1000图片 100视频 10音频 1 其他 控制tabs的显示
tabsShow: { type: Number, default: SHOW.ALL },
// 是否可以多选,默认单选
// 该值为true时inputType必须是selector暂不支持其他type的多选
multiple: { type: Boolean, default: false },
// 是否可选该参数用于只上传和展示而不选择和绑定model的情况
selectable: { type: Boolean, default: true },
// 该参数用于控制是否显示表单item。若赋值为false则不会显示表单item也不会显示底部按钮
// 如果不显示表单item则无法触发dialog需要父组件通过修改本组件暴露的 selectVisiable 状态来控制dialog
showInput: { type: Boolean, default: true },
// 表单item类型不为selector是需要设置valueKey否则可能获取不到媒体数据
inputType: { type: Object as PropType<'selector' | 'image' | 'video' | 'audio'>, default: 'selector' },
// inputType不为selector时生效
inputSize: { type: Number, default: 100 },
// v-model绑定的值是file数据的哪个key默认是url
valueKey: { type: String, default: 'url' },
showUploadButton: { type: Boolean, default: true },
showNetButton: { type: Boolean, default: true },
} as any);
const selectVisiable = ref<boolean>(false);
const tabsActived = ref<number>([3, 2, 1, 0][((props.tabsShow & (props.tabsShow - 1)) === 0) ? Math.log2(props.tabsShow) : 3]);
const fileApiPrefix = '/api/system/file/';
const fileApi = {
GetList: (query: UserPageQuery) => request({ url: fileApiPrefix, method: 'get', params: query }),
AddObj: (obj: AddReq) => request({ url: fileApiPrefix, method: 'post', data: obj }),
DelObj: (id: DelReq) => request({ url: fileApiPrefix + id + '/', method: 'delete', data: { id } }),
GetAll: () => request({ url: fileApiPrefix + 'get_all/' }),
};
// 过滤表单
const filterForm = reactive({ name: '' });
// 分页表单
const pageForm = reactive({ page: 1, limit: 10, total: 0 });
// 展示的数据列表
const listData = ref<any[]>([]);
const listAllData = ref<any[]>([]);
const listRequest = async () => {
let res = await fileApi.GetList({
page: pageForm.page,
limit: pageForm.limit,
file_type: isTenentMode ? tabsActived.value % 4 : tabsActived.value,
system: tabsActived.value > 3,
upload_method: 1,
...filterForm
});
listData.value = [];
await nextTick();
listData.value = (res.data as any[]).map((item: any) => ({ ...item, url: getBaseURL(item.url) }));
pageForm.total = res.total;
pageForm.page = res.page;
pageForm.limit = res.limit;
selectedInit();
};
const formDisplayEnter = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'block');
const formDisplayLeave = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'none');
const listRequestAll = async () => {
if (props.inputType !== 'selector') return;
let res = await fileApi.GetAll();
listAllData.value = res.data;
};
// tab改变时触发
const handleTabChange = (name: string) => { pageForm.page = 1; listRequest(); };
// 分页器改变时触发
const handlePageChange = (currentPage: number, pageSize: number) => { pageForm.page = currentPage; pageForm.limit = pageSize; listRequest(); };
// 选择的行为
const listContainerRef = ref<any>();
const onItemClick = async (e: MouseEvent) => {
if (!props.selectable) return;
let target = e.target as HTMLElement;
let flat = 0; // -1删除 0不变 1添加
while (!target.dataset.id) target = target.parentElement as HTMLElement;
let fileId = target.dataset.id;
if (props.multiple) {
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
else { target.classList.add('active'); flat = 1; }
if (data.value.length) {
let _l = JSON.parse(JSON.stringify(data.value));
if (flat === 1) _l.push(fileId);
else _l.splice(_l.indexOf(fileId), 1);
data.value = _l;
} else data.value = [fileId];
// 去重排序,<降序,>升序
data.value = Array.from(new Set(data.value)).sort();
} else {
for (let i of listContainerRef.value?.children) (i as HTMLElement).classList.remove('active');
target.classList.add('active');
data.value = fileId;
}
// onDataChange(data.value);
};
// 每次列表刷新都得更新一下选择状态,因为所有标签页共享列表
const selectedInit = async () => {
if (!props.selectable) return;
await nextTick(); // 不等待一次不会刷新
for (let i of (listContainerRef.value?.children || [])) {
i.classList.remove('active');
let fid = (i as HTMLElement).dataset.id;
if (props.multiple) { if (data.value?.includes(fid)) i.classList.add('active'); }
else { if (fid === data.value) i.classList.add('active'); }
}
};
const uploadRef = ref<any>();
const onSave = () => {
onDataChange(data.value);
emit('onSave', data.value);
selectVisiable.value = false;
};
const onClose = () => {
data.value = props.modelValue;
emit('onClose');
selectVisiable.value = false;
};
const onClosed = () => {
clearState();
emit('onClosed');
};
// 清空状态
const clearState = () => {
filterForm.name = '';
pageForm.page = 1;
pageForm.limit = 10;
pageForm.total = 0;
listData.value = [];
// all数据不能清因为all只会在挂载的时候赋值一次
// listAllData.value = [];
};
const clear = () => { data.value = null; onDataChange(null); }
// 网络文件部分
const netLoading = ref<boolean>(false);
const netVisiable = ref<boolean>(false);
const netUrl = ref<string>('');
const netPrefix = ref<string>('HTTP://');
const netChange = () => {
let s = netUrl.value.trim();
if (s.toUpperCase().startsWith('HTTP://') || s.toUpperCase().startsWith('HTTPS://')) s = s.split('://')[1];
if (s.startsWith('/')) s = s.substring(1);
netUrl.value = s;
};
const confirmNetUrl = () => {
if (!netUrl.value) return;
netLoading.value = true;
let controller = new AbortController();
let timeout = setTimeout(() => {
controller.abort();
}, 10 * 1000);
fetch(netPrefix.value + netUrl.value, { signal: controller.signal }).then(async (res: Response) => {
clearTimeout(timeout);
if (!res.ok) errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
const _ = res.url.split('?')[0].split('/');
let filename = _[_.length - 1];
// let filetype = res.headers.get('content-type')?.split('/')[1] || '';
let blob = await res.blob();
let file = new File([blob], filename, { type: blob.type });
let form = new FormData();
form.append('file', file);
form.append('upload_method', '1');
fetch(getBaseURL() + 'api/system/file/', { method: 'post', body: form })
.then(() => successNotification('网络文件上传成功!'))
.then(() => { netVisiable.value = false; listRequest(); listRequestAll(); })
.catch(() => errorNotification('网络文件上传失败!'))
.then(() => netLoading.value = false);
}).catch((err: any) => {
console.log(err);
clearTimeout(timeout);
errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
netLoading.value = false;
});
};
// fs-crud部分
const data = ref<any>(null);
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
watch(
() => props.modelValue,
(val) => data.value = props.multiple ? JSON.parse(JSON.stringify(val)) : val,
{ immediate: true }
);
const { ui } = useUi();
const formValidator = ui.formItem.injectFormItemContext();
const onDataChange = (value: any) => {
emit('update:modelValue', value);
formValidator.onChange();
formValidator.onBlur();
};
defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
onMounted(() => {
if (props.multiple && props.inputType !== 'selector')
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
listRequestAll();
console.log('fileselector tenentmdoe', isTenentMode);
console.log('fileselector supertenent', isSuperTenent);
});
</script>
<style scoped>
.form-display {
--fileselector-close-display: none;
overflow: hidden;
}
._overlay {
width: unset !important;
}
.headerBar>* {
display: flex;
justify-content: space-between;
gap: 12px;
}
:deep(.el-input-group__prepend) {
padding: 0 20px;
}
.listContainer {
display: grid;
justify-items: center;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: min-content;
grid-gap: 36px;
margin-top: 24px;
padding: 8px;
height: calc(50vh);
overflow-y: auto;
scrollbar-width: thin;
}
.listContainer>* {
aspect-ratio: 1 / 1;
box-shadow: 0 0 4px rgba(0, 0, 0, .2);
border-radius: 8px;
overflow: hidden;
}
.active {
box-shadow: 0 0 8px var(--el-color-primary);
}
.listPaginator {
display: flex;
justify-content: flex-end;
justify-items: center;
padding-top: 24px;
}
.addControllorHover {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
cursor: pointer;
border-radius: 8px;
border: 1px solid #dcdfe6;
}
.addControllorHover:hover {
border-color: #c0c4cc;
}
.closeHover {
display: var(--fileselector-close-display);
position: absolute;
right: 2px;
top: 2px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,7 @@
export const SHOW = {
IMAGE: 0b1000, // 图片
VIDEO: 0b0100, // 视频
AUDIO: 0b0010, // 音频
OTHER: 0b0001, // 其他
ALL: 0b1111, // 全部
};

View File

@@ -1,203 +1,211 @@
<template>
<el-select popper-class="popperClass" class="tableSelector" :multiple="props.tableConfig.isMultiple"
@remove-tag="removeTag" v-model="data" placeholder="请选择" @visible-change="visibleChange">
<template #empty>
<div class="option">
<el-input style="margin-bottom: 10px" v-model="search" clearable placeholder="请输入关键词" @change="getDict"
@clear="getDict">
<template #append>
<el-button type="primary" icon="Search"/>
</template>
</el-input>
<el-table
ref="tableRef"
:data="tableData"
size="mini"
border
row-key="id"
style="width: 400px"
max-height="200"
height="200"
:highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange"
@current-change="handleCurrentChange"
>
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55"/>
<el-table-column fixed type="index" label="#" width="50"/>
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
v-for="(item,index) in props.tableConfig.columns" :key="index"/>
</el-table>
<el-pagination style="margin-top: 10px" background
v-model:current-page="pageConfig.page"
v-model:page-size="pageConfig.limit"
layout="prev, pager, next"
:total="pageConfig.total"
@current-change="handlePageChange"
/>
</div>
</template>
</el-select>
<el-select
popper-class="popperClass"
class="tableSelector"
multiple
@remove-tag="removeTag"
v-model="data"
placeholder="请选择"
@visible-change="visibleChange"
>
<template #empty>
<div class="option">
<el-input style="margin-bottom: 10px" v-model="search" clearable placeholder="请输入关键词" @change="getDict" @clear="getDict">
<template #append>
<el-button type="primary" icon="Search" />
</template>
</el-input>
<el-table
ref="tableRef"
:data="tableData"
size="mini"
border
row-key="id"
:lazy="props.tableConfig.lazy"
:load="props.tableConfig.load"
:tree-props="props.tableConfig.treeProps"
style="width: 400px"
max-height="200"
height="200"
:highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange"
@current-change="handleCurrentChange"
>
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55" />
<el-table-column fixed type="index" label="#" width="50" />
<el-table-column
:prop="item.prop"
:label="item.label"
:width="item.width"
v-for="(item, index) in props.tableConfig.columns"
:key="index"
/>
</el-table>
<el-pagination
style="margin-top: 10px"
background
v-model:current-page="pageConfig.page"
v-model:page-size="pageConfig.limit"
layout="prev, pager, next"
:total="pageConfig.total"
@current-change="handlePageChange"
/>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
import {defineProps, onMounted, reactive, ref, toRaw, watch} from 'vue'
import {dict} from '@fast-crud/fast-crud'
import XEUtils from 'xe-utils'
import {request} from '/@/utils/service'
import { defineProps, reactive, ref, watch } from 'vue';
import XEUtils from 'xe-utils';
import { request } from '/@/utils/service';
const props = defineProps({
modelValue: {},
tableConfig: {
url: null,
label: null, //显示值
value: null, //数据值
isTree: false,
data: [],//默认数据
isMultiple: false, //是否多选
columns: [], //每一项对应的列表项
},
displayLabel: {}
} as any)
const emit = defineEmits(['update:modelValue'])
modelValue: {},
tableConfig: {
url: null,
label: null, //显示值
value: null, //数据值
isTree: false,
lazy: true,
load: () => {},
data: [], //默认数据
isMultiple: false, //是否多选
treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项
},
displayLabel: {},
} as any);
const emit = defineEmits(['update:modelValue']);
// tableRef
const tableRef = ref()
const tableRef = ref();
// template上使用data
const data = ref()
const data = ref();
// 多选值
const multipleSelection = ref()
watch(multipleSelection, // 监听multipleSelection的变化
(value) => {
const {tableConfig} = props
//是否多选
if (!tableConfig.isMultiple) {
data.value = value ? value[tableConfig.label] : null
} else {
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
data.value = result
}
}, // 当multipleSelection值触发后同步修改data.value的值
{immediate: true} // 立即触发一次给data赋值初始值
)
const multipleSelection = ref();
// 搜索值
const search = ref(undefined)
const search = ref(undefined);
//表格数据
const tableData = ref()
const tableData = ref();
// 分页的配置
const pageConfig = reactive({
page: 1,
limit: 10,
total: 0
})
page: 1,
limit: 10,
total: 0,
});
/**
* 表格多选
* @param val:Array
*/
const handleSelectionChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
const result = val.map((item: any) => {
return item[tableConfig.value]
})
emit('update:modelValue', result)
}
multipleSelection.value = val;
const { tableConfig } = props;
const result = val.map((item: any) => {
return item[tableConfig.value];
});
data.value = val.map((item: any) => {
return item[tableConfig.label];
});
emit('update:modelValue', result);
};
/**
* 表格单选
* @param val:Object
*/
const handleCurrentChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
emit('update:modelValue', val[tableConfig.value])
}
const { tableConfig } = props;
if (!tableConfig.isMultiple && val) {
data.value = [val[tableConfig.label]];
emit('update:modelValue', val[tableConfig.value]);
}
};
/**
* 获取字典值
*/
const getDict = async () => {
const url = props.tableConfig.url
const params = {
page: pageConfig.page,
limit: pageConfig.limit,
search: search.value
}
const {data, page, limit, total} = await request({
url:url,
params:params
})
pageConfig.page = page
pageConfig.limit = limit
pageConfig.total = total
if (props.tableConfig.data === undefined || props.tableConfig.data.length === 0) {
if (props.tableConfig.isTree) {
tableData.value = XEUtils.toArrayTree(data, {parentKey: 'parent', key: 'id', children: 'children'})
} else {
tableData.value = data
}
} else {
tableData.value = props.tableConfig.data
}
}
const url = props.tableConfig.url;
const params = {
page: pageConfig.page,
limit: pageConfig.limit,
search: search.value,
};
const { data, page, limit, total } = await request({
url: url,
params: params,
});
pageConfig.page = page;
pageConfig.limit = limit;
pageConfig.total = total;
if (props.tableConfig.data === undefined || props.tableConfig.data.length === 0) {
if (props.tableConfig.isTree) {
tableData.value = XEUtils.toArrayTree(data, { parentKey: 'parent', key: 'id', children: 'children' });
} else {
tableData.value = data;
}
} else {
tableData.value = props.tableConfig.data;
}
};
/**
* 下拉框展开/关闭
* @param bool
*/
const visibleChange = (bool: any) => {
if (bool) {
getDict()
}
}
if (bool) {
getDict();
}
};
/**
* 分页
* @param page
*/
const handlePageChange = (page: any) => {
pageConfig.page = page
getDict()
}
pageConfig.page = page;
getDict();
};
// 监听displayLabel的变化更新数据
watch(() => {
return props.displayLabel
}, (value) => {
const {tableConfig} = props
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
data.value = result
}, {immediate: true})
watch(
() => {
return props.displayLabel;
},
(value) => {
const { tableConfig } = props;
const result = value
? value.map((item: any) => { return item[tableConfig.label];})
: null;
data.value = result;
},
{ immediate: true }
);
</script>
<style scoped>
.option {
height: auto;
line-height: 1;
padding: 5px;
background-color: #fff;
height: auto;
line-height: 1;
padding: 5px;
background-color: #fff;
}
</style>
<style lang="scss">
.popperClass {
height: 320px;
height: 320px;
}
.el-select-dropdown__wrap {
max-height: 310px !important;
max-height: 310px !important;
}
.tableSelector {
.el-icon, .el-tag__close {
display: none;
}
.el-icon,
.el-tag__close {
display: none;
}
}
</style>

View File

@@ -3,6 +3,7 @@ export default {
label: {
one1: 'User name login',
two2: 'Mobile number',
changePwd: 'Change The Password',
},
link: {
one3: 'Third party login',

View File

@@ -3,15 +3,18 @@ export default {
label: {
one1: '账号密码登录',
two2: '手机号登录',
changePwd: '密码修改',
},
link: {
one3: '第三方登录',
two4: '友情链接',
},
account: {
accountPlaceholder1: '请输入登录账号',
accountPlaceholder1: '请输入登录账号/邮箱/手机号',
accountPlaceholder2: '请输入登录密码',
accountPlaceholder3: '请输入验证码',
accountPlaceholder4:'请输入新密码',
accountPlaceholder5:'请再次输入新密码',
accountBtnText: '登 录',
},
mobile: {

View File

@@ -3,6 +3,7 @@ export default {
label: {
one1: '用戶名登入',
two2: '手機號登入',
changePwd: '密码修改',
},
link: {
one3: '協力廠商登入',

View File

@@ -44,7 +44,7 @@ export async function initBackEndControlRoutes() {
if (!Session.get('token')) return false;
// 触发初始化用户信息 pinia
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
await useUserInfo().setUserInfos();
await useUserInfo().getApiUserInfo();
// 获取路由菜单数据
const res = await getBackEndControlRoutes();
// 无登录权限时,添加判断

View File

@@ -13,6 +13,7 @@ import {initBackEndControlRoutes, setRouters} from '/@/router/backEnd';
import {useFrontendMenuStore} from "/@/stores/frontendMenu";
import {useTagsViewRoutes} from "/@/stores/tagsViewRoutes";
import {toRaw} from "vue";
import {checkVersion} from "/@/utils/upgrade";
/**
* 1、前端控制路由时isRequestRoutes 为 false需要写 roles需要走 setFilterRoute 方法。
@@ -27,6 +28,8 @@ import {toRaw} from "vue";
const storesThemeConfig = useThemeConfig(pinia);
const {themeConfig} = storeToRefs(storesThemeConfig);
const {isRequestRoutes} = themeConfig.value;
import {useUserInfo} from "/@/stores/userInfo";
const { userInfos } = storeToRefs(useUserInfo());
/**
* 创建一个可以被 Vue 应用程序使用的路由实例
@@ -93,8 +96,12 @@ export function formatTwoStageRoutes(arr: any) {
return newArr;
}
const frameOutRoutes = staticRoutes.map(item => item.path)
// 路由加载前
router.beforeEach(async (to, from, next) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
await checkVersion()
NProgress.configure({showSpinner: false});
if (to.meta.title) NProgress.start();
const token = Session.get('token');
@@ -106,11 +113,15 @@ router.beforeEach(async (to, from, next) => {
next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
Session.clear();
NProgress.done();
} else if (token && to.path === '/login') {
}else if (token && to.path === '/login' && userInfos.value.pwd_change_count===0 ) {
next('/login');
NProgress.done();
} else if (token && to.path === '/login' && userInfos.value.pwd_change_count>0) {
next('/home');
NProgress.done();
}else if(token && frameOutRoutes.includes(to.path) ){
next()
} else {
const storesRoutesList = useRoutesList(pinia);
const {routesList} = storeToRefs(storesRoutesList);
if (routesList.value.length === 0) {

View File

@@ -12,6 +12,7 @@ export interface UserInfosState {
email: string;
mobile: string;
gender: string;
pwd_change_count:null|number;
dept_info: {
dept_id: number;
dept_name: string;

View File

@@ -2,6 +2,9 @@ import { defineStore } from 'pinia';
import { UserInfosStates } from './interface';
import { Session } from '/@/utils/storage';
import { request } from '../utils/service';
import { getBaseURL } from '../utils/baseUrl';
import headerImage from '/@/assets/img/headerImage.png';
/**
* 用户信息
* @methods setUserInfos 设置用户信息
@@ -15,6 +18,7 @@ export const useUserInfo = defineStore('userInfo', {
email: '',
mobile: '',
gender: '',
pwd_change_count:null,
dept_info: {
dept_id: 0,
dept_name: '',
@@ -29,16 +33,19 @@ export const useUserInfo = defineStore('userInfo', {
isSocketOpen: false
}),
actions: {
async updateUserInfos() {
let userInfos: any = await this.getApiUserInfo();
this.userInfos.username = userInfos.data.name;
this.userInfos.avatar = userInfos.data.avatar;
this.userInfos.name = userInfos.data.name;
this.userInfos.email = userInfos.data.email;
this.userInfos.mobile = userInfos.data.mobile;
this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info;
async setPwdChangeCount(count: number) {
this.userInfos.pwd_change_count = count;
},
async updateUserInfos(userInfos:any) {
this.userInfos.username = userInfos.name;
this.userInfos.avatar = userInfos.avatar;
this.userInfos.name = userInfos.name;
this.userInfos.email = userInfos.email;
this.userInfos.mobile = userInfos.mobile;
this.userInfos.gender = userInfos.gender;
this.userInfos.dept_info = userInfos.dept_info;
this.userInfos.role_info = userInfos.role_info;
this.userInfos.pwd_change_count = userInfos.pwd_change_count;
Session.set('userInfo', this.userInfos);
},
async setUserInfos() {
@@ -55,6 +62,7 @@ export const useUserInfo = defineStore('userInfo', {
this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info;
this.userInfos.pwd_change_count = userInfos.data.pwd_change_count;
Session.set('userInfo', this.userInfos);
}
},
@@ -65,7 +73,18 @@ export const useUserInfo = defineStore('userInfo', {
return request({
url: '/api/system/user/user_info/',
method: 'get',
});
}).then((res:any)=>{
this.userInfos.username = res.data.name;
this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage;
this.userInfos.name = res.data.name;
this.userInfos.email = res.data.email;
this.userInfos.mobile = res.data.mobile;
this.userInfos.gender = res.data.gender;
this.userInfos.dept_info = res.data.dept_info;
this.userInfos.role_info = res.data.role_info;
this.userInfos.pwd_change_count = res.data.pwd_change_count;
Session.set('userInfo', this.userInfos);
})
},
},
});

View File

@@ -1,3 +1,4 @@
import XEUtils from 'xe-utils';
import {useColumnPermission} from '/@/stores/columnPermission';
type permissionType = 'is_create' | 'is_query' | 'is_update';
@@ -22,41 +23,18 @@ export const handleColumnPermission = async (func: Function, crudOptions: any,ex
}
}
const columns = crudOptions.columns;
const excludeColumns = ['_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn)
for (let col in columns) {
if (excludeColumns.includes(col)) {
continue
}else{
if (columns[col].column) {
columns[col].column.show = false
} else {
columns[col]['column'] = {
show: false
}
}
columns[col].addForm = {
show: false
}
columns[col].editForm = {
show: false
const excludeColumns = ['checked','_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn)
XEUtils.eachTree(columns, (item, key) => {
if (!excludeColumns.includes(String(key)) && key in res.data) {
// 如果列表不可见,则禁止在列设置中选择
// 只有列表不可见,才修改列配置,这样才不影响默认的配置
if (!res.data[key]['is_query']) {
item.column.show = false;
item.column.columnSetDisabled = true;
}
item.addForm = { show: res.data[key]['is_create'] };
item.editForm = { show: res.data[key]['is_update'] };
}
for (let item of res.data) {
if (excludeColumns.includes(item.field_name)) {
continue
} else if(item.field_name === col) {
columns[col].column.show = item['is_query']
// 如果列表不可见,则禁止在列设置中选择
if(!item['is_query'])columns[col].column.columnSetDisabled = true
columns[col].addForm = {
show: item['is_create']
}
columns[col].editForm = {
show: item['is_update']
}
}
}
}
});
return crudOptions
}

View File

@@ -4,7 +4,7 @@ import { DictionaryStore } from '/@/stores/dictionary';
/**
* @method 获取指定name字典
*/
export const dictionary = (name: string,key:string|number|undefined) => {
export const dictionary = (name: string,key?:string|number|undefined) => {
const dict = DictionaryStore()
const dictionary = toRaw(dict.data)
if(key!=undefined){

View File

@@ -1,5 +1,7 @@
import { nextTick } from 'vue';
import '/@/theme/loading.scss';
import { showUpgrade } from "/@/utils/upgrade";
/**
* 页面全局 Loading
@@ -9,6 +11,8 @@ import '/@/theme/loading.scss';
export const NextLoading = {
// 创建 loading
start: () => {
// 显示升级提示
showUpgrade()
const bodys: Element = document.body;
const div = <HTMLElement>document.createElement('div');
div.setAttribute('class', 'loading-next');

View File

@@ -10,6 +10,7 @@ import { errorLog, errorCreate } from './tools.ts';
import { Local, Session } from '/@/utils/storage';
import qs from 'qs';
import { getBaseURL } from './baseUrl';
import { successMessage } from './message.js';
/**
* @description 创建请求实例
*/
@@ -82,7 +83,7 @@ function createService() {
ElMessageBox.alert(dataAxios.msg, '提示', {
confirmButtonText: 'OK',
callback: (action: Action) => {
window.location.reload();
// window.location.reload();
},
});
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
@@ -204,6 +205,8 @@ export const requestForMock = createRequestFunction(serviceForMock);
* @param filename
*/
export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) {
// return request({ url: url, method: method, params: params })
// .then((res: any) => successMessage(res.msg));
request({
url: url,
method: method,
@@ -211,6 +214,9 @@ export const downloadFile = function ({ url, params, method, filename = '文件
responseType: 'blob'
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then((res: any) => {
// console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载
// if (res.headers && res.headers['Content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
const fileName = xlsxName || `${filename}.xlsx`
if (res) {

55
web/src/utils/upgrade.ts Normal file
View File

@@ -0,0 +1,55 @@
import axios from "axios";
import * as process from "process";
import {Local, Session} from '/@/utils/storage';
import {ElNotification} from "element-plus";
import fs from "fs";
// 是否显示升级提示信息框
const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade';
const VERSION_KEY = 'DVADMIN3_VERSION'
const VERSION_FILE_NAME = 'version-build'
export function showUpgrade () {
const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false
if (isShowUpgrade) {
Session.remove(IS_SHOW_UPGRADE_SESSION_KEY)
ElNotification({
title: '新版本升级',
message: "检测到系统新版本,正在更新中!不用担心,更新很快的哦!",
type: 'success',
duration: 5000,
});
}
}
// 生产环境前端版本校验,
export async function checkVersion(){
if (process.env.NODE_ENV === 'development') {
// 开发环境无需校验前端版本
return
}
// 获取线上版本号 t为时间戳防止缓存
await axios.get(`${import.meta.env.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then(res => {
const {status, data} = res || {}
if (status === 200) {
// 获取当前版本号
const localVersion = Local.get(VERSION_KEY)
// 将当前版本号持久缓存至本地
Local.set(VERSION_KEY, data)
// 当用户本地存在版本号并且和线上版本号不一致时,进行页面刷新操作
if (localVersion && localVersion !== data) {
// 本地缓存版本号和线上版本号不一致,弹出升级提示框
// 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框
Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true)
window.location.reload()
}
}
})
}
export function generateVersionFile (){
// 生成版本文件到public目录下version文件中
const version = `${process.env.npm_package_version}.${new Date().getTime()}`;
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
}

View File

@@ -1,46 +0,0 @@
import axios from 'axios'
import VFormRender from '@/components/form-render/index.vue'
import ContainerItems from '@/components/form-render/container-item/index'
import {registerIcon} from '@/utils/el-icons'
import 'virtual:svg-icons-register'
import '@/iconfont/iconfont.css'
import { installI18n } from '@/utils/i18n'
import { loadExtension } from '@/extension/extension-loader'
VFormRender.install = function (app) {
installI18n(app)
loadExtension(app)
app.use(ContainerItems)
registerIcon(app)
app.component(VFormRender.name, VFormRender)
}
const components = [
VFormRender
]
const install = (app) => {
installI18n(app)
loadExtension(app)
app.use(ContainerItems)
registerIcon(app)
components.forEach(component => {
app.component(component.name, component)
})
window.axios = axios
}
if (typeof window !== 'undefined' && window.Vue) { /* script<EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>ֵaxios<EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
//window.axios = axios
}
export default {
install,
VFormRender
}

View File

@@ -1,73 +0,0 @@
import axios from 'axios'
import VFormDesigner from '@/components/form-designer/index.vue'
import VFormRender from '@/components/form-render/index.vue'
import Draggable from '@/../lib/vuedraggable/dist/vuedraggable.umd.js'
import {registerIcon} from '@/utils/el-icons'
import 'virtual:svg-icons-register'
import '@/iconfont/iconfont.css'
import ContainerWidgets from '@/components/form-designer/form-widget/container-widget/index'
import ContainerItems from '@/components/form-render/container-item/index'
import { addDirective } from '@/utils/directive'
import { installI18n } from '@/utils/i18n'
import { loadExtension } from '@/extension/extension-loader'
VFormDesigner.install = function (app) {
addDirective(app)
installI18n(app)
loadExtension(app)
app.use(ContainerWidgets)
app.use(ContainerItems)
registerIcon(app)
app.component('draggable', Draggable)
app.component(VFormDesigner.name, VFormDesigner)
}
VFormRender.install = function (app) {
installI18n(app)
loadExtension(app)
app.use(ContainerItems)
registerIcon(app)
app.component(VFormRender.name, VFormRender)
}
const components = [
VFormDesigner,
VFormRender
]
const install = (app) => {
addDirective(app)
installI18n(app)
loadExtension(app)
app.use(ContainerWidgets)
app.use(ContainerItems)
registerIcon(app)
app.component('draggable', Draggable)
components.forEach(component => {
app.component(component.name, component)
})
window.axios = axios
}
if (typeof window !== 'undefined' && window.Vue) { /* script<EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>ֵaxios<EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
//window.axios = axios
}
export default {
install,
VFormDesigner,
VFormRender
}

View File

@@ -1,29 +0,0 @@
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
import DVAFormDesigner from './components/DVAFormDesigner.vue'
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<E6B5BD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
const components = [
DVAFormDesigner
]
// <20><><EFBFBD><EFBFBD> install <20><><EFBFBD><EFBFBD>
const install = function (Vue) {
if (install.installed) return
install.installed = true
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1><EFBFBD>ע<EFBFBD><D7A2>ȫ<EFBFBD><C8AB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
components.map(component => {
Vue.component(component.name, component) //component.name <20>˴<EFBFBD>ʹ<EFBFBD>õ<EFBFBD><C3B5><EFBFBD><EFBFBD><EFBFBD>vue<75>ļ<EFBFBD><C4BC>е<EFBFBD> name <20><><EFBFBD><EFBFBD>
})
}
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export default {
// <20><><EFBFBD><EFBFBD><EFBFBD>Ķ<EFBFBD><C4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߱<EFBFBD>һ<EFBFBD><D2BB> install <20><><EFBFBD><EFBFBD>
install,
// <20><><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>
...components
}

View File

@@ -39,3 +39,9 @@ export function DelObj(id: DelReq) {
data: { id },
});
}
export function GetPermission() {
return request({
url: apiPrefix + 'field_permission/',
method: 'get',
});
}

View File

@@ -1,244 +1,202 @@
import * as api from './api';
import {
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
compute,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import {dictionary} from '/@/utils/dictionary';
import {successMessage} from '/@/utils/message';
import {auth} from "/@/utils/authFunction";
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import { auth } from '/@/utils/authFunction';
import tableSelector from '/@/components/tableSelector/index.vue';
import { shallowRef } from 'vue';
export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({form, row}: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
return await api.AddObj(form);
};
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
/**
* 懒加载
* @param row
* @returns {Promise<unknown>}
*/
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
pageRequest({pcode: tree.code}).then((res: APIResponseData) => {
resolve(res.data);
});
};
/**
* 懒加载
* @param row
* @returns {Promise<unknown>}
*/
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
pageRequest({ pcode: tree.code }).then((res: APIResponseData) => {
resolve(res.data);
});
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('area:Create'),
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: auth('area:Update')
},
remove: {
iconRight: 'Delete',
type: 'text',
show: auth('area:Delete')
},
},
},
pagination: {
show: false,
},
table: {
rowKey: 'id',
lazy: true,
load: loadContentMethod,
treeProps: {children: 'children', hasChildren: 'hasChild'},
},
columns: {
_index: {
title: '序号',
form: {show: false},
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
// pcode: {
// title: '父级地区',
// show: false,
// search: {
// show: true,
// },
// type: 'dict-tree',
// form: {
// component: {
// showAllLevels: false, // 仅显示最后一级
// props: {
// elProps: {
// clearable: true,
// showAllLevels: false, // 仅显示最后一级
// props: {
// checkStrictly: true, // 可以不需要选到最后一级
// emitPath: false,
// clearable: true,
// },
// },
// },
// },
// },
// },
name: {
title: '名称',
search: {
show: true,
},
treeNode: true,
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{required: true, message: '名称必填项'},
],
component: {
placeholder: '请输入名称',
},
},
},
code: {
title: '地区编码',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 90,
},
form: {
rules: [
// 表单校验规则
{required: true, message: '地区编码必填项'},
],
component: {
placeholder: '请输入地区编码',
},
},
},
pinyin: {
title: '拼音',
search: {
disabled: true,
},
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{required: true, message: '拼音必填项'},
],
component: {
placeholder: '请输入拼音',
},
},
},
level: {
title: '地区层级',
search: {
disabled: true,
},
type: 'input',
column: {
minWidth: 100,
},
form: {
disabled: false,
rules: [
// 表单校验规则
{required: true, message: '拼音必填项'},
],
component: {
placeholder: '请输入拼音',
},
},
},
initials: {
title: '首字母',
column: {
minWidth: 100,
},
form: {
rules: [
// 表单校验规则
{required: true, message: '首字母必填项'},
],
component: {
placeholder: '请输入首字母',
},
},
},
enable: {
title: '是否启用',
search: {
show: true,
},
type: 'dict-radio',
column: {
minWidth: 90,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
},
},
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('area:Create'),
},
},
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: auth('area:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: auth('area:Delete'),
},
},
},
pagination: {
show: false,
},
table: {
rowKey: 'id',
lazy: true,
load: loadContentMethod,
treeProps: { children: 'children', hasChildren: 'hasChild' },
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
name: {
title: '名称',
search: {
show: true,
},
treeNode: true,
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '名称必填项' },
],
component: {
placeholder: '请输入名称',
},
},
},
pcode: {
title: '父级地区',
search: {
disabled: true,
},
width: 130,
type: 'table-selector',
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ row }) => {
if (row) {
return row.pcode_info;
}
return null;
}),
tableConfig: {
url: '/api/system/area/',
label: 'name',
value: 'id',
isTree: true,
isMultiple: false,
lazy: true,
load: loadContentMethod,
treeProps: { children: 'children', hasChildren: 'hasChild' },
columns: [
{
prop: 'name',
label: '地区',
width: 150,
},
{
prop: 'code',
label: '地区编码',
},
],
},
},
},
column: {
show: false,
},
},
code: {
title: '地区编码',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 90,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '地区编码必填项' },
],
component: {
placeholder: '请输入地区编码',
},
},
},
enable: {
title: '是否启用',
search: {
show: true,
},
type: 'dict-radio',
column: {
minWidth: 90,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
},
},
};
};

View File

@@ -5,14 +5,21 @@
</template>
<script lang="ts" setup name="areas">
import { ref, onMounted } from 'vue';
import { onMounted } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { GetPermission } from './api';
import { handleColumnPermission } from '/@/utils/columnPermission';
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
onMounted(async () => {
// 设置列权限
const newOptions = await handleColumnPermission(GetPermission, crudOptions);
//重置crudBinding
resetCrudOptions(newOptions);
// 刷新
crudExpose.doRefresh();
});
</script>

View File

@@ -5,13 +5,13 @@ type GetListType = PageQuery & { show_all: string };
export const apiPrefix = '/api/system/user/';
export function GetDept(query: PageQuery) {
return request({
url: '/api/system/dept/dept_lazy_tree/',
method: 'get',
params: query,
});
}
// export function GetDept(query: PageQuery) {
// return request({
// url: '/api/system/dept/dept_all/',
// method: 'get',
// params: query,
// });
// }
export function GetList(query: GetListType) {
return request({

View File

@@ -4,7 +4,7 @@ import { request } from '/@/utils/service';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import {auth} from "/@/utils/authFunction";
import { auth } from "/@/utils/authFunction";
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
@@ -100,10 +100,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
placement: 'top',
content: '重设密码',
},
click: (ctx: any) => {
const { row } = ctx;
context?.handleResetPwdOpen(row);
},
click: (ctx: any) => context?.handleResetPwdOpen(ctx.row),
},
},
},
@@ -185,10 +182,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
// value: vm.systemConfig('base.default_password'),
},
/* valueResolve(row, key) {
if (row.password) {
row.password = vm.$md5(row.password)
}
} */
if (row.password) {
row.password = vm.$md5(row.password)
}
} */
},
name: {
title: '姓名',
@@ -220,7 +217,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
label: 'name',
}),
column: {
minWidth: 150, //最小列宽
minWidth: 200, //最小列宽
formatter({ value, row, index }) {
return row.dept_name_all
}
},
form: {
rules: [
@@ -259,7 +259,11 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
label: 'name',
}),
column: {
minWidth: 100, //最小列宽
minWidth: 200, //最小列宽
// formatter({ value, row, index }) {
// const values = row.role_info.map((item: any) => item.name);
// return values.join(',')
// }
},
form: {
rules: [
@@ -378,10 +382,14 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
},
avatar: {
title: '头像',
type: 'avatar-cropper',
type: 'avatar-uploader',
form: {
show: false,
},
column: {
width: 100,
showOverflowTooltip: true,
}
},
},
},

View File

@@ -42,6 +42,15 @@
<template #actionbar-right>
<importExcel api="api/system/user/" v-auth="'user:Import'">导入 </importExcel>
</template>
<template #cell_avatar="scope">
<div v-if="scope.row.avatar" style="display: flex; justify-content: center; align-items: center;">
<el-image
style="width: 50px; height: 50px; border-radius: 50%; aspect-ratio: 1 /1 ; "
:src="getBaseURL(scope.row.avatar)"
:preview-src-list="[getBaseURL(scope.row.avatar)]"
:preview-teleported="true" />
</div>
</template>
</fs-crud>
<el-dialog v-model="resetPwdVisible" title="重设密码" width="400px" draggable :before-close="handleResetPwdClose">
@@ -69,6 +78,7 @@ import { ECharts, EChartsOption, init } from 'echarts';
import { getDeptInfoById, resetPwd } from './api';
import { warningNotification, successNotification } from '/@/utils/message';
import { HeadDeptInfoType } from '../../types';
import {getBaseURL} from '/@/utils/baseUrl';
let deptCountChart: ECharts;
let deptSexChart: ECharts;
@@ -277,7 +287,8 @@ const { resetCrudOptions } = useCrud({
padding: 0 10px;
border-radius: 8px 0 0 8px;
box-sizing: border-box;
background-color: #fff;
color: var(--next-bg-topBarColor);
background-color: var(--el-fill-color-blank);;
}
.dept-user-com-table {
height: calc(100% - 200px);

View File

@@ -133,7 +133,7 @@ onMounted(() => {
}
.dept-left {
background-color: #fff;
background-color: var(--el-fill-color-blank);;
border-radius: 0 8px 8px 0;
padding: 10px;
}

View File

@@ -1,309 +1,319 @@
import * as api from './api';
import {
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import {dictionary} from '/@/utils/dictionary';
import { dictionary } from '/@/utils/dictionary';
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({form, row}: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
const data = crudExpose!.getSearchFormData()
const parent = data.parent
form.parent = parent
if (parent) {
return await api.AddObj(form);
} else {
return undefined
}
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const data = crudExpose!.getSearchFormData()
const parent = data.parent
form.parent = parent
if (parent) {
return await api.AddObj(form);
} else {
return undefined
}
};
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
columns: {
_index: {
title: '序号',
form: {show: false},
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
// @ts-ignore
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
label: {
title: '名称',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'input',
form: {
rules: [
// 表单校验规则
{required: true, message: '名称必填项'},
],
component: {
props: {
clearable: true,
},
placeholder: '请输入名称',
},
},
},
type: {
title: '数据值类型',
type: 'dict-select',
search: {
disabled: true,
component: {
props: {
clearable: true,
},
},
},
show: false,
dict: dict({
data: [
{label: 'text', value: 0},
{label: 'number', value: 1},
{label: 'date', value: 2},
{label: 'datetime', value: 3},
{label: 'time', value: 4},
{label: 'file', value: 5},
{label: 'boolean', value: 6},
{label: 'images', value: 7},
],
}),
form: {
rules: [
// 表单校验规则
{required: true, message: '数据值类型必填项'},
],
value: 0,
component: {
props: {
clearable: true,
},
placeholder: '请选择数据值类型',
},
/* valueChange(key, value, form, { getColumn, mode, component, immediate, getComponent }) {
const template = vm.getEditFormTemplate('value')
// 选择框重新选择后情况value值
if (!immediate) {
form.value = undefined
}
if (value === 0) {
template.component.name = 'el-input'
} else if (value === 1) {
template.component.name = 'el-input-number'
} else if (value === 2) {
template.component.name = 'el-date-picker'
template.component.props = {
type: 'date',
valueFormat: 'yyyy-MM-dd'
}
} else if (value === 3) {
template.component.name = 'el-date-picker'
template.component.props = {
type: 'datetime',
valueFormat: 'yyyy-MM-dd HH:mm:ss'
}
} else if (value === 4) {
template.component.name = 'el-time-picker'
template.component.props = {
pickerOptions: {
arrowControl: true
},
valueFormat: 'HH:mm:ss'
}
} else if (value === 5) {
template.component.name = 'd2p-file-uploader'
template.component.props = { elProps: { listType: 'text' } }
} else if (value === 6) {
template.component.name = 'dict-switch'
template.component.value = true
template.component.props = {
dict: {
data: [
{ label: '是', value: 'true' },
{ label: '否', value: 'false' }
]
}
}
} else if (value === 7) {
template.component.name = 'd2p-cropper-uploader'
template.component.props = { accept: '.png,.jpeg,.jpg,.ico,.bmp,.gif', cropper: { viewMode: 1 } }
}
}, */
},
},
value: {
title: '数据值',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
view: {
component: {props: {height: 100, width: 100}},
},
/* // 提交时,处理数据
valueResolve(row: any, col: any) {
const value = row[col.key]
const type = row.type
if (type === 5 || type === 7) {
if (value != null) {
if (value.length >= 0) {
if (value instanceof Array) {
row[col.key] = value.toString()
} else {
row[col.key] = value
}
} else {
row[col.key] = null
}
}
} else {
row[col.key] = value
}
},
// 接收时,处理数据
valueBuilder(row: any, col: any) {
const value = row[col.key]
const type = row.type
if (type === 5 || type === 7) {
if (value != null && value) {
row[col.key] = value.split(',')
}
} else {
row[col.key] = value
}
}, */
type: 'input',
form: {
rules: [
// 表单校验规则
{required: true, message: '数据值必填项'},
],
component: {
props: {
clearable: true,
},
placeholder: '请输入数据值',
},
},
},
status: {
title: '状态',
width: 80,
search: {
show: true
},
type: 'dict-radio',
dict: dict({
data: dictionary('button_status_bool'),
}),
form: {
value: true,
rules: [
// 表单校验规则
{required: true, message: '状态必填项'},
],
},
},
sort: {
title: '排序',
width: 70,
type: 'number',
form: {
value: 1,
component: {},
rules: [
// 表单校验规则
{required: true, message: '排序必填项'},
],
},
},
color: {
title: '标签颜色',
width: 90,
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
data: [
{label: 'success', value: 'success', color: 'success'},
{label: 'primary', value: 'primary', color: 'primary'},
{label: 'info', value: 'info', color: 'info'},
{label: 'danger', value: 'danger', color: 'danger'},
{label: 'warning', value: 'warning', color: 'warning'},
],
}),
form: {
component: {
props: {
clearable: true,
},
},
},
},
},
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
};
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
// @ts-ignore
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
label: {
title: '名称',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'input',
form: {
rules: [
// 表单校验规则
{ required: true, message: '名称必填项' },
],
component: {
props: {
clearable: true,
},
placeholder: '请输入名称',
},
},
},
type: {
title: '数据值类型',
type: 'dict-select',
search: {
disabled: true,
component: {
props: {
clearable: true,
},
},
},
show: false,
dict: dict({
data: [
{ label: 'text', value: 0 },
{ label: 'number', value: 1 },
{ label: 'date', value: 2 },
{ label: 'datetime', value: 3 },
{ label: 'time', value: 4 },
{ label: 'file', value: 5 },
{ label: 'boolean', value: 6 },
{ label: 'images', value: 7 },
],
}),
form: {
rules: [
// 表单校验规则
{ required: true, message: '数据值类型必填项' },
],
value: 0,
component: {
props: {
clearable: true,
},
placeholder: '请选择数据值类型',
},
/* valueChange(key, value, form, { getColumn, mode, component, immediate, getComponent }) {
const template = vm.getEditFormTemplate('value')
// 选择框重新选择后情况value值
if (!immediate) {
form.value = undefined
}
if (value === 0) {
template.component.name = 'el-input'
} else if (value === 1) {
template.component.name = 'el-input-number'
} else if (value === 2) {
template.component.name = 'el-date-picker'
template.component.props = {
type: 'date',
valueFormat: 'yyyy-MM-dd'
}
} else if (value === 3) {
template.component.name = 'el-date-picker'
template.component.props = {
type: 'datetime',
valueFormat: 'yyyy-MM-dd HH:mm:ss'
}
} else if (value === 4) {
template.component.name = 'el-time-picker'
template.component.props = {
pickerOptions: {
arrowControl: true
},
valueFormat: 'HH:mm:ss'
}
} else if (value === 5) {
template.component.name = 'd2p-file-uploader'
template.component.props = { elProps: { listType: 'text' } }
} else if (value === 6) {
template.component.name = 'dict-switch'
template.component.value = true
template.component.props = {
dict: {
data: [
{ label: '是', value: 'true' },
{ label: '否', value: 'false' }
]
}
}
} else if (value === 7) {
template.component.name = 'd2p-cropper-uploader'
template.component.props = { accept: '.png,.jpeg,.jpg,.ico,.bmp,.gif', cropper: { viewMode: 1 } }
}
}, */
},
},
value: {
title: '数据值',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
view: {
component: { props: { height: 100, width: 100 } },
},
/* // 提交时,处理数据
valueResolve(row: any, col: any) {
const value = row[col.key]
const type = row.type
if (type === 5 || type === 7) {
if (value != null) {
if (value.length >= 0) {
if (value instanceof Array) {
row[col.key] = value.toString()
} else {
row[col.key] = value
}
} else {
row[col.key] = null
}
}
} else {
row[col.key] = value
}
},
// 接收时,处理数据
valueBuilder(row: any, col: any) {
const value = row[col.key]
const type = row.type
if (type === 5 || type === 7) {
if (value != null && value) {
row[col.key] = value.split(',')
}
} else {
row[col.key] = value
}
}, */
type: 'input',
form: {
rules: [
// 表单校验规则
{ required: true, message: '数据值必填项' },
],
component: {
props: {
clearable: true,
},
placeholder: '请输入数据值',
},
},
},
status: {
title: '状态',
width: 80,
search: {
show: true
},
type: 'dict-radio',
dict: dict({
data: dictionary('button_status_bool'),
}),
form: {
value: true,
rules: [
// 表单校验规则
{ required: true, message: '状态必填项' },
],
},
},
sort: {
title: '排序',
width: 70,
type: 'number',
form: {
value: 1,
component: {},
rules: [
// 表单校验规则
{ required: true, message: '排序必填项' },
],
},
},
color: {
title: '标签颜色',
width: 90,
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
data: [
{ label: 'success', value: 'success', color: 'success' },
{ label: 'primary', value: 'primary', color: 'primary' },
{ label: 'info', value: 'info', color: 'info' },
{ label: 'danger', value: 'danger', color: 'danger' },
{ label: 'warning', value: 'warning', color: 'warning' },
],
}),
form: {
component: {
props: {
clearable: true,
},
},
},
},
is_value: {
title: '是否值',
column: {
show: false
},
form: {
show: false,
value: true
}
}
},
},
};
};

View File

@@ -0,0 +1,49 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/download_center/';
export function GetPermission() {
return request({
url: apiPrefix + 'field_permission/',
method: 'get',
});
}
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,160 @@
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction'
interface CreateCrudOptionsTypes {
output: any;
crudOptions: CrudOptions;
}
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose; }): CreateCrudOptionsTypes {
const pageRequest = async (query: any) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
//权限判定
// @ts-ignore
// @ts-ignore
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: true
},
actionbar: {
buttons: {
add: {
show: false
}
}
},
toolbar: {
buttons: {
export: {
show: false
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 120,
buttons: {
view: {
show: false
},
edit: {
show: false
},
remove: {
show: false
},
download: {
show: compute(ctx => ctx.row.task_status === 2),
text: '下载文件',
type: 'warning',
click: (ctx) => window.open(ctx.row.url, '_blank')
}
},
},
form: {
col: { span: 24 },
labelWidth: '100px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
task_name: {
title: '任务名',
type: 'text',
column: {
minWidth: 160,
align: 'left'
},
search: {
show: true
}
},
file_name: {
title: '文件名',
type: 'text',
column: {
minWidth: 160,
align: 'left'
},
search: {
show: true
}
},
size: {
title: '文件大小(b)',
type: 'number',
column: {
width: 100
}
},
task_status: {
title: '任务状态',
type: 'dict-select',
dict: dict({
data: [
{ label: '任务已创建', value: 0 },
{ label: '任务进行中', value: 1 },
{ label: '任务完成', value: 2 },
{ label: '任务失败', value: 3 },
]
}),
column: {
width: 120
},
search: {
show: true
}
},
create_datetime: {
title: '创建时间',
column: {
width: 160
}
},
update_datetime: {
title: '创建时间',
column: {
width: 160
}
}
},
},
};
};

View File

@@ -0,0 +1,42 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #cell_url="scope">
<el-tag size="small">{{ scope.row.url }}</el-tag>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="downloadCenter">
import { ref, onMounted, inject, onBeforeUpdate } from 'vue';
import { GetPermission } from './api';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import PermissionComNew from './components/PermissionComNew/index.vue';
import _ from "lodash-es";
import { handleColumnPermission } from "/@/utils/columnPermission";
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
// 页面打开后获取列表数据
onMounted(async () => {
crudExpose.doRefresh();
});
</script>

View File

@@ -1,7 +1,19 @@
import * as api from './api';
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import {
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudExpose,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet,
dict
} from '@fast-crud/fast-crud';
import fileSelector from '/@/components/fileSelector/index.vue';
import { shallowRef } from 'vue';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
@@ -20,7 +32,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
actionbar: {
buttons: {
add: {
show: false,
show: true,
click: () => context.openAddHandle?.()
},
},
},
@@ -30,11 +43,22 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
editRequest,
delRequest,
},
tabs: {
show: true,
name: 'file_type',
type: '',
options: [
{ value: 0, label: '图片' },
{ value: 1, label: '视频' },
{ value: 2, label: '音频' },
{ value: 3, label: '其他' },
]
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
show:false,
show: false,
buttons: {
view: {
show: false,
@@ -95,23 +119,34 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
show: true,
},
type: 'input',
column:{
minWidth: 120,
column: {
minWidth: 200,
},
form: {
component: {
placeholder: '请输入文件名称',
clearable: true
},
},
},
preview: {
title: '预览',
column: {
minWidth: 120,
align: 'center'
},
form: {
show: false
}
},
url: {
title: '文件地址',
type: 'file-uploader',
search: {
disabled: true,
},
column:{
minWidth: 200,
column: {
minWidth: 360,
},
},
md5sum: {
@@ -119,13 +154,99 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
search: {
disabled: true,
},
column:{
minWidth: 120,
column: {
minWidth: 300,
},
form: {
disabled: false,
disabled: false
},
},
mime_type: {
title: '文件类型',
type: 'input',
form: {
show: false,
},
column: {
minWidth: 160
}
},
file_type: {
title: '文件类型',
type: 'dict-select',
dict: dict({
data: [
{ label: '图片', value: 0, color: 'success' },
{ label: '视频', value: 1, color: 'warning' },
{ label: '音频', value: 2, color: 'danger' },
{ label: '其他', value: 3, color: 'primary' },
]
}),
column: {
show: false
},
search: {
show: true
},
form: {
show: false,
component: {
placeholder: '请选择文件类型'
}
}
},
size: {
title: '文件大小',
column: {
minWidth: 120
},
form: {
show: false
}
},
upload_method: {
title: '上传方式',
type: 'dict-select',
dict: dict({
data: [
{ label: '默认上传', value: 0, color: 'primary' },
{ label: '文件选择器上传', value: 1, color: 'warning' },
]
}),
column: {
minWidth: 140
},
search: {
show: true
}
},
create_datetime: {
title: '创建时间',
column: {
minWidth: 160
},
form: {
show: false
}
},
// fileselectortest: {
// title: '文件选择器测试',
// type: 'file-selector',
// width: 200,
// form: {
// component: {
// name: shallowRef(fileSelector),
// vModel: 'modelValue',
// tabsShow: 0b0100,
// itemSize: 100,
// multiple: false,
// selectable: true,
// showInput: true,
// inputType: 'video',
// valueKey: 'url',
// }
// }
// }
},
},
};

View File

@@ -1,13 +1,85 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<FileSelector v-model="selected" :showInput="false" ref="fileSelectorRef" :tabsShow="SHOW.ALL" :itemSize="120"
:multiple="false" :selectable="true" valueKey="url" inputType="image">
<!-- <template #input="scope">
input{{ scope }}
</template> -->
<!-- <template #actionbar-left="scope">
actionbar-left{{ scope }}
</template> -->
<!-- <template #actionbar-right="scope">
actionbar-right{{ scope }}
</template> -->
<!-- <template #empty="scope">
empty{{ scope }}
</template> -->
<!-- <template #item="{ data }">
{{ data }}
</template> -->
</FileSelector>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-left="scope">
<el-upload :action="getBaseURL() + 'api/system/file/'" :multiple="false"
:on-success="() => crudExpose.doRefresh()" :drag="false" :show-file-list="false">
<el-button type="primary" icon="plus">上传</el-button>
</el-upload>
</template>
<template #cell_size="scope">
<span>{{ scope.row.size ? getSizeDisplay(scope.row.size) : '0b' }}</span>
</template>
<template #cell_preview="scope">
<div v-if="scope.row.file_type === 0">
<el-image style="width: 100%; aspect-ratio: 1 /1 ;" :src="getBaseURL(scope.row.url)"
:preview-src-list="[getBaseURL(scope.row.url)]" :preview-teleported="true" />
</div>
<div v-if="scope.row.file_type === 1" class="_preview"
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
<el-icon :size="60">
<VideoCamera />
</el-icon>
</div>
<div v-if="scope.row.file_type === 2" class="_preview"
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
<el-icon :size="60">
<Headset />
</el-icon>
</div>
<el-icon v-if="scope.row.file_type === 3" :size="60">
<Document />
</el-icon>
<div v-if="scope.row.file_type > 3">未知类型</div>
</template>
</fs-crud>
<div class="preview" :class="{ show: openPreview }">
<video v-show="videoPreviewSrc" :src="videoPreviewSrc" class="previewItem" :controls="true" :autoplay="true"
:muted="true" :loop="false" ref="videoPreviewRef"></video>
<audio v-show="audioPreviewSrc" :src="audioPreviewSrc" class="previewItem" :controls="true" :autoplay="false"
:muted="true" :loop="false" ref="audioPreviewRef"></audio>
<div class="closePreviewBtn">
<el-icon :size="48" color="white" style="cursor: pointer;" @click="closePreview">
<CircleClose />
</el-icon>
</div>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, nextTick } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { getBaseURL } from '/@/utils/baseUrl';
import FileSelector from '/@/components/fileSelector/index.vue';
import { SHOW } from '/@/components/fileSelector/types';
const fileSelectorRef = ref<any>(null);
const getSizeDisplay = (n: number) => n < 1024 ? n + 'b' : (n < 1024 * 1024 ? (n / 1024).toFixed(2) + 'Kb' : (n / (1024 * 1024)).toFixed(2) + 'Mb');
const openAddHandle = async () => {
fileSelectorRef.value.selectVisiable = true;
await nextTick();
};
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
@@ -15,12 +87,81 @@ const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
const { crudOptions } = createCrudOptions({ crudExpose, context: { openAddHandle } });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
const selected = ref<any>([]);
const openPreview = ref<boolean>(false);
const videoPreviewSrc = ref<string>('');
const audioPreviewSrc = ref<string>('');
const videoPreviewRef = ref<HTMLVideoElement>();
const audioPreviewRef = ref<HTMLAudioElement>();
const openPreviewHandle = (src: string, type: string) => {
openPreview.value = true;
(videoPreviewRef.value as HTMLVideoElement).muted = true;
(audioPreviewRef.value as HTMLAudioElement).muted = true;
if (type === 'video') videoPreviewSrc.value = src;
else audioPreviewSrc.value = src;
window.addEventListener('keydown', onPreviewKeydown);
};
const closePreview = () => {
openPreview.value = false;
videoPreviewSrc.value = '';
audioPreviewSrc.value = '';
window.removeEventListener('keydown', onPreviewKeydown);
};
const onPreviewKeydown = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
openPreview.value = false;
videoPreviewSrc.value = '';
audioPreviewSrc.value = '';
window.removeEventListener('keydown', onPreviewKeydown);
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="css" scoped>
.preview {
display: none;
position: fixed;
top: 0;
height: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
z-index: 9999;
}
.show {
display: block !important;
}
.previewItem {
width: 50%;
position: absolute;
top: 50%;
right: 50%;
transform: translate(25%, -50%);
}
.closePreviewBtn {
width: 50%;
position: absolute;
bottom: 10%;
left: 50%;
transform: translate(-75%);
display: flex;
justify-content: center;
}
._preview {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
</style>

View File

@@ -39,3 +39,10 @@ export function DelObj(id: DelReq) {
data: { id },
});
}
export function GetPermission() {
return request({
url: apiPrefix + 'field_permission/',
method: 'get',
});
}

View File

@@ -5,14 +5,21 @@
</template>
<script lang="ts" setup name="loginLog">
import { ref, onMounted } from 'vue';
import { onMounted } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { GetPermission } from './api';
import { handleColumnPermission } from '/@/utils/columnPermission';
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
onMounted(async () => {
// 设置列权限
const newOptions = await handleColumnPermission(GetPermission, crudOptions);
//重置crudBinding
resetCrudOptions(newOptions);
// 刷新
crudExpose.doRefresh();
});
</script>

View File

@@ -13,6 +13,15 @@ export function login(params: object) {
data: params
});
}
export function loginChangePwd(data: object) {
return request({
url: '/api/system/user/login_change_password/',
method: 'post',
data: data
});
}
export function getUserInfo() {
return request({
url: '/api/system/user/user_info/',

View File

@@ -45,6 +45,12 @@
</el-button>
</el-form-item>
</el-form>
<!-- 申请试用-->
<div style="text-align: center" v-if="showApply()">
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
<span>申请试用</span>
</el-button>
</div>
</template>
<script lang="ts">
@@ -67,6 +73,7 @@ import { SystemConfigStore } from '/@/stores/systemConfig';
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
import { Md5 } from 'ts-md5';
import { errorMessage } from '/@/utils/message';
import {getBaseURL} from "/@/utils/baseUrl";
export default defineComponent({
name: 'loginAccount',
@@ -125,7 +132,10 @@ export default defineComponent({
state.ruleForm.captchaKey = ret.data.key;
});
};
const refreshCaptcha = async () => {
const applyBtnClick = async () => {
window.open(getBaseURL('/api/system/apply_for_trial/'));
};
const refreshCaptcha = async () => {
state.ruleForm.captcha=''
loginApi.getCaptcha().then((ret: any) => {
state.ruleForm.captchaImgBase = ret.data.image_base;
@@ -138,8 +148,13 @@ export default defineComponent({
if (valid) {
loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => {
if (res.code === 2000) {
Session.set('token', res.data.access);
Cookies.set('username', res.data.name);
const {data} = res
Cookies.set('username', res.data.username);
Session.set('token', res.data.access);
useUserInfo().setPwdChangeCount(data.pwd_change_count)
if(data.pwd_change_count==0){
return router.push('/login');
}
if (!themeConfig.value.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
initFrontEndControlRoutes();
@@ -162,35 +177,33 @@ export default defineComponent({
})
};
const getUserInfo = () => {
useUserInfo().setUserInfos();
};
// 登录成功后的跳转
const loginSuccess = () => {
//登录成功获取用户信息,获取系统字典数据
getUserInfo();
//获取所有字典
DictionaryStore().getSystemDictionarys();
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
} else {
router.push('/');
}
// 登录成功提示
// 关闭 loading
state.loading.signIn = true;
const signInText = t('message.signInText');
ElMessage.success(`${currentTimeInfo}${signInText}`);
const pwd_change_count = userInfos.value.pwd_change_count
if(pwd_change_count>0){
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
} else {
router.push('/');
}
// 登录成功提示
// 关闭 loading
state.loading.signIn = true;
const signInText = t('message.signInText');
ElMessage.success(`${currentTimeInfo}${signInText}`);
}
// 添加 loading防止第一次进入界面时出现短暂空白
NextLoading.start();
};
@@ -199,7 +212,10 @@ export default defineComponent({
//获取系统配置
SystemConfigStore().getSystemConfigs();
});
// 是否显示申请试用按钮
const showApply = () => {
return window.location.href.indexOf('public') != -1
}
return {
refreshCaptcha,
@@ -209,6 +225,8 @@ export default defineComponent({
state,
formRef,
rules,
applyBtnClick,
showApply,
...toRefs(state),
};
},
@@ -249,7 +267,7 @@ export default defineComponent({
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 300;
font-weight: 800;
margin-top: 15px;
}
}

View File

@@ -0,0 +1,276 @@
<template>
<el-form ref="formRef" size="large" class="login-content-form" :model="state.ruleForm" :rules="rules"
@keyup.enter="loginClick">
<el-form-item class="login-animation1" prop="username">
<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" readonly
v-model="ruleForm.username" clearable autocomplete="off">
<template #prefix>
<el-icon class="el-input__icon"><ele-User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation2" prop="password">
<el-input :type="isShowPassword ? 'text' : 'password'"
:placeholder="$t('message.account.accountPlaceholder4')" v-model="ruleForm.password">
<template #prefix>
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
</template>
<template #suffix>
<i class="iconfont el-input__icon login-content-password"
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
@click="isShowPassword = !isShowPassword">
</i>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation3" prop="password_regain">
<el-input :type="isShowPassword ? 'text' : 'password'"
:placeholder="$t('message.account.accountPlaceholder5')" v-model="ruleForm.password_regain">
<template #prefix>
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
</template>
<template #suffix>
<i class="iconfont el-input__icon login-content-password"
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
@click="isShowPassword = !isShowPassword">
</i>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation4">
<el-button type="primary" class="login-content-submit" round @click="loginClick" :loading="loading.signIn">
<span>{{ $t('message.account.accountBtnText') }}</span>
</el-button>
</el-form-item>
</el-form>
<!-- 申请试用-->
<div style="text-align: center" v-if="showApply()">
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
<span>申请试用</span>
</el-button>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, FormInstance, FormRules } from 'element-plus';
import { useI18n } from 'vue-i18n';
import Cookies from 'js-cookie';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { initFrontEndControlRoutes } from '/@/router/frontEnd';
import { initBackEndControlRoutes } from '/@/router/backEnd';
import { Session } from '/@/utils/storage';
import { formatAxis } from '/@/utils/formatTime';
import { NextLoading } from '/@/utils/loading';
import * as loginApi from '/@/views/system/login/api';
import { useUserInfo } from '/@/stores/userInfo';
import { DictionaryStore } from '/@/stores/dictionary';
import { SystemConfigStore } from '/@/stores/systemConfig';
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
import { Md5 } from 'ts-md5';
import { errorMessage } from '/@/utils/message';
import { getBaseURL } from "/@/utils/baseUrl";
import { loginChangePwd } from "/@/views/system/login/api";
export default defineComponent({
name: 'changePwd',
setup() {
const { t } = useI18n();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { userInfos } = storeToRefs(useUserInfo());
const route = useRoute();
const router = useRouter();
const state = reactive({
isShowPassword: false,
ruleForm: {
username: '',
password: '',
password_regain: ''
},
loading: {
signIn: false,
},
});
const validatePass = (rule, value, callback) => {
const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}');
if (value === '') {
callback(new Error('请输入密码'));
} else if (!pwdRegex.test(value)) {
callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)'));
} else {
if (state.ruleForm.password !== '') {
formRef.value.validateField('password');
}
callback();
}
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== state.ruleForm.password) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请填写账号', trigger: 'blur' },
],
password: [
{
required: true,
message: '请填写密码',
trigger: 'blur',
},
{
validator: validatePass,
trigger: 'blur',
},
],
password_regain: [
{
required: true,
message: '请填写密码',
trigger: 'blur',
},
{
validator: validatePass2,
trigger: 'blur',
},
],
})
const formRef = ref();
// 时间获取
const currentTime = computed(() => {
return formatAxis(new Date());
});
const applyBtnClick = async () => {
window.open(getBaseURL('/api/system/apply_for_trial/'));
};
const loginClick = async () => {
if (!formRef.value) return
await formRef.value.validate((valid: any) => {
if (valid) {
loginApi.loginChangePwd({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password), password_regain: Md5.hashStr(state.ruleForm.password_regain) }).then((res: any) => {
if (res.code === 2000) {
if (!themeConfig.value.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
initFrontEndControlRoutes();
loginSuccess();
} else {
// 模拟后端控制路由isRequestRoutes 为 true则开启后端控制路由
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
initBackEndControlRoutes();
// 执行完 initBackEndControlRoutes再执行 signInSuccess
loginSuccess();
}
}
}).catch((err: any) => {
// 登录错误之后,刷新验证码
errorMessage("登录失败")
});
} else {
errorMessage("请填写登录信息")
}
})
};
// 登录成功后的跳转
const loginSuccess = () => {
//获取所有字典
DictionaryStore().getSystemDictionarys();
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
} else {
router.push('/');
}
// 登录成功提示
// 关闭 loading
state.loading.signIn = true;
const signInText = t('message.signInText');
ElMessage.success(`${currentTimeInfo}${signInText}`);
// 添加 loading防止第一次进入界面时出现短暂空白
NextLoading.start();
};
onMounted(() => {
state.ruleForm.username = Cookies.get('username')
//获取系统配置
SystemConfigStore().getSystemConfigs();
});
// 是否显示申请试用按钮
const showApply = () => {
return window.location.href.indexOf('public') != -1
}
return {
loginClick,
loginSuccess,
state,
formRef,
rules,
applyBtnClick,
showApply,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
@for $i from 1 through 5 {
.login-animation#{$i} {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
animation-delay: calc($i/10) + s;
}
}
.login-content-password {
display: inline-block;
width: 20px;
cursor: pointer;
&:hover {
color: #909399;
}
}
.login-content-captcha {
width: 100%;
padding: 0;
font-weight: bold;
letter-spacing: 5px;
}
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 800;
margin-top: 15px;
}
}
</style>

View File

@@ -5,51 +5,52 @@
<img :src="siteLogo" />
<div class="login-left-logo-text">
<span>{{ getSystemConfig['login.site_title'] || getThemeConfig.globalViceTitle }}</span>
<span class="login-left-logo-text-msg">{{
<span class="login-left-logo-text-msg" style="margin-top: 5px;">{{
getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
</div>
</div>
<div class="login-left-img">
<img :src="loginMain" />
</div>
<img :src="loginBg" class="login-left-waves" />
</div>
<div class="login-right flex z-10">
<div class="login-right-warp flex-margin">
<span class="login-right-warp-one"></span>
<span class="login-right-warp-two"></span>
<!-- <span class="login-right-warp-one"></span>-->
<!-- <span class="login-right-warp-two"></span>-->
<div class="login-right-warp-mian">
<div class="login-right-warp-main-title">{{ getSystemConfig['login.site_title'] ||
getThemeConfig.globalTitle }} 欢迎您</div>
<div class="login-right-warp-main-title">
{{userInfos.pwd_change_count===0?'初次登录修改密码':'欢迎登录'}}
</div>
<div class="login-right-warp-main-form">
<div v-if="!state.isScan">
<el-tabs v-model="state.tabsActiveName">
<el-tab-pane :label="$t('message.label.one1')" name="account">
<el-tab-pane :label="$t('message.label.changePwd')" name="changePwd" v-if="userInfos.pwd_change_count===0">
<ChangePwd />
</el-tab-pane>
<el-tab-pane :label="$t('message.label.one1')" name="account" v-else>
<Account />
</el-tab-pane>
<!-- TODO 手机号码登录未接入展示隐藏 -->
<!-- <el-tab-pane :label="$t('message.label.two2')" name="mobile">
<Mobile />
</el-tab-pane> -->
</el-tabs>
</div>
<Scan v-if="state.isScan" />
<div class="login-content-main-sacn" @click="state.isScan = !state.isScan">
<i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>
<div class="login-content-main-sacn-delta"></div>
</div>
<!-- <Scan v-if="state.isScan" />-->
<!-- <div class="login-content-main-sacn" @click="state.isScan = !state.isScan">-->
<!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>-->
<!-- <div class="login-content-main-sacn-delta"></div>-->
<!-- </div>-->
</div>
</div>
</div>
</div>
<div class="login-authorization z-10">
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 django-vue-admin.com' }} 版权所有</p>
<p class="la-other">
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 北京信码新创科技有限公司' }} 版权所有</p>
<p class="la-other" style="margin-top: 5px;">
<a href="https://beian.miit.gov.cn" target="_blank">{{ getSystemConfig['login.keep_record'] ||
'ICP备18005113号-3' }}</a>
'ICP备2021031018号' }}</a>
|
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : 'https://django-vue-admin.com'"
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : '#'"
target="_blank">帮助</a>
|
<a
@@ -60,26 +61,29 @@
</p>
</div>
</div>
<div v-if="siteBg">
<img :src="siteBg" class="fixed inset-0 z-1 w-full h-full" />
<div v-if="loginBg">
<img :src="loginBg" class="loginBg fixed inset-0 z-1 w-full h-full" />
</div>
</template>
<script setup lang="ts" name="loginIndex">
import { defineAsyncComponent, onMounted, reactive, computed } from 'vue';
import {defineAsyncComponent, onMounted, reactive, computed, watch} from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading';
import logoMini from '/@/assets/logo-mini.svg';
import loginMain from '/@/assets/login-main.svg';
import loginBg from '/@/assets/login-bg.svg';
import loginBg from '/@/assets/login-bg.png';
import { SystemConfigStore } from '/@/stores/systemConfig'
import { getBaseURL } from "/@/utils/baseUrl";
// 引入组件
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
const ChangePwd = defineAsyncComponent(() => import('/@/views/system/login/component/changePwd.vue'));
import _ from "lodash-es";
import {useUserInfo} from "/@/stores/userInfo";
const { userInfos } = storeToRefs(useUserInfo());
// 定义变量内容
const storesThemeConfig = useThemeConfig();
@@ -89,6 +93,16 @@ const state = reactive({
isScan: false,
});
watch(()=>userInfos.value.pwd_change_count,(val)=>{
if(val===0){
state.tabsActiveName ='changePwd'
}else{
state.tabsActiveName ='account'
}
},{deep:true,immediate:true})
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
@@ -187,13 +201,13 @@ onMounted(() => {
width: 700px;
.login-right-warp {
border: 1px solid var(--el-color-primary-light-3);
//border: 1px solid var(--el-color-primary-light-3);
border-radius: 3px;
width: 500px;
height: 500px;
position: relative;
overflow: hidden;
background-color: var(--el-color-white);
//background-color: var(--el-color-white);
.login-right-warp-one,
.login-right-warp-two {
@@ -265,7 +279,8 @@ onMounted(() => {
.login-right-warp-main-title {
height: 130px;
line-height: 130px;
font-size: 27px;
font-size: 32px;
font-weight: 600;
text-align: center;
letter-spacing: 3px;
animation: logoAnimation 0.3s ease;
@@ -321,7 +336,7 @@ onMounted(() => {
}
.login-authorization {
position: fixed;
position: absolute;
bottom: 30px;
left: 0;
right: 0;

View File

@@ -48,3 +48,10 @@ export function BatchAdd(obj: AddReq) {
});
}
export function BatchDelete(keys: any) {
return request({
url: apiPrefix + 'multiple_delete/',
method: 'delete',
data: { keys },
});
}

View File

@@ -4,6 +4,8 @@ import {auth} from '/@/utils/authFunction'
import {request} from '/@/utils/service';
import { successNotification } from '/@/utils/message';
import { ElMessage } from 'element-plus';
import { nextTick, ref } from 'vue';
import XEUtils from 'xe-utils';
//此处为crudOptions配置
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async () => {
@@ -22,7 +24,42 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
const addRequest = async ({form}: AddReq) => {
return await api.AddObj({...form, ...{menu: context!.selectOptions.value.id}});
};
// 记录选中的行
const selectedRows = ref<any>([]);
const onSelectionChange = (changed: any) => {
const tableData = crudExpose.getTableData();
const unChanged = tableData.filter((row: any) => !changed.includes(row));
// 添加已选择的行
XEUtils.arrayEach(changed, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
if (!ids.includes(item.id)) {
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
}
});
// 剔除未选择的行
XEUtils.arrayEach(unChanged, (unItem: any) => {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
});
};
const toggleRowSelection = () => {
// 多选后,回显默认勾选
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
const selected = XEUtils.filter(tableData, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
return ids.includes(item.id);
});
nextTick(() => {
XEUtils.arrayEach(selected, (item) => {
tableRef.toggleRowSelection(item, true);
});
});
};
return {
selectedRows,
crudOptions: {
pagination:{
show:false
@@ -84,6 +121,11 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
editRequest,
delRequest,
},
table: {
rowKey: 'id', //设置你的主键id 默认rowKey=id
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
form: {
col: {span: 24},
labelWidth: '100px',
@@ -93,6 +135,16 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
},
},
columns: {
$checked: {
title: '选择',
form: { show: false },
column: {
type: 'selection',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
_index: {
title: '序号',
form: {show: false},

View File

@@ -1,19 +1,72 @@
<template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<el-tooltip content="批量删除">
<el-button text type="danger" :disabled="selectedRowsCount === 0" :icon="Delete" circle @click="handleBatchDelete" />
</el-tooltip>
</template>
<template #pagination-right>
<el-popover placement="top" :width="400" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small">
<el-table-column width="150" property="id" label="id" />
<el-table-column fixed="right" label="操作" min-width="60">
<template #default="scope">
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</fs-crud>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { MenuTreeItemType } from '../../types';
import { ElMessage, ElMessageBox } from 'element-plus';
import XEUtils from 'xe-utils';
import { BatchDelete } from './api';
import { Close, Delete } from '@element-plus/icons-vue';
// 当前选择的菜单信息
let selectOptions: any = ref({ name: null });
const { crudRef, crudBinding, crudExpose, context } = useFs({ createCrudOptions, context: { selectOptions } });
const { crudRef, crudBinding, crudExpose, context,selectedRows } = useFs({ createCrudOptions, context: { selectOptions } });
const { doRefresh, setTableData } = crudExpose;
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
// 批量删除
const handleBatchDelete = async () => {
await ElMessageBox.confirm(`确定要批量删除这${selectedRows.value.length}条记录吗`, '确认', {
distinguishCancelAndClose: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
});
await BatchDelete(XEUtils.pluck(selectedRows.value, 'id'));
ElMessage.info('删除成功');
selectedRows.value = [];
await crudExpose.doRefresh();
};
// 移除已选中的行
const removeSelectedRows = (row: any) => {
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
tableRef.toggleRowSelection(row, false);
} else {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
}
};
const handleRefreshTable = (record: MenuTreeItemType) => {
if (!record.is_catalog && record.id) {
selectOptions.value = record;

View File

@@ -42,6 +42,13 @@ export function DelObj(id: DelReq) {
});
}
export function BatchDelete(keys: any) {
return request({
url: apiPrefix + 'multiple_delete/',
method: 'delete',
data: { keys },
});
}
/**
* 获取所有model
*/

View File

@@ -2,8 +2,9 @@ import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { inject } from 'vue';
import { inject, nextTick, ref } from 'vue';
import {auth} from "/@/utils/authFunction";
import XEUtils from 'xe-utils';
@@ -27,8 +28,41 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
form.menu = selectOptions.value.id;
return await api.AddObj(form);
};
// 记录选中的行
const selectedRows = ref<any>([]);
const onSelectionChange = (changed: any) => {
const tableData = crudExpose.getTableData();
const unChanged = tableData.filter((row: any) => !changed.includes(row));
// 添加已选择的行
XEUtils.arrayEach(changed, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
if (!ids.includes(item.id)) {
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
}
});
// 剔除未选择的行
XEUtils.arrayEach(unChanged, (unItem: any) => {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
});
};
const toggleRowSelection = () => {
// 多选后,回显默认勾选
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
const selected = XEUtils.filter(tableData, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
return ids.includes(item.id);
});
nextTick(() => {
XEUtils.arrayEach(selected, (item) => {
tableRef.toggleRowSelection(item, true);
});
});
};
return {
selectedRows,
crudOptions: {
request: {
pageRequest,
@@ -77,7 +111,22 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
width: '600px',
},
},
table: {
rowKey: 'id', //设置你的主键id 默认rowKey=id
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
columns: {
$checked: {
title: '选择',
form: { show: false },
column: {
type: 'selection',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
_index: {
title: '序号',
form: { show: false },

View File

@@ -1,137 +1,177 @@
<template>
<div>
<el-dialog ref="modelRef" v-model="modelDialog" title="选择model">
<div v-show="props.model">
<el-tag>已选择:{{ props.model }}</el-tag>
</div>
<!-- 搜索输入框 -->
<el-input
v-model="searchQuery"
placeholder="搜索模型..."
style="margin-bottom: 10px;"
></el-input>
<div class="model-card">
<!--注释编号:django-vue3-admin-index483211: 对请求回来的allModelData进行computed计算返加搜索框匹配到的内容-->
<div v-for="(item,index) in filteredModelData" :value="item.key" :key="index">
<el-text :type="modelCheckIndex===index?'primary':''" @click="onModelChecked(item,index)">
{{ item.app + '--' + item.title + '(' + item.key + ')' }}
</el-text>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="modelDialog = false">取消</el-button>
<el-button type="primary" @click="handleAutomatch">
确定
</el-button>
</span>
</template>
</el-dialog>
<div style="height: 80vh">
<fs-crud ref="crudRef" v-bind="crudBinding">
</fs-crud>
</div>
</div>
<div>
<el-dialog ref="modelRef" v-model="modelDialog" title="选择model">
<div v-show="props.model">
<el-tag>已选择:{{ props.model }}</el-tag>
</div>
<!-- 搜索输入框 -->
<el-input v-model="searchQuery" placeholder="搜索模型..." style="margin-bottom: 10px"></el-input>
<div class="model-card">
<!--注释编号:django-vue3-admin-index483211: 对请求回来的allModelData进行computed计算返加搜索框匹配到的内容-->
<div v-for="(item, index) in filteredModelData" :value="item.key" :key="index">
<el-text :type="modelCheckIndex === index ? 'primary' : ''" @click="onModelChecked(item, index)">
{{ item.app + '--' + item.title + '(' + item.key + ')' }}
</el-text>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="modelDialog = false">取消</el-button>
<el-button type="primary" @click="handleAutomatch"> 确定 </el-button>
</span>
</template>
</el-dialog>
<div style="height: 72vh">
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<el-tooltip content="批量删除">
<el-button text type="danger" :disabled="selectedRowsCount === 0" :icon="Delete" circle @click="handleBatchDelete" />
</el-tooltip>
</template>
<template #pagination-right>
<el-popover placement="top" :width="400" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small">
<el-table-column width="150" property="id" label="id" />
<el-table-column fixed="right" label="操作" min-width="60">
<template #default="scope">
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</fs-crud>
</div>
</div>
</template>
<script lang="ts" setup>
import {ref, onMounted, reactive, computed } from 'vue';
import {useFs} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
import {getModelList} from './api'
import {MenuTreeItemType} from "/@/views/system/menu/types";
import {successMessage, successNotification, warningNotification} from '/@/utils/message';
import {automatchColumnsData} from '/@/views/system/columns/components/ColumnsTableCom/api';
import { ref, onMounted, reactive, computed } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { BatchDelete, getModelList } from './api';
import { Close, Delete } from '@element-plus/icons-vue';
import { MenuTreeItemType } from '/@/views/system/menu/types';
import { successMessage, successNotification, warningNotification } from '/@/utils/message';
import { automatchColumnsData } from '/@/views/system/columns/components/ColumnsTableCom/api';
import XEUtils from 'xe-utils';
import { ElMessage, ElMessageBox } from 'element-plus';
// 当前选择的菜单信息
let selectOptions: any = ref({name: null});
let selectOptions: any = ref({ name: null });
const props = reactive({
model: '',
app: '',
menu: ''
})
model: '',
app: '',
menu: '',
});
//model弹窗
const modelDialog = ref(false)
const modelDialog = ref(false);
// 获取所有model
const allModelData = ref<any[]>([]);
const modelCheckIndex = ref(null)
const modelCheckIndex = ref(null);
const onModelChecked = (row, index) => {
modelCheckIndex.value = index
props.model = row.key
props.app = row.app
}
modelCheckIndex.value = index;
props.model = row.key;
props.app = row.app;
};
// 注释编号:django-vue3-admin-index083311:代码开始行
// 功能说明:搭配搜索的处理,返回搜索结果
const searchQuery = ref('');
const filteredModelData = computed(() => {
if (!searchQuery.value) {
return allModelData.value;
}
const query = searchQuery.value.toLowerCase();
return allModelData.value.filter(item =>
item.app.toLowerCase().includes(query) ||
item.title.toLowerCase().includes(query) ||
item.key.toLowerCase().includes(query)
);
});
if (!searchQuery.value) {
return allModelData.value;
}
const query = searchQuery.value.toLowerCase();
return allModelData.value.filter(
(item) => item.app.toLowerCase().includes(query) || item.title.toLowerCase().includes(query) || item.key.toLowerCase().includes(query)
);
});
// 注释编号:django-vue3-admin-index083311:代码结束行
/**
* 菜单选中时,加载表格数据
* @param record
*/
const handleRefreshTable = (record: MenuTreeItemType) => {
if (!record.is_catalog && record.id) {
selectOptions.value = record;
crudExpose.doRefresh();
} else {
//清空表格数据
crudExpose.setTableData([]);
}
if (!record.is_catalog && record.id) {
selectOptions.value = record;
crudExpose.doRefresh();
} else {
//清空表格数据
crudExpose.setTableData([]);
}
};
/**
* 自动匹配列
*/
const handleAutomatch = async () => {
props.menu = selectOptions.value.id
modelDialog.value = false
if (props.menu && props.model) {
const res = await automatchColumnsData(props);
if (res?.code === 2000) {
successNotification('匹配成功');
}
crudExpose.doSearch({form: {menu: props.menu, model: props.model}});
}else {
warningNotification('请选择角色和模型表!');
}
props.menu = selectOptions.value.id;
modelDialog.value = false;
if (props.menu && props.model) {
const res = await automatchColumnsData(props);
if (res?.code === 2000) {
successNotification('匹配成功');
}
crudExpose.doSearch({ form: { menu: props.menu, model: props.model } });
} else {
warningNotification('请选择角色和模型表!');
}
};
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, props, modelDialog, selectOptions,allModelData});
onMounted(async () => {
const res = await getModelList();
allModelData.value = res.data;
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
defineExpose({selectOptions, handleRefreshTable});
// 批量删除
const handleBatchDelete = async () => {
await ElMessageBox.confirm(`确定要批量删除这${selectedRows.value.length}条记录吗`, '确认', {
distinguishCancelAndClose: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
});
await BatchDelete(XEUtils.pluck(selectedRows.value, 'id'));
ElMessage.info('删除成功');
selectedRows.value = [];
await crudExpose.doRefresh();
};
// 移除已选中的行
const removeSelectedRows = (row: any) => {
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
tableRef.toggleRowSelection(row, false);
} else {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
}
};
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, props, modelDialog, selectOptions, allModelData });
onMounted(async () => {
const res = await getModelList();
allModelData.value = res.data;
});
defineExpose({ selectOptions, handleRefreshTable });
</script>
<style scoped lang="scss">
.model-card {
margin-top: 10px;
height: 30vh;
overflow-y: scroll;
margin-top: 10px;
height: 30vh;
overflow-y: scroll;
div {
margin: 15px 0;
cursor: pointer;
}
div {
margin: 15px 0;
cursor: pointer;
}
}
</style>

View File

@@ -10,21 +10,12 @@
<el-input v-model="menuFormData.name" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="父级菜单" prop="parent">
<el-tree-select
v-model="menuFormData.parent"
:props="defaultTreeProps"
:data="deptDefaultList"
:cache-data="props.cacheData"
lazy
check-strictly
clearable
:load="handleTreeLoad"
placeholder="请选择父级菜单"
style="width: 100%"
/>
<el-tree-select v-model="menuFormData.parent" :props="defaultTreeProps" :data="deptDefaultList"
:cache-data="props.cacheData" lazy check-strictly clearable :load="handleTreeLoad"
placeholder="请选择父级菜单" style="width: 100%" />
</el-form-item>
<el-form-item label="路由地址" prop="web_path">
<el-form-item label="路由地址" prop="web_path">
<el-input v-model="menuFormData.web_path" placeholder="请输入路由地址,请以/开头" />
</el-form-item>
@@ -35,12 +26,14 @@
<el-row>
<el-col :span="12">
<el-form-item required label="状态">
<el-switch v-model="menuFormData.status" width="60" inline-prompt active-text="启用" inactive-text="禁用" />
<el-switch v-model="menuFormData.status" width="60" inline-prompt active-text="启用"
inactive-text="禁用" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="menuFormData.status" required label="侧边显示">
<el-switch v-model="menuFormData.visible" width="60" inline-prompt active-text="显示" inactive-text="隐藏" />
<el-switch v-model="menuFormData.visible" width="60" inline-prompt active-text="显示"
inactive-text="隐藏" />
</el-form-item>
</el-col>
</el-row>
@@ -48,46 +41,45 @@
<el-row>
<el-col :span="12">
<el-form-item required label="是否目录">
<el-switch v-model="menuFormData.is_catalog" width="60" inline-prompt active-text="是" inactive-text="否" />
<el-switch v-model="menuFormData.is_catalog" width="60" inline-prompt active-text="是"
inactive-text="" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="!menuFormData.is_catalog" required label="外链接">
<el-switch v-model="menuFormData.is_link" width="60" inline-prompt active-text="是" inactive-text="否" />
<el-switch v-model="menuFormData.is_link" width="60" inline-prompt active-text="是"
inactive-text="" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item required v-if="!menuFormData.is_catalog" label="是否固定">
<el-switch v-model="menuFormData.is_affix" width="60" inline-prompt active-text="是"
inactive-text="" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" required label="是否内嵌">
<el-switch v-model="menuFormData.is_iframe" width="60" inline-prompt active-text="是"
inactive-text="" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item required v-if="!menuFormData.is_catalog" label="是否固定">
<el-switch v-model="menuFormData.is_affix" width="60" inline-prompt active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" required label="是否内嵌">
<el-switch v-model="menuFormData.is_iframe" width="60" inline-prompt active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="menuFormData.description" maxlength="200" show-word-limit type="textarea" placeholder="请输入备注" />
<el-input v-model="menuFormData.description" maxlength="200" show-word-limit type="textarea"
placeholder="请输入备注" />
</el-form-item>
<el-divider></el-divider>
<div style="min-height: 184px">
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件地址" prop="component">
<el-autocomplete
class="w-full"
v-model="menuFormData.component"
:fetch-suggestions="querySearch"
:trigger-on-focus="false"
clearable
:debounce="100"
placeholder="输入组件地址"
/>
<el-autocomplete class="w-full" v-model="menuFormData.component" :fetch-suggestions="querySearch"
:trigger-on-focus="false" clearable :debounce="100" placeholder="输入组件地址" />
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件名称" prop="component_name">
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件名称"
prop="component_name">
<el-input v-model="menuFormData.component_name" placeholder="请输入组件名称" />
</el-form-item>
@@ -96,7 +88,8 @@
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog" label="缓存">
<el-switch v-model="menuFormData.cache" width="60" inline-prompt active-text="启用" inactive-text="禁用" />
<el-switch v-model="menuFormData.cache" width="60" inline-prompt active-text="启用"
inactive-text="禁用" />
</el-form-item>
</div>
@@ -111,6 +104,7 @@
</template>
<script lang="ts" setup>
import XEUtils from 'xe-utils';
import { ref, onMounted, reactive } from 'vue';
import { ElForm, FormRules } from 'element-plus';
import IconSelector from '/@/components/iconSelector/index.vue';
@@ -148,14 +142,14 @@ const validateWebPath = (rule: any, value: string, callback: Function) => {
};
const validateLinkUrl = (rule: any, value: string, callback: Function) => {
let pattern = /^\/.*?/;
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
const reg = pattern.test(value) || patternUrl.test(value)
if (reg) {
callback();
} else {
callback(new Error('请输入正确的地址'));
}
let pattern = /^\/.*?/;
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
const reg = pattern.test(value) || patternUrl.test(value)
if (reg) {
callback();
} else {
callback(new Error('请输入正确的地址'));
}
};
const props = withDefaults(defineProps<IProps>(), {
@@ -172,7 +166,7 @@ const rules = reactive<FormRules>({
name: [{ required: true, message: '菜单名称必填', trigger: 'blur' }],
component: [{ required: true, message: '请输入组件地址', trigger: 'blur' }],
component_name: [{ required: true, message: '请输入组件名称', trigger: 'blur' }],
link_url: [{ required: true, message: '请输入外链接地址',validator:validateLinkUrl, trigger: 'blur' }],
link_url: [{ required: true, message: '请输入外链接地址', validator: validateLinkUrl, trigger: 'blur' }],
});
let deptDefaultList = ref<MenuTreeItemType[]>([]);
@@ -189,9 +183,9 @@ let menuFormData = reactive<MenuFormDataType>({
description: '',
is_catalog: false,
is_link: false,
is_iframe: false,
is_affix: false,
link_url:''
is_iframe: false,
is_affix: false,
link_url: ''
});
let menuBtnLoading = ref(false);
@@ -210,9 +204,9 @@ const setMenuFormData = () => {
menuFormData.description = props.initFormData?.description || '';
menuFormData.is_catalog = !!props.initFormData.is_catalog;
menuFormData.is_link = !!props.initFormData.is_link;
menuFormData.is_iframe =!!props.initFormData.is_iframe;
menuFormData.is_affix =!!props.initFormData.is_affix;
menuFormData.link_url =props.initFormData.link_url;
menuFormData.is_iframe = !!props.initFormData.is_iframe;
menuFormData.is_affix = !!props.initFormData.is_affix;
menuFormData.link_url = props.initFormData.link_url;
}
};
@@ -246,7 +240,7 @@ const createFilter = (queryString: string) => {
const handleTreeLoad = (node: Node, resolve: Function) => {
if (node.level !== 0) {
lazyLoadMenu({ parent: node.data.id }).then((res: APIResponseData) => {
resolve(res.data);
resolve(XEUtils.filter(res.data, (i: MenuTreeItemType) => i.is_catalog));
});
}
};
@@ -278,9 +272,14 @@ const handleCancel = (type: string = '') => {
formRef.value?.resetFields();
};
/**
* 初始化
*/
onMounted(async () => {
props.treeData.map((item) => {
deptDefaultList.value.push(item);
if (item.is_catalog) {
deptDefaultList.value.push(item);
}
});
setMenuFormData();
});
@@ -290,6 +289,7 @@ onMounted(async () => {
.menu-form-com {
margin: 10px;
overflow-y: auto;
.menu-form-alert {
color: #fff;
line-height: 24px;
@@ -298,6 +298,7 @@ onMounted(async () => {
border-radius: 4px;
background-color: var(--el-color-primary);
}
.menu-form-btns {
padding-bottom: 10px;
box-sizing: border-box;

View File

@@ -16,12 +16,12 @@
<el-col :span="18">
<el-tabs type="border-card">
<el-tab-pane label="按钮权限配置" >
<div style="height: 80vh">
<div style="height: 72vh">
<MenuButtonCom ref="menuButtonRef" />
</div>
</el-tab-pane>
<el-tab-pane label="列权限配置">
<div style="height: 80vh">
<div style="height: 72vh">
<MenuFieldCom ref="menuFieldRef"></MenuFieldCom>
</div>
</el-tab-pane>
@@ -138,7 +138,7 @@ onMounted(() => {
.menu-box {
height: 100%;
padding: 10px;
background-color: #fff;
background-color: var(--el-fill-color-blank);;
box-sizing: border-box;
}

View File

@@ -65,5 +65,5 @@ export interface MenuFormDataType {
is_link: boolean;
is_iframe:boolean;
is_affix:boolean;
link_url: string;
link_url: string|undefined;
}

View File

@@ -1,46 +1,38 @@
import * as api from './api';
import {dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions} from '@fast-crud/fast-crud';
import { dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import tableSelector from '/@/components/tableSelector/index.vue';
import {shallowRef, computed, ref, inject} from 'vue';
import { shallowRef, computed } from 'vue';
import manyToMany from '/@/components/manyToMany/index.vue';
import {auth} from '/@/utils/authFunction'
import {createCrudOptions as userCrudOptions } from "/@/views/system/user/crud";
import {request} from '/@/utils/service'
const {compute} = useCompute();
import { auth } from '/@/utils/authFunction';
const { compute } = useCompute();
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { tabActivted } = context; //从context中获取tabActivted
export const createCrudOptions = function ({
crudExpose,
tabActivted
}: { crudExpose: CrudExpose; tabActivted: any }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
if (tabActivted.value === 'receive') {
return await api.GetSelfReceive(query);
}
return await api.GetList(query);
};
const editRequest = async ({form, row}: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
return await api.AddObj(form);
};
const pageRequest = async (query: PageQuery) => {
if (tabActivted.value === 'receive') {
return await api.GetSelfReceive(query);
}
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
const viewRequest = async ({row}: { row: any }) => {
return await api.GetObj(row.id);
};
const IsReadFunc = computed(() => {
return tabActivted.value === 'receive';
});
const viewRequest = async ({ row }: { row: any }) => {
return await api.GetObj(row.id);
};
const IsReadFunc = computed(() => {
return tabActivted.value === 'receive';
});
return {
crudOptions: {
@@ -50,27 +42,27 @@ export const createCrudOptions = function ({
editRequest,
delRequest,
},
actionbar:{
buttons:{
add:{
show:computed(() =>{
actionbar: {
buttons: {
add: {
show: computed(() => {
return tabActivted.value !== 'receive' && auth('messageCenter:Create');
})
}),
},
}
},
},
rowHandle: {
fixed:'right',
width:150,
fixed: 'right',
width: 150,
buttons: {
edit: {
show: false,
},
view: {
text:"查看",
type:'text',
iconRight:'View',
show:auth("messageCenter:Search"),
text: '查看',
type: 'text',
iconRight: 'View',
show: auth('messageCenter:Search'),
click({ index, row }) {
crudExpose.openView({ index, row });
if (tabActivted.value === 'receive') {
@@ -82,7 +74,7 @@ export const createCrudOptions = function ({
remove: {
iconRight: 'Delete',
type: 'text',
show:auth('messageCenter:Delete')
show: auth('messageCenter:Delete'),
},
},
},
@@ -99,7 +91,7 @@ export const createCrudOptions = function ({
show: true,
},
type: ['text', 'colspan'],
column:{
column: {
minWidth: 120,
},
form: {
@@ -132,7 +124,7 @@ export const createCrudOptions = function ({
target_type: {
title: '目标类型',
type: ['dict-radio', 'colspan'],
column:{
column: {
minWidth: 120,
},
dict: dict({
@@ -285,7 +277,7 @@ export const createCrudOptions = function ({
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ form }) => {
return form.target_dept_name;
return form.dept_info;
}),
tableConfig: {
url: '/api/system/dept/all_dept/',
@@ -297,7 +289,7 @@ export const createCrudOptions = function ({
{
prop: 'name',
label: '部门名称',
width: 150,
width: 150,
},
{
prop: 'status_label',
@@ -349,7 +341,7 @@ export const createCrudOptions = function ({
},
],
component: {
disabled: true,
disabled: false,
id: '1', // 当同一个页面有多个editor时需要配置不同的id
editorConfig: {
// 是否只读
@@ -373,4 +365,4 @@ export const createCrudOptions = function ({
},
},
};
};
}

View File

@@ -1,43 +1,35 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #header-middle>
<el-tabs v-model="tabActivted" @tab-click="onTabClick">
<el-tab-pane label="我的发布" name="send"></el-tab-pane>
<el-tab-pane label="我的接收" name="receive"></el-tab-pane>
</el-tabs>
</template>
</fs-crud>
</fs-page>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #header-middle>
<el-tabs v-model="tabActivted" @tab-click="onTabClick">
<el-tab-pane label="我的发布" name="send"></el-tab-pane>
<el-tab-pane label="我的接收" name="receive"></el-tab-pane>
</el-tabs>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="messageCenter">
import {ref, onMounted} from 'vue';
import {useExpose, useCrud} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const {crudExpose} = useExpose({crudRef, crudBinding});
import { ref, onMounted } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import createCrudOptions from './crud';
//tab选择
const tabActivted = ref('send')
const onTabClick= (tab:any)=> {
const { paneName } = tab
tabActivted.value = paneName
crudExpose.doRefresh();
}
// 你的crud配置
const {crudOptions} = createCrudOptions({crudExpose,tabActivted});
const tabActivted = ref('send');
const onTabClick = (tab: any) => {
const { paneName } = tab;
tabActivted.value = paneName;
crudExpose.doRefresh();
};
const context: any = { tabActivted }; //将 tabActivted 通过context传递给crud.tsx
// 初始化crud配置
const {resetCrudOptions} = useCrud({crudExpose, crudOptions});
const { crudRef, crudBinding, crudExpose } = useFs({ createCrudOptions, context });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
crudExpose.doRefresh();
});
</script>

View File

@@ -32,7 +32,7 @@
<el-col :xs="24" :sm="24" class="personal-item mb6">
<div class="personal-item-label">角色</div>
<div class="personal-item-value">
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index">{{ item.name }}</el-tag>
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index" style="margin-right: 5px;">{{ item.name }}</el-tag>
</div>
</el-col>
</el-row>
@@ -153,7 +153,7 @@
center
>
<el-form-item label="原密码" required prop="oldPassword">
<el-input v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" clearable></el-input>
<el-input type="password" v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" show-password clearable></el-input>
</el-form-item>
<el-form-item required prop="newPassword" label="新密码">
<el-input type="password" v-model="userPasswordInfo.newPassword" placeholder="请输入新密码" show-password clearable></el-input>
@@ -354,7 +354,8 @@ const uploadImg = (data: any) => {
api.uploadAvatar(formdata).then((res: any) => {
if (res.code === 2000) {
selectImgVisible.value = false;
state.personalForm.avatar = getBaseURL() + res.data.url;
// state.personalForm.avatar = getBaseURL() + res.data.url;
state.personalForm.avatar = res.data.url;
api.updateUserInfo(state.personalForm).then((res: any) => {
successMessage('更新成功');
getUserInfo();

View File

@@ -1,59 +0,0 @@
import { request } from "/@/utils/service";
/**
* 获取角色的授权列表
* @param roleId
* @param query
*/
export function getRolePremission(query:object) {
return request({
url: '/api/system/role_menu_button_permission/get_role_premission/',
method: 'get',
params:query
})
}
/***
* 设置角色的权限
* @param roleId
* @param data
*/
export function setRolePremission(roleId:any,data:object) {
return request({
url: `/api/system/role_menu_button_permission/${roleId}/set_role_premission/`,
method: 'put',
data
})
}
export function getDataPermissionRange() {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
})
}
export function getDataPermissionDept() {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get'
})
}
export function getDataPermissionMenu() {
return request({
url: '/api/system/role_menu_button_permission/get_role_permissions/',
method: 'get'
})
}
/**
* 设置按钮的数据范围
*/
export function setBtnDatarange(roleId:number,data:object) {
return request({
url: `/api/system/role_menu_button_permission/${roleId}/set_btn_datarange/`,
method: 'put',
data
})
}

View File

@@ -1,392 +0,0 @@
<template>
<el-drawer v-model="drawerVisible" title="权限配置" direction="rtl" size="60%" :close-on-click-modal="false"
:before-close="handleDrawerClose" :destroy-on-close="true">
<template #header>
<el-row>
<el-col :span="4">
<div>当前授权角色
<el-tag>{{ props.roleName }}</el-tag>
</div>
</el-col>
<el-col :span="6">
<div>
<el-button size="small" type="primary" class="pc-save-btn" @click="handleSavePermission">保存菜单授权
</el-button>
</div>
</el-col>
</el-row>
</template>
<div class="permission-com">
<el-tabs>
<el-tab-pane v-for="(item, mIndex) in menuData" :key="mIndex" :label="item.name">
<el-tabs tab-position="left">
<el-tab-pane v-for="(menu, mIndex) in item.menus" :key="mIndex" :label="menu.name" >
<el-checkbox v-model="menu.isCheck">页面显示权限</el-checkbox>
<div class="pc-collapse-main">
<div class="pccm-item">
<div class="menu-form-alert"> 配置操作功能接口权限,配置数据权限点击小齿轮 </div>
<el-checkbox v-for="(btn, bIndex) in menu.btns" :key="bIndex" v-model="btn.isCheck"
:label="btn.value">
<div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(menu, btn.id)">
<el-icon>
<Setting />
</el-icon>
</span>
</div>
</el-checkbox>
</div>
<div class="pccm-item" v-if="menu.columns && menu.columns.length > 0">
<div class="menu-form-alert"> 配置数据列字段权限 </div>
<ul class="columns-list">
<li class="columns-head">
<div class="width-txt">
<span>字段</span>
</div>
<div v-for="(head, hIndex) in column.header" :key="hIndex" class="width-check">
<el-checkbox :label="head.value" @change="handleColumnChange($event, menu, head.value)">
<span>{{ head.label }}</span>
</el-checkbox>
</div>
</li>
<li v-for="(c_item, c_index) in menu.columns" :key="c_index" class="columns-item">
<div class="width-txt">{{ c_item.title }}</div>
<div v-for="(col, cIndex) in column.header" :key="cIndex" class="width-check">
<el-checkbox v-model="c_item[col.value]" class="ci-checkout"></el-checkbox>
</div>
</li>
</ul>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false"
:before-close="handleDialogClose">
<div class="pc-dialog">
<el-select v-model="dataPermission" @change="handlePermissionRangeChange" class="dialog-select"
placeholder="请选择">
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-tree-select v-show="dataPermission === 4" node-key="id" v-model="customDataPermission"
:props="defaultTreeProps" :data="deptData" multiple check-strictly :render-after-expand="false"
show-checkbox class="dialog-tree" />
</div>
<template #footer>
<div>
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
<el-button @click="handleDialogClose"> 取消</el-button>
</div>
</template>
</el-dialog>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, onMounted, defineProps, watch, computed, reactive } from 'vue';
import XEUtils from 'xe-utils';
import { errorNotification } from '/@/utils/message';
import {
getDataPermissionRange,
getDataPermissionDept,
getRolePremission,
setRolePremission,
setBtnDatarange
} from './api';
import { MenuDataType, MenusType, DataPermissionRangeType, CustomDataPermissionDeptType } from './types';
import { ElMessage } from 'element-plus'
const props = defineProps({
roleId: {
type: Number,
default: -1
},
roleName: {
type: String,
default: ''
},
drawerVisible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:drawerVisible'])
const drawerVisible = ref(false)
watch(
() => props.drawerVisible,
(val) => {
drawerVisible.value = val;
getMenuBtnPermission()
fetchData()
}
);
const handleDrawerClose = () => {
emit('update:drawerVisible', false);
}
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
let menuData = ref<MenuDataType[]>([]);
let collapseCurrent = ref<number[]>([]);
let menuCurrent = ref<Partial<MenuDataType>>({});
let menuBtnCurrent = ref<number>(-1);
let dialogVisible = ref(false);
let dataPermissionRange = ref<DataPermissionRangeType[]>([]);
const formatDataRange = computed(() => {
return function (datarange: number) {
const findItem = dataPermissionRange.value.find((i) => i.value === datarange);
return findItem?.label || ''
}
})
let deptData = ref<CustomDataPermissionDeptType[]>([]);
let dataPermission = ref();
let customDataPermission = ref([]);
//获取菜单,按钮,权限
const getMenuBtnPermission = async () => {
const resMenu = await getRolePremission({ role: props.roleId })
menuData.value = resMenu.data
}
const fetchData = async () => {
try {
const resRange = await getDataPermissionRange();
if (resRange?.code === 2000) {
dataPermissionRange.value = resRange.data;
}
} catch {
return;
}
};
const handleCollapseChange = (val: number) => {
collapseCurrent.value = [val];
};
/**
* 设置按钮数据权限
* @param record 当前菜单
* @param btnType 按钮类型
*/
const handleSettingClick = (record: MenusType, btnId: number) => {
menuCurrent.value = record;
menuBtnCurrent.value = btnId;
dialogVisible.value = true;
};
const handleColumnChange = (val: boolean, record: MenusType, btnType: string) => {
for (const iterator of record.columns) {
iterator[btnType] = val;
}
};
const handlePermissionRangeChange = async (val: number) => {
if (val === 4) {
const res = await getDataPermissionDept();
const data = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
deptData.value = data;
}
};
/**
* 数据权限设置确认
*/
const handleDialogConfirm = () => {
if (dataPermission.value !== 0 && !dataPermission.value) {
errorNotification('请选择');
return;
}
//if (dataPermission.value !== 4) {}
for (const item of menuData.value) {
for (const iterator of item.menus) {
if (iterator.id === menuCurrent.value.id) {
for (const btn of iterator.btns) {
if (btn.id === menuBtnCurrent.value) {
const findItem = dataPermissionRange.value.find((i) => i.value === dataPermission.value);
btn.data_range = findItem?.value || 0;
if (btn.data_range === 4) {
btn.dept = customDataPermission.value
}
}
}
}
}
}
handleDialogClose();
};
const handleDialogClose = () => {
dialogVisible.value = false;
customDataPermission.value = [];
dataPermission.value = null;
};
//保存权限
const handleSavePermission = () => {
setRolePremission(props.roleId, menuData.value).then((res: any) => {
ElMessage({
message: res.msg,
type: 'success',
})
})
}
const column = reactive({
header: [{ value: 'is_create', label: '新增可见' }, { value: 'is_update', label: '编辑可见' }, {
value: 'is_query',
label: '列表可见'
}]
})
onMounted(() => {
});
</script>
<style lang="scss" scoped>
.permission-com {
margin: 15px;
box-sizing: border-box;
.pc-save-btn {
margin-bottom: 15px;
}
.pc-collapse-title {
line-height: 32px;
text-align: left;
span {
font-size: 16px;
}
}
.pc-collapse-main {
padding-top: 15px;
box-sizing: border-box;
.pccm-item {
margin-bottom: 10px;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
.btn-item {
display: flex;
align-items: center;
span {
margin-left: 5px;
}
}
.columns-list {
.width-txt {
width: 200px;
}
.width-check {
width: 100px;
}
.width-icon {
cursor: pointer;
}
.columns-head {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
span {
font-weight: 900;
}
}
.columns-item {
display: flex;
align-items: center;
padding: 6px 0;
box-sizing: border-box;
.ci-checkout {
height: auto !important;
}
}
}
}
}
.pc-dialog {
.dialog-select {
width: 100%;
}
.dialog-tree {
width: 100%;
margin-top: 20px;
}
}
}
</style>
<style lang="scss">
.permission-com {
.el-collapse {
border-top: none;
border-bottom: none;
}
.el-collapse-item {
margin-bottom: 15px;
}
.el-collapse-item__header {
height: auto;
padding: 15px;
border-radius: 8px;
border-top: 1px solid #ebeef5;
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
box-sizing: border-box;
background-color: #fafafa;
}
.el-collapse-item__header.is-active {
border-radius: 8px 8px 0 0;
background-color: #fafafa;
}
.el-collapse-item__wrap {
padding: 15px;
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
border-top: 1px solid #ebeef5;
border-radius: 0 0 8px 8px;
background-color: #fafafa;
box-sizing: border-box;
.el-collapse-item__content {
padding-bottom: 0;
}
}
}
</style>

View File

@@ -1,36 +0,0 @@
export interface DataPermissionRangeType {
label: string;
value: number;
}
export interface CustomDataPermissionDeptType {
id: number;
name: string;
patent: number;
children: CustomDataPermissionDeptType[]
}
export interface CustomDataPermissionMenuType {
id: number;
name: string;
is_catalog: boolean;
menuPermission: { id: number; name: string; value: string }[] | null;
columns: { id: number; name: string; title: string }[] | null;
children: CustomDataPermissionMenuType[]
}
export interface MenusType{
id: string;
name: string;
isCheck: boolean;
radio: string;
btns: { id:number,name: string; value: string; isCheck: boolean; data_range: number; dept:object; name:string }[];
columns: { [key: string]: boolean | string; }[]
}
export interface MenuDataType {
id: string;
name: string;
menus:MenusType[];
}

View File

@@ -0,0 +1,76 @@
<template>
<el-drawer
v-model="RoleDrawer.drawerVisible"
title="权限配置"
direction="rtl"
size="80%"
:close-on-click-modal="false"
:before-close="RoleDrawer.handleDrawerClose"
:destroy-on-close="true"
>
<template #header>
<div>
当前授权角色
<el-tag style="margin-right: 20px">{{ RoleDrawer.roleName }}</el-tag>
授权人员
<el-button size="small" :icon="UserFilled" @click="handleUsers">{{ RoleDrawer.users.length }}</el-button>
</div>
</template>
<splitpanes class="default-theme" style="height: 100%">
<pane min-size="20" size="22">
<div class="pane-box">
<MenuTreeCom />
</div>
</pane>
<pane min-size="20">
<div class="pane-box">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="接口权限" name="first"><MenuBtnCom /></el-tab-pane>
<el-tab-pane label="列字段权限" name="second"><MenuFieldCom /></el-tab-pane>
</el-tabs>
</div>
</pane>
</splitpanes>
</el-drawer>
<el-dialog v-model="dialogVisible" title="授权用户" width="700px" :close-on-click-modal="false">
<RoleUsersCom />
</el-dialog>
</template>
<script setup lang="ts">
import { Splitpanes, Pane } from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import { UserFilled } from '@element-plus/icons-vue';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { defineAsyncComponent, ref } from 'vue';
import { RoleUsersStores } from '../stores/RoleUsersStores';
const MenuTreeCom = defineAsyncComponent(() => import('./RoleMenuTree.vue'));
const MenuBtnCom = defineAsyncComponent(() => import('./RoleMenuBtn.vue'));
const MenuFieldCom = defineAsyncComponent(() => import('./RoleMenuField.vue'));
const RoleUsersCom = defineAsyncComponent(() => import('./RoleUsers.vue'));
const RoleDrawer = RoleDrawerStores(); // 抽屉参数
const RoleUsers = RoleUsersStores(); // 角色-用户
const activeName = ref('first');
const dialogVisible = ref(false);
const handleUsers = () => {
dialogVisible.value = true;
RoleUsers.get_all_users(); // 获取所有用户
RoleUsers.set_right_users(RoleDrawer.$state.users); // 设置已选中用户
};
</script>
<style lang="scss" scoped>
.pane-box {
width: 100vw; /* 视口宽度 */
height: 100vh; /* 视口高度 */
max-width: 100%; /* 确保不超过父元素的宽度 */
max-height: 100%; /* 确保不超过父元素的高度 */
overflow: auto; /* 当内容超出容器尺寸时显示滚动条 */
padding: 10px;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
<div class="menu-form-alert">配置操作功能接口权限配置数据权限点击小齿轮</div>
<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
<div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
<el-icon>
<Setting />
</el-icon>
</span>
</div>
</el-checkbox>
</div>
<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false" :before-close="handleDialogClose">
<div class="pc-dialog">
<el-select v-model="selectBtn.data_range" @change="handlePermissionRangeChange" placeholder="请选择">
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-tree-select
v-show="selectBtn.data_range === 4"
node-key="id"
v-model="selectBtn.dept"
:props="defaultTreeProps"
:data="deptData"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="dialog-tree"
/>
</div>
<template #footer>
<div>
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
<el-button @click="handleDialogClose"> 取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
import { RoleMenuBtnType } from '../types';
import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
import XEUtils from 'xe-utils';
import { ElMessage } from 'element-plus';
const RoleDrawer = RoleDrawerStores(); // 角色-菜单
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
const dialogVisible = ref(false);
// 选中的按钮
const selectBtn = ref<RoleMenuBtnType>({
id: 0,
menu_btn_pre_id: 0,
/** 是否选中 */
isCheck: false,
/** 按钮名称 */
name: '',
/** 数据权限范围 */
data_range: 0,
dept: [],
});
/**
* 数据权限范围
*/
const dataPermissionRange = ref([
{ label: '仅本人数据权限', value: 0 },
{ label: '本部门及以下数据权限', value: 1 },
{ label: '本部门数据权限', value: 2 },
{ label: '全部数据权限', value: 3 },
{ label: '自定数据权限', value: 4 },
]);
/**
* 自定义数据权限的部门树配置
*/
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
/**
* 自定数据权限下拉选择事件
*/
const handlePermissionRangeChange = async (val: number) => {
if (val < 4) {
selectBtn.value.dept = [];
}
};
/**
* 格式化按钮数据范围
*/
const formatDataRange = computed(() => {
return function (datarange: number) {
const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
if (item.value === datarange) {
return item.label;
}
});
return datarangeitem.label;
};
});
/**
* 勾选按钮
*/
const handleCheckChange = async (btn: RoleMenuBtnType) => {
const put_data = {
isCheck: btn.isCheck,
roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id,
btnId: btn.id,
};
const { data, msg } = await setRoleMenuBtn(put_data);
RoleMenuBtn.updateState(data);
ElMessage({ message: msg, type: 'success' });
};
/**
* 按钮-数据范围确定
*/
const handleDialogConfirm = async () => {
const { data, msg } = await setRoleMenuBtnDataRange(selectBtn.value);
selectBtn.value = data;
dialogVisible.value = false;
ElMessage({ message: msg, type: 'success' });
};
/**
* 数据范围关闭
*/
const handleDialogClose = () => {
dialogVisible.value = false;
};
/**
* 齿轮点击
*/
const handleSettingClick = async (btn: RoleMenuBtnType) => {
selectBtn.value = btn;
dialogVisible.value = true;
};
/**
* 部门数据
*
*/
const deptData = ref<number[]>([]);
// 页面打开后获取列表数据
onMounted(async () => {
const res = await getRoleToDeptAll({ role: RoleDrawer.roleId, menu_button: selectBtn.value.id });
const depts = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
deptData.value = depts;
});
</script>
<style lang="scss" scoped>
.pccm-item {
margin-bottom: 10px;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
}
// .el-checkbox {
// width: 200px;
// }
.btn-item {
display: flex;
align-items: center;
justify-content: center; /* 水平居中 */
.el-icon {
margin-left: 5px;
}
}
.dialog-tree {
width: 100%;
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="pccm-item" v-if="RoleMenuField.$state.length > 0">
<div class="menu-form-alert">
<el-button size="small" @click="handleSaveField">保存 </el-button>
配置数据列字段权限
</div>
<ul class="columns-list">
<li class="columns-head">
<div class="width-txt">
<span>字段</span>
</div>
<div v-for="(head, hIndex) in RoleMenuFieldHeader.$state" :key="hIndex" class="width-check">
<el-checkbox v-model="head.checked" @change="handleColumnChange($event, head.value, head.disabled)">
<span>{{ head.label }}</span>
</el-checkbox>
</div>
</li>
<div class="columns-content">
<li v-for="(c_item, c_index) in RoleMenuField.$state" :key="c_index" class="columns-item">
<div class="width-txt">{{ c_item.title }}</div>
<div v-for="(col, cIndex) in RoleMenuFieldHeader.$state" :key="cIndex" class="width-check">
<el-checkbox v-model="c_item[col.value]" class="ci-checkout" :disabled="c_item[col.disabled]"></el-checkbox>
</div>
</li>
</div>
</ul>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { RoleMenuFieldStores, RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
import { setRoleMenuField } from './api';
const RoleMenuField = RoleMenuFieldStores();
const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();
const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
/** 全选 */
const handleColumnChange = (val: boolean, btnType: string, disabledType: string) => {
for (const iterator of RoleMenuField.$state) {
iterator[btnType] = iterator[disabledType] ? iterator[btnType] : val;
}
};
const handleSaveField = async () => {
const res = await setRoleMenuField(RoleDrawer.$state.roleId, RoleMenuField.$state);
ElMessage({ message: res.msg, type: 'success' });
};
</script>
<style lang="scss" scoped>
.pccm-item {
margin-bottom: 10px;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
.menu-form-btn {
margin-left: 10px;
height: 40px;
padding: 8px 16px;
margin-bottom: 20px;
}
.btn-item {
display: flex;
align-items: center;
span {
margin-left: 5px;
}
}
.columns-list {
.width-txt {
width: 200px;
}
.width-check {
width: 100px;
}
.width-icon {
cursor: pointer;
}
.columns-head {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
span {
font-weight: 900;
}
}
.columns-content {
max-height: calc(100vh - 240px); /* 视口高度 */
overflow-y: auto; /* 当内容超出高度时显示垂直滚动条 */
.columns-item {
display: flex;
align-items: center;
padding: 6px 0;
box-sizing: border-box;
.ci-checkout {
height: auto !important;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<el-tree
ref="treeRef"
:data="menuData"
:props="defaultTreeProps"
:default-checked-keys="menuDefaultCheckedKeys"
@check-change="handleMenuChange"
@node-click="handleMenuClick"
node-key="id"
check-strictly
highlight-current
show-checkbox
default-expand-all
>
</el-tree>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
import { RoleMenuFieldStores } from '../stores/RoleMenuFieldStores';
import { RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
import { getRoleMenu, getRoleMenuBtnField, setRoleMenu } from './api';
import { ElMessage } from 'element-plus';
import XEUtils from 'xe-utils';
import { RoleMenuTreeType } from '../types';
const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
const RoleMenuField = RoleMenuFieldStores(); // 角色-菜单-列字段
const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();// 角色-菜单-列字段
const menuData = ref<RoleMenuTreeType[]>([]); // 菜单列表数据
const menuDefaultCheckedKeys = ref<(number | string | undefined)[]>([]); // 默认选中的菜单列表
// 菜单配置
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
/**
* 菜单复选框选中
* @param node当前节点的 Node 对象
* @param checked布尔值表示当前节点是否被选中
*/
const handleMenuChange = (node: any, checked: boolean) => {
setRoleMenu({ roleId: RoleDrawer.roleId, menuId: node.id, isCheck: checked }).then((res: any) => {
ElMessage({ message: res.msg, type: 'success' });
});
};
/**
* 菜单点击事件
*/
const handleMenuClick = async (selectNode: RoleMenuTreeType) => {
if (!selectNode.is_catalog) {
RoleMenuTree.setRoleMenuTree(selectNode); // 更新当前选中的菜单
// 获取当前菜单的按钮列表
const { data } = await getRoleMenuBtnField({
roleId: RoleDrawer.roleId,
menuId: selectNode.id,
});
RoleMenuBtn.setState(data.menu_btn); // 更新按钮列表
RoleMenuField.setState(data.menu_field); // 更新列字段列表
} else {
RoleMenuBtn.setState([]); // 更新按钮列表
RoleMenuField.setState([]); // 更新列字段列表
}
RoleMenuFieldHeader.$reset()
};
// 页面打开后获取列表数据
onMounted(async () => {
menuData.value = await getRoleMenu({ roleId: RoleDrawer.roleId });
menuDefaultCheckedKeys.value = XEUtils.toTreeArray(menuData.value)
.filter((i) => i.isCheck)
.map((i) => i.id);
});
</script>

View File

@@ -0,0 +1,35 @@
<template>
<el-transfer
v-model="RoleUsers.$state.right_users"
filterable
:titles="['未授权用户', '已授权用户']"
:data="RoleUsers.$state.all_users"
:props="{
key: 'id',
label: 'name',
}"
@change="handleChange"
/>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus';
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
import { RoleUsersStores } from '../stores/RoleUsersStores';
import { setRoleUsers } from './api';
const RoleDrawer = RoleDrawerStores(); // 抽屉参数
const RoleUsers = RoleUsersStores(); // 角色-用户
/**
*
* @param value 当前右侧选中的用户
* @param direction 移动的方向
* @param movedKeys 移动的用户
*/
const handleChange = (value: number[] | string[], direction: 'left' | 'right', movedKeys: string[] | number[]) => {
setRoleUsers(RoleDrawer.$state.roleId, { direction, movedKeys }).then((res:any) => {
RoleDrawer.set_state(res.data)
ElMessage({ message: res.msg, type: 'success' });
});
};
</script>

View File

@@ -0,0 +1,121 @@
import { request } from '/@/utils/service';
import XEUtils from 'xe-utils';
/**
* 获取 角色-菜单
* @param query
*/
export function getRoleMenu(query: object) {
return request({
url: '/api/system/role_menu_button_permission/get_role_menu/',
method: 'get',
params: query,
}).then((res: any) => {
return XEUtils.toArrayTree(res.data, { key: 'id', parentKey: 'parent', children: 'children', strict: false });
});
}
/**
* 设置 角色-菜单
* @param data
* @returns
*/
export function setRoleMenu(data: object) {
return request({
url: '/api/system/role_menu_button_permission/set_role_menu/',
method: 'put',
data,
});
}
/**
* 获取 角色-菜单-按钮-列字段
* @param query
*/
export function getRoleMenuBtnField(query: object) {
return request({
url: '/api/system/role_menu_button_permission/get_role_menu_btn_field/',
method: 'get',
params: query,
});
}
/**
* 设置 角色-菜单-按钮
* @param data
*/
export function setRoleMenuBtn(data: object) {
return request({
url: '/api/system/role_menu_button_permission/set_role_menu_btn/',
method: 'put',
data,
});
}
/**
* 设置 角色-菜单-列字段
* @param data
*/
export function setRoleMenuField(roleId: string | number | undefined, data: object) {
return request({
url: `/api/system/role_menu_button_permission/${roleId}/set_role_menu_field/`,
method: 'put',
data,
});
}
/**
* 设置 角色-菜单-按钮-数据权限
* @param query
* @returns
*/
export function setRoleMenuBtnDataRange(data: object) {
return request({
url: '/api/system/role_menu_button_permission/set_role_menu_btn_data_range/',
method: 'put',
data,
});
}
/**
* 获取当前用户角色下所能授权的部门
* @param query
* @returns
*/
export function getRoleToDeptAll(query: object) {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get',
params: query,
});
}
/**
* 获取所有用户
* @param query
* @returns
*/
export function getAllUsers() {
return request({
url: '/api/system/user/',
method: 'get',
params: { limit: 999 },
}).then((res: any) => {
return XEUtils.map(res.data, (item: any) => {
return {
id: item.id,
name: item.name,
};
});
});
}
/**
* 设置角色-用户
* @param query
* @returns
*/
export function setRoleUsers(roleId: string | number | undefined, data: object) {
return request({
url: `/api/system/role/${roleId}/set_role_users/`,
method: 'put',
data,
});
}

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