72 Commits

Author SHA1 Message Date
dvadmin
1e500bd683 !115 Merge remote-tracking branch 'origin/develop' into develop
Merge pull request !115 from dvadmin/develop
2025-05-06 02:11:02 +00:00
liqiang
b4ffb2105f Merge remote-tracking branch 'origin/develop' into develop 2025-05-06 10:06:22 +08:00
1638245306
3398aa3ba9 build(flowH5): 更新库入口文件路径和外部依赖
- 修改入口文件路径为 src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts
- 添加 xe-utils 到外部依赖列表
2025-05-01 00:04:01 +08:00
1638245306
94149161b3 build(flowH5): 添加 flowH5 项目的配置文件
- 新增 flowH5.config.ts 文件,配置 Vite 构建项目- 设置 Vue 插件和路径别名
- 配置 Roll
2025-04-30 12:55:58 +08:00
liqiang
1907f1ac0a Merge remote-tracking branch 'origin/develop' into develop 2025-04-30 10:35:52 +08:00
1638245306
bda002398c refactor: 修改 API 文档权限为仅允许认证用户访问
- 将 API 文档的权限设置从 AllowAny 更改为 IsAuthenticated- 确保只有经过身份验证的用户才能访问 API 文档
2025-04-29 14:45:45 +08:00
阿辉
66c28bd389 日历选择器组件 2025-04-07 15:20:52 +08:00
1638245306
8eacc4aad3 feat(system): 角色模型添加工作流支持
- 引入 FlowBaseModel 以支持工作流功能
- 使 Role 模型继承 CoreModel 和 FlowBaseModel
2025-03-28 18:24:13 +08:00
1638245306
7a152d3591 feat(system): 添加获取用户递归部门名称功能
- 在 Department模型中添加了 _recursion 类方法,用于递归获取指定属性值
- 新增 get_region_name 类方法,用于获取用户的所有上级部门名称
- 该功能可以用于显示用户所在的完整部门路径
2025-03-28 15:08:56 +08:00
1638245306
4b05a28f4c fix(system): 修改用户密码设置逻辑
- 移除了对新密码进行 MD5 加密的步骤- 直接使用明文密码进行加密存储
- 保留了密码修改次数的计数功能
2025-03-28 14:44:39 +08:00
1638245306
0392b3b101 feat(plugins): 动态加载插件
- 新增插件动态加载逻辑,遍历已发现的插件列表
- 对于每个插件,尝试导入其 index.ts 文件
- 导入成功后,将插件注册到应用中,并打印加载信息
-导入失败时,打印错误信息,提示插件下无 index.ts 文件
2025-03-28 13:39:44 +08:00
dvadmin
421a44b26c !111 发布到正式版本
Merge pull request !111 from dvadmin/develop
2025-03-20 20:43:47 +00:00
liqiang
fad2cb2e18 README.zh.md更新 2025-03-21 04:42:46 +08:00
liqiang
df0b78cafc README.zh.md更新 2025-03-21 04:41:36 +08:00
liqiang
c69ea7b33e Merge remote-tracking branch 'origin/develop' into develop 2025-03-21 04:34:30 +08:00
liqiang
ce5c4c9d8d README.zh.md更新 2025-03-21 04:33:45 +08:00
liqiang
3f7aaa0228 Merge remote-tracking branch 'origin/develop' into develop 2025-03-21 04:09:33 +08:00
liqiang
e37909d478 feat(core): 新增核心工具模块并优化通知功能
- 新增 cores.tsx 文件,实现核心工具模块
- 添加任务列表和事件总线功能
- 实现系统通知和任务处理逻辑
- 在 App.vue 中集成新功能
- 优化通知显示逻辑,支持不同内容类型的通知
2025-03-21 04:09:26 +08:00
dvadmin
1578c9d710 !108 修复字段权限筛选错误,update backend/dvadmin/utils/viewset.py.
Merge pull request !108 from lxy/N/A
2025-03-20 19:54:37 +00:00
dvadmin
01604a27db !109 修复无法下载问题 update web/src/views/system/downloadCenter/crud.tsx.
Merge pull request !109 from lxy/N/A
2025-03-20 19:53:57 +00:00
dvadmin
58036dbeb9 !110 修复文件管理点击地址无法下载问题 update web/src/views/system/fileList/crud.tsx.
Merge pull request !110 from lxy/N/A
2025-03-20 19:53:34 +00:00
lxy
7917a118c8 修复文件管理点击地址无法下载问题 update web/src/views/system/fileList/crud.tsx.
Signed-off-by: lxy <46486798@qq.com>
2025-03-20 11:33:01 +00:00
lxy
86c202b94a 修复无法下载问题 update web/src/views/system/downloadCenter/crud.tsx.
Signed-off-by: lxy <46486798@qq.com>
2025-03-20 11:28:09 +00:00
lxy
a5cc87eb55 修复字段权限筛选错误,update backend/dvadmin/utils/viewset.py.
解决不是超级管理员用户加载报错,匿名用户没有角色报错

Signed-off-by: lxy <46486798@qq.com>
2025-03-20 06:47:21 +00:00
1638245306
2fabc210b2 refactor(system): 恢复密码加密逻辑
- 将明文密码修改为经过 MD5 加密的密码
- 保留了原有的密码加密方式,提高了系统安全性
2025-03-19 15:54:58 +08:00
1638245306
4c644ae8cb build(development): 修改开发环境 API 地址并添加新的构建脚本
- 将开发环境 API 地址从 http://127.0.0.1:8000 改为 http://127.0.0.1:8001- 在 package.json 中添加了 build:dev 脚本,用于开发环境的构建
2025-03-17 14:40:12 +08:00
liqiang
351bae1fea build(.env): 修改 API 请求基础 URL
- 将 VITE_API_URL 从 'http://dvadmin3api.django.icu:8001' 修改为 'http://127.0.0.1:8000'- 此修改便于本地开发和测试
2025-03-15 14:52:29 +08:00
1638245306
bc7bc3cda6 feat(system): 优化配置页面表单组件
- 在 crudTable组件中添加 afterSubmit 钩子,更新 modelValue
-重构 formContent 组件中的 formData,使用 ref 替代 reactive
- 优化 formContent 组件的提交逻辑,简化代码结构
2025-03-14 17:33:51 +08:00
liqiang
aea5ca938b Merge branch 'develop' of gitee.com:huge-dream/django-vue3-admin into develop 2025-03-14 16:59:46 +08:00
1638245306
8706de5ef4 feat(system): 优化配置组件的样式和功能
- 调整表单布局,设置宽度为 500px,栏数为 24
- 在标题、键名和键值字段的表单中添加占位符
2025-03-14 16:42:04 +08:00
dvadmin
ed96740ab1 !106 完善初始化配置导出
Merge pull request !106 from lorenzo/dev
2025-03-13 10:32:43 +00:00
dvadmin
f719241938 !105 feat: 从表结构生成前端基础文件
Merge pull request !105 from lorenzo/master
2025-03-13 10:31:04 +00:00
1638245306
ac22e47883 docs(README): 添加给框架点赞二维码图片
- 在 README.md 中添加了支付宝和微信的二维码图片链接
- 通过此修改,方便用户扫描二维码给框架进行赞赏
2025-03-13 17:49:35 +08:00
1638245306
7555ba1707 Merge remote-tracking branch 'origin/develop' into develop 2025-03-13 17:40:39 +08:00
1638245306
341ea62412 feat(system): 优化配置管理数组展示方式
- 新增 crudTable 组件用于数组数据的展示和编辑
- 替换原有的 vxe-table 实现,简化代码结构
- 优化数组类型表单项的渲染逻辑
- 调整环境变量中的 API 地址
2025-03-13 17:40:30 +08:00
liqiang
2ab80758f0 fix(permission): 修复权限获取 bug
- 将 MenuButton 查询结果改为 values_list,获取多个匹配项的 ID
- 更新 RoleMenuButtonPermission 查询条件,支持多个菜单按钮 ID
2025-03-02 01:00:43 +08:00
liqiang
a030409c46 fix(system): 修复权限获取 bug
- 优化权限查询逻辑,先判断菜单按钮对象是否存在
- 修复了在某些情况下可能导致权限获取失败的问题
2025-03-02 00:05:44 +08:00
liqiang
036d8da9b6 build(web): 更新基础镜像版本
- 将 dvadmin3-base-web 镜像版本从16.19-alpine升级到 18.20-alpine
2025-03-02 00:04:42 +08:00
阿辉
7fb219bb31 文件选择器修复清空时会报错null的bug 2025-02-28 18:42:28 +08:00
阿辉
b10c3ebdc0 文件选择器修复windows下文件路径反斜杠问题 2025-02-24 10:26:49 +08:00
阿辉
778401dd2d Merge branch 'develop' of https://e.coding.net/dvadmin-private/code/dvadmin3 into develop 2025-02-21 17:11:44 +08:00
阿辉
418c78fa83 文件选择器多选图片情况下无法删除图片 2025-02-21 17:11:41 +08:00
1638245306
48d3d86017 Merge remote-tracking branch 'origin/develop' into develop 2025-02-09 23:17:35 +08:00
1638245306
c6a2073537 feat(utils): 完善字段权限控制并添加角色过滤
- 添加了对普通用户进行字段权限过滤的逻辑
- 使用 deepcopy 复制 serializer_class.Meta 以避免直接修改原类
- 修改 get_menu_field 方法,根据用户角色过滤字段权限
2025-02-09 23:17:26 +08:00
7emotions
78ec0ce069 fix: 修复导出Users配置缺失的role与dept 2025-02-06 20:20:12 +08:00
7emotions
857d2940f8 fix: 修复RoleMenuButtonInitSerializer重置逻辑 2025-02-06 19:56:20 +08:00
7emotions
82e689fd83 feat: 导出角色权限配置与按钮权限 2025-02-06 19:21:14 +08:00
liqiang
0cda4c04f7 默认添加dvadmin3-celery 2025-02-06 16:37:52 +08:00
7emotions
91742ee27b feat: 从表结构生成前端基础文件 2025-02-04 00:10:46 +08:00
liqiang
3c6afdac76 支持npm 安装的@great-dream插件动态菜单 2025-02-02 09:04:13 +08:00
liqiang
f299889c4a 支持npm 安装的@great-dream插件动态菜单 2025-02-01 23:29:52 +08:00
liqiang
c82fcbb468 celery 优化 2025-02-01 18:29:51 +08:00
liqiang
eaf5fdcc55 框架优化 2025-02-01 18:29:43 +08:00
liqiang
40b7bd2c94 组件更新 2025-01-30 12:55:58 +08:00
liqiang
868621a3f1 Merge branch 'master' into develop 2025-01-30 12:47:26 +08:00
dvadmin
eefc43d863 !95 角色授权增加默认接口权限
1.授权时带入默认权限
2.查看权限,如果单接口权限与默认接口权限一致,显示默认接口权限
3.复选框等宽显示
2025-01-30 04:44:58 +00:00
dvadmin
8b21a69a15 !104 feat: 优化docker部署
Merge pull request !104 from lorenzo/master
2025-01-30 04:44:28 +00:00
dvadmin
0964c6300a !103 [fix]修复bug
[fix]修复更新时间晒选取值错误bug
[fix]优化commonCrudConfig写法,兼容ts语法,修复使用commonCrudConfig IDE提示飘红的问题&增加width可自定义功能
[fix]修复版本号生成可能为undefind的bug&开发环境版本不进行版本校验代码不生效bug
2025-01-30 04:43:07 +00:00
dvadmin
8c2db4f5ff !101 增加角色批量授权用户
Merge pull request !101 from lxy/cherry-pick-1736306630
2025-01-30 04:42:23 +00:00
liqiang
e15a49a2bd 支持外链接 2025-01-30 12:40:30 +08:00
lorenzo
ecfad1952a feat: 优化docker部署
Signed-off-by: lorenzo <paradise_c@qq.com>
2025-01-27 15:57:12 +00:00
lorenzo
92b8cde7ae feat: 优化docker部署
Signed-off-by: lorenzo <paradise_c@qq.com>
2025-01-27 15:54:13 +00:00
vFeng
6fbe3a214d [fix]注释无效接口dept_lazy_tree
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 09:01:17 +00:00
vFeng
4695066e11 [fix]修复开发环境无需校验前端版本bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 06:33:37 +00:00
vFeng
d125eac5a6 [fix]修复版本号生成可能为undefind的bug&开发环境版本不进行版本校验代码不生效bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 06:22:21 +00:00
vFeng
83d6565cad [fix]优化commonCrudConfig写法,兼容ts语法,修复使用commonCrudConfig IDE提示飘红的问题&增加width可自定义功能
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 05:33:54 +00:00
vFeng
1e2a9a652e [fix]修复更新时间晒选取值错误bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 05:25:49 +00:00
阿辉
c781d1f559 文件选择器支持image类型的多选 2025-01-22 15:53:01 +08:00
lxy
ffc6246105 增加角色批量授权用户
(cherry picked commit from <gitee.com//lxy0722/django-vue3-admin/commit/702eb7d7aeb2a9c30416a8ebb503a4b8e3367492>
2025-01-08 03:23:50 +00:00
1638245306
77bfc87679 Merge remote-tracking branch 'origin/develop' into develop 2025-01-07 21:38:22 +08:00
1638245306
6ab0c3e758 refactor: 修复主键列表字段的 Swagger 文档生成
- 更新 keys 变量定义,使用 Schema 嵌套来正确表示主键列表的类型
-优化 Swagger 文档中的请求体定义,提高 API 文档的准确性和可读性
2025-01-07 20:06:24 +08:00
lxy
9d230e4752 角色授权增加默认设置
(cherry picked commit from <gitee.com//huge-dream/django-vue3-admin/commit/06a772fcd5335c272159d9c45590cf8b559210d1>
2025-01-02 06:28:06 +00:00
61 changed files with 2934 additions and 865 deletions

View File

@@ -54,16 +54,21 @@
## 交流 ## 交流
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦 - 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦 - 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦
- django-vue-admin交流01群(已满)812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi) - django-vue-admin交流01群(已满)812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi)
- django-vue-admin交流02群(已满)687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi) - django-vue-admin交流02群(已满)687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi)
- django-vue-admin交流03群442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) - django-vue-admin交流03群(已满)442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
- django-vue-admin交流04群442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
- 二维码
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
## 给框架点赞
<div style="display: flex; gap: 10px;">
<img src='https://django-vue-admin.com/alipay.jpg' width='200'>
<img src='https://django-vue-admin.com/wechat.jpg' width='200'>
</div>
## 源码地址 ## 源码地址
@@ -88,7 +93,19 @@ github地址[https://github.com/huge-dream/django-vue3-admin](https://github.
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。 13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌 ## 插件市场 🔌
更新中... 1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html)
2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html)
3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html)
4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html)
5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git)
6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线)
## 仓库分支说明 💈 ## 仓库分支说明 💈
主分支master稳定版本 主分支master稳定版本
@@ -210,5 +227,19 @@ docker-compose up -d --build
![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png) ![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png)
## 审批流插件
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/97fbbf29673edfd66a1edd49237791bb.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/c43aa51278cbc478287c718d22397479.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/9732a5cca9c1166d1a65c35e313ab90d.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/3ca9dd0801ce76d21435abcc8a3d505a.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/a87a8d2329ef66880af5b0f16c5ff823.png)

1
backend/.gitignore vendored
View File

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

View File

