Accept Merge Request #20: (develop -> master)

Merge Request: 新功能

Created By: @dvadmin-开发-李强
Accepted By: @dvadmin-开发-李强
URL: https://dvadmin-private.coding.net/p/code/d/dvadmin3/git/merge/20?initial=true
This commit is contained in:
dvadmin-开发-李强
2024-06-21 08:08:05 +08:00
committed by Coding
11 changed files with 485 additions and 397 deletions

View File

@@ -1,214 +1,214 @@
# Django-Vue3-Admin # Django-Vue3-Admin
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin) [![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin) [预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
💡 **「关于」** 💡 **「关于」**
我们是一群热爱代码的青年在这个炙热的时代下我们希望静下心来通过Code带来一点我们的色彩和颜色。 我们是一群热爱代码的青年在这个炙热的时代下我们希望静下心来通过Code带来一点我们的色彩和颜色。
因为热爱,所以拥抱未来! 因为热爱,所以拥抱未来!
## 平台简介 ## 平台简介
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。 💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。
django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus
* 🧑‍🤝‍🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)) * 🧑‍🤝‍🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/))
* 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。 * 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。 * 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
* 👬支持加载动态权限菜单,多方式轻松权限控制。 * 👬支持加载动态权限菜单,多方式轻松权限控制。
* 👬全新的列权限管控,粒度细化到每一列。 * 👬全新的列权限管控,粒度细化到每一列。
* 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。 * 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。
* 💡特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。 * 💡特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。
#### 🏭 环境支持 #### 🏭 环境支持
| Edge | Firefox | Chrome | Safari | | Edge | Firefox | Chrome | Safari |
| --------- | ------------ | ----------- | ----------- | | --------- | ------------ | ----------- | ----------- |
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 | | Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。 > 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
## 在线体验 ## 在线体验
👩‍👧‍👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com) 👩‍👧‍👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
- 账号superadmin - 账号superadmin
- 密码admin123456 - 密码admin123456
👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2) 👩‍👦‍👦文档地址:[DVAdmin官网](https://www.django-vue-admin.com)
## 交流 ## 交流
- 交流社区:[戳我](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)
- 二维码 - 二维码
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'> <img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
## 源码地址 ## 源码地址
gitee地址(主推)[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩‍👦‍👦 gitee地址(主推)[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩‍👦‍👦
github地址[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩‍👦‍👦 github地址[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩‍👦‍👦
## 内置功能 ## 内置功能
1. 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。 1. 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
2. 🧑‍⚕️部门管理:配置系统组织机构(公司、部门、角色)。 2. 🧑‍⚕️部门管理:配置系统组织机构(公司、部门、角色)。
3. 👩‍⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。 3. 👩‍⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
4. 🧑‍🎓按钮权限控制:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。 4. 🧑‍🎓按钮权限控制:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
5. 🧑‍🎓字段列权限控制:授权页面的字段显示权限,具体到某一列的显示权限。 5. 🧑‍🎓字段列权限控制:授权页面的字段显示权限,具体到某一列的显示权限。
7. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。 7. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
8. 👬接口白名单:配置不需要进行权限校验的接口。 8. 👬接口白名单:配置不需要进行权限校验的接口。
9. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。 9. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
10. 🧑‍🔧地区管理:对省市县区域进行管理。 10. 🧑‍🔧地区管理:对省市县区域进行管理。
11. 📁附件管理:对平台上所有文件、图片等进行统一管理。 11. 📁附件管理:对平台上所有文件、图片等进行统一管理。
12. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 12. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。 13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌 ## 插件市场 🔌
更新中... 更新中...
## 仓库分支说明 💈 ## 仓库分支说明 💈
主分支master稳定版本 主分支master稳定版本
开发分支develop 开发分支develop
## 准备工作 ## 准备工作
~~~ ~~~
Python >= 3.11.0 (最低3.9+版本) Python >= 3.11.0 (最低3.9+版本)
nodejs >= 16.0 nodejs >= 16.0
Mysql >= 8.0 (可选默认数据库sqlite3支持5.7+推荐8.0版本) Mysql >= 8.0 (可选默认数据库sqlite3支持5.7+推荐8.0版本)
Redis (可选,最新版) Redis (可选,最新版)
~~~ ~~~
## 前端♝ ## 前端♝
```bash ```bash
# 克隆项目 # 克隆项目
git clone https://gitee.com/huge-dream/django-vue3-admin.git git clone https://gitee.com/huge-dream/django-vue3-admin.git
# 进入项目目录 # 进入项目目录
cd web cd web
# 安装依赖 # 安装依赖
npm install yarn npm install yarn
yarn install --registry=https://registry.npm.taobao.org yarn install --registry=https://registry.npm.taobao.org
# 启动服务 # 启动服务
yarn build yarn build
# 浏览器访问 http://localhost:8080 # 浏览器访问 http://localhost:8080
# .env.development 文件中可配置启动端口等参数 # .env.development 文件中可配置启动端口等参数
# 构建生产环境 # 构建生产环境
# yarn run build # yarn run build
``` ```
## 后端💈 ## 后端💈
~~~bash ~~~bash
1. 进入项目目录 cd backend 1. 进入项目目录 cd backend
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py 2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
3. 在 env.py 中配置数据库信息 3. 在 env.py 中配置数据库信息
mysql数据库版本建议8.0 mysql数据库版本建议8.0
mysql数据库字符集utf8mb4 mysql数据库字符集utf8mb4
4. 安装依赖环境 4. 安装依赖环境
pip3 install -r requirements.txt pip3 install -r requirements.txt
5. 执行迁移命令: 5. 执行迁移命令:
python3 manage.py makemigrations python3 manage.py makemigrations
python3 manage.py migrate python3 manage.py migrate
6. 初始化数据 6. 初始化数据
python3 manage.py init python3 manage.py init
7. 初始化省市县数据: 7. 初始化省市县数据:
python3 manage.py init_area python3 manage.py init_area
8. 启动项目 8. 启动项目
python3 manage.py runserver 0.0.0.0:8000 python3 manage.py runserver 0.0.0.0:8000
或使用 uvicorn : 或使用 uvicorn :
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8 uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
~~~ ~~~
## 开发建议 ## 开发建议
前后端backend与web各自单独一个窗口打开进行开发 前后端backend与web各自单独一个窗口打开进行开发
### 访问项目 ### 访问项目
- 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件) - 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
- 账号:`superadmin` 密码:`admin123456` - 账号:`superadmin` 密码:`admin123456`
### docker-compose 运行 ### docker-compose 运行
~~~shell ~~~shell
# 先安装docker-compose (自行百度安装),执行此命令等待安装如有使用celery插件请打开docker-compose.yml中celery 部分注释 # 先安装docker-compose (自行百度安装),执行此命令等待安装如有使用celery插件请打开docker-compose.yml中celery 部分注释
docker-compose up -d docker-compose up -d
# 初始化后端数据(第一次执行即可) # 初始化后端数据(第一次执行即可)
docker exec -ti dvadmin3-django bash docker exec -ti dvadmin3-django bash
python manage.py makemigrations python manage.py makemigrations
python manage.py migrate python manage.py migrate
python manage.py init_area python manage.py init_area
python manage.py init python manage.py init
exit exit
前端地址http://127.0.0.1:8080 前端地址http://127.0.0.1:8080
后端地址http://127.0.0.1:8080/api 后端地址http://127.0.0.1:8080/api
# 在服务器上请把127.0.0.1 换成自己公网ip # 在服务器上请把127.0.0.1 换成自己公网ip
账号superadmin 密码admin123456 账号superadmin 密码admin123456
# docker-compose 停止 # docker-compose 停止
docker-compose down docker-compose down
# docker-compose 重启 # docker-compose 重启
docker-compose restart docker-compose restart
# docker-compose 启动时重新进行 build # docker-compose 启动时重新进行 build
docker-compose up -d --build docker-compose up -d --build
~~~ ~~~
## 演示图✅ ## 演示图✅
![image-01](https://foruda.gitee.com/images/1701348994587355489/1bc749e7_5074988.png) ![image-01](https://foruda.gitee.com/images/1701348994587355489/1bc749e7_5074988.png)
![image-02](https://foruda.gitee.com/images/1701349037811908960/80d361db_5074988.png) ![image-02](https://foruda.gitee.com/images/1701349037811908960/80d361db_5074988.png)
![image-03](https://foruda.gitee.com/images/1701349224478845203/954f0a7b_5074988.png) ![image-03](https://foruda.gitee.com/images/1701349224478845203/954f0a7b_5074988.png)
![image-04](https://foruda.gitee.com/images/1701349248928658877/64926724_5074988.png) ![image-04](https://foruda.gitee.com/images/1701349248928658877/64926724_5074988.png)
![image-05](https://foruda.gitee.com/images/1701349259068943299/1306ba40_5074988.png) ![image-05](https://foruda.gitee.com/images/1701349259068943299/1306ba40_5074988.png)
![image-06](https://foruda.gitee.com/images/1701349294894429495/e3b3a8cf_5074988.png) ![image-06](https://foruda.gitee.com/images/1701349294894429495/e3b3a8cf_5074988.png)
![image-07](https://foruda.gitee.com/images/1701350432536247561/3b26685e_5074988.png) ![image-07](https://foruda.gitee.com/images/1701350432536247561/3b26685e_5074988.png)
![image-08](https://foruda.gitee.com/images/1701350455264771992/b364c57f_5074988.png) ![image-08](https://foruda.gitee.com/images/1701350455264771992/b364c57f_5074988.png)
![image-09](https://foruda.gitee.com/images/1701350479266000753/e4e4f7c5_5074988.png) ![image-09](https://foruda.gitee.com/images/1701350479266000753/e4e4f7c5_5074988.png)
![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png) ![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png)

View File

@@ -80,4 +80,29 @@ class MenuButtonViewSet(CustomModelViewSet):
else: else:
role_id = request.user.role.values_list('id', flat=True) role_id = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values_list('menu_button__value',flat=True).distinct() queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values_list('menu_button__value',flat=True).distinct()
return DetailResponse(data=queryset) return DetailResponse(data=queryset)
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
def batch_create(self, request, *args, **kwargs):
"""
批量创建菜单“增删改查查”权限
创建的数据来源于菜单,需要规范创建菜单参数
value菜单的component_name:method
api:菜单的web_path增加'/api'前缀并根据method增加{id}
"""
menu_obj = Menu.objects.filter(id=request.data['menu']).first()
result_list = [
{'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api{menu_obj.web_path}/',
'method': 1},
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api{menu_obj.web_path}/{{id}}/',
'method': 3},
{'menu': menu_obj.id, 'name': '修改', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api{menu_obj.web_path}/{{id}}/',
'method': 2},
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api{menu_obj.web_path}/',
'method': 0},
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api{menu_obj.web_path}/{{id}}/',
'method': 0}]
serializer = self.get_serializer(data=result_list, many=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return SuccessResponse(serializer.data, msg="批量创建成功")

View File

@@ -23,13 +23,13 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
""" """
菜单按钮-序列化器 菜单按钮-序列化器
""" """
class Meta: class Meta:
model = RoleMenuButtonPermission model = RoleMenuButtonPermission
fields = "__all__" fields = "__all__"
read_only_fields = ["id"] read_only_fields = ["id"]
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer): class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
""" """
初始化菜单按钮-序列化器 初始化菜单按钮-序列化器
@@ -67,17 +67,17 @@ class RoleButtonPermissionSerializer(CustomModelSerializer):
return None return None
return obj.data_range return obj.data_range
class Meta: class Meta:
model = MenuButton model = MenuButton
fields = ['id','name','value','isCheck','data_range'] fields = ['id', 'name', 'value', 'isCheck', 'data_range']
class RoleFieldPermissionSerializer(CustomModelSerializer): class RoleFieldPermissionSerializer(CustomModelSerializer):
class Meta: class Meta:
model = FieldPermission model = FieldPermission
fields = "__all__" fields = "__all__"
class RoleMenuFieldSerializer(CustomModelSerializer): class RoleMenuFieldSerializer(CustomModelSerializer):
is_query = serializers.SerializerMethodField() is_query = serializers.SerializerMethodField()
is_create = serializers.SerializerMethodField() is_create = serializers.SerializerMethodField()
@@ -103,24 +103,38 @@ class RoleMenuFieldSerializer(CustomModelSerializer):
if queryset: if queryset:
return queryset.is_update return queryset.is_update
return False return False
class Meta: class Meta:
model = MenuField model = MenuField
fields = ['id','field_name','title','is_query','is_create','is_update'] fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update']
class RoleMenuSerializer(CustomModelSerializer):
menus = serializers.SerializerMethodField()
def get_menus(self, instance):
menu_list = Menu.objects.filter(parent=instance['id']).values('id', 'name')
serializer = RoleMenuPermissionSerializer(menu_list, many=True, request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id', 'name', 'menus']
class RoleMenuPermissionSerializer(CustomModelSerializer): class RoleMenuPermissionSerializer(CustomModelSerializer):
""" """
菜单和按钮权限 菜单和按钮权限
""" """
name = serializers.SerializerMethodField() # name = serializers.SerializerMethodField()
isCheck = serializers.SerializerMethodField() isCheck = serializers.SerializerMethodField()
btns = serializers.SerializerMethodField() btns = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField() columns = serializers.SerializerMethodField()
def get_name(self, instance): # def get_name(self, instance):
parent_list = Menu.get_all_parent(instance['id']) # parent_list = Menu.get_all_parent(instance['id'])
names = [d["name"] for d in parent_list] # names = [d["name"] for d in parent_list]
return "/".join(names) # return "/".join(names)
def get_isCheck(self, instance): def get_isCheck(self, instance):
params = self.request.query_params params = self.request.query_params
return RoleMenuPermission.objects.filter( return RoleMenuPermission.objects.filter(
@@ -130,19 +144,18 @@ class RoleMenuPermissionSerializer(CustomModelSerializer):
def get_btns(self, instance): def get_btns(self, instance):
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value') btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
serializer = RoleButtonPermissionSerializer(btn_list,many=True,request=self.request) serializer = RoleButtonPermissionSerializer(btn_list, many=True, request=self.request)
return serializer.data return serializer.data
def get_columns(self, instance): def get_columns(self, instance):
col_list = MenuField.objects.filter(menu=instance['id']) col_list = MenuField.objects.filter(menu=instance['id'])
serializer = RoleMenuFieldSerializer(col_list,many=True,request=self.request) serializer = RoleMenuFieldSerializer(col_list, many=True, request=self.request)
return serializer.data return serializer.data
class Meta: class Meta:
model = Menu model = Menu
fields = ['id','name','isCheck','btns','columns'] fields = ['id', 'name', 'isCheck', 'btns', 'columns']
class RoleMenuButtonPermissionViewSet(CustomModelViewSet): class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
""" """
@@ -167,54 +180,54 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
:return: menu,btns,columns :return: menu,btns,columns
""" """
params = request.query_params params = request.query_params
role = params.get('role',None) role = params.get('role', None)
if role is None: if role is None:
return ErrorResponse(msg="未获取到角色信息") return ErrorResponse(msg="未获取到角色信息")
is_superuser = request.user.is_superuser is_superuser = request.user.is_superuser
# if is_superuser:
# queryset = Menu.objects.filter(status=1,is_catalog=False).values('name', 'id').all()
# else:
# role_id = request.user.role.values_list('id', flat=True)
# menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True)
# queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id').all()
# serializer = RoleMenuPermissionSerializer(queryset,many=True,request=request)
# data = serializer.data
# return DetailResponse(data=data)
data = []
if is_superuser: if is_superuser:
queryset = Menu.objects.filter(status=1,is_catalog=False).values('name', 'id').all() queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all()
else: else:
role_id = request.user.role.values_list('id', flat=True) role_id = request.user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True) menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True)
queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id') queryset = Menu.objects.filter(status=1, is_catalog=True, id__in=menu_list).values('name', 'id').all()
for item in queryset: serializer = RoleMenuSerializer(queryset, many=True, request=request)
parent_list = Menu.get_all_parent(item['id']) data = serializer.data
names = [d["name"] for d in parent_list]
completeName = "/".join(names)
isCheck = RoleMenuPermission.objects.filter(
menu__id=item['id'],
role__id=role,
).exists()
mbCheck = RoleMenuButtonPermission.objects.filter(
menu_button=OuterRef("pk"),
role__id=role,
)
btns = MenuButton.objects.filter(
menu__id=item['id'],
).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',
data_range=F('menu_button_permission__data_range'))
dicts = {
'name': completeName,
'id': item['id'],
'isCheck': isCheck,
'btns': btns,
}
data.append(dicts)
return DetailResponse(data=data) return DetailResponse(data=data)
# data = []
# if is_superuser:
# queryset = Menu.objects.filter(status=1, is_catalog=False).values('name', 'id').all()
# else:
# role_id = request.user.role.values_list('id', flat=True)
# menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True)
# queryset = Menu.objects.filter(status=1, is_catalog=False, id__in=menu_list).values('name', 'id')
# for item in queryset:
# parent_list = Menu.get_all_parent(item['id'])
# names = [d["name"] for d in parent_list]
# completeName = "/".join(names)
# isCheck = RoleMenuPermission.objects.filter(
# menu__id=item['id'],
# role__id=role,
# ).exists()
# mbCheck = RoleMenuButtonPermission.objects.filter(
# menu_button=OuterRef("pk"),
# role__id=role,
# )
# btns = MenuButton.objects.filter(
# menu__id=item['id'],
# ).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',
# data_range=F('menu_button_permission__data_range'))
# dicts = {
# 'name': completeName,
# 'id': item['id'],
# 'isCheck': isCheck,
# 'btns': btns,
#
# }
# data.append(dicts)
# return DetailResponse(data=data)
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_premission(self,request,pk): def set_role_premission(self, request, pk):
""" """
对角色的菜单和按钮及按钮范围授权: 对角色的菜单和按钮及按钮范围授权:
:param request: :param request:
@@ -224,29 +237,30 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
body = request.data body = request.data
RoleMenuPermission.objects.filter(role=pk).delete() RoleMenuPermission.objects.filter(role=pk).delete()
RoleMenuButtonPermission.objects.filter(role=pk).delete() RoleMenuButtonPermission.objects.filter(role=pk).delete()
for menu in body: for item in body:
if menu.get('isCheck'): for menu in item["menus"]:
menu_parent = Menu.get_all_parent(menu.get('id')) if menu.get('isCheck'):
role_menu_permission_list = [] menu_parent = Menu.get_all_parent(menu.get('id'))
for d in menu_parent: role_menu_permission_list = []
role_menu_permission_list.append(RoleMenuPermission(role_id=pk, menu_id=d["id"])) for d in menu_parent:
RoleMenuPermission.objects.bulk_create(role_menu_permission_list) role_menu_permission_list.append(RoleMenuPermission(role_id=pk, menu_id=d["id"]))
# RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id')) RoleMenuPermission.objects.bulk_create(role_menu_permission_list)
for btn in menu.get('btns', []): # RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
if btn.get('isCheck'): for btn in menu.get('btns'):
data_range = btn.get('data_range',0) or 0 if btn.get('isCheck'):
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),data_range=data_range) data_range = btn.get('data_range', 0) or 0
instance.dept.set(btn.get('dept',[])) instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),
for col in menu.get('columns', []): data_range=data_range)
FieldPermission.objects.update_or_create(role_id=pk,field_id=col.get('id'),defaults={ instance.dept.set(btn.get('dept', []))
"is_query":col.get('is_query'), for col in menu.get('columns'):
"is_create":col.get('is_create'), FieldPermission.objects.update_or_create(role_id=pk, field_id=col.get('id'),
"is_update":col.get('is_update') defaults={
}) 'is_query': col.get('is_query'),
'is_create': col.get('is_create'),
'is_update': col.get('is_update')
})
return DetailResponse(msg="授权成功") return DetailResponse(msg="授权成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_menu_get_button(self, request): def role_menu_get_button(self, request):
""" """

View File

@@ -1,6 +0,0 @@
# extra-hooks/hooks-uvicorn.py
from PyInstaller.utils.hooks import get_package_paths, collect_submodules
datas = [(get_package_paths('uvicorn')[1], 'uvicorn'), (get_package_paths('whitenoise')[1], 'whitenoise')]
hiddenimports = collect_submodules('whitenoise')

View File

@@ -1,4 +1,4 @@
FROM node:16.19-alpine FROM node:16.19-alpine
WORKDIR / WORKDIR /
COPY ./web/package.json . COPY ./web/package.json .
RUN yarn install --registry=https://registry.npm.taobao.org RUN yarn install --registry=https://registry.npmmirror.com

View File

@@ -47,6 +47,8 @@ export const handleColumnPermission = async (func: Function, crudOptions: any,ex
continue continue
} else if(item.field_name === col) { } else if(item.field_name === col) {
columns[col].column.show = item['is_query'] columns[col].column.show = item['is_query']
// 如果列表不可见,则禁止在列设置中选择
if(!item['is_query'])columns[col].column.columnSetDisabled = true
columns[col].addForm = { columns[col].addForm = {
show: item['is_create'] show: item['is_create']
} }

View File

@@ -39,3 +39,12 @@ export function DelObj(id: DelReq) {
data: { id }, data: { id },
}); });
} }
export function BatchAdd(obj: AddReq) {
return request({
url: apiPrefix + 'batch_create/',
method: 'post',
data: obj,
});
}

View File

@@ -2,6 +2,8 @@ import {AddReq, DelReq, EditReq, dict, CreateCrudOptionsRet, CreateCrudOptionsPr
import * as api from './api'; import * as api from './api';
import {auth} from '/@/utils/authFunction' import {auth} from '/@/utils/authFunction'
import {request} from '/@/utils/service'; import {request} from '/@/utils/service';
import { successNotification } from '/@/utils/message';
import { ElMessage } from 'element-plus';
//此处为crudOptions配置 //此处为crudOptions配置
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet { export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async () => { const pageRequest = async () => {
@@ -40,6 +42,22 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
add: { add: {
show: auth('btn:Create') show: auth('btn:Create')
}, },
batchAdd: {
show: true,
type: 'primary',
text: '批量生成',
click: async () => {
if (context!.selectOptions.value.id == undefined) {
ElMessage.error('请选择菜单');
return;
}
const result = await api.BatchAdd({ menu: context!.selectOptions.value.id });
if (result.code == 2000) {
successNotification(result.msg);
crudExpose.doRefresh();
}
},
},
}, },
}, },
rowHandle: { rowHandle: {

View File

@@ -4,8 +4,15 @@
<div v-show="props.model"> <div v-show="props.model">
<el-tag>已选择:{{ props.model }}</el-tag> <el-tag>已选择:{{ props.model }}</el-tag>
</div> </div>
<!-- 搜索输入框 -->
<el-input
v-model="searchQuery"
placeholder="搜索模型..."
style="margin-bottom: 10px;"
></el-input>
<div class="model-card"> <div class="model-card">
<div v-for="(item,index) in allModelData" :value="item.key" :key="index"> <!--注释编号:django-vue3-admin-index483211: 对请求回来的allModelData进行computed计算返加搜索框匹配到的内容-->
<div v-for="(item,index) in filteredModelData" :value="item.key" :key="index">
<el-text :type="modelCheckIndex===index?'primary':''" @click="onModelChecked(item,index)"> <el-text :type="modelCheckIndex===index?'primary':''" @click="onModelChecked(item,index)">
{{ item.app + '--' + item.title + '(' + item.key + ')' }} {{ item.app + '--' + item.title + '(' + item.key + ')' }}
</el-text> </el-text>
@@ -29,7 +36,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {ref, onMounted, reactive} from 'vue'; import {ref, onMounted, reactive, computed } 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 {getModelList} from './api' import {getModelList} from './api'
@@ -55,6 +62,26 @@ const onModelChecked = (row, index) => {
props.model = row.key props.model = row.key
props.app = row.app props.app = row.app
} }
// 注释编号:django-vue3-admin-index083311:代码开始行
// 功能说明:搭配搜索的处理,返回搜索结果
const searchQuery = ref('');
const filteredModelData = computed(() => {
if (!searchQuery.value) {
return allModelData.value;
}
const query = searchQuery.value.toLowerCase();
return allModelData.value.filter(item =>
item.app.toLowerCase().includes(query) ||
item.title.toLowerCase().includes(query) ||
item.key.toLowerCase().includes(query)
);
});
// 注释编号:django-vue3-admin-index083311:代码结束行
/** /**
* 菜单选中时,加载表格数据 * 菜单选中时,加载表格数据
* @param record * @param record

View File

@@ -1,8 +1,6 @@
<template> <template>
<el-drawer v-model="drawerVisible" title="权限配置" direction="rtl" size="60%" :close-on-click-modal="false" <el-drawer v-model="drawerVisible" title="权限配置" direction="rtl" size="60%" :close-on-click-modal="false"
:before-close="handleDrawerClose" :before-close="handleDrawerClose" :destroy-on-close="true">
:destroy-on-close="true"
>
<template #header> <template #header>
<el-row> <el-row>
<el-col :span="4"> <el-col :span="4">
@@ -19,82 +17,65 @@
</el-row> </el-row>
</template> </template>
<div class="permission-com"> <div class="permission-com">
<el-collapse v-model="collapseCurrent" @change="handleCollapseChange" accordion> <el-tabs>
<el-collapse-item v-for="(item,mIndex) in menuData" :key="mIndex" :name="mIndex" style="background-color: #fafafa;"> <el-tab-pane v-for="(item, mIndex) in menuData" :key="mIndex" :label="item.name">
<template #title> <el-tabs tab-position="left">
<div> <el-tab-pane v-for="(menu, mIndex) in item.menus" :key="mIndex" :label="menu.name" >
<div class="pc-collapse-title"> <el-checkbox v-model="menu.isCheck">页面显示权限</el-checkbox>
<el-checkbox v-model="item.isCheck" @click.stop="null"> <div class="pc-collapse-main">
<span>{{ item.name }}</span> <div class="pccm-item">
</el-checkbox> <div class="menu-form-alert"> 配置操作功能点权限 </div>
</div> <el-checkbox v-for="(btn, bIndex) in menu.btns" :key="bIndex" v-model="btn.isCheck"
<div v-show="!collapseCurrent.includes(mIndex)" @click.stop="null" style="text-align: left;"> :label="btn.value">
<el-checkbox v-for="btn in item.btns" :key="btn.value" :label="btn.value" v-model="btn.isCheck"> <div class="btn-item">
{{ btn.name }} {{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
</el-checkbox> <span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(menu, btn.id)">
</div> <el-icon>
</div> <Setting />
</template> </el-icon>
<div class="pc-collapse-main"> </span>
<div class="pccm-item"> </div>
<p>允许对这些数据有以下操作</p> </el-checkbox>
<el-checkbox v-for="(btn,bIndex) in item.btns" :key="bIndex" v-model="btn.isCheck" :label="btn.value">
<div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(item, btn.id)">
<el-icon><Setting/></el-icon>
</span>
</div> </div>
</el-checkbox>
</div>
<div class="pccm-item" v-if="item.columns&&item.columns.length>0"> <div class="pccm-item" v-if="menu.columns && menu.columns.length > 0">
<p>对这些数据有以下字段权限</p> <div class="menu-form-alert"> 配置数据列字段权限 </div>
<ul class="columns-list">
<li class="columns-head">
<div class="width-txt">
<span>字段</span>
</div>
<div v-for="(head, hIndex) in column.header" :key="hIndex" class="width-check">
<el-checkbox :label="head.value" @change="handleColumnChange($event, menu, head.value)">
<span>{{ head.label }}</span>
</el-checkbox>
</div>
</li>
<ul class="columns-list"> <li v-for="(c_item, c_index) in menu.columns" :key="c_index" class="columns-item">
<li class="columns-head"> <div class="width-txt">{{ c_item.title }}</div>
<div class="width-txt"> <div v-for="(col, cIndex) in column.header" :key="cIndex" class="width-check">
<span>字段</span> <el-checkbox v-model="c_item[col.value]" class="ci-checkout"></el-checkbox>
</div> </div>
</li>
<div v-for="(head,hIndex) in column.header" :key="hIndex" class="width-check"> </ul>
<el-checkbox :label="head.value" @change="handleColumnChange($event, item, head.value)"> </div>
<span>{{ head.label }}</span> </div>
</el-checkbox> </el-tab-pane>
</div> </el-tabs>
</li> </el-tab-pane>
</el-tabs>
<li v-for="(c_item, c_index) in item.columns" :key="c_index" class="columns-item">
<div class="width-txt">{{ c_item.title }}</div>
<div v-for="(col,cIndex) in column.header" :key="cIndex" class="width-check">
<el-checkbox v-model="c_item[col.value]" class="ci-checkout"></el-checkbox>
</div>
</li>
</ul>
</div>
</div>
</el-collapse-item>
</el-collapse>
<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false" <el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false"
:before-close="handleDialogClose"> :before-close="handleDialogClose">
<div class="pc-dialog"> <div class="pc-dialog">
<el-select v-model="dataPermission" @change="handlePermissionRangeChange" class="dialog-select" <el-select v-model="dataPermission" @change="handlePermissionRangeChange" class="dialog-select"
placeholder="请选择"> placeholder="请选择">
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value"/> <el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
<el-tree-select <el-tree-select v-show="dataPermission === 4" node-key="id" v-model="customDataPermission"
v-show="dataPermission === 4" :props="defaultTreeProps" :data="deptData" multiple check-strictly :render-after-expand="false"
node-key="id" show-checkbox class="dialog-tree" />
v-model="customDataPermission"
:props="defaultTreeProps"
:data="deptData"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="dialog-tree"
/>
</div> </div>
<template #footer> <template #footer>
<div> <div>
@@ -108,9 +89,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, onMounted, defineProps, watch, computed, reactive} from 'vue'; import { ref, onMounted, defineProps, watch, computed, reactive } from 'vue';
import XEUtils from 'xe-utils'; import XEUtils from 'xe-utils';
import {errorNotification} from '/@/utils/message'; import { errorNotification } from '/@/utils/message';
import { import {
getDataPermissionRange, getDataPermissionRange,
getDataPermissionDept, getDataPermissionDept,
@@ -118,8 +99,8 @@ import {
setRolePremission, setRolePremission,
setBtnDatarange setBtnDatarange
} from './api'; } from './api';
import {MenuDataType, DataPermissionRangeType, CustomDataPermissionDeptType} from './types'; import { MenuDataType, MenusType, DataPermissionRangeType, CustomDataPermissionDeptType } from './types';
import {ElMessage} from 'element-plus' import { ElMessage } from 'element-plus'
const props = defineProps({ const props = defineProps({
roleId: { roleId: {
@@ -139,12 +120,12 @@ const emit = defineEmits(['update:drawerVisible'])
const drawerVisible = ref(false) const drawerVisible = ref(false)
watch( watch(
() => props.drawerVisible, () => props.drawerVisible,
(val) => { (val) => {
drawerVisible.value = val; drawerVisible.value = val;
getMenuBtnPermission() getMenuBtnPermission()
fetchData() fetchData()
} }
); );
const handleDrawerClose = () => { const handleDrawerClose = () => {
emit('update:drawerVisible', false); emit('update:drawerVisible', false);
@@ -174,7 +155,7 @@ let dataPermission = ref();
let customDataPermission = ref([]); let customDataPermission = ref([]);
//获取菜单,按钮,权限 //获取菜单,按钮,权限
const getMenuBtnPermission = async () => { const getMenuBtnPermission = async () => {
const resMenu = await getRolePremission({role: props.roleId}) const resMenu = await getRolePremission({ role: props.roleId })
menuData.value = resMenu.data menuData.value = resMenu.data
} }
@@ -198,13 +179,13 @@ const handleCollapseChange = (val: number) => {
* @param record 当前菜单 * @param record 当前菜单
* @param btnType 按钮类型 * @param btnType 按钮类型
*/ */
const handleSettingClick = (record: MenuDataType, btnId: number) => { const handleSettingClick = (record: MenusType, btnId: number) => {
menuCurrent.value = record; menuCurrent.value = record;
menuBtnCurrent.value = btnId; menuBtnCurrent.value = btnId;
dialogVisible.value = true; dialogVisible.value = true;
}; };
const handleColumnChange = (val: boolean, record: MenuDataType, btnType: string) => { const handleColumnChange = (val: boolean, record: MenusType, btnType: string) => {
for (const iterator of record.columns) { for (const iterator of record.columns) {
iterator[btnType] = val; iterator[btnType] = val;
} }
@@ -213,7 +194,7 @@ const handleColumnChange = (val: boolean, record: MenuDataType, btnType: string)
const handlePermissionRangeChange = async (val: number) => { const handlePermissionRangeChange = async (val: number) => {
if (val === 4) { if (val === 4) {
const res = await getDataPermissionDept(); const res = await getDataPermissionDept();
const data = XEUtils.toArrayTree(res.data, {parentKey: 'parent', strict: false}); const data = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
deptData.value = data; deptData.value = data;
} }
}; };
@@ -228,14 +209,16 @@ const handleDialogConfirm = () => {
} }
//if (dataPermission.value !== 4) {} //if (dataPermission.value !== 4) {}
for (const iterator of menuData.value) { for (const item of menuData.value) {
if (iterator.id === menuCurrent.value.id) { for (const iterator of item.menus) {
for (const btn of iterator.btns) { if (iterator.id === menuCurrent.value.id) {
if (btn.id === menuBtnCurrent.value) { for (const btn of iterator.btns) {
const findItem = dataPermissionRange.value.find((i) => i.value === dataPermission.value); if (btn.id === menuBtnCurrent.value) {
btn.data_range = findItem?.value || 0; const findItem = dataPermissionRange.value.find((i) => i.value === dataPermission.value);
if (btn.data_range === 4) { btn.data_range = findItem?.value || 0;
btn.dept = customDataPermission.value if (btn.data_range === 4) {
btn.dept = customDataPermission.value
}
} }
} }
} }
@@ -260,7 +243,7 @@ const handleSavePermission = () => {
} }
const column = reactive({ const column = reactive({
header: [{value: 'is_create', label: '新增可见'}, {value: 'is_update', label: '编辑可见'}, { header: [{ value: 'is_create', label: '新增可见' }, { value: 'is_update', label: '编辑可见' }, {
value: 'is_query', value: 'is_query',
label: '列表可见' label: '列表可见'
}] }]
@@ -295,6 +278,15 @@ onMounted(() => {
.pccm-item { .pccm-item {
margin-bottom: 10px; margin-bottom: 10px;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
.btn-item { .btn-item {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -19,11 +19,18 @@ export interface CustomDataPermissionMenuType {
children: CustomDataPermissionMenuType[] children: CustomDataPermissionMenuType[]
} }
export interface MenuDataType { export interface MenusType{
id: string; id: string;
name: string; name: string;
isCheck: boolean; isCheck: boolean;
radio: string; radio: string;
btns: { id:number,label: string; value: string; isCheck: boolean; data_range: number; dept:object; name:string }[]; btns: { id:number,name: string; value: string; isCheck: boolean; data_range: number; dept:object; name:string }[];
columns: { [key: string]: boolean | string; }[] columns: { [key: string]: boolean | string; }[]
} }
export interface MenuDataType {
id: string;
name: string;
menus:MenusType[];
}