@@ -1,6 +1,8 @@
import functools import functools
import os import os
from celery.signals import task_postrun
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
from django.conf import settings from django.conf import settings
@@ -38,3 +40,12 @@ def retry_base_task_error():
return wrapper return wrapper
return wraps return wraps
@task_postrun.connect
def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras):
periodic_task_name = kwargs.get('periodic_task_name')
if periodic_task_name:
from django_celery_results.models import TaskResult
# 更新 TaskResult 表中的 periodic_task_name 字段
TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name)

View File

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

View File

@@ -50,7 +50,7 @@ schema_view = get_schema_view(
license=openapi.License(name="BSD License"), license=openapi.License(name="BSD License"),
), ),
public=True, public=True,
permission_classes=(permissions.AllowAny,), permission_classes=(permissions.IsAuthenticated,),
generator_class=CustomOpenAPISchemaGenerator, generator_class=CustomOpenAPISchemaGenerator,
) )
# 前端页面映射 # 前端页面映射

View File

@@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer):
""" """
初始化获取数信息(用于生成初始化json文件) 初始化获取数信息(用于生成初始化json文件)
""" """
role_key = serializers.SerializerMethodField()
dept_key = serializers.SerializerMethodField()
def get_dept_key(self, obj):
if obj.dept:
return obj.dept.key
else:
return None
def get_role_key(self, obj):
if obj.role.all():
return [role.key for role in obj.role.all()]
else:
return []
def save(self, **kwargs): def save(self, **kwargs):
instance = super().save(**kwargs) instance = super().save(**kwargs)
@@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer):
model = Users model = Users
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type', fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type',
'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id',
'password', 'last_login', 'is_superuser'] 'password', 'last_login', 'is_superuser', 'role_key' ,'dept_key']
read_only_fields = ['id'] read_only_fields = ['id']
extra_kwargs = { extra_kwargs = {
'creator': {'write_only': True}, 'creator': {'write_only': True},
@@ -175,15 +189,21 @@ class RoleMenuInitSerializer(CustomModelSerializer):
""" """
初始化角色菜单(用于生成初始化json文件) 初始化角色菜单(用于生成初始化json文件)
""" """
role__key = serializers.CharField(max_length=100, required=True) role__key = serializers.CharField(source='role.key')
menu__web_path = serializers.CharField(max_length=100, required=True) menu__web_path = serializers.CharField(source='menu.web_path')
menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True) menu__component_name = serializers.CharField(source='menu.component_name', allow_blank=True)
def update(self, instance, validated_data):
init_data = self.initial_data
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
validated_data['role'] = role_id
validated_data['menu'] = menu_id
return super().update(instance, validated_data)
def create(self, validated_data): def create(self, validated_data):
init_data = self.initial_data init_data = self.initial_data
validated_data.pop('menu__web_path')
validated_data.pop('menu__component_name')
validated_data.pop('role__key')
role_id = Role.objects.filter(key=init_data['role__key']).first() role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first() menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
validated_data['role'] = role_id validated_data['role'] = role_id
@@ -206,14 +226,22 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
""" """
初始化角色菜单按钮(用于生成初始化json文件) 初始化角色菜单按钮(用于生成初始化json文件)
""" """
role__key = serializers.CharField(max_length=100, required=True) role__key = serializers.CharField(source='role.key')
menu_button__value = serializers.CharField(max_length=100, required=True) menu_button__value = serializers.CharField(source='menu_button.value')
data_range = serializers.CharField(max_length=100, required=False) data_range = serializers.CharField(max_length=100, required=False)
def update(self, instance, validated_data):
init_data = self.initial_data
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
validated_data['role'] = role_id
validated_data['menu_button'] = menu_button_id
instance = super().create(validated_data)
instance.dept.set([])
return super().update(instance, validated_data)
def create(self, validated_data): def create(self, validated_data):
init_data = self.initial_data init_data = self.initial_data
validated_data.pop('menu_button__value')
validated_data.pop('role__key')
role_id = Role.objects.filter(key=init_data['role__key']).first() role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first() menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
validated_data['role'] = role_id validated_data['role'] = role_id
@@ -223,7 +251,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
return instance return instance
def save(self, **kwargs): def save(self, **kwargs):
if self.instance and self.initial_data.get('reset'): if not self.instance or self.initial_data.get('reset'):
return super().save(**kwargs) return super().save(**kwargs)
return self.instance return self.instance

View File

@@ -10,7 +10,7 @@ django.setup()
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from application.settings import BASE_DIR from application.settings import BASE_DIR
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig, RoleMenuButtonPermission, RoleMenuPermission
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \ from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \ MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
RoleMenuInitSerializer, RoleMenuButtonInitSerializer RoleMenuInitSerializer, RoleMenuButtonInitSerializer
@@ -57,6 +57,12 @@ class Command(BaseCommand):
def generate_system_config(self): def generate_system_config(self):
self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True)) self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True))
def generate_role_menu(self):
self.serializer_data(RoleMenuInitSerializer, RoleMenuPermission.objects.all())
def generate_role_menu_button(self):
self.serializer_data(RoleMenuButtonInitSerializer, RoleMenuButtonPermission.objects.all())
def handle(self, *args, **options): def handle(self, *args, **options):
generate_name = options.get('generate_name') generate_name = options.get('generate_name')
generate_name_dict = { generate_name_dict = {
@@ -67,6 +73,8 @@ class Command(BaseCommand):
"api_white_list": self.generate_api_white_list, "api_white_list": self.generate_api_white_list,
"dictionary": self.generate_dictionary, "dictionary": self.generate_dictionary,
"system_config": self.generate_system_config, "system_config": self.generate_system_config,
"role_menu": self.generate_role_menu,
"role_menu_button": self.generate_role_menu_button,
} }
if not generate_name: if not generate_name:
for ele in generate_name_dict.keys(): for ele in generate_name_dict.keys():

View File

@@ -9,8 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from application import dispatch from application import dispatch
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
from dvadmin3_flow.base_model import FlowBaseModel
class Role(CoreModel): class Role(CoreModel,FlowBaseModel):
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称") name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符") key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序") sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
@@ -122,6 +122,27 @@ class Dept(CoreModel):
help_text="上级部门", help_text="上级部门",
) )
@classmethod
def _recursion(cls, instance, parent, result):
new_instance = getattr(instance, parent, None)
res = []
data = getattr(instance, result, None)
if data:
res.append(data)
if new_instance:
array = cls._recursion(new_instance, parent, result)
res += array
return res
@classmethod
def get_region_name(cls, obj):
"""
获取某个用户的递归所有部门名称
"""
dept_name_all = cls._recursion(obj, "parent", "name")
dept_name_all.reverse()
return "/".join(dept_name_all)
@classmethod @classmethod
def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None): def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
""" """

View File

@@ -49,7 +49,7 @@ urlpatterns = [
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})), # path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), # path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
path('clause/privacy.html', PrivacyView.as_view()), path('clause/privacy.html', PrivacyView.as_view()),
path('clause/terms_service.html', TermsServiceView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()),
] ]

View File

@@ -10,16 +10,17 @@ from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Role, Menu, MenuButton, Dept from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
from dvadmin.system.views.dept import DeptSerializer from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.permission import CustomPermission
class RoleSerializer(CustomModelSerializer): class RoleSerializer(CustomModelSerializer):
@@ -107,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
""" """
角色管理接口 角色管理接口
@@ -142,3 +142,62 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
role.users_set.add(*movedKeys) role.users_set.add(*movedKeys)
serializer = RoleSerializer(role) serializer = RoleSerializer(role)
return DetailResponse(data=serializer.data, msg="更新成功") return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission])
def get_role_users(self, request):
"""
获取角色已授权、未授权的用户
已授权的用户:1
未授权的用户:0
"""
role_id = request.query_params.get('role_id', None)
if not role_id:
return ErrorResponse(msg="请选择角色")
if request.query_params.get('authorized', 0) == "1":
queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True)
else:
queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True)
if name := request.query_params.get('name', None):
queryset = queryset.filter(name__icontains=name)
if dept := request.query_params.get('dept', None):
queryset = queryset.filter(dept=dept)
page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name'))
if page is not None:
return self.get_paginated_response(page)
return SuccessResponse(data=page)
@action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def remove_role_user(self, request, pk):
"""
角色-删除用户
"""
user_id = request.data.get('user_id', None)
if not user_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.remove(*user_id)
return SuccessResponse(msg="删除成功")
@action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def add_role_users(self, request, pk):
"""
角色-添加用户
"""
users_id = request.data.get('users_id', None)
if not users_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.add(*users_id)
return DetailResponse(msg="添加成功")

View File

@@ -231,9 +231,17 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
isCheck = data.get('isCheck', None) isCheck = data.get('isCheck', None)
roleId = data.get('roleId', None) roleId = data.get('roleId', None)
btnId = data.get('btnId', None) btnId = data.get('btnId', None)
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
dept = data.get('dept', None) or [] # 默认空部门
if isCheck: if isCheck:
# 添加权限:创建关联记录 # 添加权限:创建关联记录
RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId) instance = RoleMenuButtonPermission.objects.create(role_id=roleId,
menu_button_id=btnId,
data_range=data_range)
# 自定义部门权限
if data_range == 4 and dept:
instance.dept.set(dept)
else: else:
# 删除权限:移除关联记录 # 删除权限:移除关联记录
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()

View File

@@ -336,7 +336,7 @@ class UserViewSet(CustomModelViewSet):
verify_password = check_password(str(old_pwd_md5), request.user.password) verify_password = check_password(str(old_pwd_md5), request.user.password)
if verify_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.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.pwd_change_count += 1 request.user.pwd_change_count += 1
request.user.save() request.user.save()
return DetailResponse(data=None, msg="修改成功") return DetailResponse(data=None, msg="修改成功")
@@ -352,7 +352,7 @@ class UserViewSet(CustomModelViewSet):
if new_pwd != new_pwd2: if new_pwd != new_pwd2:
return ErrorResponse(msg="两次密码不匹配") return ErrorResponse(msg="两次密码不匹配")
else: else:
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.pwd_change_count += 1
request.user.save() request.user.save()
return DetailResponse(data=None, msg="修改成功") return DetailResponse(data=None, msg="修改成功")

View File

@@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
from django_filters.conf import settings from django_filters.conf import settings
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton
from dvadmin.utils.models import CoreModel from dvadmin.utils.models import CoreModel
class CoreModelFilterBankend(BaseFilterBackend): class CoreModelFilterBankend(BaseFilterBackend):
@@ -33,7 +33,7 @@ class CoreModelFilterBankend(BaseFilterBackend):
create_datetime_after = request.query_params.get('create_datetime_after', None) create_datetime_after = request.query_params.get('create_datetime_after', None)
create_datetime_before = request.query_params.get('create_datetime_before', None) create_datetime_before = request.query_params.get('create_datetime_before', None)
update_datetime_after = request.query_params.get('update_datetime_after', None) update_datetime_after = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_after', None) update_datetime_before = request.query_params.get('update_datetime_before', None)
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]): if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q() create_filter = Q()
if create_datetime_after and create_datetime_before: if create_datetime_after and create_datetime_before:
@@ -149,11 +149,14 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
if _pk: # 判断是否是单例查询 if _pk: # 判断是否是单例查询
re_api = re.sub(_pk,'{id}', api) re_api = re.sub(_pk,'{id}', api)
role_id_list = request.user.role.values_list('id', flat=True) role_id_list = request.user.role.values_list('id', flat=True)
# 修复权限获取bug
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
role_permission_list = []
if menu_button_ids:
role_permission_list=RoleMenuButtonPermission.objects.filter( role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list, role__in=role_id_list,
role__status=1, role__status=1,
menu_button__api=re_api, menu_button_id__in=menu_button_ids).values(
menu_button__method=method).values(
'data_range' 'data_range'
) )
dataScope_list = [] # 权限范围列表 dataScope_list = [] # 权限范围列表

View File

@@ -6,6 +6,8 @@
@Created on: 2021/6/1 001 22:57 @Created on: 2021/6/1 001 22:57
@Remark: 自定义视图集 @Remark: 自定义视图集
""" """
import copy
from django.db import transaction from django.db import transaction
from django_filters import DateTimeFromToRangeFilter from django_filters import DateTimeFromToRangeFilter
from django_filters.rest_framework import FilterSet from django_filters.rest_framework import FilterSet
@@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
kwargs.setdefault('context', self.get_serializer_context()) kwargs.setdefault('context', self.get_serializer_context())
# 全部以可见字段为准 # 全部以可见字段为准
can_see = self.get_menu_field(serializer_class) can_see = self.get_menu_field(serializer_class)
# 排除掉序列化器级的字段 # 排除掉序列化器级的字段(排除字段权限中未授权的字段)
# sub_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in sub_set:
# serializer_class._declared_fields.pop(field)
# if not self.request.user.is_superuser: # if not self.request.user.is_superuser:
# serializer_class.Meta.fields = can_see # exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in exclude_set:
# serializer_class._declared_fields.pop(field)
# meta = copy.deepcopy(serializer_class.Meta)
# meta.fields = list(can_see)
# serializer_class.Meta = meta
# 在分页器中使用 # 在分页器中使用
self.request.permission_fields = can_see self.request.permission_fields = can_see
if isinstance(self.request.data, list): if isinstance(self.request.data, list):
@@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
def get_menu_field(self, serializer_class): def get_menu_field(self, serializer_class):
"""获取字段权限""" """获取字段权限"""
finded = False
for model in get_custom_app_models(): if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()):
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded is False:
return [] return []
return MenuField.objects.filter(model=model['model']
).values('field_name', 'title') # 匿名用户没有角色
ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__)
if hasattr(self.request.user, 'role'):
roles = self.request.user.role.values_list('id', flat=True)
ret = ret.filter(is_query=True, role__in=roles)
return ret.values_list('field__field_name', flat=True)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request) serializer = self.get_serializer(data=request.data, request=request)
@@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
instance.delete() instance.delete()
return DetailResponse(data=[], msg="删除成功") return DetailResponse(data=[], msg="删除成功")
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING) keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING))
@swagger_auto_schema(request_body=openapi.Schema( @swagger_auto_schema(request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
required=['keys'], required=['keys'],

View File

@@ -29,3 +29,4 @@ gunicorn==22.0.0
gevent==24.2.1 gevent==24.2.1
Pillow==10.4.0 Pillow==10.4.0
pyinstaller==6.9.0 pyinstaller==6.9.0
dvadmin3-celery==3.1.6

87
crud-gen.sh Normal file
View File

@@ -0,0 +1,87 @@
if ! [ -f ".env" ];then
echo ".env file not found"
exit 1
fi
if [ -z "$3" ]; then
echo "Use: $0 <app_name> <view_name> <table_name>"
exit 1
fi
DIR=./web/src/views/$1/$2
# 设置数据库连接信息
HOST="177.10.0.13"
USER="root"
PASSWORD=$(cat .env | grep MYSQL_PASSWORD | sed 's/^.*MYSQL_PASSWORD=//g')
DATABASE="django-vue3-admin"
TABLE=$3
TARGET_FILE="./web/src/views/$1/$2/crud.tsx"
# 表是否存在
TABLE_EXISTS=$(mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "SHOW TABLES LIKE '$TABLE';" -N | grep "$TABLE" | wc -l)
if [ "$TABLE_EXISTS" -eq 0 ]; then
echo "Table $TABLE does not exist in database $DATABASE."
exit 1
fi
mkdir -p $DIR
cp -r ./web/src/views/template/* $DIR
sed -i "s/VIEWSETNAME/$2/g" $DIR/*
sed -n -e :a -e '1,5!{P;N;D;};N;ba' -i $TARGET_FILE
# 查询表结构
QUERY="SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$DATABASE' AND TABLE_NAME = '$TABLE' ORDER BY ORDINAL_POSITION;"
# 使用 MySQL 查询获取字段信息,并生成 fast-crud 配置
mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "$QUERY" -N | while read COLUMN_NAME DATA_TYPE COLUMN_COMMENT IS_NULLABLE; do
# 映射 MySQL 数据类型到 fast-crud 类型
case "$DATA_TYPE" in
"int"|"bigint"|"smallint"|"mediumint"|"tinyint"|"decimal"|"float"|"double")
TYPE="number"
;;
"date"|"datetime"|"timestamp")
TYPE="date"
;;
*)
TYPE="text"
;;
esac
echo " $COLUMN_NAME: {
title: '$COLUMN_NAME',
type: '$TYPE',
search: { show: true },
column: {
minWidth: 120,
sortable: 'custom',
},
form: {" >> $TARGET_FILE
if [ "$IS_NULLABLE" = "NO" ]; then
echo " helper: {
render() {
return <div style={"color:blue"}>$COLUMN_NAME 是必填的</div>;
}
},
rules: [{
required: true, message: '$COLUMN_NAME 是必填的'
}]," >> $TARGET_FILE
fi
echo " component: {
placeholder: '请输入 $COLUMN_NAME',
},
},
}," >> $TARGET_FILE
done
echo " },
},
};
}" >> $TARGET_FILE

View File

@@ -1,4 +1,4 @@
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:18.20-alpine
WORKDIR /web/ WORKDIR /web/
COPY web/. . COPY web/. .
RUN yarn install --registry=https://registry.npmmirror.com RUN yarn install --registry=https://registry.npmmirror.com

52
init.sh
View File

@@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
ENV_FILE=".env" ENV_FILE=".env"
HOST="177.10.0.13"
# 检查 .env 文件是否存在 # 检查 .env 文件是否存在
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE 文件已存在。" echo "$ENV_FILE 文件已存在。"
@@ -15,17 +16,60 @@ else
echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。" echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。"
awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }' awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }'
sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.13'|g" ./backend/conf/env.py sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '$HOST'|g" ./backend/conf/env.py
sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py
sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py
sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py
echo "初始化密码创建成功" echo "初始化密码创建成功"
fi fi
echo "正在启动容器..."
docker-compose up -d docker-compose up -d
docker exec dvadmin3-django python manage.py makemigrations
docker exec dvadmin3-django python manage.py migrate if [ $? -ne 0 ]; then
docker exec dvadmin3-django python manage.py init echo "docker-compose up -d 执行失败!"
exit 1
fi
MYSQL_PORT=3306
REDIS_PORT=6379
check_mysql() {
if nc -z "$HOST" "$MYSQL_PORT" >/dev/null 2>&1; then
echo "MySQL 服务正在运行在 $HOST:$MYSQL_PORT"
return 0
else
return 1
fi
}
check_redis() {
if nc -z "$HOST" "$REDIS_PORT" >/dev/null 2>&1; then
echo "Redis 服务正在运行在 $HOST:$REDIS_PORT"
return 0
else
return 1
fi
}
i=1
while [ $i -le 8 ]; do
if check_mysql || check_redis; then
echo "正在迁移数据..."
docker exec dvadmin3-django python3 manage.py makemigrations
docker exec dvadmin3-django python3 manage.py migrate
echo "正在初始化数据..."
docker exec dvadmin3-django python3 manage.py init
echo "欢迎使用dvadmin3项目" echo "欢迎使用dvadmin3项目"
echo "登录地址http://ip:8080" echo "登录地址http://ip:8080"
echo "如访问不到,请检查防火墙配置" echo "如访问不到,请检查防火墙配置"
exit 0
else
echo "$i 次尝试MySQL 或 REDIS服务未运行等待 2 秒后重试..."
sleep 2
fi
i=$((i+1))
done
echo "尝试 5 次后MySQL 或 REDIS服务仍未运行"
exit 1

View File

@@ -1,6 +1,6 @@
# port 端口号 # port 端口号
VITE_PORT = 8080 VITE_PORT = 8080
VITE_API_URL = 'http://dvadmin3api.django.icu:8001' VITE_API_URL = 'http://127.0.0.1:8000'
# open 运行 npm run dev 时自动打开浏览器 # open 运行 npm run dev 时自动打开浏览器
VITE_OPEN = false VITE_OPEN = false

View File

@@ -2,7 +2,7 @@
ENV = 'development' ENV = 'development'
# 本地环境接口地址 # 本地环境接口地址
VITE_API_URL = 'http://127.0.0.1:8000' VITE_API_URL = 'http://127.0.0.1:8001'
# 是否启用按钮权限 # 是否启用按钮权限
VITE_PM_ENABLED = true VITE_PM_ENABLED = true

View File

@@ -49,6 +49,10 @@
👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2) 👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
## 给框架点赞
<img src='https://django-vue-admin.com/alipay.jpg' width='200'>
<img src='https://django-vue-admin.com/wechat.jpg' width='200'>
## 交流 ## 交流

77
web/flowH5.config.ts Normal file
View File

@@ -0,0 +1,77 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path, {resolve} from 'path';
import vueJsx from "@vitejs/plugin-vue-jsx";
import vueSetupExtend from "vite-plugin-vue-setup-extend";
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
import pxtorem from 'postcss-pxtorem';
const pathResolve = (dir: string) => {
return resolve(__dirname, '.', dir);
};
export default defineConfig({
build: {
// outDir: '../backend/static/previewer',
lib: {
entry: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'), // 库的入口文件
name: 'previewer', // 库的全局变量名称
fileName: (format) => `index.${format}.js`, // 输出文件名格式
},
rollupOptions: {
input:{
previewer: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'),
},
external: ['vue','xe-utils'], // 指定外部依赖
output:{
// dir: '../backend/static/previewer', // 输出目录
entryFileNames: 'index.[format].js', // 入口文件名格式
format: 'commonjs',
globals: {
vue: 'Vue'
},
chunkFileNames: `[name].[hash].js`
},
plugins: [
terser({
compress: {
drop_console: false, // 确保不移除 console.log
},
}),
postcss({
plugins: [
pxtorem({
rootValue: 37.5,
unitPrecision: 5,
propList: ['*'],
selectorBlackList: [],
replace: true,
mediaQuery: false,
minPixelValue: 0,
exclude: /node_modules/i,
}),
],
}),
],
},
},
plugins: [
vue(),
vueJsx(),
vueSetupExtend(),
],
resolve: {
alias: {
'/@': path.resolve(__dirname, 'src'), // '@' 别名指向 'src' 目录
'@views': pathResolve('./src/views'),
'/src':path.resolve(__dirname, 'src')
},
},
css:{
postcss:{
}
},
define: {
'process.env': {}
}
});

View File

@@ -1,10 +1,11 @@
{ {
"name": "django-vue3-admin", "name": "django-vue3-admin",
"version": "3.0.4", "version": "3.1.0",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus", "description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite --force", "dev": "vite --force",
"build:dev": "vite build --mode development",
"build": "vite build", "build": "vite build",
"build:local": "vite build --mode local_prod", "build:local": "vite build --mode local_prod",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
@@ -15,6 +16,7 @@
"@fast-crud/fast-extends": "^1.21.2", "@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2", "@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
@@ -24,6 +26,7 @@
"axios": "^1.7.4", "axios": "^1.7.4",
"countup.js": "^2.8.0", "countup.js": "^2.8.0",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"date-holidays": "^3.24.1",
"e-icon-picker": "2.1.1", "e-icon-picker": "2.1.1",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"echarts-gl": "^2.0.9", "echarts-gl": "^2.0.9",
@@ -35,6 +38,7 @@
"js-table2excel": "^1.1.2", "js-table2excel": "^1.1.2",
"jsplumb": "^2.15.6", "jsplumb": "^2.15.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lunar-javascript": "^1.7.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.28", "pinia": "^2.0.28",

View File

@@ -11,7 +11,7 @@
<script setup lang="ts" name="app"> <script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue'; import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
@@ -26,7 +26,8 @@ const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index
const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue')); const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue')); const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue')); const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
import { ElMessageBox, ElNotification, NotificationHandle } from 'element-plus';
import { useCore } from '/@/utils/cores';
// 定义变量内容 // 定义变量内容
const { messages, locale } = useI18n(); const { messages, locale } = useI18n();
const setingsRef = ref(); const setingsRef = ref();
@@ -35,7 +36,8 @@ const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from '/@/utils/websocket'; import websocket from '/@/utils/websocket';
import { ElNotification } from 'element-plus'; const core = useCore();
const router = useRouter();
// 获取版本号 // 获取版本号
const getVersion = computed(() => { const getVersion = computed(() => {
let isVersion = false; let isVersion = false;
@@ -67,7 +69,15 @@ onMounted(() => {
mittBus.on('openSetingsDrawer', () => { mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer(); setingsRef.value.openDrawer();
}); });
// 设置皮肤缓存版本,每次更新版本可以所有用户清空缓存
const themeConfigVersion = '1.0.0'
// 获取缓存中的布局配置 // 获取缓存中的布局配置
if (Local.get('themeConfigVersion') !== themeConfigVersion) {
Local.clear();
Local.set('themeConfigVersion', themeConfigVersion);
window.location.reload();
return
}
if (Local.get('themeConfig')) { if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') }); storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle'); document.documentElement.style.cssText = Local.get('themeConfigStyle');
@@ -117,7 +127,25 @@ const wsReceive = (message: any) => {
position: 'bottom-right', position: 'bottom-right',
duration: 5000, duration: 5000,
}); });
} else if (data.contentType === 'Content') {
ElMessageBox.confirm(data.content, data.notificationTitle, {
confirmButtonText: data.notificationButton,
dangerouslyUseHTMLString: true,
cancelButtonText: '关闭',
type: 'info',
closeOnClickModal: false,
}).then(() => {
ElMessageBox.close();
const path = data.path;
if (route.path === path) {
core.bus.emit('onNewTask', { name: 'onNewTask' });
} else {
router.push({ path});
} }
})
.catch(() => {});
}
}; };
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 关闭连接 // 关闭连接

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="user-info-head" @click="editCropper()"> <div class="user-info-head" @click="editCropper()">
<el-avatar :size="100" :src="options.img" /> <el-avatar :size="100" :src="getBaseURL(options.img)" />
<el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog"> <el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row> <el-row>
<el-col class="flex justify-center"> <el-col class="flex justify-center">
@@ -50,10 +50,11 @@ import { VueCropper } from 'vue-cropper';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue'; import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue';
import { base64ToFile } from '/@/utils/tools'; import { base64ToFile } from '/@/utils/tools';
import headerImage from "/@/assets/img/headerImage.png";
import {getBaseURL} from "/@/utils/baseUrl";
const userStore = useUserInfo(); const userStore = useUserInfo();
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false); const visible = ref(false);
const title = ref('修改头像'); const title = ref('修改头像');
const emit = defineEmits(['uploadImg']); const emit = defineEmits(['uploadImg']);
@@ -75,7 +76,7 @@ const dialogVisiable = computed({
//图片裁剪数据 //图片裁剪数据
const options = reactive({ const options = reactive({
img: userStore.userInfos.avatar, // 裁剪图片的地址 img: userStore.userInfos.avatar || headerImage, // 裁剪图片的地址
fileName: '', fileName: '',
autoCrop: true, // 是否默认生成截图框 autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度 autoCropWidth: 200, // 默认生成截图框宽度
@@ -165,6 +166,7 @@ const updateAvatar = (img) => {
defineExpose({ defineExpose({
updateAvatar, updateAvatar,
editCropper
}); });
</script> </script>
@@ -172,7 +174,6 @@ defineExpose({
.user-info-head { .user-info-head {
position: relative; position: relative;
display: inline-block; display: inline-block;
height: 120px;
} }
.user-info-head:hover:after { .user-info-head:hover:after {

View File

@@ -0,0 +1,403 @@
<template>
<div style="width: 100%; height: 100%;">
<div class="selected-show" v-if="props.modelValue && props.selectable">
<el-text>已选择:</el-text>
<el-tag v-if="props.multiple" v-for="item in data" closable @close="handleTagClose(item)">
{{ item.toLocaleDateString('en-CA') }}
</el-tag>
<el-tag v-else closable @close="handleTagClose(data)">{{ data?.toLocaleDateString('en-CA') }}</el-tag>
<el-button v-if="props.modelValue" size="small" type="text" @click="clear">清空</el-button>
</div>
<div class="controls">
<div>
今天<el-text size="large">{{ today.toLocaleDateString('en-CA') }}</el-text>
</div>
<!-- <div class="current-month">
<el-tag size="large" type="primary">
{{ currentCalendarDate.getFullYear() }}{{ currentCalendarDate.getMonth() + 1 }}
</el-tag>
</div> -->
<div class="control-button" v-if="!(!!props.range && props.range[0] && props.range[1]) && props.showPageTurn">
<el-button-group size="small" type="default" v-if="props.pageTurn">
<el-popover trigger="click" width="160px">
<template #reference>
<el-button type="text" size="small">节假日设置</el-button>
</template>
<el-switch v-model="showHoliday" active-text="显示节日" inactive-text="关闭节日" inline-prompt />
<el-checkbox v-model="showLunarHoliday" label="农历节日" />
<el-checkbox v-model="showJieQi" label="节气" />
<el-checkbox v-model="showDetailedHoliday" label="更多节日" />
</el-popover>
<el-button icon="DArrowLeft" @click="turnToPreY">上年</el-button>
<el-button icon="ArrowLeft" @click="turnToPreM">上月</el-button>
<el-button @click="turnToToday">今天</el-button>
<el-button icon="ArrowRight" @click="turnToNextM">下月</el-button>
<el-button icon="DArrowRight" @click="turnToNextY">下年</el-button>
</el-button-group>
</div>
</div>
<el-divider style="margin: 4px;" />
<div class="calender">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th class="calender-header" v-for="item, ind in ['日', '一', '二', '三', '四', '五', '六']" :key="ind">
{{ item }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="week in calendarList">
<td class="calender-td" v-for="item in week">
<div class="calender-cell" :data-date="item.date.toLocaleDateString('en-CA')" :class="{
'no-current-month': item.date.getMonth() !== currentCalendarDate.getMonth(),
'today': item.date.toDateString() === today.toDateString(),
'selected': item.selected,
'disabled': item.disabled,
}" @mouseenter="onCalenderCellHover" @mouseleave="onCalenderCellUnhover"
@click="(e: MouseEvent) => item.disabled ? null : onCalenderCellClick(e)">
<div class="calender-cell-header calender-cell-line">
<span>{{ item.date.getDate() }}</span>
<span v-if="item.selected">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M77.248 415.04a64 64 0 0 1 90.496 0l226.304 226.304L846.528 188.8a64 64 0 1 1 90.56 90.496l-543.04 543.04-316.8-316.8a64 64 0 0 1 0-90.496z">
</path>
</svg>
</span>
</div>
<div class="calender-cell-body calender-cell-line">
<slot name="cell-body" v-bind="item">
</slot>
</div>
<div class="calender-cell-footer calender-cell-line">
<span>{{ item.holiday || '&nbsp;' }}</span>
<el-text v-if="item.date.toDateString() === today.toDateString()" type="danger">今天</el-text>
</div>
<!-- {{ item }} -->
</div>
</td>
</tr>
</tbody>
</table>
<div class="watermark" v-if="props.watermark" :style="watermarkPositionMap[props.watermarkPosition]">
{{ (currentCalendarDate.toLocaleDateString('en-CA').split('-').slice(0, 2)).join('-') }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useUi } from '@fast-crud/fast-crud';
import { ref, defineProps, PropType, watch, computed, onMounted } from 'vue';
import Holidays from 'date-holidays';
import Lunar from 'lunar-javascript';
const LUNAR = Lunar.Lunar; // 农历
const SOLAR = Lunar.Solar; // 阳历
const props = defineProps({
modelValue: {},
// 日期多选
multiple: { type: Boolean, default: false },
// 日期范围
range: { type: Object as PropType<[Date, Date]> },
// 可以翻页
pageTurn: { type: Boolean, default: true },
// 跨页选择
crossPage: { type: Boolean, default: false },
// 显示年月水印和水印位置
watermark: { type: Boolean, default: true },
watermarkPosition: { type: Object as PropType<PositionType>, default: 'bottom-right' },
// 显示翻页控件
showPageTurn: { type: Boolean, default: true },
// 是否可选
selectable: { type: Boolean, default: true },
// 验证日期是否有效
validDate: { type: Object as PropType<ValidDateFunc>, default: () => ((d: Date) => true) }
});
type ValidDateFunc = (d: Date) => boolean;
type PositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'center-left' | 'center-right' | 'center-top';
const today = new Date();
const showHoliday = ref<boolean>(true); // 显示节日
const showDetailedHoliday = ref<boolean>(false); // 显示详细的国际节日
const showJieQi = ref<boolean>(true); // 显示节气
const showLunarHoliday = ref<boolean>(true); // 显示农历节日
const watermarkPositionMap: { [key: string]: any } = {
'top-left': { top: '40px', left: 0, transformOrigin: '0 0' },
'top-right': { top: '40px', right: 0, transformOrigin: '100% 0' },
'bottom-left': { bottom: 0, left: 0, transformOrigin: '0 100%' },
'bottom-right': { bottom: 0, right: 0, transformOrigin: '100% 100%' },
'center': { top: '50%', left: '50%', transformOrigin: '50% 50%', transform: 'translate(-50%, -50%) scale(10)' },
'center-left': { top: '50%', left: 0, transformOrigin: '0 50%' },
'center-right': { top: '50%', right: 0, transformOrigin: '100% 50%' },
'center-top': { top: 0, left: '50%', transformOrigin: '50% 0', transform: 'translate(-50%, 40px) scale(10)' },
'center-bottom': { bottom: 0, left: '50%', transformOrigin: '50% 100%', transform: 'translate(-50%, 0) scale(10)' },
};
// 获取当月第一周的第一天(包括上个月)
const calendarFirstDay = (current: Date = new Date()) => {
let today = new Date(current); // 指定天
let firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // 月初天
let weekOfFirstDay = firstDayOfMonth.getDay(); // 周几0日
if (weekOfFirstDay === 0) return new Date(firstDayOfMonth); // 是周日则直接返回
let firstDayOfWeek = new Date(firstDayOfMonth);
// 月初减去周几,不+1是因为从日历周日开始
firstDayOfWeek.setDate(firstDayOfMonth.getDate() - weekOfFirstDay);
return new Date(firstDayOfWeek);
};
// 获取当月最后一周的最后一天(包括下个月)
const calendarLastDay = (current: Date = new Date()) => {
let today = new Date(current); // 指定天
let lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 1); // 月末天
lastDayOfMonth.setDate(lastDayOfMonth.getDate() - 1);
let weekOfFirstDay = lastDayOfMonth.getDay();
if (weekOfFirstDay === 6) return new Date(lastDayOfMonth); // 是周六则直接返回
let lastDayOfWeek = new Date(lastDayOfMonth);
// 月末加剩下周几,要-1是因为日历到周六结束
lastDayOfWeek.setDate(lastDayOfMonth.getDate() + (7 - weekOfFirstDay - 1));
return new Date(lastDayOfWeek);
};
const generateDateList = (startDate: Date, endDate: Date): Date[] => { // 生成日期列表
let dates = [];
let s = new Date(startDate);
let e = new Date(endDate);
while (s <= e) {
dates.push(new Date(s));
s.setDate(s.getDate() + 1);
}
return dates;
};
// 日历当前页范围
interface CalendarCell {
date: Date;
selected: boolean;
disabled: boolean;
currentMonth: boolean;
holiday: string;
};
const currentCalendarDate = ref<Date>(new Date());
const calendarList = computed(() => {
let dates = (!!props.range && props.range[0] && props.range[1]) ?
generateDateList(props.range[0], props.range[1]) :
generateDateList(calendarFirstDay(currentCalendarDate.value), calendarLastDay(currentCalendarDate.value));
let proce_dates: CalendarCell[] = dates.map((value) => {
let solarDate = SOLAR.fromDate(value);
let lunarDate = solarDate.getLunar();
let solarHolidays: string[] = solarDate.getFestivals(); // 国历节日
let lunarHolidays: string[] = lunarDate.getFestivals(); // 农历节日
let jieQi: string = lunarDate.getJieQi(); // 节气
// 农历节日、国际节日、节气三选一
let holiday = showHoliday.value ? ((showLunarHoliday.value ? lunarHolidays[0] : '') || (showJieQi.value ? jieQi : '') ||
(showDetailedHoliday.value ? solarHolidays[0] : yearHolidays.value[value.toLocaleDateString('en-CA')])) : ''; // yearHolidays国际的
return {
date: value,
selected: props.multiple ?
(data.value as Date[]).findIndex((v) => v.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA')) !== -1 :
data.value?.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA'),
disabled: !props.validDate(value),
currentMonth: value.getMonth() === currentCalendarDate.value.getMonth(),
// 农历节日、节气、法定日三选一
holiday: holiday
}
});
let res: CalendarCell[][] = [];
for (let i = 0; i < 6; i++) res.push(proce_dates.slice(i * 7, (i + 1) * 7));
return res;
});
// 控件
const turnToPreM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() - 1, 1);
const turnToNextM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() + 1, 1);
const turnToPreY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() - 1, currentCalendarDate.value.getMonth(), 1);
const turnToNextY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() + 1, currentCalendarDate.value.getMonth(), 1);
const turnToToday = () => currentCalendarDate.value = new Date();
// 如果禁止跨页则跨页时清空选择
watch(
() => currentCalendarDate.value,
(v, ov) => props.crossPage ? {} :
(v.toLocaleDateString('en-CA') === ov.toLocaleDateString('en-CA') ? {} : clear())
);
// 单元格事件
const onCalenderCellHover = ({ target }: MouseEvent) => (target as HTMLElement).classList.add('onhover');
const onCalenderCellUnhover = ({ target }: MouseEvent) => (target as HTMLElement).classList.remove('onhover');
const onCalenderCellClick = (e: MouseEvent) => {
if (!props.selectable) return;
let strValue = (e.target as HTMLElement).dataset.date as string;
if (strValue === undefined) return;
let value = new Date(strValue);
if (props.multiple) {
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
let ind = d.findIndex((v) => v === strValue);
if (ind === -1) d.push(strValue);
else d.splice(ind, 1);
onDataChange(d);
}
// 这里阻止了点击取消选中需要通过tag的x来取消
else (data.value?.toLocaleDateString('en-CA') === strValue ? {} : onDataChange(value));
};
// 选择回显
const handleTagClose = (d: Date) => {
let strValue = d.toLocaleDateString('en-CA');
if (props.multiple) {
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
d.splice(d.findIndex((v) => v === strValue), 1);
onDataChange(d);
}
else onDataChange(null);
};
// 节假日
const holidays = new Holidays('CN');
const yearHolidays = computed(() => {
let h = holidays.getHolidays(currentCalendarDate.value.getFullYear());
let proce_h: { [key: string]: string } = {};
let _h: string[] = [];
for (let i of h) {
let d = i.date.split(' ')[0];
let hn = i.name.split(' ')[0];
if (_h.includes(hn)) continue;
proce_h[d] = hn;
_h.push(hn);
}
return proce_h
});
// fs-crud部分
const data = ref<any>();
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
watch(
() => props.modelValue,
(val) => {
if (val === undefined) data.value = props.multiple ? [] : null;
else data.value = props.multiple ? (val as Date[]).map((v: Date) => new Date(v)) : val;
},
{ immediate: true }
);
const { ui } = useUi();
const formValidator = ui.formItem.injectFormItemContext();
const onDataChange = (value: any) => {
emit('update:modelValue', value);
formValidator.onChange();
formValidator.onBlur();
};
const reset = () => { // 重置日历
currentCalendarDate.value = new Date();
onDataChange(props.multiple ? [] : null);
};
const clear = () => onDataChange(props.multiple ? [] : null); // 清空数据
defineExpose({
data,
onDataChange,
reset,
clear,
showHoliday,
showDetailedHoliday,
showJieQi,
showLunarHoliday
});
</script>
<style lang="scss" scoped>
.selected-show {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.controls {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
}
.calender {
position: relative;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
.watermark {
position: absolute;
font-size: 16px;
transform: scale(10);
color: #aaa;
opacity: 0.1;
pointer-events: none;
}
table {
position: relative;
}
.calender-header {
padding: 16px 0;
}
.calender-td {
border: 1px solid #eee;
width: calc(100% / 7);
}
.calender-cell {
min-height: 96px;
min-width: 100px;
box-sizing: border-box;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
&.today {
color: var(--el-color-warning) !important;
background-color: var(--el-color-warning-light-9) !important;
}
&.onhover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
&.disabled {
cursor: not-allowed;
color: #bbb;
background: none;
}
&.no-current-month {
color: #bbb;
}
&.selected {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.calender-cell-line {
min-height: 0px;
&.calender-cell-header {
display: flex;
gap: 4px;
align-items: center;
pointer-events: none;
}
&.calender-cell-body {
display: flex;
gap: 4px;
align-items: center;
}
&.calender-cell-footer {
display: flex;
justify-content: space-between;
pointer-events: none;
}
}
}
}
</style>

View File

@@ -8,7 +8,29 @@
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])" <el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
:label="item.name" /> :label="item.name" />
</el-select> </el-select>
<div v-if="props.inputType === 'image'" style="position: relative;" class="form-display"
<div v-if="props.inputType === 'image' && props.multiple"
style="width: 100%; display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px;">
<div v-for="item, index in (data || [])" style="position: relative;"
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<el-image :src="item" :key="index" fit="scale-down" class="itemList"
:style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }" />
<el-icon v-show="(!!data && !props.disabled)" class="closeHover" :size="16" @click="clearOne(item)">
<Close />
</el-icon>
</div>
<div style="position: relative;" :style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<div
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>
</div>
</div>
<div v-if="props.inputType === 'image' && !props.multiple" class="form-display" style="position: relative;"
@mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave" @mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }"> :style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }"> <el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
@@ -24,10 +46,11 @@
</div> </div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover" <div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div> :style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear"> <el-icon v-show="(!!data && !props.disabled) && !props.multiple" class="closeHover" :size="16" @click="clear">
<Close /> <Close />
</el-icon> </el-icon>
</div> </div>
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter" <div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave" @mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;" style="position: relative; display: flex; align-items: center; justify-items: center;"
@@ -46,6 +69,7 @@
<Close /> <Close />
</el-icon> </el-icon>
</div> </div>
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter" <div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave" @mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;" style="position: relative; display: flex; align-items: center; justify-items: center;"
@@ -199,7 +223,7 @@ const props = defineProps({
tabsShow: { type: Number, default: SHOW.ALL }, tabsShow: { type: Number, default: SHOW.ALL },
// 是否可以多选,默认单选 // 是否可以多选,默认单选
// 该值为true时inputType必须是selector暂不支持其他type的多选 // 该值为true时inputType必须是selector或image暂不支持其他type的多选
multiple: { type: Boolean, default: false }, multiple: { type: Boolean, default: false },
// 是否可选该参数用于只上传和展示而不选择和绑定model的情况 // 是否可选该参数用于只上传和展示而不选择和绑定model的情况
@@ -274,6 +298,7 @@ const onItemClick = async (e: MouseEvent) => {
while (!target.dataset.id) target = target.parentElement as HTMLElement; while (!target.dataset.id) target = target.parentElement as HTMLElement;
let fileId = target.dataset.id; let fileId = target.dataset.id;
if (props.multiple) { if (props.multiple) {
if (!!!data.value) data.value = [];
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; } if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
else { target.classList.add('active'); flat = 1; } else { target.classList.add('active'); flat = 1; }
if (data.value.length) { if (data.value.length) {
@@ -327,8 +352,12 @@ const clearState = () => {
// all数据不能清因为all只会在挂载的时候赋值一次 // all数据不能清因为all只会在挂载的时候赋值一次
// listAllData.value = []; // listAllData.value = [];
}; };
const clear = () => { data.value = null; onDataChange(null); } const clear = () => { data.value = null; onDataChange(null); };
const clearOne = (item: any) => {
let _l = (JSON.parse(JSON.stringify(data.value)) as any[]).filter((i: any) => i !== item)
data.value = _l;
onDataChange(_l);
};
// 网络文件部分 // 网络文件部分
const netLoading = ref<boolean>(false); const netLoading = ref<boolean>(false);
@@ -386,7 +415,15 @@ watch(
const { ui } = useUi(); const { ui } = useUi();
const formValidator = ui.formItem.injectFormItemContext(); const formValidator = ui.formItem.injectFormItemContext();
const onDataChange = (value: any) => { const onDataChange = (value: any) => {
emit('update:modelValue', value); let _v = null;
if (value) {
if (typeof value === 'string') _v = value.replace(/\\/g, '/');
else {
_v = [];
for (let i of value) _v.push(i.replace(/\\/g, '/'));
}
}
emit('update:modelValue', _v);
formValidator.onChange(); formValidator.onChange();
formValidator.onBlur(); formValidator.onBlur();
}; };
@@ -394,7 +431,8 @@ const onDataChange = (value: any) => {
defineExpose({ data, onDataChange, selectVisiable, clearState, clear }); defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
onMounted(() => { onMounted(() => {
if (props.multiple && props.inputType !== 'selector')
if (props.multiple && !['selector', 'image'].includes(props.inputType))
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector'); throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
listRequestAll(); listRequestAll();
console.log('fileselector tenentmdoe', isTenentMode); console.log('fileselector tenentmdoe', isTenentMode);
@@ -475,4 +513,9 @@ onMounted(() => {
top: 2px; top: 2px;
cursor: pointer; cursor: pointer;
} }
.itemList {
border: 1px solid #dcdfe6;
border-radius: 8px;
}
</style> </style>

View File

@@ -3,6 +3,7 @@
popper-class="popperClass" popper-class="popperClass"
class="tableSelector" class="tableSelector"
multiple multiple
:collapseTags="props.tableConfig.collapseTags"
@remove-tag="removeTag" @remove-tag="removeTag"
v-model="data" v-model="data"
placeholder="请选择" placeholder="请选择"
@@ -18,20 +19,22 @@
<el-table <el-table
ref="tableRef" ref="tableRef"
:data="tableData" :data="tableData"
size="mini" :size="props.tableConfig.size"
border border
row-key="id" row-key="id"
:lazy="props.tableConfig.lazy" :lazy="props.tableConfig.lazy"
:load="props.tableConfig.load" :load="props.tableConfig.load"
:tree-props="props.tableConfig.treeProps" :tree-props="props.tableConfig.treeProps"
style="width: 400px" style="width: 600px"
max-height="200" max-height="200"
height="200" height="200"
:highlight-current-row="!props.tableConfig.isMultiple" :highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@select="handleSelectionChange"
@selectAll="handleSelectionChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55" /> <el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
<el-table-column fixed type="index" label="#" width="50" /> <el-table-column fixed type="index" label="#" width="50" />
<el-table-column <el-table-column
:prop="item.prop" :prop="item.prop"
@@ -56,23 +59,31 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, reactive, ref, watch } from 'vue'; import {computed, defineProps, onMounted, reactive, ref, watch} from 'vue';
import XEUtils from 'xe-utils'; import XEUtils from 'xe-utils';
import { request } from '/@/utils/service'; import { request } from '/@/utils/service';
const props = defineProps({ const props = defineProps({
modelValue: {}, modelValue: {
type: Array || String || Number,
default: () => []
},
tableConfig: { tableConfig: {
type: Object,
default:{
url: null, url: null,
label: null, //显示值 label: null, //显示值
value: null, //数据值 value: null, //数据值
isTree: false, isTree: false,
lazy: true, lazy: true,
size:'default',
load: () => {}, load: () => {},
data: [], //默认数据 data: [], //默认数据
isMultiple: false, //是否多选 isMultiple: false, //是否多选
collapseTags:false,
treeProps: { children: 'children', hasChildren: 'hasChildren' }, treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项 columns: [], //每一项对应的列表项
},
}, },
displayLabel: {}, displayLabel: {},
} as any); } as any);
@@ -86,7 +97,7 @@ const multipleSelection = ref();
// 搜索值 // 搜索值
const search = ref(undefined); const search = ref(undefined);
//表格数据 //表格数据
const tableData = ref(); const tableData = ref([]);
// 分页的配置 // 分页的配置
const pageConfig = reactive({ const pageConfig = reactive({
page: 1, page: 1,
@@ -99,7 +110,6 @@ const pageConfig = reactive({
* @param val:Array * @param val:Array
*/ */
const handleSelectionChange = (val: any) => { const handleSelectionChange = (val: any) => {
multipleSelection.value = val;
const { tableConfig } = props; const { tableConfig } = props;
const result = val.map((item: any) => { const result = val.map((item: any) => {
return item[tableConfig.value]; return item[tableConfig.value];
@@ -117,7 +127,7 @@ const handleSelectionChange = (val: any) => {
const handleCurrentChange = (val: any) => { const handleCurrentChange = (val: any) => {
const { tableConfig } = props; const { tableConfig } = props;
if (!tableConfig.isMultiple && val) { if (!tableConfig.isMultiple && val) {
data.value = [val[tableConfig.label]]; // data.value = [val[tableConfig.label]];
emit('update:modelValue', val[tableConfig.value]); emit('update:modelValue', val[tableConfig.value]);
} }
}; };
@@ -150,6 +160,32 @@ const getDict = async () => {
} }
}; };
// 获取节点值
const getNodeValues = () => {
request({
url:props.tableConfig.valueUrl,
method:'post',
data:{ids:props.modelValue}
}).then(res=>{
if(res.data.length>0){
data.value = res.data.map((item:any)=>{
return item[props.tableConfig.label]
})
tableRef.value!.clearSelection()
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(
row,
true,
false
)
})
}
})
}
/** /**
* 下拉框展开/关闭 * 下拉框展开/关闭
* @param bool * @param bool
@@ -169,20 +205,12 @@ const handlePageChange = (page: any) => {
getDict(); getDict();
}; };
// 监听displayLabel的变化更新数据 onMounted(()=>{
watch( setTimeout(()=>{
() => { getNodeValues()
return props.displayLabel; },1000)
}, })
(value) => {
const { tableConfig } = props;
const result = value
? value.map((item: any) => { return item[tableConfig.label];})
: null;
data.value = result;
},
{ immediate: true }
);
</script> </script>
<style scoped> <style scoped>

View File

@@ -17,13 +17,15 @@ import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
import { useRoute } from 'vue-router';
const route = useRoute();
// 引入组件 // 引入组件
const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue')); const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue')); const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
// 定义变量内容 // 定义变量内容
const layoutAsideScrollbarRef = ref(); const layoutAsideScrollbarRef = ref();
const routesIndex = ref(0);
const stores = useRoutesList(); const stores = useRoutesList();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes(); const storesTagsViewRoutes = useTagsViewRoutes();
@@ -83,10 +85,36 @@ const closeLayoutAsideMobileMode = () => {
if (clientWidth < 1000) themeConfig.value.isCollapse = false; if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', ''); document.body.setAttribute('class', '');
}; };
const findFirstLevelIndex = (data, path) => {
for (let index = 0; index < data.length; index++) {
const item = data[index];
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
if (item.children && item.children.length > 0) {
// 检查子菜单中是否有匹配的路径
const childIndex = item.children.findIndex((child) => child.path === path);
if (childIndex !== -1) {
return index; // 返回当前一级菜单的索引
}
// 递归查找子菜单
const foundIndex = findFirstLevelIndex(item.children, path);
if (foundIndex !== null) {
return index; // 返回找到的索引
}
}
}
return null; // 找不到路径时返回 null
};
// 设置/过滤路由(非静态路由/是否显示在菜单中) // 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => { const setFilterRoutes = (path='') => {
if (themeConfig.value.layout === 'columns') return false; if (themeConfig.value.layout === 'columns') return false;
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
// 获取当前地址的索引,不用从参数选取
routesIndex.value = findFirstLevelIndex(routesList.value,path || route.path) || 0
state.menuList = filterRoutesFun(routesList.value[routesIndex.value].children || [routesList.value[routesIndex.value]]);
} else {
state.menuList = filterRoutesFun(routesList.value); state.menuList = filterRoutesFun(routesList.value);
}
}; };
// 路由过滤递归函数 // 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => { const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
@@ -122,7 +150,8 @@ onBeforeMount(() => {
let { layout, isClassicSplitMenu } = themeConfig.value; let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) { if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = []; state.menuList = [];
state.menuList = res.children; // state.menuList = res.children;
setFilterRoutes(res.path);
} }
}); });
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => { mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {

View File

@@ -102,6 +102,5 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
background: var(--next-bg-topBar); background: var(--next-bg-topBar);
border-bottom: 1px solid var(--next-border-color-light);
} }
</style> </style>

View File

@@ -37,7 +37,7 @@
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i> <i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div> </div>
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false"> <el-popover placement="bottom" trigger="hover" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference> <template #reference>
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0"> <el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
<el-icon :title="$t('message.user.title4')"> <el-icon :title="$t('message.user.title4')">
@@ -58,7 +58,7 @@
></i> ></i>
</div> </div>
<div> <div>
<span v-if="!isSocketOpen"> <span v-if="!isSocketOpen" class="online-status-span">
<el-popconfirm <el-popconfirm
width="250" width="250"
ref="onlinePopoverRef" ref="onlinePopoverRef"
@@ -71,7 +71,7 @@
> >
<template #reference> <template #reference>
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}"> <el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" /> <img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge> </el-badge>
</template> </template>
</el-popconfirm> </el-popconfirm>
@@ -93,7 +93,7 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> <el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item> <el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item> <el-dropdown-item command="/versionUpgradeLog">更新日志</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item> <el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -250,6 +250,7 @@ onMounted(() => {
//消息中心的未读数量 //消息中心的未读数量
import { messageCenterStore } from '/@/stores/messageCenter'; import { messageCenterStore } from '/@/stores/messageCenter';
import {getBaseURL} from "/@/utils/baseUrl";
const messageCenter = messageCenterStore(); const messageCenter = messageCenterStore();
</script> </script>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="el-menu-horizontal-warp"> <div class="el-menu-horizontal-warp">
<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef"> <!-- <el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">-->
<el-menu router :default-active="state.defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal"> <el-menu :default-active="defaultActive" background-color="transparent" mode="horizontal">
<template v-for="val in menuLists"> <template v-for="(val,index) in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path"> <el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title> <template #title>
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
@@ -11,7 +11,7 @@
<SubItem :chil="val.children" /> <SubItem :chil="val.children" />
</el-sub-menu> </el-sub-menu>
<template v-else> <template v-else>
<el-menu-item :index="val.path" :key="val.path"> <el-menu-item :index="val.path" :key="val.path" style="--el-menu-active-color: #fff" @click="onToRouteClick(val,index)">
<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)"> <template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }} {{ $t(val.meta.title) }}
@@ -26,22 +26,25 @@
</template> </template>
</template> </template>
</el-menu> </el-menu>
</el-scrollbar> <!-- </el-scrollbar>-->
</div> </div>
</template> </template>
<script setup lang="ts" name="navMenuHorizontal"> <script setup lang="ts" name="navMenuHorizontal">
import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue'; import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue';
import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router'; import {useRoute, onBeforeRouteUpdate, RouteRecordRaw, useRouter} from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList'; import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other'; import other from '/@/utils/other';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
const router = useRouter()
// 引入组件 // 引入组件
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue')); const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
const state = reactive<AsideState>({
menuList: [],
clientWidth: 0
});
// 定义父组件传过来的值 // 定义父组件传过来的值
const props = defineProps({ const props = defineProps({
// 菜单列表 // 菜单列表
@@ -58,19 +61,33 @@ const storesThemeConfig = useThemeConfig();
const { routesList } = storeToRefs(stores); const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute(); const route = useRoute();
const state = reactive({ const defaultActive = ref('')
defaultActive: '' as string | undefined,
});
// 获取父级菜单数据 // 获取父级菜单数据
const menuLists = computed(() => { const menuLists = computed(() => {
<RouteItems>props.menuList.shift()
return <RouteItems>props.menuList; return <RouteItems>props.menuList;
}); });
// 设置横向滚动条可以鼠标滚轮滚动 // 递归获取当前路由的顶级索引
const onElMenuHorizontalScroll = (e: WheelEventType) => { const findFirstLevelIndex = (data, path) => {
const eventDelta = e.wheelDelta || -e.deltaY * 40; for (let index = 0; index < data.length; index++) {
elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft + eventDelta / 4; const item = data[index];
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
if (item.children && item.children.length > 0) {
// 检查子菜单中是否有匹配的路径
const childIndex = item.children.findIndex((child) => child.path === path);
if (childIndex !== -1) {
return index; // 返回当前一级菜单的索引
}
// 递归查找子菜单
const foundIndex = findFirstLevelIndex(item.children, path);
if (foundIndex !== null) {
return index; // 返回找到的索引
}
}
}
return null; // 找不到路径时返回 null
}; };
// 初始化数据,页面刷新时,滚动条滚动到对应位置 // 初始化数据,页面刷新时,滚动条滚动到对应位置
const initElMenuOffsetLeft = () => { const initElMenuOffsetLeft = () => {
nextTick(() => { nextTick(() => {
@@ -107,17 +124,41 @@ const setSendClassicChildren = (path: string) => {
const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => { const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
const { path, meta } = currentRoute; const { path, meta } = currentRoute;
if (themeConfig.value.layout === 'classic') { if (themeConfig.value.layout === 'classic') {
state.defaultActive = `/${path?.split('/')[1]}`; let firstLevelIndex = (findFirstLevelIndex(routesList.value, route.path) || 0) - 1
defaultActive.value = firstLevelIndex < 0 ? defaultActive.value : menuLists.value[firstLevelIndex].path
} else { } else {
const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/'); const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
if (pathSplit.length >= 4 && meta?.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/'); if (pathSplit.length >= 4 && meta?.isHide) defaultActive.value = pathSplit.splice(0, 3).join('/');
else state.defaultActive = path; else defaultActive.value = path;
} }
}; };
// 打开外部链接 // 打开外部链接
const onALinkClick = (val: RouteItem) => { const onALinkClick = (val: RouteItem) => {
other.handleOpenLink(val); other.handleOpenLink(val);
}; };
// 跳转页面
const onToRouteClick = (val: RouteItem,index) => {
// 跳转到子级页面
let children = val.children
if (children === undefined){
defaultActive.value = val.path
children = setSendClassicChildren(val.path).children
}
if (children.length >= 1){
if (children[0].is_catalog) {
onToRouteClick(children[0],index)
return
}
router.push(children[0].path)
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', children[0]);
}
} else {
router.push('/home')
}
};
// 页面加载前 // 页面加载前
onBeforeMount(() => { onBeforeMount(() => {
setCurrentRouterHighlight(route); setCurrentRouterHighlight(route);
@@ -126,16 +167,6 @@ onBeforeMount(() => {
onMounted(() => { onMounted(() => {
initElMenuOffsetLeft(); initElMenuOffsetLeft();
}); });
// 路由更新时
onBeforeRouteUpdate((to) => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
setCurrentRouterHighlight(to);
// 修复经典布局开启切割菜单时点击tagsView后左侧导航菜单数据不变的问题
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
}
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -8,7 +8,21 @@
<sub-item :chil="val.children" /> <sub-item :chil="val.children" />
</el-sub-menu> </el-sub-menu>
<template v-else> <template v-else>
<el-menu-item :index="val.path" :key="val.path"> <a v-if="val.name==='templateCenter'" href="#/templateCenter" target="_blank">
<el-menu-item :key="val.path">
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>
</template>
<template v-else>
<a class="w100" @click.prevent="onALinkClick(val)">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</a>
</template>
</el-menu-item>
</a>
<el-menu-item v-else :index="val.path" :key="val.path">
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)"> <template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span> <span>{{ $t(val.meta.title) }}</span>

View File

@@ -23,6 +23,7 @@
<script setup lang="ts" name="layoutIframeView"> <script setup lang="ts" name="layoutIframeView">
import { computed, watch, ref, nextTick } from 'vue'; import { computed, watch, ref, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import {cookie} from "xe-utils";
// 定义父组件传过来的值 // 定义父组件传过来的值
const props = defineProps({ const props = defineProps({
@@ -49,7 +50,15 @@ const route = useRoute();
// 处理 list 列表,当打开时,才进行加载 // 处理 list 列表,当打开时,才进行加载
const setIframeList = computed(() => { const setIframeList = computed(() => {
return (<RouteItems>props.list).filter((v: RouteItem) => v.meta?.isIframeOpen); return (<RouteItems>props.list).filter((v: RouteItem) => {
if (v.meta?.isIframeOpen) {
const isLink = v.meta?.isLink || '';
if (isLink.includes("{{token}}")) {
v.meta.isLink = isLink.replace("{{token}}", cookie.get('token'))
}
}
return v.meta?.isIframeOpen
});
}); });
// 获取 iframe 当前路由 path // 获取 iframe 当前路由 path
const getRoutePath = computed(() => { const getRoutePath = computed(() => {

View File

@@ -17,19 +17,31 @@
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { verifyUrl } from '/@/utils/toolsValidate'; import { verifyUrl } from '/@/utils/toolsValidate';
import {cookie} from "xe-utils";
// 定义变量内容 // 定义变量内容
const route = useRoute(); const route = useRoute();
const state = reactive<LinkViewState>({ const state = reactive<LinkViewState>({
title: '', title: '',
isLink: '', isLink: '',
query: null
}); });
// 立即前往 // 立即前往
const onGotoFullPage = () => { const onGotoFullPage = () => {
const { origin, pathname } = window.location; const { origin, pathname } = window.location;
if (state.isLink.includes("{{token}}")) {
state.isLink = state.isLink.replace("{{token}}", cookie.get('token'))
}
if (verifyUrl(<string>state.isLink)) window.open(state.isLink); if (verifyUrl(<string>state.isLink)) window.open(state.isLink);
else window.open(`${origin}${pathname}#${state.isLink}`); else {
function objectToUrlParams(obj: { [key: string]: string | number }): string {
return Object.keys(obj)
.map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
.join('&');
}
window.open(`${origin}${pathname}#${state.isLink}?${objectToUrlParams(state.query)}`)
};
}; };
// 监听路由的变化,设置内容 // 监听路由的变化,设置内容
watch( watch(
@@ -37,6 +49,7 @@ watch(
() => { () => {
state.title = <string>route.meta.title; state.title = <string>route.meta.title;
state.isLink = <string>route.meta.isLink; state.isLink = <string>route.meta.isLink;
state.query = <any>route.query;
}, },
{ {
immediate: true, immediate: true,

View File

@@ -21,13 +21,14 @@ const menuApi = useMenuApi();
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}'); const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}'); const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
const greatDream: any = import.meta.glob('@great-dream/**/*.{vue,tsx}');
/** /**
* 获取目录下的 .vue、.tsx 全部文件 * 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob * @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json * @link 参考https://cn.vitejs.dev/guide/features.html#json
*/ */
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules }); const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules }, { ...greatDream });
/** /**
* 后端控制路由:初始化方法,防止刷新时路由丢失 * 后端控制路由:初始化方法,防止刷新时路由丢失
@@ -198,7 +199,10 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
const keys = Object.keys(dynamicViewsModules); const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => { const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, ''); const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`); const k0 = k.replace("ode_modules/@great-dream/", '')
const k1 = k0.replace("/plugins", '')
const newComponent = component.replace("plugins/", "")
return k1.startsWith(`${newComponent}`) || k1.startsWith(`/${newComponent}`);
}); });
if (matchKeys?.length === 1) { if (matchKeys?.length === 1) {
const matchKey = matchKeys[0]; const matchKey = matchKeys[0];

View File

@@ -53,9 +53,12 @@ export default {
}, },
form: { form: {
afterSubmit(ctx: any) { afterSubmit(ctx: any) {
const {res} = ctx
// 增加crud提示 // 增加crud提示
if (ctx.res.code == 2000) { if (res?.code == 2000) {
successNotification(ctx.res.msg); successNotification(ctx.res.msg);
}else{
return
} }
}, },
}, },

View File

@@ -56,4 +56,5 @@ declare type ParentViewState<T = any> = {
declare type LinkViewState = { declare type LinkViewState = {
title: string; title: string;
isLink: string; isLink: string;
query: any;
}; };

View File

@@ -1,45 +1,65 @@
import {dict} from "@fast-crud/fast-crud"; import {dict} from '@fast-crud/fast-crud';
import {shallowRef} from 'vue' import {shallowRef} from 'vue';
import deptFormat from "/@/components/dept-format/index.vue"; import deptFormat from '/@/components/dept-format/index.vue';
export const commonCrudConfig = (options = { /** 1. 每个字段可选属性 */
create_datetime: { export interface CrudFieldOption {
form: false, form?: boolean;
table: false, table?: boolean;
search: false search?: boolean;
}, width?: number;
update_datetime: { }
form: false,
table: false, /** 2. 总配置接口 */
search: false export interface CrudOptions {
}, create_datetime?: CrudFieldOption;
creator_name: { update_datetime?: CrudFieldOption;
form: false, creator_name?: CrudFieldOption;
table: false, modifier_name?: CrudFieldOption;
search: false dept_belong_id?: CrudFieldOption;
}, description?: CrudFieldOption;
modifier_name: { }
form: false,
table: false, /** 3. 默认完整配置 */
search: false const defaultOptions: Required<CrudOptions> = {
}, create_datetime: { form: false, table: false, search: false, width: 160 },
dept_belong_id: { update_datetime: { form: false, table: false, search: false, width: 160 },
form: false, creator_name: { form: false, table: false, search: false, width: 100 },
table: false, modifier_name: { form: false, table: false, search: false, width: 100 },
search: false dept_belong_id: { form: false, table: false, search: false, width: 300 },
}, description: { form: false, table: false, search: false, width: 100 },
description: { };
form: false,
table: false, /** 4. mergeOptions 函数 */
search: false function mergeOptions(baseOptions: Required<CrudOptions>, userOptions: CrudOptions = {}): Required<CrudOptions> {
}, const result = { ...baseOptions };
}) => { for (const key in userOptions) {
if (Object.prototype.hasOwnProperty.call(userOptions, key)) {
const baseField = result[key as keyof CrudOptions];
const userField = userOptions[key as keyof CrudOptions];
if (baseField && userField) {
result[key as keyof CrudOptions] = { ...baseField, ...userField };
}
}
}
return result;
}
/**
* 最终暴露的 commonCrudConfig
* @param options 用户自定义配置(可传可不传,不传就用默认)
*/
export const commonCrudConfig = (options: CrudOptions = {}) => {
// ① 合并
const merged = mergeOptions(defaultOptions, options);
// ② 用 merged 中的值生成真正的 CRUD 配置
return { return {
dept_belong_id: { dept_belong_id: {
title: '所属部门', title: '所属部门',
type: 'dict-tree', type: 'dict-tree',
search: { search: {
show: options.dept_belong_id?.search || false show: merged.dept_belong_id.search,
}, },
dict: dict({ dict: dict({
url: '/api/system/dept/all_dept/', url: '/api/system/dept/all_dept/',
@@ -50,90 +70,92 @@ export const commonCrudConfig = (options = {
}), }),
column: { column: {
align: 'center', align: 'center',
width: 300, width: merged.dept_belong_id.width,
show: options.dept_belong_id?.table || false, show: merged.dept_belong_id.table,
component: { component: {
name: shallowRef(deptFormat), // fast-crud里自定义组件常用"component.is"
vModel: "modelValue", is: shallowRef(deptFormat),
} vModel: 'modelValue',
},
}, },
form: { form: {
show: options.dept_belong_id?.form || false, show: merged.dept_belong_id.form,
component: { component: {
multiple: false, multiple: false,
clearable: true, clearable: true,
props: { props: {
checkStrictly: true, checkStrictly: true,
props: { props: {
// 为什么这里要写两层props label: 'name',
// 因为props属性名与fs的动态渲染的props命名冲突所以要多写一层 value: 'id',
label: "name", },
value: "id", },
} },
} helper: '默认不填则为当前创建用户的部门ID',
}, },
helper: "默认不填则为当前创建用户的部门ID"
}
}, },
description: { description: {
title: '备注', title: '备注',
search: { search: {
show: options.description?.search || false show: merged.description.search,
}, },
type: 'textarea', type: 'textarea',
column: { column: {
width: 100, width: merged.description.width,
show: options.description?.table || false, show: merged.description.table,
}, },
form: { form: {
show: options.description?.form || false, show: merged.description.form,
component: { component: {
placeholder: '请输入内容', placeholder: '请输入内容',
showWordLimit: true, showWordLimit: true,
maxlength: '200', maxlength: '200',
} },
}, },
viewForm: { viewForm: {
show: true show: true,
}
}, },
},
modifier_name: { modifier_name: {
title: '修改人', title: '修改人',
search: { search: {
show: options.modifier_name?.search || false show: merged.modifier_name.search,
}, },
column: { column: {
width: 100, width: merged.modifier_name.width,
show: options.modifier_name?.table || false, show: merged.modifier_name.table,
}, },
form: { form: {
show: false, show: merged.modifier_name.form,
}, },
viewForm: { viewForm: {
show: true show: true,
}
}, },
},
creator_name: { creator_name: {
title: '创建人', title: '创建人',
search: { search: {
show: options.creator_name?.search || false show: merged.creator_name.search,
}, },
column: { column: {
width: 100, width: merged.creator_name.width,
show: options.creator_name?.table || false, show: merged.creator_name.table,
}, },
form: { form: {
show: false, show: merged.creator_name.form,
}, },
viewForm: { viewForm: {
show: true show: true,
}
}, },
},
update_datetime: { update_datetime: {
title: '更新时间', title: '更新时间',
type: 'datetime', type: 'datetime',
search: { search: {
show: options.update_datetime?.search || false, show: merged.update_datetime.search,
col: { span: 8 }, col: { span: 8 },
component: { component: {
type: 'datetimerange', type: 'datetimerange',
@@ -142,61 +164,64 @@ export const commonCrudConfig = (options = {
'end-placeholder': '结束时间', 'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss', 'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': { 'picker-options': {
shortcuts: [{ shortcuts: [
{
text: '最近一周', text: '最近一周',
onClick(picker) { onClick(picker: any) {
const end = new Date(); const end = new Date();
const start = new Date(); const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]); picker.$emit('pick', [start, end]);
} },
}, { },
{
text: '最近一个月', text: '最近一个月',
onClick(picker) { onClick(picker: any) {
const end = new Date(); const end = new Date();
const start = new Date(); const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]); picker.$emit('pick', [start, end]);
} },
}, { },
{
text: '最近三个月', text: '最近三个月',
onClick(picker) { onClick(picker: any) {
const end = new Date(); const end = new Date();
const start = new Date(); const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]); picker.$emit('pick', [start, end]);
} },
}] },
} ],
} },
},
}, },
valueResolve(context: any) { valueResolve(context: any) {
const {key, value} = context const { value } = context;
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
if (value) { if (value) {
context.form.update_datetime_after = value[0] context.form.update_datetime_after = value[0];
context.form.update_datetime_before = value[1] context.form.update_datetime_before = value[1];
} delete context.form.update_datetime;
// ↑↑↑↑↑ 注意这里是form不是row
} }
}, },
},
column: { column: {
width: 160, width: merged.update_datetime.width,
show: options.update_datetime?.table || false, show: merged.update_datetime.table,
}, },
form: { form: {
show: false, show: merged.update_datetime.form,
}, },
viewForm: { viewForm: {
show: true show: true,
}
}, },
},
create_datetime: { create_datetime: {
title: '创建时间', title: '创建时间',
type: 'datetime', type: 'datetime',
search: { search: {
show: options.create_datetime?.search || false, show: merged.create_datetime.search,
col: { span: 8 }, col: { span: 8 },
component: { component: {
type: 'datetimerange', type: 'datetimerange',
@@ -205,55 +230,57 @@ export const commonCrudConfig = (options = {
'end-placeholder': '结束时间', 'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss', 'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': { 'picker-options': {
shortcuts: [{ shortcuts: [
{
text: '最近一周', text: '最近一周',
onClick(picker) { onClick(picker: any) {
const end = new Date(); const end = new Date();
const start = new Date(); const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]); picker.$emit('pick', [start, end]);
} },
}, { },
{
text: '最近一个月', text: '最近一个月',
onClick(picker) { onClick(picker: any) {
const end = new Date(); const end = new Date();
const start = new Date(); const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]); picker.$emit('pick', [start, end]);
} },
}, { },
{
text: '最近三个月', text: '最近三个月',
onClick(picker) { onClick(picker: any) {
const end = new Date(); const end = new Date();
const start = new Date(); const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]); picker.$emit('pick', [start, end]);
} },
}] },
} ],
} },
},
}, },
valueResolve(context: any) { valueResolve(context: any) {
const {key, value} = context const { value } = context;
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
if (value) { if (value) {
context.form.create_datetime_after = value[0] context.form.create_datetime_after = value[0];
context.form.create_datetime_before = value[1] context.form.create_datetime_before = value[1];
} delete context.form.create_datetime;
// ↑↑↑↑↑ 注意这里是form不是row
} }
}, },
},
column: { column: {
width: 160, width: merged.create_datetime.width,
show: options.create_datetime?.table || false, show: merged.create_datetime.table,
}, },
form: { form: {
show: false show: merged.create_datetime.form,
}, },
viewForm: { viewForm: {
show: true show: true,
} },
} },
} };
} };

57
web/src/utils/cores.tsx Normal file
View File

@@ -0,0 +1,57 @@
import mitt, { Emitter } from 'mitt';
export interface TaskProps {
name: string;
custom?: any;
}
// 定义自定义事件类型
export type BusEvents = {
onNewTask: TaskProps | undefined;
};
export interface Task {
id: number;
handle: string;
data: any;
createTime: Date;
custom?: any;
}
export interface Core {
bus: Emitter<BusEvents>;
// eslint-disable-next-line no-unused-vars
showNotification(body: string, title?: string): Notification | undefined;
taskList: Map<String, Task>;
}
const bus = mitt<BusEvents>();
export function getSystemNotification(body: string, title?: string) {
if (!title) {
title = '通知';
}
return new Notification(title ?? '通知', {
body: body,
});
}
export function showSystemNotification(body: string, title?: string): Notification | undefined {
if (Notification.permission === 'granted') {
return getSystemNotification(body, title);
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
return getSystemNotification(body, title);
}
});
}
return void 0;
}
const taskList = new Map<String, Task>();
export function useCore(): Core {
return {
bus,
showNotification: showSystemNotification,
taskList,
};
}

View File

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

View File

@@ -2,7 +2,7 @@ import { defineAsyncComponent, AsyncComponentLoader } from 'vue';
export let pluginsAll: any = []; export let pluginsAll: any = [];
// 扫描插件目录并注册插件 // 扫描插件目录并注册插件
export const scanAndInstallPlugins = (app: any) => { export const scanAndInstallPlugins = (app: any) => {
const components = import.meta.glob('./**/*.vue'); const components = import.meta.glob('./**/*.ts');
const pluginNames = new Set(); const pluginNames = new Set();
// 遍历对象并注册异步组件 // 遍历对象并注册异步组件
for (const [key, value] of Object.entries(components)) { for (const [key, value] of Object.entries(components)) {
@@ -11,6 +11,24 @@ export const scanAndInstallPlugins = (app: any) => {
const pluginsName = key.match(/\/([^\/]*)\//)?.[1]; const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
pluginNames.add(pluginsName); pluginNames.add(pluginsName);
} }
const dreamComponents = import.meta.glob('/node_modules/@great-dream/**/*.ts');
// 遍历对象并注册异步组件
for (let [key, value] of Object.entries(dreamComponents)) {
key = key.replace('node_modules/@great-dream/', '');
const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.'));
app.component(name, defineAsyncComponent(value as AsyncComponentLoader));
const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
pluginNames.add(pluginsName);
}
pluginsAll = Array.from(pluginNames); pluginsAll = Array.from(pluginNames);
console.log('已发现插件:', pluginsAll); console.log('已发现插件:', pluginsAll);
for (const pluginName of pluginsAll) {
const plugin = import(`./${pluginName}/index.ts`);
plugin.then((module) => {
app.use(module.default)
console.log(`${pluginName}插件已加载`)
}).catch((error) => {
console.log(`${pluginName}插件下无index.ts`)
})
}
}; };

View File

@@ -0,0 +1,122 @@
<template>
<div>
<fs-crud ref="crudRef" v-bind="crudBinding">
</fs-crud>
</div>
</template>
<script setup lang="ts">
import {computed, defineComponent, onMounted, watch} from "vue";
import {CreateCrudOptionsProps, CreateCrudOptionsRet, useFs, AddReq,
compute,
DelReq,
dict,
EditReq,
UserPageQuery,
UserPageRes} from "@fast-crud/fast-crud";
const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
return {
crudOptions: {
mode: {
name: "local",
isMergeWhenUpdate: true,
isAppendWhenAdd: true
},
actionbar: { buttons: { add: { show: true }, addRow: { show: false } } },
editable: {
enabled: true,
mode: "row",
activeDefault:true
},
form:{
wrapper:{
width:"500px"
},
col:{
span:24
},
afterSubmit({mode}){
emit('update:modelValue', crudBinding.value.data);
}
},
toolbar:{
show:false
},
search: {
disabled: true,
show: false
},
pagination: {
show: false
},
columns: {
title: {
title: "标题",
form:{
component:{
placeholder:"请输入标题"
},
rules:[{
required: true,
message: '必须填写',
}]
}
},
key: {
title: "键名",
form:{
component:{
placeholder:"请输入键名"
},
rules:[{
required: true,
message: '必须填写',
}]
}
},
value: {
title: "键值",
form:{
component:{
placeholder:"请输入键值"
},
rules:[{
required: true,
message: '必须填写',
}]
}
}
}
}
};
}
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue'])
//通过导出modelValue, 可以导出成为一个input组件
watch(
() => {
return props.modelValue;
},
(value = []) => {
crudBinding.value.data = value;
},
{
immediate: true
}
);
// 页面打开后获取列表数据
// onMounted(() => {
// crudExpose.doRefresh();
// // crudExpose.setTableData([])
// // crudExpose.editable.enable();
// });
</script>

View File

@@ -175,48 +175,7 @@
</div> </div>
<!-- 数组 --> <!-- 数组 -->
<div v-else-if="item.form_item_type_label === 'array'" :key="index + 10"> <div v-else-if="item.form_item_type_label === 'array'" :key="index + 10">
<vxe-table <crudTable v-model="formData[item.key]"></crudTable>
border
resizable
auto-resize
show-overflow
keep-source
:ref="'xTable_' + item.key"
height="200"
:edit-rules="validRules"
:edit-config="{ trigger: 'click', mode: 'row', showStatus: true }"
>
<vxe-column field="title" title="标题" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.title" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="key" title="键名" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.key" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="value" title="键值" :edit-render="{}">
<template #edit="{ row }">
<vxe-input v-model="row.value" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column title="操作" width="100" show-overflow>
<template #default="{ row, index }">
<el-popover placement="top" width="160" v-model="childRemoveVisible">
<p>删除后无法恢复,确定删除吗?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="childRemoveVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="onRemoveChild(row, index, item.key)">确定</el-button>
</div>
<el-button type="text" slot="reference">删除</el-button>
</el-popover>
</template>
</vxe-column>
</vxe-table>
<div>
<el-button size="mini" @click="onAppend('xTable_' + item.key)">追加</el-button>
</div>
</div> </div>
</el-col> </el-col>
<el-col :span="2" :offset="1"> <el-col :span="2" :offset="1">
@@ -248,32 +207,11 @@ import type { FormInstance, FormRules, TableInstance } from 'element-plus';
import { successMessage, errorMessage } from '/@/utils/message'; import { successMessage, errorMessage } from '/@/utils/message';
import { Session } from '/@/utils/storage'; import { Session } from '/@/utils/storage';
import {Edit,Finished,Delete} from "@element-plus/icons-vue"; import {Edit,Finished,Delete} from "@element-plus/icons-vue";
import crudTable from "./components/crudTable.vue"
const props = defineProps(['options', 'editableTabsItem']); const props = defineProps(['options', 'editableTabsItem']);
let formData: any = reactive({}); let formData: any = ref({});
let formList: any = ref([]); let formList: any = ref([]);
let childTableData = ref([]);
let childRemoveVisible = ref(false);
const validRules = reactive<FormRules>({
title: [
{
required: true,
message: '必须填写',
},
],
key: [
{
required: true,
message: '必须填写',
},
],
value: [
{
required: true,
message: '必须填写',
},
],
});
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
let uploadUrl = ref(getBaseURL() + 'api/system/file/'); let uploadUrl = ref(getBaseURL() + 'api/system/file/');
let uploadHeaders = ref({ let uploadHeaders = ref({
@@ -294,65 +232,27 @@ const getInit = () => {
if (item.value) { if (item.value) {
_formData[key] = item.value; _formData[key] = item.value;
} else { } else {
if ([5, 12, 14].indexOf(item.form_item_type) !== -1) { if ([5, 12,11, 14].indexOf(item.form_item_type) !== -1) {
_formData[key] = []; _formData[key] = item.value || [];
} else { } else {
_formData[key] = item.value; _formData[key] = item.value;
} }
} }
if (item.form_item_type_label === 'array') {
console.log('test');
nextTick(() => {
const tableName = 'xTable_' + key;
const tabelRef = ref<TableInstance>();
console.log(tabelRef);
// const $table = this.$refs[tableName][0];
// $table.loadData(item.chinldern);
});
} }
} formData.value = Object.assign({}, _formData)
formData = Object.assign(formData, _formData)
}); });
}; };
// 提交数据 // 提交数据
const onSubmit = (formEl: FormInstance | undefined) => { const onSubmit = (formEl: FormInstance | undefined) => {
// const form = JSON.parse(JSON.stringify(form)); const keys = Object.keys(formData.value);
const keys = Object.keys(formData); const values = Object.values(formData.value);
const values = Object.values(formData);
for (const index in formList.value) { for (const index in formList.value) {
const item = formList.value[index]; const item = formList.value[index];
// eslint-disable-next-line camelcase
const form_item_type_label = item.form_item_type_label;
// eslint-disable-next-line camelcase
if (form_item_type_label === 'array') {
const parentId = item.id;
const tableName = 'xTable_' + item.key;
// const $table = this.$refs[tableName][0];
// const { tableData } = $table.getTableData();
// for (const child of tableData) {
// if (!child.id && child.key && child.value) {
// child.parent = parentId;
// child.id = null;
// formList.push(child);
// }
// }
// // 必填项的判断
// for (const arr of item.rule) {
// if (arr.required && tableData.length === 0) {
// errorMessage(item.title + '不能为空');
// return;
// }
// }
// item.value = tableData;
}
// 赋值操作 // 赋值操作
keys.map((mapKey, mapIndex) => { keys.forEach((mapKey, mapIndex) => {
if (mapKey === item.key) { if (mapKey === item.key) {
if (item.form_item_type_label !== 'array') {
item.value = values[mapIndex]; item.value = values[mapIndex];
}
// 必填项的验证 // 必填项的验证
if (['img', 'imgs'].indexOf(item.form_item_type_label) > -1) { if (['img', 'imgs'].indexOf(item.form_item_type_label) > -1) {
for (const arr of item.rule) { for (const arr of item.rule) {
@@ -380,39 +280,6 @@ const onSubmit = (formEl: FormInstance | undefined) => {
}); });
}; };
// 追加
const onAppend = (tableName: any) => {
// const $table = this.$refs[tableName][0];
// const { tableData } = $table.getTableData();
// const tableLength = tableData.length;
// if (tableLength === 0) {
// const { row: newRow } = $table.insert();
// console.log(newRow);
// } else {
// const errMap = $table.validate().catch((errMap: any) => errMap);
// if (errMap) {
// errorMessage('校验不通过!');
// } else {
// const { row: newRow } = $table.insert();
// console.log(newRow);
// }
// }
};
// 子表删除
const onRemoveChild = (row: any, index: any, refName: any) => {
console.log(row, index);
if (row.id) {
api.DelObj(row.id).then((res: any) => {
// this.refreshView();
});
} else {
// this.childTableData.splice(index, 1);
// const tableName = 'xTable_' + refName;
// const tableData = this.$refs[tableName][0].remove(row);
// console.log(tableData);
}
};
// 图片预览 // 图片预览
const handlePictureCardPreview = (file: any) => { const handlePictureCardPreview = (file: any) => {

View File

@@ -2,7 +2,8 @@ import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from
import * as api from './api'; import * as api from './api';
import { dictionary } from '/@/utils/dictionary'; import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message'; import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction' import { auth } from '/@/utils/authFunction';
import { getBaseURL } from '/@/utils/baseUrl';
interface CreateCrudOptionsTypes { interface CreateCrudOptionsTypes {
output: any; output: any;
@@ -27,7 +28,6 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
//权限判定 //权限判定
// @ts-ignore
// @ts-ignore // @ts-ignore
return { return {
crudOptions: { crudOptions: {
@@ -72,7 +72,7 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
show: compute(ctx => ctx.row.task_status === 2), show: compute(ctx => ctx.row.task_status === 2),
text: '下载文件', text: '下载文件',
type: 'warning', type: 'warning',
click: (ctx) => window.open(ctx.row.url, '_blank') click: (ctx) => window.open(getBaseURL(ctx.row.url), '_blank')
} }
}, },
}, },

View File

@@ -11,7 +11,7 @@ import {
dict dict
} from '@fast-crud/fast-crud'; } from '@fast-crud/fast-crud';
import fileSelector from '/@/components/fileSelector/index.vue'; import fileSelector from '/@/components/fileSelector/index.vue';
import { shallowRef } from 'vue'; import { getBaseURL } from '/@/utils/baseUrl';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => { const pageRequest = async (query: UserPageQuery) => {
@@ -147,6 +147,11 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
}, },
column: { column: {
minWidth: 360, minWidth: 360,
component: {
async buildUrl(value: any) {
return getBaseURL(value);
}
}
}, },
}, },
md5sum: { md5sum: {
@@ -232,17 +237,19 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
// fileselectortest: { // fileselectortest: {
// title: '文件选择器测试', // title: '文件选择器测试',
// type: 'file-selector', // type: 'file-selector',
// width: 200, // column: {
// minWidth: 200
// },
// form: { // form: {
// component: { // component: {
// name: shallowRef(fileSelector), // name: fileSelector,
// vModel: 'modelValue', // vModel: 'modelValue',
// tabsShow: 0b0100, // tabsShow: 0b1111,
// itemSize: 100, // itemSize: 100,
// multiple: false, // multiple: true,
// selectable: true, // selectable: true,
// showInput: true, // showInput: true,
// inputType: 'video', // inputType: 'image',
// valueKey: 'url', // valueKey: 'url',
// } // }
// } // }

View File

@@ -85,6 +85,7 @@
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="外链接" prop="link_url"> <el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="外链接" prop="link_url">
<el-input v-model="menuFormData.link_url" placeholder="请输入外链接地址" /> <el-input v-model="menuFormData.link_url" placeholder="请输入外链接地址" />
<el-alert :title="`输入{{token}}可自动替换系统 token `" type="info" />
</el-form-item> </el-form-item>
<el-form-item v-if="!menuFormData.is_catalog" label="缓存"> <el-form-item v-if="!menuFormData.is_catalog" label="缓存">

View File

@@ -1,9 +1,39 @@
<template> <template>
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0"> <div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
<div class="menu-form-alert">配置操作功能接口权限配置数据权限点击小齿轮</div> <div class="menu-form-alert">
<div style="display:flex; align-items: center; white-space: nowrap; margin-bottom: 10px;">
<span>默认接口权限:</span>
<el-select
v-model="default_selectBtn.data_range"
@change="defaulthandlePermissionRangeChange"
placeholder="请选择"
style="margin-left: 5px; width: 250px; min-width: 250px;"
>
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-tree-select
v-show="default_selectBtn.data_range === 4"
node-key="id"
v-model="default_selectBtn.dept"
:props="defaultTreeProps"
:data="deptData"
@change="customhandlePermissionRangeChange(default_selectBtn.dept)"
placeholder="请选择自定义部门"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="dialog-tree"
style="margin-left: 15px; width: AUTO; min-width: 250px; margin-top: 0;"
/>
</div>
<span>配置操作功能接口权限配置数据权限点击小齿轮</span>
</div>
<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)"> <el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
<div class="btn-item"> <div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }} {{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range, btn.dept)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)"> <span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
<el-icon> <el-icon>
<Setting /> <Setting />
@@ -48,10 +78,26 @@ import { RoleMenuBtnType } from '../types';
import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api'; import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
import XEUtils from 'xe-utils'; import XEUtils from 'xe-utils';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { Local } from '/@/utils/storage';
const RoleDrawer = RoleDrawerStores(); // 角色-菜单 const RoleDrawer = RoleDrawerStores(); // 角色-菜单
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单 const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮 const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
const dialogVisible = ref(false); const dialogVisible = ref(false);
// 默认按钮
const default_selectBtn = ref<RoleMenuBtnType>({
id: 0,
menu_btn_pre_id: 0,
/** 是否选中 */
isCheck: false,
/** 按钮名称 */
name: '',
/** 数据权限范围 */
data_range: Local.get('role_default_data_range'),
dept: Local.get('role_default_custom_dept'),
});
// 选中的按钮 // 选中的按钮
const selectBtn = ref<RoleMenuBtnType>({ const selectBtn = ref<RoleMenuBtnType>({
id: 0, id: 0,
@@ -83,6 +129,29 @@ const defaultTreeProps = {
value: 'id', value: 'id',
}; };
/**
* 默认数据权限下拉选择事件
* 保留数据到cache
*/
const defaulthandlePermissionRangeChange = async (val: number) => {
if (val < 4) {
// default_selectBtn.value.dept = [];
// Local.set('role_default_custom_dept', []);
}
default_selectBtn.value.data_range = val;
Local.set('role_default_data_range', val);
};
/**
* 默认部门下拉选择事件
* 保留数据到cache
*/
const customhandlePermissionRangeChange = async (dept: Array<number>) => {
default_selectBtn.value.dept = dept;
Local.set('role_default_custom_dept', dept);
};
/** /**
* 自定数据权限下拉选择事件 * 自定数据权限下拉选择事件
*/ */
@@ -95,12 +164,21 @@ const handlePermissionRangeChange = async (val: number) => {
* 格式化按钮数据范围 * 格式化按钮数据范围
*/ */
const formatDataRange = computed(() => { const formatDataRange = computed(() => {
return function (datarange: number) { return function (datarange: number, dept: Array<number>) {
const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => { const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
if (item.value === datarange) { if (item.value === datarange) {
return item.label; return item.label;
} }
}); });
// 数据权限与默认数据权限一致
if (datarange === default_selectBtn.value.data_range) {
// 判断选择的部门是否一致
if (datarange !== 4 || JSON.stringify(dept) === JSON.stringify(default_selectBtn.value.dept)) {
return "默认接口权限"
}
}
// datarange === 4 选择的部门不一致返回datarangeitem.label
return datarangeitem.label; return datarangeitem.label;
}; };
}); });
@@ -108,11 +186,14 @@ const formatDataRange = computed(() => {
* 勾选按钮 * 勾选按钮
*/ */
const handleCheckChange = async (btn: RoleMenuBtnType) => { const handleCheckChange = async (btn: RoleMenuBtnType) => {
selectBtn.value = default_selectBtn.value;
const put_data = { const put_data = {
isCheck: btn.isCheck, isCheck: btn.isCheck,
roleId: RoleDrawer.roleId, roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id, menuId: RoleMenuTree.id,
btnId: btn.id, btnId: btn.id,
data_range: default_selectBtn.value.data_range,
dept: default_selectBtn.value.dept,
}; };
const { data, msg } = await setRoleMenuBtn(put_data); const { data, msg } = await setRoleMenuBtn(put_data);
RoleMenuBtn.updateState(data); RoleMenuBtn.updateState(data);
@@ -168,9 +249,10 @@ onMounted(async () => {
background-color: var(--el-color-primary); background-color: var(--el-color-primary);
} }
} }
// .el-checkbox {
// width: 200px; .el-checkbox {
// } width: 20%;
}
.btn-item { .btn-item {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -0,0 +1,30 @@
import { request } from '/@/utils/service';
import { UserPageQuery} from '@fast-crud/fast-crud';
/**
* 当前角色查询未授权的用户
* @param role_id 角色id
* @param query 查询条件 需要有角色id
* @returns
*/
export function getRoleUsersUnauthorized(query: UserPageQuery) {
query["authorized"] = 0; // 未授权的用户
return request({
url: '/api/system/role/get_role_users/',
method: 'get',
params: query,
});
}
/**
* 当前用户角色添加用户
* @param role_id 角色id
* @param users_id 用户id数组
* @returns
*/
export function addRoleUsers(role_id: number, users_id: Array<Number>) {
return request({
url: `/api/system/role/${role_id}/add_role_users/`,
method: 'post',
data: {users_id: users_id},
});
}

View File

@@ -0,0 +1,184 @@
import {getRoleUsersUnauthorized} from './api';
import {
compute,
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import { ref , nextTick} from 'vue';
import XEUtils from 'xe-utils';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await getRoleUsersUnauthorized(query);
};
const editRequest = async ({ form, row }: EditReq) => {
return undefined;
};
const delRequest = async ({ row }: DelReq) => {
return undefined;
};
const addRequest = async ({ form }: AddReq) => {
return undefined;
};
// 记录选中的行
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,
addRequest,
editRequest,
delRequest,
},
actionbar: {
show: false,
buttons: {
add: {
show: false,
},
},
},
rowHandle: {
show: false,
//固定右侧
fixed: 'left',
width: 150,
buttons: {
view: {
show: false,
},
edit: {
show: false,
},
remove: {
show: false,
},
},
},
table: {
rowKey: "id",
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
columns: {
$checked: {
title: "选择",
form: { show: false},
column: {
show: true,
type: "selection",
align: "center",
width: "55px",
columnSetDisabled: true, //禁止在列设置中选择
}
},
_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;
},
},
},
name: {
title: '用户名',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'text',
form: {
show: false,
},
},
dept: {
title: '部门',
show: true,
type: 'dict-tree',
column: {
name: 'text',
formatter({value,row,index}){
return row.dept__name
}
},
search: {
show: true,
disabled: true,
col:{span: 6},
component: {
multiple: false,
props: {
checkStrictly: true,
clearable: true,
filterable: true,
},
},
},
form: {
show: false
},
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name'
}),
},
},
},
};
};

View File

@@ -0,0 +1,91 @@
<template>
<el-dialog v-model="dialog" title="添加授权用户" direction="rtl" destroy-on-close :before-close="handleDialogClose">
<div style="height: 500px;" >
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-right>
<el-popover placement="top" :width="200" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small" :max-height="500">
<!-- <el-table-column width="100" property="id" label="id" /> -->
<el-table-column width="100" property="name" label="用户名" />
<el-table-column fixed="right" label="操作" min-width="50">
<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>
<template #footer>
<div>
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
<el-button @click="handleDialogClose"> 取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { successNotification } from '/@/utils/message';
import { addRoleUsers } from './api';
import { Close } from '@element-plus/icons-vue';
import XEUtils from 'xe-utils';
const props = defineProps({
refreshCallback: {
type: Function,
required: true,
},
});
//对话框是否显示
const dialog = ref(false);
// 父组件刷新回调函数
const parentRefreshCallbackFunc = props.refreshCallback;
//抽屉关闭确认
const handleDialogClose = () => {
dialog.value = false;
selectedRows.value = [];
};
const handleDialogConfirm = async () => {
if (selectedRows.value.length === 0) {
return;
}
await addRoleUsers(crudRef.value.getSearchFormData().role_id, XEUtils.pluck(selectedRows.value, 'id')).then(res => {
successNotification(res.msg);
})
parentRefreshCallbackFunc && parentRefreshCallbackFunc(); // 刷新父组件
handleDialogClose();
};
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, context: {} });
const { setSearchFormData, doRefresh } = crudExpose;
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
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);
}
};
defineExpose({ dialog, setSearchFormData, doRefresh, parentRefreshCallbackFunc});
</script>

View File

@@ -0,0 +1,44 @@
import { request } from '/@/utils/service';
import { UserPageQuery} from '@fast-crud/fast-crud';
/**
* 当前角色查询授权的用户
* @param query 查询条件 需要有角色id
* @returns
*/
export function getRoleUsersAuthorized(query: UserPageQuery) {
query["authorized"] = 1; // 授权的用户
return request({
url: '/api/system/role/get_role_users/',
method: 'get',
params: query,
});
}
/**
* 当前角色删除授权的用户
* @param role_id 角色id
* @param user_id 用户id数组
* @returns
*/
export function removeRoleUser(role_id: number, user_id: Array<number>) {
return request({
url: `/api/system/role/${role_id}/remove_role_user/`,
method: 'delete',
data: {user_id: user_id},
});
}
/**
* 当前用户角色添加用户
* @param role_id 角色id
* @param data 用户id数组
* @returns
*/
export function addRoleUsers(role_id: number, data: Array<Number>) {
return request({
url: `/api/system/role/${role_id}/add_role_users/`,
method: 'post',
data: {users_id: data},
});
}

View File

@@ -0,0 +1,193 @@
import {getRoleUsersAuthorized, removeRoleUser} from './api';
import {
compute,
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import {auth} from "/@/utils/authFunction";
import { ref , nextTick} from 'vue';
import XEUtils from 'xe-utils';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await getRoleUsersAuthorized(query);
};
const editRequest = async ({ form, row }: EditReq) => {
return undefined;
};
const delRequest = async ({ row }: DelReq) => {
return await removeRoleUser(crudExpose.crudRef.value.getSearchFormData().role_id, [row.id]);
};
const addRequest = async ({ form }: AddReq) => {
return undefined;
};
// 记录选中的行
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,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('role:AuthorizedAdd'),
click: (ctx: any) => {
context!.subUserRef.value.dialog = true;
nextTick(() => {
context!.subUserRef.value.setSearchFormData({ form: { role_id: crudExpose.crudRef.value.getSearchFormData().role_id } });
context!.subUserRef.value.doRefresh();
});
},
},
},
},
rowHandle: {
//固定右侧
fixed: 'left',
width: 120,
show: auth('role:AuthorizedDel'),
buttons: {
view: {
show: false,
},
edit: {
show: false,
},
remove: {
iconRight: 'Delete',
show: true,
},
},
},
table: {
rowKey: "id",
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
columns: {
$checked: {
title: "选择",
form: { show: false},
column: {
show: auth('role:AuthorizedDel'),
type: "selection",
align: "center",
width: "55px",
columnSetDisabled: true, //禁止在列设置中选择
}
},
_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;
},
},
},
name: {
title: '用户名',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'text',
form: {
show: false,
},
},
dept: {
title: '部门',
show: true,
type: 'dict-tree',
column: {
name: 'text',
formatter({value,row,index}){
return row.dept__name
}
},
search: {
show: true,
disabled: true,
col:{span: 6},
component: {
multiple: false,
props: {
checkStrictly: true,
clearable: true,
filterable: true,
},
},
},
form: {
show: false
},
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name'
}),
},
},
},
};
};

View File

@@ -0,0 +1,98 @@
<template>
<el-drawer size="70%" v-model="RoleUserDrawer.drawerVisible" direction="rtl" destroy-on-close :before-close="handleClose">
<template #header>
<div>
当前授权角色
<el-tag>{{ RoleUserDrawer.role_name }}</el-tag>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-right>
<el-popover placement="top" :width="200" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small" :max-height="500" >
<!-- <el-table-column width="100" property="id" label="id" /> -->
<el-table-column width="100" property="name" label="用户名" />
<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>
<template #pagination-left>
<el-tooltip content="批量删除所选择的用户权限">
<el-button v-show="selectedRowsCount > 0 && auth('role:AuthorizedDel')" type="danger" @click="multipleDel" :icon="Delete">批量删除</el-button>
</el-tooltip>
</template>
</fs-crud>
<subUser ref="subUserRef" :refreshCallback="refreshData"> </subUser>
</el-drawer>
</template>
<script lang="ts" setup>
import {auth} from "/@/utils/authFunction";
import { ref, onMounted, defineAsyncComponent, computed } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { Close, Delete } from '@element-plus/icons-vue';
import XEUtils from 'xe-utils';
import {removeRoleUser} from "./api"
import { ElMessageBox } from 'element-plus';
import { errorMessage, successNotification } from '/@/utils/message';
import { RoleUserStores } from '../../stores/RoleUserStores';
const RoleUserDrawer = RoleUserStores(); // 授权用户抽屉参数
const subUser = defineAsyncComponent(() => import('../addUsers/index.vue'));
const subUserRef = ref();
const refreshData = () => {
crudExpose.doRefresh();
};
//抽屉是否显示
const drawer = ref(false);
//抽屉关闭确认
const handleClose = (done: () => void) => {
selectedRows.value = [];
done();
};
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
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 multipleDel = async () => {
if (selectedRows.value.length < 1) {
errorMessage("请先勾选用户");
return
}
await ElMessageBox.confirm(`确定要删除这 “${selectedRows.value.length}” 位用户的权限吗`, "确认");
const req = await removeRoleUser(crudRef.value.getSearchFormData().role_id, XEUtils.pluck(selectedRows.value, 'id'));
selectedRows.value = [];
successNotification(req.msg)
crudExpose.doRefresh()
}
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, context: {subUserRef} });
const { setSearchFormData, doRefresh } = crudExpose;
defineExpose({ drawer, setSearchFormData, doRefresh });
</script>

View File

@@ -3,6 +3,7 @@ import * as api from './api';
import { dictionary } from '/@/utils/dictionary'; import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message'; import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction'; import { auth } from '/@/utils/authFunction';
import { nextTick, computed } from 'vue';
/** /**
* *
@@ -46,7 +47,12 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
rowHandle: { rowHandle: {
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
width: 320, width: computed(() => {
if (auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch')){
return 420;
}
return 320;
}),
buttons: { buttons: {
view: { view: {
show: true, show: true,
@@ -57,6 +63,19 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
remove: { remove: {
show: auth('role:Delete'), show: auth('role:Delete'),
}, },
assignment: {
type: 'primary',
text: '授权用户',
show: auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch'),
click: (ctx: any) => {
const { row } = ctx;
context!.RoleUserDrawer.handleDrawerOpen(row);
nextTick(() => {
context!.RoleUserRef.value.setSearchFormData({ form: { role_id: row.id } });
context!.RoleUserRef.value.doRefresh();
});
},
},
permission: { permission: {
type: 'primary', type: 'primary',
text: '权限配置', text: '权限配置',

View File

@@ -2,17 +2,22 @@
<fs-page> <fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud> <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<PermissionDrawerCom /> <PermissionDrawerCom />
<RoleUser ref="RoleUserRef" />
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup name="role"> <script lang="ts" setup name="role">
import { defineAsyncComponent, onMounted } from 'vue'; import { defineAsyncComponent, onMounted, ref} from 'vue';
import { useFs } from '@fast-crud/fast-crud'; import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud'; import { createCrudOptions } from './crud';
import { RoleDrawerStores } from './stores/RoleDrawerStores'; import { RoleDrawerStores } from './stores/RoleDrawerStores';
import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores'; import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores';
import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores'; import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores';
import { RoleUsersStores } from './stores/RoleUsersStores'; import { RoleUsersStores } from './stores/RoleUsersStores';
import { RoleUserStores } from './stores/RoleUserStores';
const RoleUser = defineAsyncComponent(() => import('./components/searchUsers/index.vue'));
const RoleUserRef = ref();
const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue')); const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue'));
@@ -20,9 +25,11 @@ const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单 const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单
const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段 const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段
const RoleUsers = RoleUsersStores();// 角色-用户 const RoleUsers = RoleUsersStores();// 角色-用户
const RoleUserDrawer = RoleUserStores(); // 授权用户抽屉参数
const { crudBinding, crudRef, crudExpose } = useFs({ const { crudBinding, crudRef, crudExpose } = useFs({
createCrudOptions, createCrudOptions,
context: { RoleDrawer, RoleMenuBtn, RoleMenuField }, context: { RoleDrawer, RoleMenuBtn, RoleMenuField, RoleUserDrawer, RoleUserRef },
}); });
// 页面打开后获取列表数据 // 页面打开后获取列表数据

View File

@@ -0,0 +1,29 @@
import { defineStore } from 'pinia';
/**
* 权限抽屉:角色-用户
*/
export const RoleUserStores = defineStore('RoleUserStores', {
state: (): any => ({
drawerVisible: false,
role_id: undefined,
role_name: undefined,
}),
actions: {
/**
* 打开权限修改抽屉
*/
handleDrawerOpen(row: any) {
this.drawerVisible = true;
this.role_name = row.name;
this.role_id = row.id;
},
/**
* 关闭权限修改抽屉
*/
handleDrawerClose() {
this.drawerVisible = false;
},
},
});

View File

@@ -0,0 +1,50 @@
import { request,downloadFile } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/VIEWSETNAME/';
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 },
});
}
export function exportData(params:any){
return downloadFile({
url: apiPrefix + 'export_data/',
params: params,
method: 'get'
})
}

View File

@@ -0,0 +1,86 @@
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, UserPageQuery, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from './api';
import { request } from '/@/utils/service';
import { auth } from "/@/utils/authFunction";
//此处为crudOptions配置
export default function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsRet {
const pageRequest = async (query: any) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
if (row.id) {
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 exportRequest = async (query: UserPageQuery) => {
return await api.exportData(query)
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
export: {
// 注释编号:django-vue3-admin-crud210716:注意这个auth里面的值最好是使用index.vue文件里面的name值并加上请求动作的单词
show: auth('VIEWSETNAME:Export'),
text: "导出",//按钮文字
title: "导出",//鼠标停留显示的信息
click() {
return exportRequest(crudExpose.getSearchFormData())
// return exportRequest(crudExpose!.getSearchFormData()) // 注意这个crudExpose!.getSearchFormData(),一些低版本的环境是需要添加!的
}
},
add: {
show: auth('VIEWSETNAME:Create'),
},
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
type: 'text',
order: 1,
show: auth('VIEWSETNAME:Retrieve')
},
edit: {
type: 'text',
order: 2,
show: auth('VIEWSETNAME:Update')
},
copy: {
type: 'text',
order: 3,
show: auth('VIEWSETNAME:Copy')
},
remove: {
type: 'text',
order: 4,
show: auth('VIEWSETNAME:Delete')
},
},
},
columns: {
// COLUMNS_CONFIG
},
},
};
}

View File

@@ -0,0 +1,56 @@
<template>
<fs-page class="PageFeatureSearchMulti">
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #cell_url="scope">
<el-tag size="small">{{ scope.row.url }}</el-tag>
</template>
<!-- 注释编号: django-vue3-admin-index442216: -->
<!-- 注释编号:django-vue3-admin-index39263917:代码开始行-->
<!-- 功能说明:使用导入组件并且修改api地址为当前对应的api当前是demo的api="api/CarModelViewSet/"-->
<template #actionbar-right>
<importExcel api="api/VIEWSETNAME/" v-auth="'user:Import'">导入</importExcel>
</template>
<!-- 注释编号:django-vue3-admin-index263917:代码结束行-->
</fs-crud>
</fs-page>
</template>
<script lang="ts">
import { onMounted, getCurrentInstance, defineComponent} from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import createCrudOptions from './crud';
// 注释编号: django-vue3-admin-index192316:导入组件
import importExcel from '/@/components/importExcel/index.vue'
export default defineComponent({ //这里配置defineComponent
name: "VIEWSETNAME", //把name放在这里进行配置了
components: {importExcel}, //注释编号: django-vue3-admin-index552416: 注册组件把importExcel组件放在这里这样<template></template>中才能正确的引用到组件
setup() { //这里配置了setup()
const instance = getCurrentInstance();
const context: any = {
componentName: instance?.type.name
};
const { crudBinding, crudRef, crudExpose, resetCrudOptions } = useFs({ createCrudOptions, context});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
return {
//增加了return把需要给上面<template>内调用的<fs-crud ref="crudRef" v-bind="crudBinding">
crudBinding,
crudRef,
};
} //这里关闭setup()
}); //关闭defineComponent
</script>

View File

@@ -11,6 +11,7 @@ const pathResolve = (dir: string) => {
const alias: Record<string, string> = { const alias: Record<string, string> = {
'/@': pathResolve('./src/'), '/@': pathResolve('./src/'),
'@great-dream': pathResolve('./node_modules/@great-dream/'),
'@views': pathResolve('./src/views'), '@views': pathResolve('./src/views'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
'@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/') '@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/')