34 Commits

Author SHA1 Message Date
dvadmin-开发-李强
bc15ed15ca Accept Merge Request #2: (develop -> master)
Merge Request: 正式发布v3.0.0版本

Created By: @dvadmin-开发-李强
Accepted By: @dvadmin-开发-李强
URL: https://dvadmin-private.coding.net/p/code/d/dvadmin3/git/merge/2
2023-11-30 22:13:42 +08:00
李强
349986f14f 正式发布v3.0.0版本 2023-11-30 22:02:49 +08:00
李强
fb4bba9000 feat: 样式优化 2023-11-30 21:37:43 +08:00
李强
e0d46871c4 update README.md 2023-11-30 21:33:32 +08:00
李强
7691481006 feat: 优化页面样式 2023-11-30 21:31:55 +08:00
李强
120c737de7 feat: 优化获取所有项目下的app里的models配置项 2023-11-30 20:23:52 +08:00
猿小天
5729aeb521 refactor: 上传最新的英文README 2023-11-30 16:13:49 +08:00
猿小天
205cfcca2e refactor: 上传最新的README 2023-11-30 15:57:42 +08:00
猿小天
0fdc108ebf Merge remote-tracking branch 'origin/develop' into develop 2023-11-28 10:29:52 +08:00
猿小天
064cbee8a2 1.更新系统配置初始化文件 2023-11-28 10:29:29 +08:00
李强
a2d4d6e07e feat: 初始化菜单问题 2023-11-28 10:29:05 +08:00
猿小天
d721405ee4 1.更新列权限初始化文件 2023-11-26 11:43:43 +08:00
猿小天
0650c95745 1.更新列权限初始化文件 2023-11-26 11:37:21 +08:00
猿小天
f291885d57 1.更新按钮权限初始化文件 2023-11-26 11:03:26 +08:00
猿小天
4b77fb903c refactor: 检测目前所有菜单权限 2023-11-24 15:37:47 +08:00
李强
fca82c093a feat: 优化deep 2023-11-24 15:25:41 +08:00
猿小天
49300fb17e Merge remote-tracking branch 'origin/develop' into develop 2023-11-24 15:13:23 +08:00
猿小天
c763333024 refactor: 检测目前所有菜单权限 2023-11-24 15:12:46 +08:00
李强
42e6c7b600 feat: websocket优化 2023-11-23 18:55:25 +08:00
李强
6172dea399 feat: 优化商业授权标志 2023-11-23 16:33:19 +08:00
李强
2baba8e36f feat: 版本升级弹窗 2023-11-23 16:19:35 +08:00
李强
0131d31808 feat: 优化docker 2023-11-22 21:58:10 +08:00
李强
dd2dcd4ad1 feat: 优化docker 2023-11-22 17:52:39 +08:00
猿小天
fb8b0a5ac6 refactor: 重构权限管理
1.更新字段管理
2023-11-21 21:21:58 +08:00
猿小天
6045312f7e 1.修改菜单初始化文件 2023-11-21 19:22:00 +08:00
猿小天
4641d9c774 Merge remote-tracking branch 'origin/develop' into develop 2023-11-21 17:51:31 +08:00
猿小天
c04b33ed31 refactor: 重构权限管理
1.更新字段管理
2023-11-21 17:51:19 +08:00
李强
0286ca003f feat: 更新.gitignore 2023-11-21 16:01:42 +08:00
猿小天
54e4d23cf7 refactor: 重构权限管理
1.更新字段管理
2023-11-21 14:21:08 +08:00
猿小天
645f43c887 refactor: 重构权限管理
1.更新字段管理
2023-11-20 19:03:14 +08:00
猿小天
4ac8ed7627 refactor: 重构权限管理
1.更新字段管理
2023-11-20 17:19:20 +08:00
猿小天
94ad6b1bae refactor: 重构权限管理
1.更新字段管理
2023-11-20 17:02:37 +08:00
china_ahhui
96748da99d refactor: 优化配置文件的app配置 2023-11-13 18:27:10 +08:00
china_ahhui
b74a196c94 chore: 修改后端的忽略文件 2023-11-13 17:45:49 +08:00
94 changed files with 5212 additions and 3724 deletions

10
CHANGELOG.md Normal file
View File

@@ -0,0 +1,10 @@
# Django-Vue3-Admin 更新日志
## 正式发布v3.0.0版本
### 1.新增:列权限管理与授权;
### 2.新增:代码新版本发布后,进行升级提醒;
### 3.优化:角色管理中按钮权限的操作;
### 4.优化websocket 连接状态显示;
### 5.优化:初始化获取系统配置与字典配置,进行动态渲染登录页面;
### 6.修复:登录页面中系统配置不生效问题;
### 7.其他优化

View File

@@ -1,14 +1,14 @@
# 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/huge-dream/django-vue3-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/huge-dream/django-vue3-admin/badge/star.svg?theme=dark)](https://gitee.com/huge-dream/django-vue3-admin)
[preview](https://demo.dvadmin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
💡 **「About」**
We are a group of young people who love Code. In this hot era, we hope to calm down and bring some of our colors and colors through code.
It is a completely open-source rapid development platform, provided free for personal use and authorized for group use.
Django-Vue3-Admin is a comprehensive basic development platform based on the RBAC (Role-Based Access Control) model for permission control, with column-level granularity. It follows a frontend-backend separation architecture, with Django and Django Rest Framework used for the backend, and Vue3, Composition API, TypeScript, Vite, and Element Plus used for the frontend.
Because of love, so embrace the future
## framework introduction
@@ -18,12 +18,13 @@ Because of love, so embrace the future
* 👭The backend uses the Python language Django framework as well as the powerful[Django REST Framework](https://pypi.org/project/djangorestframework)。
* 👫Permission authentication use[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt)Supports the multi-terminal authentication system.
* 👬Support loading dynamic permission menu, multi - way easy permission control.
* 💏 Special thanks: [vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/).
* 💡 💏 Special thanks:[jetbrains](https://www.jetbrains.com/) To provide a free IntelliJ IDEA license for this open source project.
* 👬Enhanced Column Permission Control, with granularity down to each column.
* 💏Special thanks: [vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/).
* 💡Special thanks:[jetbrains](https://www.jetbrains.com/) To provide a free IntelliJ IDEA license for this open source project.
## Online experience
👩‍👧‍👦👩‍👧‍👦 demo address:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com)
👩‍👧‍👦👩‍👧‍👦 demo address:[https://demo.dvadmin.com](https://demo.dvadmin.com)
* demo accountsuperadmin
@@ -39,57 +40,61 @@ Because of love, so embrace the future
## source code url:
gitee(Main push)[https://gitee.com/liqianglog/django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)👩‍👦‍👦
gitee(Main push)[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩‍👦‍👦
github[https://github.com/liqianglog/django-vue-admin](https://github.com/liqianglog/django-vue-admin)👩‍👦‍👦
github[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩‍👦‍👦
## core function
1. 👨‍⚕️ Menu management: Configure the system menu, operation permissions, button permissions, back-end interface permissions, etc.
2. 🧑‍⚕️ Department management: Configure the system organization (company, department, role).
3. 👩‍⚕️ Role management: role menu permission allocation, data permission allocation, set roles according to the department for data range permission division.
4. 🧑‍🎓 Rights Specifies the rights of the authorization role.
5. 👨‍🎓 User management: The user is the system operator, this function mainly completes the system user configuration.
6. 👬 Interface whitelist: specifies the interface that does not need permission verification.
7. 🧑‍🔧 Dictionary management: Maintenance of some fixed data frequently used in the system.
8. 🧑‍🔧 Regional management: to manage provinces, cities, counties and regions.
9. 📁 Attachment management: Unified management of all files and pictures on the platform.
10. 🗓 operation logs: log and query the system normal operation; Log and query system exception information.
11.🔌 [plugins market] (<https://bbs.django-vue-admin.com/plugMarket.html>) : based on the Django framework - Vue - Admin application and plug-in development.
1. 👨Menu Management: Configure system menus, operation permissions, button permission flags, backend interface permissions, etc.
2. 🧑Department Management: Configure system organizational structure (company, department, role).
3. 👩Role Management: Role menu permission assignment, data permission assignment, set role-based data scope permissions by department.
4. 🧑‍🎓Button Permission Control: Authorize role-specific button permissions and interface permissions, enabling authorization of data scope for each interface.
5. 🧑‍🎓Field Column Permission Control: Authorize page field display permissions, specifically for the display permissions of a certain column.
6. 👨🎓User Management: Users are system operators, and this function is mainly used for system user configuration.
7. 👬API Whitelist: Configure interfaces that do not require permission verification.
8. 🧑‍🔧Dictionary Management: Maintain frequently used and relatively fixed data in the system.
9. 🧑🔧Region Management: Manage provinces, cities, counties, and districts.
10. 📁File Management: Unified management of all files, images, etc., on the platform.
11. 🗓Operation Logs: Record and query logs for normal system operations and exceptional system information.
12. 🔌[Plugin Market](https://bbs.django-vue-admin.com/plugMarket.html): Applications and plugins developed based on the Django-Vue-Admin framework.
## plugins market 🔌
* Celery Asynchronous task[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
* Upgrade center backend[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
* Upgrade center front[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
Updating...
## Repository Branch Explanation 💈
Main Branch: master (stable version)
Development Branch: develop
## before start project you need:
~~~
Python >= 3.8.0
nodejs >= 14.0
Mysql >= 5.7.0 (Optional. The default database is sqlite3. 8.0 is recommended)
Redis(Optional, the latest edition)
Python >= 3.11.0 (Minimum version 3.9+)
Node.js >= 16.0
Mysql >= 8.0 (Optional, default database: SQLite3, supports 5.7+, recommended version: 8.0)
Redis (Optional, latest version)
~~~
## frontend♝
```bash
# clone code
git clone https://gitee.com/liqianglog/django-vue-admin.git
git clone https://gitee.com/huge-dream/django-vue3-admin.git
# enter code dir
cd web
# install dependence
npm install --registry=https://registry.npm.taobao.org
npm install yarn
yarn install --registry=https://registry.npm.taobao.org
# Start service
npm run dev
yarn run dev
# Visit http://localhost:8080 in your browser
# Parameters such as boot port can be configured in the #.env.development file
# Build the production environment
# npm run build
# yarn run build
```
## backend💈
@@ -111,8 +116,8 @@ npm run dev
python3 manage.py init_area
8. start backend
python3 manage.py runserver 0.0.0.0:8000
or daphne :
daphne -b 0.0.0.0 -p 8000 application.asgi:application
or uvicorn :
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
~~~
### visit backend swagger
@@ -120,12 +125,12 @@ or daphne :
* visit url[http://localhost:8080](http://localhost:8080) (The default address is this one. If you want to change it, follow the configuration file)
* account`superadmin` password`admin123456`
### docker-compose
### docker-compose
~~~shell
docker-compose up -d
# Initialize backend data (first execution only)
docker exec -ti dvadmin-django bash
docker exec -ti dvadmin3-django bash
python manage.py makemigrations
python manage.py migrate
python manage.py init_area
@@ -147,22 +152,24 @@ docker-compose up -d --build
## Demo screenshot✅
![image-01](https://images.gitee.com/uploads/images/2022/0530/234137_b58c8f98_5074988.png)
![image-01](https://foruda.gitee.com/images/1701348994587355489/1bc749e7_5074988.png)
![image-02](https://images.gitee.com/uploads/images/2022/0530/234240_39834603_5074988.png)
![image-02](https://foruda.gitee.com/images/1701349037811908960/80d361db_5074988.png)
![image-03](https://images.gitee.com/uploads/images/2022/0530/234339_35e728a0_5074988.png)
![image-03](https://foruda.gitee.com/images/1701349224478845203/954f0a7b_5074988.png)
![image-04](https://images.gitee.com/uploads/images/2022/0530/234426_957036b0_5074988.png)
![image-04](https://foruda.gitee.com/images/1701349248928658877/64926724_5074988.png)
![image-05](https://images.gitee.com/uploads/images/2022/0530/234458_898be492_5074988.png)
![image-05](https://foruda.gitee.com/images/1701349259068943299/1306ba40_5074988.png)
![image-06](https://images.gitee.com/uploads/images/2022/0530/234521_35b40076_5074988.png)
![image-06](https://foruda.gitee.com/images/1701349294894429495/e3b3a8cf_5074988.png)
![image-07](https://images.gitee.com/uploads/images/2022/0530/234615_c2325639_5074988.png)
![image-07](https://foruda.gitee.com/images/1701350432536247561/3b26685e_5074988.png)
![image-08](https://images.gitee.com/uploads/images/2022/0530/234639_1ed6cc93_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-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png)
![image-09](https://images.gitee.com/uploads/images/2022/0530/234815_cea2c53f_5074988.png)
![image-10](https://images.gitee.com/uploads/images/2022/0530/234840_5f3e5f53_5074988.png)

104
README.md
View File

@@ -2,7 +2,7 @@
[![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)
@@ -10,11 +10,14 @@
我们是一群热爱代码的青年在这个炙热的时代下我们希望静下心来通过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
@@ -22,20 +25,29 @@
* 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
* 👬支持加载动态权限菜单,多方式轻松权限控制。
* 👬全新的列权限管控,粒度细化到每一列。
* 💏特别鸣谢:[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 ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
## 在线体验
👩‍👧‍👦演示地址:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com)
👩‍👧‍👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
- 账号superadmin
- 账号superadmin
- 密码admin123456
👩‍👦‍👦文档地址:[https://django-vue-admin.com](https://django-vue-admin.com)
👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
@@ -46,7 +58,8 @@
- 插件市场:[戳我](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交流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)
- 二维码
@@ -54,10 +67,9 @@
## 源码地址
gitee地址(主推)[https://gitee.com/liqianglog/django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)👩‍👦‍👦
github地址[https://github.com/liqianglog/django-vue-admin](https://github.com/liqianglog/django-vue-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)👩‍👦‍👦
## 内置功能
@@ -65,47 +77,51 @@ github地址[https://github.com/liqianglog/django-vue-admin](https://github.c
1. 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
2. 🧑‍⚕️部门管理:配置系统组织机构(公司、部门、角色)。
3. 👩‍⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
4. 🧑‍🎓权限权限:授权角色的权限范围。
5. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置
6. 👬接口白名单:配置不需要进行权限校验的接口
7. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护
8. 🧑‍🔧地区管理:对省市县区域进行管理
9. 📁附件管理:对平台上所有文件、图片等进行统一管理。
10. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询
11. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件
4. 🧑‍🎓按钮权限控制:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
5. 🧑‍🎓字段列权限控制:授权页面的字段显示权限,具体到某一列的显示权限
7. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置
8. 👬接口白名单:配置不需要进行权限校验的接口
9. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护
10. 🧑‍🔧地区管理:对省市县区域进行管理。
11. 📁附件管理:对平台上所有文件、图片等进行统一管理
12. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌
更新中...
## 仓库分支说明 💈
主分支master稳定版本
开发分支develop
- Celery异步任务[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
- 升级中心后端:[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
- 升级中心前端:[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
## 准备工作
~~~
Python >= 3.8.0 (推荐3.8+版本)
nodejs >= 14.0 (推荐最新)
Mysql >= 5.7.0 (可选默认数据库sqlite3推荐8.0版本)
Redis(可选,最新版)
Python >= 3.11.0 (最低3.9+版本)
nodejs >= 16.0
Mysql >= 8.0 (可选默认数据库sqlite3支持5.7+推荐8.0版本)
Redis (可选,最新版)
~~~
## 前端♝
```bash
# 克隆项目
git clone https://gitee.com/liqianglog/django-vue-admin.git
git clone https://gitee.com/huge-dream/django-vue3-admin.git
# 进入项目目录
cd web
# 安装依赖
npm install --registry=https://registry.npm.taobao.org
npm install yarn
yarn install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
yarn build
# 浏览器访问 http://localhost:8080
# .env.development 文件中可配置启动端口等参数
# 构建生产环境
# npm run build
# yarn run build
```
@@ -129,9 +145,11 @@ npm run dev
python3 manage.py init_area
8. 启动项目
python3 manage.py runserver 0.0.0.0:8000
或使用 daphne :
daphne -b 0.0.0.0 -p 8000 application.asgi:application
或使用 uvicorn :
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
~~~
## 开发建议
前后端backend与web各自单独一个窗口打开进行开发
### 访问项目
@@ -148,7 +166,7 @@ npm run dev
# 先安装docker-compose (自行百度安装),执行此命令等待安装如有使用celery插件请打开docker-compose.yml中celery 部分注释
docker-compose up -d
# 初始化后端数据(第一次执行即可)
docker exec -ti dvadmin-django bash
docker exec -ti dvadmin3-django bash
python manage.py makemigrations
python manage.py migrate
python manage.py init_area
@@ -172,25 +190,25 @@ docker-compose up -d --build
## 演示图✅
![image-01](https://images.gitee.com/uploads/images/2022/0530/234137_b58c8f98_5074988.png)
![image-01](https://foruda.gitee.com/images/1701348994587355489/1bc749e7_5074988.png)
![image-02](https://images.gitee.com/uploads/images/2022/0530/234240_39834603_5074988.png)
![image-02](https://foruda.gitee.com/images/1701349037811908960/80d361db_5074988.png)
![image-03](https://images.gitee.com/uploads/images/2022/0530/234339_35e728a0_5074988.png)
![image-03](https://foruda.gitee.com/images/1701349224478845203/954f0a7b_5074988.png)
![image-04](https://images.gitee.com/uploads/images/2022/0530/234426_957036b0_5074988.png)
![image-04](https://foruda.gitee.com/images/1701349248928658877/64926724_5074988.png)
![image-05](https://images.gitee.com/uploads/images/2022/0530/234458_898be492_5074988.png)
![image-05](https://foruda.gitee.com/images/1701349259068943299/1306ba40_5074988.png)
![image-06](https://images.gitee.com/uploads/images/2022/0530/234521_35b40076_5074988.png)
![image-06](https://foruda.gitee.com/images/1701349294894429495/e3b3a8cf_5074988.png)
![image-07](https://images.gitee.com/uploads/images/2022/0530/234615_c2325639_5074988.png)
![image-07](https://foruda.gitee.com/images/1701350432536247561/3b26685e_5074988.png)
![image-08](https://images.gitee.com/uploads/images/2022/0530/234639_1ed6cc93_5074988.png)
![image-08](https://foruda.gitee.com/images/1701350455264771992/b364c57f_5074988.png)
![image-09](https://images.gitee.com/uploads/images/2022/0530/234815_cea2c53f_5074988.png)
![image-09](https://foruda.gitee.com/images/1701350479266000753/e4e4f7c5_5074988.png)
![image-10](https://images.gitee.com/uploads/images/2022/0530/234840_5f3e5f53_5074988.png)
![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png)

1
backend/.gitignore vendored
View File

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

View File

@@ -43,10 +43,8 @@ sys.path.insert(0, os.path.join(PLUGINS_PATH))
DEBUG = locals().get("DEBUG", True)
ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"])
# Application definition
CUSTOM_APPS = [
"dvadmin.system",
]
# 列权限需要排除的App应用
COLUMN_EXCLUDE_APPS = ['channels', 'captcha'] + locals().get("COLUMN_EXCLUDE_APPS", [])
INSTALLED_APPS = [
"django.contrib.auth",
@@ -60,8 +58,8 @@ INSTALLED_APPS = [
"corsheaders", # 注册跨域app
"drf_yasg",
"captcha",
'channels',
*locals().get("CUSTOM_APPS", []), # 所有项目里写的app需要在env.py文件里的CUSTOM_APPS中
"channels",
"dvadmin.system",
]
MIDDLEWARE = [
@@ -178,7 +176,6 @@ CHANNEL_LAYERS = {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
REDIS_URL = locals().get('REDIS_URL', "")
# CHANNEL_LAYERS = {
# 'default': {
# 'BACKEND': 'channels_redis.core.RedisChannelLayer',
@@ -400,11 +397,10 @@ TENANT_SHARED_APPS = []
PLUGINS_URL_PATTERNS = []
# ********** 一键导入插件配置开始 **********
# 例如:
#from dvadmin3_upgrade_center.settings import * # 升级中心
# from dvadmin_upgrade_center.settings import * # 升级中心
# from dvadmin_celery.settings import * # celery 异步任务
# from dvadmin_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理
# from dvadmin_uniapp.settings import *
# ...
# ********** 一键导入插件配置结束 **********

View File

@@ -73,7 +73,7 @@ class DvadminWebSocket(AsyncJsonWebsocketConsumer):
unread_count = await _get_message_unread(self.user_id)
if unread_count == 0:
# 发送连接成功
await self.send_json(set_message('system', 'SYSTEM', '连接成功'))
await self.send_json(set_message('system', 'SYSTEM', '您已上线'))
else:
await self.send_json(
set_message('system', 'SYSTEM', "请查看您的未读消息~",

View File

@@ -44,6 +44,5 @@ LOGIN_NO_CAPTCHA_AUTH = True
# ================================================= #
ALLOWED_HOSTS = ["*"]
CUSTOM_APPS = [
"dvadmin.system",
]
# 列权限中排除App应用
COLUMN_EXCLUDE_APPS = []

View File

@@ -9,7 +9,7 @@ django.setup()
from dvadmin.system.models import (
Role, Dept, Users, Menu, MenuButton,
ApiWhiteList, Dictionary, SystemConfig,
RoleMenuPermission, RoleApiPermission, Columns
RoleMenuPermission, RoleMenuButtonPermission, MenuField
)
from dvadmin.utils.serializers import CustomModelSerializer
@@ -42,14 +42,35 @@ class UsersInitSerializer(CustomModelSerializer):
}
class MenuButtonInitSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method', 'menu']
read_only_fields = ["id"]
class MenuFieldInitSerializer(CustomModelSerializer):
"""
初始化列权限-序列化器
"""
class Meta:
model = MenuField
fields = ['id', 'menu','field_name','title','model']
read_only_fields = ["id"]
class MenuInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
"""
name = serializers.CharField(required=True)
menu_type = serializers.IntegerField(required=True)
name = serializers.CharField(required=False)
children = serializers.SerializerMethodField()
menu_button = serializers.SerializerMethodField()
menu_field = serializers.SerializerMethodField()
def get_children(self, obj: Menu):
data = []
instance = Menu.objects.filter(parent_id=obj.id)
@@ -58,18 +79,34 @@ class MenuInitSerializer(CustomModelSerializer):
data = serializer.data
return data
def get_menu_button(self, obj: Menu):
data = []
instance = obj.menuPermission.order_by('method')
if instance:
data = list(instance.values('name', 'value', 'api', 'method'))
return data
def get_menu_field(self, obj: Menu):
data = []
instance = obj.menufield_set.order_by('field_name')
if instance:
data = list(instance.values('field_name', 'title','model'))
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
menu_button = self.initial_data.get('menu_button')
menu_field = self.initial_data.get('menu_field')
# 菜单表
if children:
for menu_data in children:
menu_data['parent'] = instance.id
filter_data = {
"name": menu_data['name'],
"web_path": menu_data['web_path'],
"component": menu_data['component'],
"menu_type": menu_data['menu_type'],
"component_name": menu_data['component_name'],
}
instance_obj = Menu.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
@@ -77,12 +114,36 @@ class MenuInitSerializer(CustomModelSerializer):
serializer = MenuInitSerializer(instance_obj, data=menu_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
# 菜单按钮
if menu_button:
for menu_button_data in menu_button:
menu_button_data['menu'] = instance.id
filter_data = {
"menu": menu_button_data['menu'],
"value": menu_button_data['value']
}
instance_obj = MenuButton.objects.filter(**filter_data).first()
serializer = MenuButtonInitSerializer(instance_obj, data=menu_button_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
# 列权限
if menu_field:
for field_data in menu_field:
field_data['menu'] = instance.id
filter_data = {
'menu':field_data['menu'],
'field_name':field_data['field_name']
}
instance_obj = MenuField.objects.filter(**filter_data).first()
serializer = MenuFieldInitSerializer(instance_obj, data=field_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = Menu
fields = ['name', 'icon', 'sort', 'is_link', 'menu_type', 'web_path', 'component', 'component_name', 'status',
'cache', 'visible', 'parent', 'children', 'creator', 'dept_belong_id']
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
'cache', 'visible', 'parent', 'children', 'menu_button','menu_field', 'creator', 'dept_belong_id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
@@ -135,62 +196,37 @@ class RoleMenuInitSerializer(CustomModelSerializer):
}
class RoleApiPermissionInitSerializer(CustomModelSerializer):
class RoleMenuButtonInitSerializer(CustomModelSerializer):
"""
初始化角色接口权限(用于生成初始化json文件)
初始化角色菜单按钮(用于生成初始化json文件)
"""
role_key = serializers.CharField(max_length=100, required=True)
name = serializers.CharField(max_length=255, required=True)
api = serializers.CharField(max_length=255, required=True)
method = serializers.IntegerField(required=True)
menu_button_value = serializers.CharField(max_length=100, required=True)
data_range = serializers.CharField(max_length=100, required=False)
def create(self, validated_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()
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 instance
class Meta:
model = RoleApiPermission
fields = ['role_key', 'name','api','method', 'data_range', 'dept', 'creator', 'dept_belong_id']
model = RoleMenuButtonPermission
fields = ['role_key', 'menu_button_value', 'data_range', 'dept', 'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'role': {'required': False},
'menu': {'required': False},
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class RoleColumnInitSerializer(CustomModelSerializer):
"""
初始化角色字段权限(用于生成初始化json文件)
"""
role_key = serializers.CharField(max_length=100, required=True)
app = serializers.CharField(max_length=255, required=True)
model = serializers.CharField(max_length=255, required=True)
field_name = serializers.CharField(max_length=255, required=True)
title = serializers.CharField(max_length=255, required=True)
def create(self, validated_data):
init_data = self.initial_data
validated_data.pop('role_key')
role_id = Role.objects.filter(key=init_data['role_key']).first()
validated_data['role'] = role_id
instance = super().create(validated_data)
return instance
class Meta:
model = Columns
fields = ['role_key', 'app','model','field_name', 'title']
read_only_fields = ["id"]
extra_kwargs = {
'role': {'required': False},
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class ApiWhiteListInitSerializer(CustomModelSerializer):
"""

View File

@@ -1,123 +0,0 @@
[
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "id",
"title": "Id",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "description",
"title": "描述",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "creator",
"title": "创建人",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "modifier",
"title": "修改人",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "dept_belong_id",
"title": "数据归属部门",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "update_datetime",
"title": "修改时间",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "create_datetime",
"title": "创建时间",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "name",
"title": "角色名称",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "key",
"title": "权限字符",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "sort",
"title": "角色顺序",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
},
{
"dept_belong_id": "1",
"app": "dvadmin.system",
"model": "Role",
"field_name": "status",
"title": "角色状态",
"is_query": 1,
"is_create": 1,
"is_update": 1,
"role_key": "admin"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
[
{
"role_key": "admin",
"name": "菜单列表查询",
"api": "/api/system/menu/",
"method": 0
},
{
"role_key": "public",
"name": "菜单列表查询",
"api": "/api/system/menu/",
"method": 0
}
]

View File

@@ -0,0 +1,12 @@
[
{
"role_key": "admin",
"menu_button_value": "menu:Search",
"data_range": 0
},
{
"role_key": "public",
"menu_button_value":"menu:Search",
"data_range": 0
}
]

View File

@@ -65,6 +65,20 @@
"placeholder": null,
"setting": null,
"children": [
{
"parent": 1,
"title": "网站标题",
"key": "site_title",
"value": "Dvadmin",
"sort": 1,
"status": true,
"data_options": null,
"form_item_type": 0,
"rule": [],
"placeholder": "请输入网站标题",
"setting": null,
"children": []
},
{
"parent": 1,
"title": "网站名称",
@@ -116,7 +130,7 @@
"parent": 1,
"title": "版权信息",
"key": "copyright",
"value": "2021-2022 django-vue-admin.com 版权所有",
"value": "2021-2024 django-vue-admin.com 版权所有",
"sort": 4,
"status": true,
"data_options": null,

View File

@@ -11,7 +11,7 @@ from dvadmin.utils.core_initialize import CoreInitialize
from dvadmin.system.fixtures.initSerializer import (
UsersInitSerializer, DeptInitSerializer, RoleInitSerializer,
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer,
SystemConfigInitSerializer, RoleMenuInitSerializer, RoleApiPermissionInitSerializer, RoleColumnInitSerializer
SystemConfigInitSerializer, RoleMenuInitSerializer, RoleMenuButtonInitSerializer
)
@@ -39,7 +39,7 @@ class Initialize(CoreInitialize):
"""
初始化菜单信息
"""
self.init_base(MenuInitSerializer, unique_fields=['name'])
self.init_base(MenuInitSerializer, unique_fields=['name', 'web_path', 'component', 'component_name'])
def init_role_menu(self):
"""
@@ -47,17 +47,11 @@ class Initialize(CoreInitialize):
"""
self.init_base(RoleMenuInitSerializer, unique_fields=['role', 'menu'])
def init_role_api_permission(self):
def init_role_menu_button(self):
"""
初始化角色菜单按钮信息
"""
self.init_base(RoleApiPermissionInitSerializer, unique_fields=['role', 'api','name'])
def init_role_column(self):
"""
初始化角色字段权限
"""
self.init_base(RoleColumnInitSerializer, unique_fields=['app','model','field_name'])
self.init_base(RoleMenuButtonInitSerializer, unique_fields=['role', 'menu_button'])
def init_api_white_list(self):
"""
@@ -83,8 +77,7 @@ class Initialize(CoreInitialize):
self.init_users()
self.init_menu()
self.init_role_menu()
self.init_role_api_permission()
self.init_role_column()
self.init_role_menu_button()
self.init_api_white_list()
self.init_dictionary()
self.init_system_config()

View File

@@ -155,24 +155,22 @@ class Menu(CoreModel):
help_text="上级菜单",
)
icon = models.CharField(max_length=64, verbose_name="菜单图标", null=True, blank=True, help_text="菜单图标")
name = models.CharField(max_length=64, verbose_name="目录名称/菜单名称/按钮名称", help_text="目录名称/菜单名称/按钮名称")
name = models.CharField(max_length=64, verbose_name="菜单名称", help_text="菜单名称")
sort = models.IntegerField(default=1, verbose_name="显示排序", null=True, blank=True, help_text="显示排序")
is_link = models.BooleanField(default=False, verbose_name="是否外链", help_text="是否外链")
MENU_TYPE_CHOICES =(
(0, "目录"),
(1, "菜单"),
(2, "按钮"),
ISLINK_CHOICES = (
(0, ""),
(1, ""),
)
menu_type = models.IntegerField(default=0, verbose_name="菜单类型", help_text="菜单类型")
web_path = models.CharField(max_length=128,default="/", verbose_name="路由地址", null=True, blank=True, help_text="路由地址")
component = models.CharField(max_length=200, verbose_name="组件地址/按钮权限值", null=True, blank=True, help_text="组件地址/按钮权限值")
is_link = models.BooleanField(default=False, verbose_name="是否外链", help_text="是否外链")
is_catalog = models.BooleanField(default=False, verbose_name="是否目录", help_text="是否目录")
web_path = models.CharField(max_length=128, verbose_name="路由地址", null=True, blank=True, help_text="路由地址")
component = models.CharField(max_length=128, verbose_name="组件地址", null=True, blank=True, help_text="组件地址")
component_name = models.CharField(max_length=50, verbose_name="组件名称", null=True, blank=True,
help_text="组件名称")
status = models.BooleanField(default=True, blank=True, verbose_name="菜单状态", help_text="菜单状态")
cache = models.BooleanField(default=False, blank=True, verbose_name="是否页面缓存", help_text="是否页面缓存")
visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示",
help_text="侧边栏中是否显示")
frame_out = models.BooleanField(default=False, blank=True, verbose_name="是否主框架外", help_text="是否主框架外")
class Meta:
db_table = table_prefix + "system_menu"
@@ -180,20 +178,27 @@ class Menu(CoreModel):
verbose_name_plural = verbose_name
ordering = ("sort",)
class Columns(CoreModel):
role = models.ForeignKey(to='Role', on_delete=models.CASCADE, verbose_name='角色', db_constraint=False)
app = models.CharField(max_length=64, verbose_name='应用名')
class MenuField(CoreModel):
model = models.CharField(max_length=64, verbose_name='表名')
menu = models.ForeignKey(to='Menu', on_delete=models.CASCADE, verbose_name='菜单', db_constraint=False)
field_name = models.CharField(max_length=64, verbose_name='模型表字段名')
title = models.CharField(max_length=64, verbose_name='字段显示名')
class Meta:
db_table = table_prefix + "system_menu_field"
verbose_name = "菜单字段表"
verbose_name_plural = verbose_name
ordering = ("id",)
class FieldPermission(CoreModel):
role = models.ForeignKey(to='Role', on_delete=models.CASCADE, verbose_name='角色', db_constraint=False)
field = models.ForeignKey(to='MenuField', on_delete=models.CASCADE,related_name='menu_field', verbose_name='字段', db_constraint=False)
is_query = models.BooleanField(default=1, verbose_name='是否可查询')
is_create = models.BooleanField(default=1, verbose_name='是否可创建')
is_update = models.BooleanField(default=1, verbose_name='是否可更新')
class Meta:
db_table = table_prefix + "system_columns"
verbose_name = "权限表"
db_table = table_prefix + "system_field_permission"
verbose_name = "字段权限表"
verbose_name_plural = verbose_name
ordering = ("id",)
@@ -251,25 +256,25 @@ class RoleMenuPermission(CoreModel):
# ordering = ("-create_datetime",)
class RoleApiPermission(CoreModel):
class RoleMenuButtonPermission(CoreModel):
role = models.ForeignKey(
to="Role",
db_constraint=False,
related_name="role_api",
related_name="role_menu_button",
on_delete=models.CASCADE,
verbose_name="关联角色",
help_text="关联角色",
)
name = models.CharField(max_length=64, verbose_name="名称", help_text="名称")
api = models.CharField(max_length=200, verbose_name="接口地址", help_text="接口地址")
METHOD_CHOICES = (
(0, "GET"),
(1, "POST"),
(2, "PUT"),
(3, "DELETE"),
menu_button = models.ForeignKey(
to="MenuButton",
db_constraint=False,
related_name="menu_button_permission",
on_delete=models.CASCADE,
verbose_name="关联菜单按钮",
help_text="关联菜单按钮",
null=True,
blank=True
)
method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True,
help_text="接口请求方法")
DATASCOPE_CHOICES = (
(0, "仅本人数据权限"),
(1, "本部门及以下数据权限"),
@@ -283,8 +288,8 @@ class RoleApiPermission(CoreModel):
help_text="数据权限-关联部门")
class Meta:
db_table = table_prefix + "role_api_permission"
verbose_name = "角色接口权限表"
db_table = table_prefix + "role_menu_button_permission"
verbose_name = "角色按钮权限表"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)
@@ -457,7 +462,7 @@ class SystemConfig(CoreModel):
help_text="父级",
)
title = models.CharField(max_length=50, verbose_name="标题", help_text="标题")
key = models.CharField(max_length=50, verbose_name="", help_text="", db_index=True)
key = models.CharField(max_length=20, verbose_name="", help_text="", db_index=True)
value = models.JSONField(max_length=100, verbose_name="", help_text="", null=True, blank=True)
sort = models.IntegerField(default=0, verbose_name="排序", help_text="排序", blank=True)
status = models.BooleanField(default=True, verbose_name="启用状态", help_text="启用状态")

View File

@@ -8,17 +8,19 @@ from dvadmin.system.views.dictionary import DictionaryViewSet
from dvadmin.system.views.file_list import FileViewSet
from dvadmin.system.views.login_log import LoginLogViewSet
from dvadmin.system.views.menu import MenuViewSet
from dvadmin.system.views.menu_button import MenuButtonViewSet
from dvadmin.system.views.message_center import MessageCenterViewSet
from dvadmin.system.views.operation_log import OperationLogViewSet
from dvadmin.system.views.role import RoleViewSet
from dvadmin.system.views.role_menu import RoleMenuPermissionViewSet
from dvadmin.system.views.role_api_permission import RoleApiPermissionViewSet
from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermissionViewSet
from dvadmin.system.views.system_config import SystemConfigViewSet
from dvadmin.system.views.user import UserViewSet
from dvadmin.system.views.column import ColumnViewSet
from dvadmin.system.views.menu_field import MenuFieldViewSet
system_url = routers.SimpleRouter()
system_url.register(r'menu', MenuViewSet)
system_url.register(r'menu_button', MenuButtonViewSet)
system_url.register(r'role', RoleViewSet)
system_url.register(r'dept', DeptViewSet)
system_url.register(r'user', UserViewSet)
@@ -29,9 +31,9 @@ system_url.register(r'file', FileViewSet)
system_url.register(r'api_white_list', ApiWhiteListViewSet)
system_url.register(r'system_config', SystemConfigViewSet)
system_url.register(r'message_center', MessageCenterViewSet)
system_url.register(r'role_api_permission', RoleApiPermissionViewSet)
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
system_url.register(r'column', ColumnViewSet)
system_url.register(r'column', MenuFieldViewSet)
urlpatterns = [

View File

@@ -9,7 +9,7 @@ from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Dept, RoleApiPermission, Users
from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users
from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -135,7 +135,7 @@ class DeptViewSet(CustomModelViewSet):
queryset = Dept.objects.values('id', 'name', 'parent')
else:
role_ids = request.user.role.values_list('id', flat=True)
data_range = RoleApiPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
user_dept_id = request.user.dept.id
dept_list = [user_dept_id]
data_range_list = list(set(data_range))

View File

@@ -8,10 +8,10 @@
"""
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Menu, RoleMenuPermission
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -21,7 +21,7 @@ class MenuSerializer(CustomModelSerializer):
菜单表的简单序列化器
"""
menuPermission = serializers.SerializerMethodField(read_only=True)
hasChildren = serializers.SerializerMethodField()
hasChild = serializers.SerializerMethodField()
def get_menuPermission(self, instance):
queryset = instance.menuPermission.order_by('-name').values('id', 'name', 'value')
@@ -31,7 +31,7 @@ class MenuSerializer(CustomModelSerializer):
else:
return None
def get_hasChildren(self, instance):
def get_hasChild(self, instance):
hasChild = Menu.objects.filter(parent=instance.id)
if hasChild:
return True
@@ -71,7 +71,7 @@ class WebRouterSerializer(CustomModelSerializer):
class Meta:
model = Menu
fields = (
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'menu_type', 'web_path', 'component',
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible', 'status')
read_only_fields = ["id"]
@@ -90,57 +90,45 @@ class MenuViewSet(CustomModelViewSet):
create_serializer_class = MenuCreateSerializer
update_serializer_class = MenuCreateSerializer
search_fields = ['name', 'status']
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'menu_type']
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
@action(methods=['get'], detail=False)
def tree(self, request):
def list(self, request):
"""懒加载"""
request.query_params._mutable = True
params = request.query_params
menu_type = params.get('menu_type', 0)
queryset = Menu.objects.filter(menu_type=menu_type).order_by('sort')
parent = params.get('parent', None)
page = params.get('page', None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
if params:
if parent:
queryset = self.queryset.filter(parent=parent)
else:
queryset = self.queryset.filter()
else:
queryset = self.queryset.filter(parent__isnull=True)
queryset = self.filter_queryset(queryset)
serializer = MenuSerializer(queryset, many=True, request=request)
data = serializer.data
return DetailResponse(data=data)
@action(methods=['get'], detail=True)
def getChildren(self,request,pk):
"""
获取子菜单,用于菜单页面的懒加载
"""
queryset = Menu.objects.filter(parent=pk)
serializer = MenuSerializer(queryset, many=True, request=request)
data = serializer.data
return DetailResponse(data=data)
return SuccessResponse(data=data)
@action(methods=['GET'], detail=False, permission_classes=[])
def web_router(self, request):
"""用于前端获取当前角色的路由"""
user = request.user
if user.is_superuser:
queryset = self.queryset.filter(status=1,menu_type__in=[0,1])
queryset = self.queryset.filter(status=1)
else:
role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
queryset = Menu.objects.filter(id__in=menu_list,menu_type__in=[0,1])
queryset = Menu.objects.filter(id__in=menu_list)
serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def menu_button_all_permission(self, request):
"""
获取所有的按钮权限
:param request:
:return:
"""
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Menu.objects.filter(menu_type=2).values_list('component', flat=True)
else:
role_id = request.user.role.values_list('id', flat=True)
queryset = Menu.objects.filter(role__in=role_id,menu_type=2).values_list('component',flat=True).distinct()
return DetailResponse(data=queryset)
@action(methods=['GET'], detail=False, permission_classes=[])
def get_all_menu(self, request):
"""用于菜单管理获取所有的菜单"""

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""
@author: 猿小天
@contact: QQ:1638245306
@Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理
"""
from django.db.models import F
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class MenuButtonSerializer(CustomModelSerializer):
"""
菜单按钮-序列化器
"""
class Meta:
model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method','menu']
read_only_fields = ["id"]
class MenuButtonCreateUpdateSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = MenuButton
fields = "__all__"
read_only_fields = ["id"]
class MenuButtonViewSet(CustomModelViewSet):
"""
菜单按钮接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = MenuButton.objects.order_by('create_datetime')
serializer_class = MenuButtonSerializer
create_serializer_class = MenuButtonCreateUpdateSerializer
update_serializer_class = MenuButtonCreateUpdateSerializer
extra_filter_class = []
def list(self, request, *args, **kwargs):
"""
重写list方法
:param request:
:param args:
:param kwargs:
:return:
"""
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(serializer.data,msg="获取成功")
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
def menu_button_all_permission(self,request):
"""
获取所有的按钮权限
:param request:
:return:
"""
is_superuser = request.user.is_superuser
if is_superuser:
queryset = MenuButton.objects.values_list('value',flat=True)
else:
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()
return DetailResponse(data=queryset)

View File

@@ -1,52 +1,51 @@
# -*- coding: utf-8 -*-
from django.apps import apps
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Columns, Role
from dvadmin.system.models import Role, MenuField
from dvadmin.utils.models import get_custom_app_models
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse
class ColumnSerializer(CustomModelSerializer):
class MenuFieldSerializer(CustomModelSerializer):
"""
列权限序列化器
"""
class Meta:
model = Columns
model = MenuField
fields = '__all__'
read_only_fields = ['id']
class ColumnViewSet(CustomModelViewSet):
class MenuFieldViewSet(CustomModelViewSet):
"""
列权限视图集
"""
queryset = Columns.objects.all()
serializer_class = ColumnSerializer
queryset = MenuField.objects.all()
serializer_class = MenuFieldSerializer
def list(self, request, *args, **kwargs):
role_id = request.query_params.get('role')
app_name = request.query_params.get('app')
model_name = request.query_params.get('model')
if not role_id or not model_name or not app_name:
return ErrorResponse(msg="参数错误")
queryset = Columns.objects.filter(role_id=role_id, model=model_name, app=app_name)
serializer = ColumnSerializer(queryset, many=True, request=request)
menu = request.query_params.get('menu')
if not menu:
return SuccessResponse([])
queryset = self.filter_queryset(self.get_queryset().filter(menu=menu))
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")
def create(self, request, *args, **kwargs):
payload = request.data
for model in get_custom_app_models(payload.get('app')):
if payload.get('model') == model['model']:
for model in apps.get_models():
if payload.get('model') == model.__name__:
break
else:
return ErrorResponse(msg='模型表不存在')
if Columns.objects.filter(app=model['app'], model=model['model'], field_name=payload.get('field_name')).exists():
if MenuField.objects.filter(menu=payload.get('menu'),model=model.__name__, field_name=payload.get('field_name')).exists():
return ErrorResponse(msg='%s 字段权限已有,不可重复创建' % payload.get('title'))
return super().create(request, *args, **kwargs)
@@ -55,34 +54,31 @@ class ColumnViewSet(CustomModelViewSet):
def get_models(self, request):
"""获取所有项目app下的model"""
res = []
for app in get_custom_app_models():
for model in app:
res.append({
'app': model['app'],
'title': model['verbose'],
'key': model['model']
})
for model in get_custom_app_models():
res.append({
'app': model['app'],
'title': model['verbose'],
'key': model['model']
})
return DetailResponse(res)
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
def auto_match_fields(self, request):
"""自动匹配已有的字段"""
role_id = request.data.get('role')
app_name = request.data.get('app')
menu_id = request.data.get('menu')
model_name = request.data.get('model')
if not role_id or not model_name or not app_name:
return DetailResponse([], msg='无操作')
for model in get_custom_app_models(app_name):
if not menu_id or not model_name:
return ErrorResponse( msg='参数错误')
for model in get_custom_app_models():
if model['model'] != model_name:
continue
for field in model['fields']:
if Columns.objects.filter(
role_id=role_id, app=app_name, model=model_name, field_name=field['name']
if MenuField.objects.filter(
menu_id=menu_id, model=model_name, field_name=field['name']
).exists():
continue
data = {
'role': role_id,
'app': app_name,
'menu': menu_id,
'model': model_name,
'field_name': field['name'],
'title': str(field['title']),
@@ -90,4 +86,4 @@ class ColumnViewSet(CustomModelViewSet):
serializer = self.get_serializer(data=data, request=request)
serializer.is_valid(raise_exception=True)
serializer.save()
return DetailResponse(msg='匹配成功')
return SuccessResponse(msg='匹配成功')

View File

@@ -13,6 +13,7 @@ from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Role, Menu, MenuButton, Dept
from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse
@@ -38,6 +39,7 @@ class RoleCreateUpdateSerializer(CustomModelSerializer):
"""
menu = MenuSerializer(many=True, read_only=True)
dept = DeptSerializer(many=True, read_only=True)
permission = MenuButtonSerializer(many=True, read_only=True)
key = serializers.CharField(max_length=50,
validators=[CustomUniqueValidator(queryset=Role.objects.all(), message="权限字符必须唯一")])
name = serializers.CharField(max_length=50, validators=[CustomUniqueValidator(queryset=Role.objects.all())])
@@ -61,11 +63,21 @@ class MenuPermissionSerializer(CustomModelSerializer):
"""
菜单的按钮权限
"""
menuPermission = serializers.SerializerMethodField()
def get_menuPermission(self, instance):
is_superuser = self.request.user.is_superuser
if is_superuser:
queryset = MenuButton.objects.filter(menu__id=instance.id)
else:
menu_permission_id_list = self.request.user.role.values_list('permission', flat=True)
queryset = MenuButton.objects.filter(id__in=menu_permission_id_list, menu__id=instance.id)
serializer = MenuButtonSerializer(queryset, many=True, read_only=True)
return serializer.data
class Meta:
model = Menu
fields = ['id', 'parent', 'name']
fields = ['id', 'parent', 'name', 'menuPermission']
class MenuButtonPermissionSerializer(CustomModelSerializer):

View File

@@ -1,245 +0,0 @@
# -*- coding: utf-8 -*-
"""
@author: 猿小天
@contact: QQ:1638245306
@Created on: 2021/6/3 003 0:30
@Remark: 接口权限管理
"""
from django.db.models import F, Subquery, OuterRef, Exists
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleApiPermission, Menu, MenuButton, Dept, RoleMenuPermission, Columns
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class RoleApiPermissionSerializer(CustomModelSerializer):
"""
接口权限-序列化器
"""
dept_name = serializers.SerializerMethodField(help_text="部门名称")
def get_dept_name(self, instance):
dept_name_list = instance.dept.values_list("name",flat=True)
return ",".join(dept_name_list)
class Meta:
model = RoleApiPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleApiPermissionCreateUpdateSerializer(CustomModelSerializer):
"""
初始化接口权限-序列化器
"""
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
class Meta:
model = RoleApiPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleButtonPermissionSerializer(CustomModelSerializer):
"""
角色按钮权限
"""
isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
return RoleApiPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_data_range(self, instance):
params = self.request.query_params
obj = RoleApiPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).first()
if obj is None:
return None
return obj.data_range
class Meta:
model = MenuButton
fields = ['id','name','value','isCheck','data_range']
class RoleColumnsSerializer(CustomModelSerializer):
class Meta:
model = Columns
fields = "__all__"
class RoleMenuPermissionSerializer(CustomModelSerializer):
"""
菜单和按钮权限
"""
isCheck = serializers.SerializerMethodField()
btns = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuPermission.objects.filter(
menu__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_btns(self, instance):
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
serializer = RoleButtonPermissionSerializer(btn_list,many=True,request=self.request)
return serializer.data
def get_columns(self, instance):
params = self.request.query_params
col_list = Columns.objects.filter(role__id=params.get('role'))
serializer = RoleColumnsSerializer(col_list,many=True,request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id','name','isCheck','btns','columns']
class RoleApiPermissionViewSet(CustomModelViewSet):
"""
接口权限接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = RoleApiPermission.objects.all()
serializer_class = RoleApiPermissionSerializer
create_serializer_class = RoleApiPermissionCreateUpdateSerializer
update_serializer_class = RoleApiPermissionCreateUpdateSerializer
extra_filter_class = []
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def data_scope(self, request):
"""
获取数据权限范围:角色授权页面使用
:param request:
:return:
"""
is_superuser = request.user.is_superuser
if is_superuser:
data = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
]
return DetailResponse(data=data)
else:
data = []
role_list = request.user.role.values_list('id', flat=True)
if params := request.query_params:
if menu_button_id := params.get('menu_button', None):
role_queryset = RoleApiPermission.objects.filter(
role__in=role_list, menu_button__id=menu_button_id
).values_list('data_range', flat=True)
data_range_list = list(set(role_queryset))
for item in data_range_list:
if item == 0:
data = [{
"value": 0,
"label": '仅本人数据权限'
}]
elif item == 1:
data = [{
"value": 0,
"label": '仅本人数据权限'
}, {
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 2:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 3:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 3,
"label": '全部数据权限'
}, ]
elif item == 4:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}]
else:
data = []
return DetailResponse(data=data)
return ErrorResponse(msg="参数错误")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_dept_all(self, request):
"""
当前用户角色下所能授权的部门:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Dept.objects.values('id', 'name', 'parent')
else:
if not params:
return ErrorResponse(msg="参数错误")
menu_button = params.get('menu_button')
if menu_button is None:
return ErrorResponse(msg="参数错误")
role_list = request.user.role.values_list('id', flat=True)
dept_ids = RoleApiPermission.objects.filter(role__in=role_list).values_list('dept__id',flat=True)
queryset = Dept.objects.filter(id__in=dept_ids).values('id', 'name', 'parent')
return DetailResponse(data=queryset)

View File

@@ -58,38 +58,8 @@ class RoleMenuPermissionViewSet(CustomModelViewSet):
update_serializer_class = RoleMenuPermissionCreateUpdateSerializer
extra_filter_class = []
@action(methods=['get'],detail=False)
def menu_permission_tree(self,request):
"""
获取菜单按钮树
"""
# params = request.query_params
# role_id = params.get('role',None)
# if role_id is None:
# return ErrorResponse(msg="未获取到角色")
if request.user.is_superuser:
queryset = Menu.objects.filter(status=1).values("id", "name", "parent_id")
else:
role_id = request.user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('menu_id', flat=True)
queryset = Menu.objects.filter(status=1, id__in=menu_list).values('id','name', "parent_id").all()
return DetailResponse(data=queryset)
@action(methods=['get'],detail=False)
def get_menu_permission_checked(self,request):
"""
获取已授权的菜单ID
"""
params = request.query_params
role_id = params.get('role',None)
if role_id is None:
return ErrorResponse(msg="未获取到角色")
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('menu_id', flat=True)
queryset = Menu.objects.filter(status=1, id__in=menu_list).values_list('id',flat=True)
return DetailResponse(data=queryset)
@action(methods=['post'],detail=False)
def save_menu_permission(self,request):
def save_auth(self,request):
"""
保存页面菜单授权
:param request:

View File

@@ -0,0 +1,395 @@
# -*- coding: utf-8 -*-
"""
@author: 猿小天
@contact: QQ:1638245306
@Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理
"""
from django.db.models import F, Subquery, OuterRef, Exists
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \
MenuField
from dvadmin.system.views.menu import MenuSerializer
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
"""
菜单按钮-序列化器
"""
class Meta:
model = RoleMenuButtonPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
class Meta:
model = RoleMenuButtonPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleButtonPermissionSerializer(CustomModelSerializer):
"""
角色按钮权限
"""
isCheck = serializers.SerializerMethodField()
data_range = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_data_range(self, instance):
params = self.request.query_params
obj = RoleMenuButtonPermission.objects.filter(
menu_button__id=instance['id'],
role__id=params.get('role'),
).first()
if obj is None:
return None
return obj.data_range
class Meta:
model = MenuButton
fields = ['id','name','value','isCheck','data_range']
class RoleFieldPermissionSerializer(CustomModelSerializer):
class Meta:
model = FieldPermission
fields = "__all__"
class RoleMenuFieldSerializer(CustomModelSerializer):
is_query = serializers.SerializerMethodField()
is_create = serializers.SerializerMethodField()
is_update = serializers.SerializerMethodField()
def get_is_query(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
if queryset:
return queryset.is_query
return False
def get_is_create(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
if queryset:
return queryset.is_create
return False
def get_is_update(self, instance):
params = self.request.query_params
queryset = instance.menu_field.filter(role=params.get('role')).first()
if queryset:
return queryset.is_update
return False
class Meta:
model = MenuField
fields = ['id','field_name','title','is_query','is_create','is_update']
class RoleMenuPermissionSerializer(CustomModelSerializer):
"""
菜单和按钮权限
"""
isCheck = serializers.SerializerMethodField()
btns = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField()
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuPermission.objects.filter(
menu__id=instance['id'],
role__id=params.get('role'),
).exists()
def get_btns(self, instance):
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
serializer = RoleButtonPermissionSerializer(btn_list,many=True,request=self.request)
return serializer.data
def get_columns(self, instance):
col_list = MenuField.objects.filter(menu=instance['id'])
serializer = RoleMenuFieldSerializer(col_list,many=True,request=self.request)
return serializer.data
class Meta:
model = Menu
fields = ['id','name','isCheck','btns','columns']
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
"""
菜单按钮接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = RoleMenuButtonPermission.objects.all()
serializer_class = RoleMenuButtonPermissionSerializer
create_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer
update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer
extra_filter_class = []
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def get_role_premission(self, request):
"""
角色授权获取:
:param request: role
:return: menu,btns,columns
"""
params = request.query_params
role = params.get('role',None)
if role is None:
return ErrorResponse(msg="未获取到角色信息")
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Menu.objects.filter(status=1,is_catalog=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)
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_premission(self,request,pk):
"""
对角色的菜单和按钮及按钮范围授权:
:param request:
:param pk: role
:return:
"""
body = request.data
RoleMenuPermission.objects.filter(role=pk).delete()
RoleMenuButtonPermission.objects.filter(role=pk).delete()
for menu in body:
if menu.get('isCheck'):
menu_parent = Menu.objects.filter(id=menu.get('id')).values('parent').first()
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu_parent.get('parent'))
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
for btn in menu.get('btns'):
if btn.get('isCheck'):
data_range = btn.get('data_range',0) or 0
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),data_range=data_range)
instance.dept.set(btn.get('dept',[]))
for col in menu.get('columns'):
FieldPermission.objects.update_or_create(role_id=pk,field_id=col.get('id'),is_query=col.get('is_query'),is_create=col.get('is_create'),is_update=col.get('is_update'))
return DetailResponse(msg="授权成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_menu_get_button(self, request):
"""
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
:param request:
:return:
"""
if params := request.query_params:
if menu_id := params.get('menu', None):
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
else:
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__menu=menu_id
).values(btn_id=F('menu_button__id'), name=F('menu_button__name'))
return DetailResponse(data=queryset)
return ErrorResponse(msg="参数错误")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def data_scope(self, request):
"""
获取数据权限范围:角色授权页面使用
:param request:
:return:
"""
is_superuser = request.user.is_superuser
if is_superuser:
data = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
]
return DetailResponse(data=data)
else:
data = []
role_list = request.user.role.values_list('id', flat=True)
if params := request.query_params:
if menu_button_id := params.get('menu_button', None):
role_queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__id=menu_button_id
).values_list('data_range', flat=True)
data_range_list = list(set(role_queryset))
for item in data_range_list:
if item == 0:
data = [{
"value": 0,
"label": '仅本人数据权限'
}]
elif item == 1:
data = [{
"value": 0,
"label": '仅本人数据权限'
}, {
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 2:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 3:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 3,
"label": '全部数据权限'
}, ]
elif item == 4:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}]
else:
data = []
return DetailResponse(data=data)
return ErrorResponse(msg="参数错误")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_dept_all(self, request):
"""
当前用户角色下所能授权的部门:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = Dept.objects.values('id', 'name', 'parent')
else:
if not params:
return ErrorResponse(msg="参数错误")
menu_button = params.get('menu_button')
if menu_button is None:
return ErrorResponse(msg="参数错误")
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values(
dept_id=F('dept__id'),
name=F('dept__name'),
parent=F('dept__parent')
)
return DetailResponse(data=queryset)
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def menu_to_button(self, request):
"""
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
menu_id = params.get('menu', None)
if menu_id is None:
return ErrorResponse(msg="未获取到参数")
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
else:
if params:
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button',
'menu_button__name',
'menu_button__value'
)
return DetailResponse(data=queryset)
return ErrorResponse(msg="未获取到参数")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_menu(self, request):
"""
获取角色对应的按钮权限
:param request:
:return:
"""
params = request.query_params
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
return DetailResponse(data=queryset)

View File

@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
from django.db.models import F
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Columns
from dvadmin.system.models import FieldPermission, MenuField
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.models import get_custom_app_models
@@ -14,25 +15,24 @@ class FieldPermissionMixin:
获取字段权限
"""
finded = False
for app in get_custom_app_models():
for model in app:
if model['object'] is self.serializer_class.Meta.model:
finded = True
break
for model in get_custom_app_models():
if model['object'] is self.serializer_class.Meta.model:
finded = True
break
if finded:
break
if finded is False:
return []
roles = request.user.role.values_list('id', flat=True)
user = request.user
if user.is_superuser==1:
data = Columns.objects.filter(app=model['app'], model=model['model']).values('field_name', 'is_create', 'is_query', 'is_update')
data = MenuField.objects.filter( model=model['model']).values('field_name')
for item in data:
item['is_create'] = True
item['is_query'] = True
item['is_update'] = True
else:
data= Columns.objects.filter(
app=model['app'], model=model['model'],role__in=roles
).values('field_name', 'is_create', 'is_query', 'is_update')
roles = request.user.role.values_list('id', flat=True)
data= FieldPermission.objects.filter(
field__model=model['model'],role__in=roles
).values( 'is_create', 'is_query', 'is_update',field_name=F('field__field_name'))
return DetailResponse(data=data)

View File

@@ -20,7 +20,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend
from dvadmin.system.models import Dept, ApiWhiteList, RoleApiPermission
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
def get_dept(dept_id: int, dept_all_list=None, dept_list=None):
@@ -116,12 +116,12 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
if _pk: # 判断是否是单例查询
re_api = re.sub(_pk,'{id}', api)
role_id_list = request.user.role.values_list('id', flat=True)
role_permission_list=RoleApiPermission.objects.filter(
role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
api=re_api,
method=method).values(
'data_range',
menu_button__api=re_api,
menu_button__method=method).values(
'data_range'
)
dataScope_list = [] # 权限范围列表
for ele in role_permission_list:

View File

@@ -13,6 +13,7 @@ from django.db import models
from django.conf import settings
from application import settings
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
@@ -71,10 +72,13 @@ class CoreModel(models.Model):
id = models.BigAutoField(primary_key=True, help_text="Id", verbose_name="Id")
description = models.CharField(max_length=255, verbose_name="描述", null=True, blank=True, help_text="描述")
creator = models.ForeignKey(to=settings.AUTH_USER_MODEL, related_query_name='creator_query', null=True,
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL, db_constraint=False)
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL,
db_constraint=False)
modifier = models.CharField(max_length=255, null=True, blank=True, help_text="修改人", verbose_name="修改人")
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True, verbose_name="数据归属部门")
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间")
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True,
verbose_name="数据归属部门")
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间",
verbose_name="修改时间")
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
verbose_name="创建时间")
@@ -136,10 +140,23 @@ def get_model_from_app(app_name):
def get_custom_app_models(app_name=None):
"""获取所有项目写的app里的models"""
"""
获取所有项目下的app里的models
"""
if app_name:
return get_model_from_app(app_name)
all_apps = apps.get_app_configs()
res = []
for app in settings.CUSTOM_APPS:
res.append(get_model_from_app(app))
for app in all_apps:
if app.name.startswith('django'):
continue
if app.name in settings.COLUMN_EXCLUDE_APPS:
continue
try:
all_models = get_model_from_app(app.name)
if all_models:
for model in all_models:
res.append(model)
except Exception as e:
pass
return res

View File

@@ -12,7 +12,7 @@ from django.contrib.auth.models import AnonymousUser
from django.db.models import F
from rest_framework.permissions import BasePermission
from dvadmin.system.models import ApiWhiteList, RoleApiPermission
from dvadmin.system.models import ApiWhiteList, RoleMenuButtonPermission
def ValidationApi(reqApi, validApi):
@@ -74,18 +74,18 @@ class CustomPermission(BasePermission):
methodList = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']
method = methodList.index(method)
# ***接口白名单***
api_white_list = ApiWhiteList.objects.values('method',api=F('url'))
api_white_list = ApiWhiteList.objects.values(permission__api=F('url'), permission__method=F('method'))
api_white_list = [
str(item.get('api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
item.get('method')) + '$' for item in api_white_list if item.get('api')]
str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
item.get('permission__method')) + '$' for item in api_white_list if item.get('permission__api')]
# ********#
if not hasattr(request.user, "role"):
return False
role_id_list = request.user.role.values_list('id',flat=True)
userApiList = RoleApiPermission.objects.filter(role__in=role_id_list).values('api','method') # 获取当前用户的角色拥有的所有接口
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
ApiList = [
str(item.get('api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
item.get('method')) + '$' for item in userApiList if item.get('api')]
str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')]
new_api_ist = api_white_list + ApiList
new_api = api + ":" + str(method)
for item in new_api_ist:

View File

@@ -17,7 +17,7 @@ from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSeria
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
from dvadmin.utils.permission import CustomPermission
from dvadmin.utils.models import get_custom_app_models
from dvadmin.system.models import Columns
from dvadmin.system.models import FieldPermission, MenuField
from django_restql.mixins import QueryArgumentsMixin
@@ -64,7 +64,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
serializer_class = self.get_serializer_class()
kwargs.setdefault('context', self.get_serializer_context())
# 全部以可见字段为准
can_see = self.get_column_permission(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:
@@ -79,21 +79,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
else:
return serializer_class(*args, **kwargs)
def get_column_permission(self, serializer_class):
"""获取权限"""
def get_menu_field(self, serializer_class):
"""获取字段权限"""
finded = False
for app in get_custom_app_models():
for model in app:
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded:
for model in get_custom_app_models():
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded is False:
return []
return Columns.objects.filter(
app=model['app'], model=model['model']
).values('field_name', 'is_create', 'is_query', 'is_update')
return MenuField.objects.filter(model=model['model']
).values('field_name', 'title')
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request)

View File

@@ -1,4 +1,4 @@
Django==4.2.6
Django==4.2.7
django-comment-migrate==0.1.7
django-cors-headers==4.3.0
django-filter==23.3

View File

@@ -28,6 +28,6 @@ server {
proxy_send_timeout 600s;
real_ip_header X-Forwarded-For;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://177.8.0.12:8000/; # 设置代理服务器的协议和地址
proxy_pass http://177.10.0.12:8000/; # 设置代理服务器的协议和地址
}
}

View File

@@ -1,7 +1,7 @@
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine
WORKDIR /web/
COPY web/. .
RUN yarn install
RUN yarn install --registry=https://registry.npm.taobao.org
RUN yarn build
FROM nginx:alpine

View File

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

168
web/README.en.md Normal file
View File

@@ -0,0 +1,168 @@
# Django-Vue3-Admin
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/huge-dream/django-vue3-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/huge-dream/django-vue3-admin/badge/star.svg?theme=dark)](https://gitee.com/huge-dream/django-vue3-admin)
[preview](https://demo.dvadmin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
💡 **「About」**
We are a group of young people who love Code. In this hot era, we hope to calm down and bring some of our colors and colors through code.
Because of love, so embrace the future
## framework introduction
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) Is a set of all open source rapid development platform, no reservation for individuals and enterprises free use.
* 🧑🤝🧑Front-end adoption Vue3+TS+pinia+fastcrud。
* 👭The backend uses the Python language Django framework as well as the powerful[Django REST Framework](https://pypi.org/project/djangorestframework)。
* 👫Permission authentication use[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt)Supports the multi-terminal authentication system.
* 👬Support loading dynamic permission menu, multi - way easy permission control.
* 💏 Special thanks: [vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/).
* 💡 💏 Special thanks:[jetbrains](https://www.jetbrains.com/) To provide a free IntelliJ IDEA license for this open source project.
## Online experience
👩‍👧‍👦👩‍👧‍👦 demo address:[https://demo.dvadmin.com](https://demo.dvadmin.com)
* demo accountsuperadmin
* demo passwordadmin123456
👩👦👦docs:[https://django-vue-admin.com](https://django-vue-admin.com)
## communication
* Communication community:[click here](https://bbs.django-vue-admin.com)👩‍👦‍👦
* plugins market:[click here](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦
## source code url:
gitee(Main push)[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩‍👦‍👦
githubno data
## core function
1. 👨‍⚕️ Menu management: Configure the system menu, operation permissions, button permissions, back-end interface permissions, etc.
2. 🧑‍⚕️ Department management: Configure the system organization (company, department, role).
3. 👩‍⚕️ Role management: role menu permission allocation, data permission allocation, set roles according to the department for data range permission division.
4. 🧑‍🎓 Rights Specifies the rights of the authorization role.
5. 👨‍🎓 User management: The user is the system operator, this function mainly completes the system user configuration.
6. 👬 Interface whitelist: specifies the interface that does not need permission verification.
7. 🧑‍🔧 Dictionary management: Maintenance of some fixed data frequently used in the system.
8. 🧑‍🔧 Regional management: to manage provinces, cities, counties and regions.
9. 📁 Attachment management: Unified management of all files and pictures on the platform.
10. 🗓 operation logs: log and query the system normal operation; Log and query system exception information.
11.🔌 [plugins market] (<https://bbs.django-vue-admin.com/plugMarket.html>) : based on the Django framework - Vue - Admin application and plug-in development.
## plugins market 🔌
* Celery Asynchronous task[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
* Upgrade center backend[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
* Upgrade center front[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
## before start project you need:
~~~
Python >= 3.8.0
nodejs >= 14.0
Mysql >= 5.7.0 (Optional. The default database is sqlite3. 8.0 is recommended)
Redis(Optional, the latest edition)
~~~
## frontend♝
```bash
# clone code
git clone https://gitee.com/huge-dream/django-vue3-admin.git
# enter code dir
cd web
# install dependence
npm install --registry=https://registry.npm.taobao.org
# Start service
npm run dev
# Visit http://localhost:8080 in your browser
# Parameters such as boot port can be configured in the #.env.development file
# Build the production environment
# npm run build
```
## backend💈
~~~bash
1. enter code dir cd backend
2. copy ./conf/env.example.py to ./conf dirrename as env.py
3. in env.py configure database information
mysql database recommended version: 8.0
mysql database character set: utf8mb4
4. install pip dependence
pip3 install -r requirements.txt
5. Execute the migration command:
python3 manage.py makemigrations
python3 manage.py migrate
6. Initialization data
python3 manage.py init
7. Initialize provincial, municipal and county data:
python3 manage.py init_area
8. start backend
python3 manage.py runserver 0.0.0.0:8000
or daphne :
daphne -b 0.0.0.0 -p 8000 application.asgi:application
~~~
### visit backend swagger
* visit url[http://localhost:8080](http://localhost:8080) (The default address is this one. If you want to change it, follow the configuration file)
* account`superadmin` password`admin123456`
### docker-compose
~~~shell
docker-compose up -d
# Initialize backend data (first execution only)
docker exec -ti dvadmin-django bash
python manage.py makemigrations
python manage.py migrate
python manage.py init_area
python manage.py init
exit
frontend urlhttp://127.0.0.1:8080
backend urlhttp://127.0.0.1:8080/api
# Change 127.0.0.1 to your own public ip address on the server
account`superadmin` password`admin123456`
# docker-compose stop
docker-compose down
# docker-compose restart
docker-compose restart
# docker-compose on start build
docker-compose up -d --build
~~~
## Demo screenshot✅
![image-01](https://images.gitee.com/uploads/images/2022/0530/234137_b58c8f98_5074988.png)
![image-02](https://images.gitee.com/uploads/images/2022/0530/234240_39834603_5074988.png)
![image-03](https://images.gitee.com/uploads/images/2022/0530/234339_35e728a0_5074988.png)
![image-04](https://images.gitee.com/uploads/images/2022/0530/234426_957036b0_5074988.png)
![image-05](https://images.gitee.com/uploads/images/2022/0530/234458_898be492_5074988.png)
![image-06](https://images.gitee.com/uploads/images/2022/0530/234521_35b40076_5074988.png)
![image-07](https://images.gitee.com/uploads/images/2022/0530/234615_c2325639_5074988.png)
![image-08](https://images.gitee.com/uploads/images/2022/0530/234639_1ed6cc93_5074988.png)
![image-09](https://images.gitee.com/uploads/images/2022/0530/234815_cea2c53f_5074988.png)
![image-10](https://images.gitee.com/uploads/images/2022/0530/234840_5f3e5f53_5074988.png)

View File

@@ -1,8 +1,32 @@
<div align="center">django-vue3-admin:web </div>
# 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)
django-vue3-admin,基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!
[预 览](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带来一点我们的色彩和颜色。
因为热爱,所以拥抱未来!
## 平台简介
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
django-vue3-admin 基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!
* 🧑‍🤝‍🧑前端采用 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)。
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
* 👬支持加载动态权限菜单,多方式轻松权限控制。
* 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。
* 💡 特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。
#### 🏭 环境支持
@@ -12,21 +36,173 @@ django-vue3-admin基于 vue3 + CompositionAPI + typescript + vite + element p
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
#### ⚡ 使用说明
建议使用 yarnyarn 是一个类似于npm的包管理器 <a href="http://nodejs.cn/" target="_blank">node 版本 > 16</a>
## 在线体验
👩‍👧‍👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
- 账号superadmin
- 密码admin123456
👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
## 交流
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦
- 插件市场:[戳我](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交流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)
- 二维码
<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)👩‍👦‍👦
github地址暂无
## 内置功能
1. 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
2. 🧑‍⚕️部门管理:配置系统组织机构(公司、部门、角色)。
3. 👩‍⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
4. 🧑‍🎓按钮权限权限:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
5. 🧑‍🎓字段权限权限:授权页面的字段显示权限。
5. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
6. 👬接口白名单:配置不需要进行权限校验的接口。
7. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
8. 🧑‍🔧地区管理:对省市县区域进行管理。
9. 📁附件管理:对平台上所有文件、图片等进行统一管理。
10. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
11. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌
- Celery异步任务[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
- 升级中心后端:[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
- 升级中心前端:[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
## 准备工作
~~~
Python >= 3.8.0 (推荐3.8+版本)
nodejs >= 14.0 (推荐最新)
Mysql >= 5.7.0 (可选默认数据库sqlite3推荐8.0版本)
Redis(可选,最新版)
~~~
## 前端♝
```bash
# 克隆项目
git clone https://gitee.com/huge-dream/django-vue3-admin.git
# 进入项目
cd django-vue-admin/web
# 进入项目目录
cd web
# 安装依赖
yarn install
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
# 浏览器访问 http://localhost:8080
# .env.development 文件中可配置启动端口等参数
# 构建生产环境
# npm run build
```
## 后端💈
~~~bash
1. 进入项目目录 cd backend
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
3. 在 env.py 中配置数据库信息
mysql数据库版本建议8.0
mysql数据库字符集utf8mb4
4. 安装依赖环境
pip3 install -r requirements.txt
5. 执行迁移命令:
python3 manage.py makemigrations
python3 manage.py migrate
6. 初始化数据
python3 manage.py init
7. 初始化省市县数据:
python3 manage.py init_area
8. 启动项目
python3 manage.py runserver 0.0.0.0:8000
或使用 daphne :
daphne -b 0.0.0.0 -p 8000 application.asgi:application
~~~
### 访问项目
- 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
- 账号:`superadmin` 密码:`admin123456`
### docker-compose 运行
~~~shell
# 先安装docker-compose (自行百度安装),执行此命令等待安装如有使用celery插件请打开docker-compose.yml中celery 部分注释
docker-compose up -d
# 初始化后端数据(第一次执行即可)
docker exec -ti dvadmin-django bash
python manage.py makemigrations
python manage.py migrate
python manage.py init_area
python manage.py init
exit
前端地址http://127.0.0.1:8080
后端地址http://127.0.0.1:8080/api
# 在服务器上请把127.0.0.1 换成自己公网ip
账号superadmin 密码admin123456
# docker-compose 停止
docker-compose down
# docker-compose 重启
docker-compose restart
# docker-compose 启动时重新进行 build
docker-compose up -d --build
~~~
## 演示图✅
![image-01](https://images.gitee.com/uploads/images/2022/0530/234137_b58c8f98_5074988.png)
![image-02](https://images.gitee.com/uploads/images/2022/0530/234240_39834603_5074988.png)
![image-03](https://images.gitee.com/uploads/images/2022/0530/234339_35e728a0_5074988.png)
![image-04](https://images.gitee.com/uploads/images/2022/0530/234426_957036b0_5074988.png)
![image-05](https://images.gitee.com/uploads/images/2022/0530/234458_898be492_5074988.png)
![image-06](https://images.gitee.com/uploads/images/2022/0530/234521_35b40076_5074988.png)
![image-07](https://images.gitee.com/uploads/images/2022/0530/234615_c2325639_5074988.png)
![image-08](https://images.gitee.com/uploads/images/2022/0530/234639_1ed6cc93_5074988.png)
![image-09](https://images.gitee.com/uploads/images/2022/0530/234815_cea2c53f_5074988.png)
![image-10](https://images.gitee.com/uploads/images/2022/0530/234840_5f3e5f53_5074988.png)
# 运行项目
yarn dev
# 打包发布
yarn build
```

View File

@@ -10,23 +10,22 @@
/>
<meta
name="description"
content="django-vue3-admin基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!"
content="django-vue-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus"
/>
<link rel="icon" href="/favicon.ico" />
<title>django-vue3-admin</title>
<title>django-vue-admin</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
// let _hmt = _hmt || [];
(function () {
let hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4';
let s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<script type="module" src="/src/main.ts"></script>
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script>
<div id="app"></div>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?9ba8fc809b5584167a2fb9b31bb3970c";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,7 +1,7 @@
{
"name": "django-vue3-admin",
"version": "1.0.0",
"description": "django-vue3-admin基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!",
"version": "3.0.0",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT",
"scripts": {
"dev": "vite --force",
@@ -10,10 +10,10 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.10",
"@fast-crud/fast-crud": "^1.18.3",
"@fast-crud/fast-extends": "^1.18.3",
"@fast-crud/ui-element": "^1.18.3",
"@fast-crud/ui-interface": "^1.18.3",
"@fast-crud/fast-crud": "^1.19.2",
"@fast-crud/fast-extends": "^1.19.2",
"@fast-crud/ui-element": "^1.19.2",
"@fast-crud/ui-interface": "^1.19.2",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
@@ -62,7 +62,7 @@
"@typescript-eslint/parser": "^5.46.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/compiler-sfc": "^3.2.45",
"eslint": "^8.29.0",
"eslint": "^8.54.0",
"eslint-plugin-vue": "^9.8.0",
"prettier": "^2.8.1",
"sass": "^1.56.2",

View File

@@ -59,12 +59,6 @@ onBeforeMount(() => {
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
//websockt 模块
try {
//websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
}
});
// 页面加载时
onMounted(() => {
@@ -93,6 +87,14 @@ watch(
() => route.path,
() => {
other.useTitle();
if (!websocket.websocket) {
//websockt 模块
try {
websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
}
}
},
{
deep: true,

View File

@@ -159,14 +159,11 @@ const initModeValueEcho = () => {
// 处理 icon 类型用于回显时tab 高亮与初始化数据
const initFontIconName = () => {
let name = 'ali';
if(props.modelValue){
if (props.modelValue!.indexOf('iconfont') > -1) name = 'ali';
else if (props.modelValue!.indexOf('ele-') > -1) name = 'ele';
else if (props.modelValue!.indexOf('fa') > -1) name = 'awe';
// 初始化 tab 高亮回显
state.fontIconTabActive = name;
}
if (props.modelValue!.indexOf('iconfont') > -1) name = 'ali';
else if (props.modelValue!.indexOf('ele-') > -1) name = 'ele';
else if (props.modelValue!.indexOf('fa') > -1) name = 'awe';
// 初始化 tab 高亮回显
state.fontIconTabActive = name;
return name;
};
// 初始化数据

View File

@@ -1,123 +1,131 @@
// 定义内容
export default {
router: {
home: 'home',
system: 'system',
systemMenu: 'systemMenu',
systemRole: 'systemRole',
systemUser: 'systemUser',
systemDept: 'systemDept',
systemDic: 'systemDic',
limits: 'limits',
limitsFrontEnd: 'FrontEnd',
limitsFrontEndPage: 'FrontEndPage',
limitsFrontEndBtn: 'FrontEndBtn',
limitsBackEnd: 'BackEnd',
limitsBackEndEndPage: 'BackEndEndPage',
personal: 'personal',
},
staticRoutes: {
signIn: 'signIn',
notFound: 'notFound',
noPower: 'noPower',
},
user: {
title0: 'Component size',
title1: 'Language switching',
title2: 'Menu search',
title3: 'Layout configuration',
title4: 'news',
title5: 'Full screen on',
title6: 'Full screen off',
dropdownLarge: 'large',
dropdownDefault: 'default',
dropdownSmall: 'small',
dropdown1: 'home page',
dropdown2: 'Personal Center',
dropdown3: '404',
dropdown4: '401',
dropdown5: 'Log out',
dropdown6: 'Code warehouse',
searchPlaceholder: 'Menu search: support Chinese, routing path',
newTitle: 'notice',
newBtn: 'All read',
newGo: 'Go to the notification center',
newDesc: 'No notice',
logOutTitle: 'Tips',
logOutMessage: 'This operation will log out. Do you want to continue?',
logOutConfirm: 'determine',
logOutCancel: 'cancel',
logOutExit: 'Exiting',
},
tagsView: {
refresh: 'refresh',
close: 'close',
closeOther: 'closeOther',
closeAll: 'closeAll',
fullscreen: 'fullscreen',
closeFullscreen: 'closeFullscreen',
},
notFound: {
foundTitle: 'Wrong address input, please re-enter the address~',
foundMsg: 'You can check the web address first, and then re-enter or give us feedback.',
foundBtn: 'Back to home page',
},
noAccess: {
accessTitle: 'You are not authorized to operate~',
accessMsg: 'Contact information: add QQ group discussion 665452019',
accessBtn: 'Reauthorization',
},
layout: {
configTitle: 'Layout configuration',
oneTitle: 'Global Themes',
twoTopTitle: 'top bar set up',
twoMenuTitle: 'Menu set up',
twoColumnsTitle: 'Columns set up',
twoTopBar: 'Top bar background',
twoTopBarColor: 'Top bar default font color',
twoIsTopBarColorGradual: 'Top bar gradient',
twoMenuBar: 'Menu background',
twoMenuBarColor: 'Menu default font color',
twoIsMenuBarColorGradual: 'Menu gradient',
twoColumnsMenuBar: 'Column menu background',
twoColumnsMenuBarColor: 'Default font color bar menu',
twoIsColumnsMenuBarColorGradual: 'Column gradient',
threeTitle: 'Interface settings',
threeIsCollapse: 'Menu horizontal collapse',
threeIsUniqueOpened: 'Menu accordion',
threeIsFixedHeader: 'Fixed header',
threeIsClassicSplitMenu: 'Classic layout split menu',
threeIsLockScreen: 'Open the lock screen',
threeLockScreenTime: 'screen locking(s/s)',
fourTitle: 'Interface display',
fourIsShowLogo: 'Sidebar logo',
fourIsBreadcrumb: 'Open breadcrumb',
fourIsBreadcrumbIcon: 'Open breadcrumb icon',
fourIsTagsview: 'Open tagsview',
fourIsTagsviewIcon: 'Open tagsview Icon',
fourIsCacheTagsView: 'Enable tagsview cache',
fourIsSortableTagsView: 'Enable tagsview drag',
fourIsShareTagsView: 'Enable tagsview sharing',
fourIsFooter: 'Open footer',
fourIsGrayscale: 'Grey model',
fourIsInvert: 'Color weak mode',
fourIsDark: 'Dark Mode',
fourIsWartermark: 'Turn on watermark',
fourWartermarkText: 'Watermark copy',
fiveTitle: 'Other settings',
fiveTagsStyle: 'Tagsview style',
fiveAnimation: 'page animation',
fiveColumnsAsideStyle: 'Column style',
fiveColumnsAsideLayout: 'Column layout',
sixTitle: 'Layout switch',
sixDefaults: 'One',
sixClassic: 'Two',
sixTransverse: 'Three',
sixColumns: 'Four',
tipText: 'Click the button below to copy the layout configuration to `/src/stores/themeConfig.ts` It has been modified in.',
copyText: 'replication configuration',
resetText: 'restore default',
copyTextSuccess: 'Copy succeeded!',
copyTextError: 'Copy failed!',
},
router: {
home: 'home',
system: 'system',
systemMenu: 'systemMenu',
systemRole: 'systemRole',
systemUser: 'systemUser',
systemDept: 'systemDept',
systemDic: 'systemDic',
limits: 'limits',
limitsFrontEnd: 'FrontEnd',
limitsFrontEndPage: 'FrontEndPage',
limitsFrontEndBtn: 'FrontEndBtn',
limitsBackEnd: 'BackEnd',
limitsBackEndEndPage: 'BackEndEndPage',
personal: 'personal',
},
staticRoutes: {
signIn: 'signIn',
notFound: 'notFound',
noPower: 'noPower',
},
user: {
title0: 'Component size',
title1: 'Language switching',
title2: 'Menu search',
title3: 'Layout configuration',
title4: 'news',
title5: 'Full screen on',
title6: 'Full screen off',
dropdownLarge: 'large',
dropdownDefault: 'default',
dropdownSmall: 'small',
dropdown1: 'home page',
dropdown2: 'Personal Center',
dropdown3: '404',
dropdown4: '401',
dropdown5: 'Log out',
dropdown6: 'Code warehouse',
searchPlaceholder: 'Menu search: support Chinese, routing path',
newTitle: 'notice',
newBtn: 'All read',
newGo: 'Go to the notification center',
newDesc: 'No notice',
logOutTitle: 'Tips',
logOutMessage: 'This operation will log out. Do you want to continue?',
logOutConfirm: 'determine',
logOutCancel: 'cancel',
logOutExit: 'Exiting',
},
tagsView: {
refresh: 'refresh',
close: 'close',
closeOther: 'closeOther',
closeAll: 'closeAll',
fullscreen: 'fullscreen',
closeFullscreen: 'closeFullscreen',
},
notFound: {
foundTitle: 'Wrong address input, please re-enter the address~',
foundMsg: 'You can check the web address first, and then re-enter or give us feedback.',
foundBtn: 'Back to home page',
},
noAccess: {
accessTitle: 'You are not authorized to operate~',
accessMsg: 'Contact information: add QQ group discussion 665452019',
accessBtn: 'Reauthorization',
},
layout: {
configTitle: 'Layout configuration',
oneTitle: 'Global Themes',
twoTopTitle: 'top bar set up',
twoMenuTitle: 'Menu set up',
twoColumnsTitle: 'Columns set up',
twoTopBar: 'Top bar background',
twoTopBarColor: 'Top bar default font color',
twoIsTopBarColorGradual: 'Top bar gradient',
twoMenuBar: 'Menu background',
twoMenuBarColor: 'Menu default font color',
twoIsMenuBarColorGradual: 'Menu gradient',
twoColumnsMenuBar: 'Column menu background',
twoColumnsMenuBarColor: 'Default font color bar menu',
twoIsColumnsMenuBarColorGradual: 'Column gradient',
threeTitle: 'Interface settings',
threeIsCollapse: 'Menu horizontal collapse',
threeIsUniqueOpened: 'Menu accordion',
threeIsFixedHeader: 'Fixed header',
threeIsClassicSplitMenu: 'Classic layout split menu',
threeIsLockScreen: 'Open the lock screen',
threeLockScreenTime: 'screen locking(s/s)',
fourTitle: 'Interface display',
fourIsShowLogo: 'Sidebar logo',
fourIsBreadcrumb: 'Open breadcrumb',
fourIsBreadcrumbIcon: 'Open breadcrumb icon',
fourIsTagsview: 'Open tagsview',
fourIsTagsviewIcon: 'Open tagsview Icon',
fourIsCacheTagsView: 'Enable tagsview cache',
fourIsSortableTagsView: 'Enable tagsview drag',
fourIsShareTagsView: 'Enable tagsview sharing',
fourIsFooter: 'Open footer',
fourIsGrayscale: 'Grey model',
fourIsInvert: 'Color weak mode',
fourIsDark: 'Dark Mode',
fourIsWartermark: 'Turn on watermark',
fourWartermarkText: 'Watermark copy',
fiveTitle: 'Other settings',
fiveTagsStyle: 'Tagsview style',
fiveAnimation: 'page animation',
fiveColumnsAsideStyle: 'Column style',
fiveColumnsAsideLayout: 'Column layout',
sixTitle: 'Layout switch',
sixDefaults: 'One',
sixClassic: 'Two',
sixTransverse: 'Three',
sixColumns: 'Four',
tipText: 'Click the button below to copy the layout configuration to `/src/stores/themeConfig.ts` It has been modified in.',
copyText: 'replication configuration',
resetText: 'restore default',
copyTextSuccess: 'Copy succeeded!',
copyTextError: 'Copy failed!',
},
upgrade: {
title: 'New version upgrade',
msg: 'It\'s a new version. Update it nowDon\'t worry, update quickly oh!',
desc: 'Tip: The update restores the default configuration',
btnOne: 'Cruel refusal',
btnTwo: 'Update now',
btnTwoLoading: 'updating',
},
};

View File

@@ -1,134 +1,146 @@
// 定义内容
export default {
router: {
home: '首页',
system: '系统管理',
config: '常规配置',
log: '日志管理',
/* 常规配置 */
configSystem: '系统配置',
configDict: '字典管理',
configArea: '地区管理',
configFile: '附件管理',
/* 系统管理 */
systemMenu: '菜单管理',
systemRole: '角色管理',
systemUser: '用户管理',
systemDept: '部门管理',
/* 日志管理 */
loginLog: '登录日志',
operationLog: '操作日志',
systemApiWhiteList: '接口白名单',
limits: '权限管理',
limitsFrontEnd: '前端控制',
limitsFrontEndPage: '页面权限',
limitsFrontEndBtn: '按钮权限',
limitsBackEnd: '后端控制',
limitsBackEndEndPage: '页面权限',
personal: '个人中心',
},
staticRoutes: {
signIn: '登录',
notFound: '找不到此页面',
noPower: '没有权限',
},
user: {
title0: '组件大小',
title1: '语言切换',
title2: '菜单搜索',
title3: '布局配置',
title4: '消息',
title5: '开全屏',
title6: '关全屏',
dropdownLarge: '大型',
dropdownDefault: '默认',
dropdownSmall: '型',
dropdown1: '首页',
dropdown2: '个人中心',
dropdown3: '404',
dropdown4: '401',
dropdown5: '退出登录',
dropdown6: '代码仓库',
searchPlaceholder: '菜单搜索:支持中文、路由路径',
newTitle: '通知',
newBtn: '全部已读',
newGo: '前往通知中心',
newDesc: '暂无通知',
logOutTitle: '提示',
logOutMessage: '此操作将退出登录, 是否继续?',
logOutConfirm: '确定',
logOutCancel: '取消',
logOutExit: '退出中',
},
tagsView: {
refresh: '刷新',
close: '关闭',
closeOther: '关闭其它',
closeAll: '全部关闭',
fullscreen: '当前页全屏',
closeFullscreen: '关闭全屏',
},
notFound: {
foundTitle: '地址输入错误,请重新输入地址~',
foundMsg: '您可以先检查网址,然后重新输入或给我们反馈问题。',
foundBtn: '返回首页',
},
noAccess: {
accessTitle: '您未被授权,没有操作权限~',
accessMsg: '联系方式加QQ群探讨 665452019',
accessBtn: '重新授权',
},
layout: {
configTitle: '布局配置',
oneTitle: '全局主题',
twoTopTitle: '顶栏设置',
twoMenuTitle: '菜单设置',
twoColumnsTitle: '栏设置',
twoTopBar: '顶栏背景',
twoTopBarColor: '顶栏默认字体颜色',
twoIsTopBarColorGradual: '顶栏背景渐变',
twoMenuBar: '菜单背景',
twoMenuBarColor: '菜单默认字体颜色',
twoIsMenuBarColorGradual: '菜单背景渐变',
twoColumnsMenuBar: '分栏菜单背景',
twoColumnsMenuBarColor: '分栏菜单默认字体颜色',
twoIsColumnsMenuBarColorGradual: '分栏菜单背景渐变',
threeTitle: '界面设置',
threeIsCollapse: '菜单水平折叠',
threeIsUniqueOpened: '菜单手风琴',
threeIsFixedHeader: '固定 Header',
threeIsClassicSplitMenu: '经典布局分割菜单',
threeIsLockScreen: '开启锁屏',
threeLockScreenTime: '自动锁屏(s/秒)',
fourTitle: '界面显示',
fourIsShowLogo: '侧边栏 Logo',
fourIsBreadcrumb: '开启 Breadcrumb',
fourIsBreadcrumbIcon: '开启 Breadcrumb 图标',
fourIsTagsview: '开启 Tagsview',
fourIsTagsviewIcon: '开启 Tagsview 图标',
fourIsCacheTagsView: '开启 TagsView 缓存',
fourIsSortableTagsView: '开启 TagsView 拖拽',
fourIsShareTagsView: '开启 TagsView 共用',
fourIsFooter: '开启 Footer',
fourIsGrayscale: '灰色模式',
fourIsInvert: '色弱模式',
fourIsDark: '深色模式',
fourIsWartermark: '开启水印',
fourWartermarkText: '水印文案',
fiveTitle: '其它设置',
fiveTagsStyle: 'Tagsview 风格',
fiveAnimation: '主页面切换动画',
fiveColumnsAsideStyle: '分栏高亮风格',
fiveColumnsAsideLayout: '分栏布局风格',
sixTitle: '布局切换',
sixDefaults: '默认',
sixClassic: '经典',
sixTransverse: '横向',
sixColumns: '分栏',
tipText: '点击下方按钮,复制布局配置去 `src/stores/themeConfig.ts` 中修改。',
copyText: '一键复制配置',
resetText: '一键恢复默认',
copyTextSuccess: '复制成功!',
copyTextError: '复制失败!',
},
router: {
home: '首页',
system: '系统管理',
config: '常规配置',
log: '日志管理',
/* 常规配置 */
configSystem: '系统配置',
configDict: '字典管理',
configArea: '地区管理',
configFile: '附件管理',
/* 系统管理 */
systemMenu: '菜单管理',
systemRole: '角色管理',
systemUser: '用户管理',
systemDept: '部门管理',
/* 日志管理 */
loginLog: '登录日志',
operationLog: '操作日志',
systemApiWhiteList: '接口白名单',
limits: '权限管理',
limitsFrontEnd: '前端控制',
limitsFrontEndPage: '页面权限',
limitsFrontEndBtn: '按钮权限',
limitsBackEnd: '后端控制',
limitsBackEndEndPage: '页面权限',
personal: '个人中心',
},
staticRoutes: {
signIn: '登录',
notFound: '找不到此页面',
noPower: '没有权限',
},
user: {
title0: '组件大小',
title1: '语言切换',
title2: '菜单搜索',
title3: '布局配置',
title4: '消息',
title5: '开全屏',
title6: '关全屏',
retry: '重试上线',
onlinePrompt: '当前离线状态,是否重试上线?',
dropdownLarge: '型',
dropdownDefault: '默认',
dropdownSmall: '小型',
dropdown1: '首页',
dropdown2: '个人中心',
dropdown3: '404',
dropdown4: '401',
dropdown5: '退出登录',
dropdown6: '代码仓库',
searchPlaceholder: '菜单搜索:支持中文、路由路径',
newTitle: '通知',
newBtn: '全部已读',
newGo: '前往通知中心',
newDesc: '暂无通知',
logOutTitle: '提示',
logOutMessage: '此操作将退出登录, 是否继续?',
logOutConfirm: '确定',
logOutCancel: '取消',
logOutExit: '退出中',
},
tagsView: {
refresh: '刷新',
close: '关闭',
closeOther: '关闭其它',
closeAll: '全部关闭',
fullscreen: '当前页全屏',
closeFullscreen: '关闭全屏',
},
notFound: {
foundTitle: '地址输入错误,请重新输入地址~',
foundMsg: '您可以先检查网址,然后重新输入或给我们反馈问题。',
foundBtn: '返回首页',
},
noAccess: {
accessTitle: '您未被授权,没有操作权限~',
accessMsg: '请联系管理员',
accessBtn: '重新授权',
},
layout: {
configTitle: '布局配置',
oneTitle: '全局主题',
twoTopTitle: '栏设置',
twoMenuTitle: '菜单设置',
twoColumnsTitle: '分栏设置',
twoTopBar: '顶栏背景',
twoTopBarColor: '顶栏默认字体颜色',
twoIsTopBarColorGradual: '顶栏背景渐变',
twoMenuBar: '菜单背景',
twoMenuBarColor: '菜单默认字体颜色',
twoMenuBarActiveColor: '菜单高亮背景色',
twoIsMenuBarColorGradual: '菜单背景渐变',
twoColumnsMenuBar: '分栏菜单背景',
twoColumnsMenuBarColor: '分栏菜单默认字体颜色',
twoIsColumnsMenuBarColorGradual: '分栏菜单背景渐变',
twoIsColumnsMenuHoverPreload: '分栏菜单滑鼠悬停预加载',
threeTitle: '界面设置',
threeIsCollapse: '菜单水平折叠',
threeIsUniqueOpened: '菜单手风琴',
threeIsFixedHeader: '固定 Header',
threeIsClassicSplitMenu: '经典布局分割菜单',
threeIsLockScreen: '开启锁屏',
threeLockScreenTime: '自动锁屏(s/秒)',
fourTitle: '界面显示',
fourIsShowLogo: '侧边栏 Logo',
fourIsBreadcrumb: '开启 Breadcrumb',
fourIsBreadcrumbIcon: '开启 Breadcrumb 图标',
fourIsTagsview: '开启 Tagsview',
fourIsTagsviewIcon: '开启 Tagsview 图标',
fourIsCacheTagsView: '开启 TagsView 缓存',
fourIsSortableTagsView: '开启 TagsView 拖拽',
fourIsShareTagsView: '开启 TagsView 共用',
fourIsFooter: '开启 Footer',
fourIsGrayscale: '灰色模式',
fourIsInvert: '色弱模式',
fourIsDark: '深色模式',
fourIsWartermark: '开启水印',
fourWartermarkText: '水印文案',
fiveTitle: '其它设置',
fiveTagsStyle: 'Tagsview 风格',
fiveAnimation: '主页面切换动画',
fiveColumnsAsideStyle: '分栏高亮风格',
fiveColumnsAsideLayout: '分栏布局风格',
sixTitle: '布局切换',
sixDefaults: '默认',
sixClassic: '经典',
sixTransverse: '横向',
sixColumns: '分栏',
tipText: '点击下方按钮,复制布局配置去 `src/stores/themeConfig.ts` 中修改。',
copyText: '一键复制配置',
resetText: '一键恢复默认',
copyTextSuccess: '复制成功!',
copyTextError: '复制失败!',
},
upgrade: {
title: '新版本升级',
msg: '新版本来啦,马上更新尝鲜吧!不用担心,更新很快的哦!',
desc: '提示:更新会还原默认配寘',
btnOne: '残忍拒绝',
btnTwo: '马上更新',
btnTwoLoading: '更新中',
},
};

View File

@@ -123,7 +123,7 @@ export default {
},
noAccess: {
accessTitle: '您未被授權,沒有操作許可權~',
accessMsg: '聯繫方式加QQ群探討665452019',
accessMsg: '請聯系管理員',
accessBtn: '重新授權',
},
layout: {

View File

@@ -1,7 +1,7 @@
// 定义内容
export default {
label: {
one1: '用户名登录',
one1: '账号密码登录',
two2: '手机号登录',
},
link: {

View File

@@ -4,7 +4,7 @@
<el-scrollbar ref="layoutMainScrollbarRef" class="layout-main-scroll layout-backtop-header-fixed"
wrap-class="layout-main-scroll" view-class="layout-main-scroll">
<LayoutParentView />
<!-- <LayoutFooter v-if="isFooter" /> -->
<LayoutFooter v-if="isFooter" />
</el-scrollbar>
<el-backtop :target="setBacktopClass" />
</el-main>

View File

@@ -1,8 +1,7 @@
<template>
<div class="layout-footer pb15">
<div class="layout-footer pb5 pt2">
<div class="layout-footer-warp">
<div> Powered by Django-Vue3-Admin </div>
<div class="mt5">Copyright DVAdmin团队</div>
<div> Powered by Django-Vue3-Admin Copyright © DVAdmin团队 </div>
</div>
</div>
</template>

View File

@@ -57,9 +57,33 @@
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
></i>
</div>
<div>
<span v-if="!isSocketOpen">
<el-popconfirm
width="250"
ref="onlinePopoverRef"
:confirm-button-text="$t('message.user.retry')"
:icon="InfoFilled"
trigger="hover"
icon-color="#626AEF"
:title="$t('message.user.onlinePrompt')"
@confirm="onlineConfirmEvent"
>
<template #reference>
<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" />
</el-badge>
</template>
</el-popconfirm>
</span>
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
<span v-if="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" />
</el-badge>
</span>
{{ userInfos.username === '' ? 'common' : userInfos.username }}
<el-icon class="el-icon--right">
<ele-ArrowDown />
@@ -79,7 +103,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { defineAsyncComponent, ref, computed, reactive, onMounted } from 'vue';
import { defineAsyncComponent, ref, computed, reactive, onMounted, unref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
@@ -91,6 +115,8 @@ import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage';
import headerImage from '/@/assets/img/headerImage.png';
import websocket from '/@/utils/websocket';
import { InfoFilled } from '@element-plus/icons-vue'
// 引入组件
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
@@ -118,6 +144,21 @@ const layoutUserFlexNum = computed(() => {
else num = '';
return num;
});
// 定义变量内容
const { isSocketOpen } = storeToRefs(useUserInfo());
// websocket状态
const onlinePopoverRef = ref()
const onlineConfirmEvent = () => {
if (!isSocketOpen.value) {
websocket.is_reonnect = true
websocket.reconnect_current = 1
websocket.reconnect()
}
// 手动隐藏弹出
unref(onlinePopoverRef).popperRef?.delayHide?.()
}
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
@@ -256,5 +297,29 @@ const messageCenter = messageCenterStore();
:deep(.el-badge__content.is-fixed) {
top: 12px;
}
.online-status{
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #18bc9c;
}
}
.online-down{
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #979b9c;
}
}
}
</style>

View File

@@ -17,15 +17,15 @@
<div class="upgrade-content">
{{ getThemeConfig.globalTitle }} {{ $t('message.upgrade.msg') }}
<div class="mt5">
<el-link type="primary" class="font12" href="https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md" target="_black">
<el-link type="primary" class="font12" href="https://gitee.com/huge-dream/django-vue3-admin/blob/master/CHANGELOG.md" target="_black">
CHANGELOG.md
</el-link>
</div>
<div class="upgrade-content-desc mt5">{{ $t('message.upgrade.desc') }}</div>
</div>
<div class="upgrade-btn">
<el-button round size="default" type="info" text @click="onCancel">{{ $t('message.upgrade.btnOne') }}</el-button>
<el-button type="primary" round size="default" @click="onUpgrade" :loading="state.isLoading">{{ state.btnTxt }}</el-button>
<el-button round size="default" type="info" text @click="onCancel" >{{ $t('message.upgrade.btnOne') }}</el-button>
<el-button type="primary" round size="default" @click="onUpgrade" :loading="state.isLoading" >{{ state.btnTxt }}</el-button>
</div>
</el-dialog>
</div>
@@ -76,10 +76,10 @@ const delayShow = () => {
};
// 页面加载时
onMounted(() => {
// delayShow();
// setTimeout(() => {
// state.btnTxt = t('message.upgrade.btnTwo');
// }, 200);
delayShow();
setTimeout(() => {
state.btnTxt = t('message.upgrade.btnTwo');
}, 200);
});
</script>

View File

@@ -14,7 +14,7 @@ import piniaPersist from 'pinia-plugin-persist';
// @ts-ignore
import fastCrud from './settings.ts';
import pinia from './stores';
import permission from '/@/plugin/permission/index';
import {RegisterPermission} from '/@/plugin/permission/index';
// @ts-ignore
import eIconPicker, { iconList, analyzingIconForIconfont } from 'e-icon-picker';
import 'e-icon-picker/icon/default-icon/symbol.js'; //基本彩色图标库
@@ -54,7 +54,6 @@ other.elSvg(app);
app.use(VXETable)
app.use(permission);
app.use(pinia).use(router).use(ElementPlus, { i18n: i18n.global.t }).use(i18n).use(VueGridLayout).use(fastCrud).mount('#app');
app.config.globalProperties.mittBus = mitt();

View File

@@ -1,10 +1,6 @@
import permissionDirective from './directive.permission'
import permissionFunc from './func.permission'
const install = function (app:any) {
export const RegisterPermission = function (app:any) {
app.directive('permission', permissionDirective)
app.provide('$hasPermissions',permissionFunc.hasPermissions)
}
export default {
install
}

View File

@@ -9,7 +9,7 @@ export const BtnPermissionStore = defineStore('BtnPermission', {
actions: {
async getBtnPermissionStore() {
request({
url: '/api/system/menu/menu_button_all_permission/',
url: '/api/system/menu_button/menu_button_all_permission/',
method: 'get',
}).then((ret: {
data: []

View File

@@ -9,7 +9,7 @@ import { request } from '/@/utils/service';
//扩展包
import { FsExtendsEditor,FsExtendsUploader } from '@fast-crud/fast-extends';
import '@fast-crud/fast-extends/dist/style.css';
import { ElMessage } from "element-plus";
import { successMessage, successNotification } from '/@/utils/message';
export default {
async install(app: any, options: any) {
// 先安装ui
@@ -18,9 +18,9 @@ export default {
app.use(FastCrud, {
//i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件
// 此处配置公共的dictRequest字典请求
async dictRequest({ url }: any) {
async dictRequest({ dict }: any) {
//根据dict的url异步返回一个字典数组
return await request({ url: url, }).then((res:any)=>{
return await request({ url: dict.url, params: dict.params || {} }).then((res:any)=>{
return res.data
});
},
@@ -41,21 +41,14 @@ export default {
transformRes: ({ res }: any) => {
//将pageRequest的返回数据转换为fast-crud所需要的格式
//return {records,currentPage,pageSize,total};
if(res.page){
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total };
}else{
return { records: res.data,currentPage: 1, pageSize: res.data.length, total: res.data.length };
}
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total };
},
},
form: {
afterSubmit(ctx:any ) {
const {mode} = ctx
if (mode === "add") {
ElMessage.success({ message: "添加成功" });
} else if (mode === "edit") {
ElMessage.success({ message: "保存成功" });
afterSubmit(ctx: any) {
// 增加crud提示
if (ctx.res.code == 2000) {
successNotification(ctx.res.msg);
}
},
},
@@ -107,12 +100,10 @@ export default {
});
},
successHandle(ret) {
console.log(111,ret)
// 上传完成后的结果处理, 此处应返回格式为{url:xxx,key:xxx}
return {
url: ret.data.url,
key: ret.data.id,
...ret.data
url: getBaseURL() + ret.data.url,
key: ret.data.id
};
}
}

View File

@@ -0,0 +1,26 @@
import {defineStore} from "pinia";
import {DictionaryStates} from "/@/stores/interface";
import {request} from "/@/utils/service";
export const BtnPermissionStore = defineStore('BtnPermission', {
state: (): DictionaryStates => ({
data: []
}),
actions: {
async getBtnPermissionStore() {
request({
url: '/api/system/menu_button/menu_button_all_permission/',
method: 'get',
}).then((ret: {
data: []
}) => {
// 转换数据格式并保存到pinia
let dataList = ret.data
this.data=dataList
})
},
},
persist: {
enabled: true,
},
});

View File

@@ -19,6 +19,7 @@ export interface UserInfosState {
}
export interface UserInfosStates {
userInfos: UserInfosState;
isSocketOpen: boolean
}
// 路由缓存列表

View File

@@ -26,6 +26,7 @@ export const useUserInfo = defineStore('userInfo', {
},
],
},
isSocketOpen: false
}),
actions: {
async updateUserInfos() {
@@ -57,6 +58,9 @@ export const useUserInfo = defineStore('userInfo', {
Session.set('userInfo', this.userInfos);
}
},
async setWebSocketState(socketState: boolean) {
this.isSocketOpen = socketState;
},
async getApiUserInfo() {
return request({
url: '/api/system/user/user_info/',

View File

@@ -1,7 +1,6 @@
import type { App } from 'vue';
import { useUserInfo } from '/@/stores/userInfo';
import { judementSameArr } from '/@/utils/arrayOperation';
import {BtnPermissionStore} from "/@/stores/btnPermission";
/**
* 用户权限指令
* @directive 单个权限验证v-auth="xxx"
@@ -12,16 +11,16 @@ export function authDirective(app: App) {
// 单个权限验证v-auth="xxx"
app.directive('auth', {
mounted(el, binding) {
const stores = useUserInfo();
if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) el.parentNode.removeChild(el);
const stores = BtnPermissionStore();
if (!stores.data.some((v: string) => v === binding.value)) el.parentNode.removeChild(el);
},
});
// 多个权限验证满足一个则显示v-auths="[xxx,xxx]"
app.directive('auths', {
mounted(el, binding) {
let flag = false;
const stores = useUserInfo();
stores.userInfos.authBtnList.map((val: string) => {
const stores = BtnPermissionStore();
stores.data.map((val: string) => {
binding.value.map((v: string) => {
if (val === v) flag = true;
});
@@ -32,8 +31,8 @@ export function authDirective(app: App) {
// 多个权限验证全部满足则显示v-auth-all="[xxx,xxx]"
app.directive('auth-all', {
mounted(el, binding) {
const stores = useUserInfo();
const flag = judementSameArr(binding.value, stores.userInfos.authBtnList);
const stores = BtnPermissionStore();
const flag = judementSameArr(binding.value, stores.data);
if (!flag) el.parentNode.removeChild(el);
},
});

View File

@@ -1,14 +1,13 @@
import { useUserInfo } from '/@/stores/userInfo';
import { judementSameArr } from '/@/utils/arrayOperation';
import {BtnPermissionStore} from "/@/stores/btnPermission";
/**
* 单个权限验证
* @param value 权限值
* @returns 有权限,返回 `true`,反之则反
*/
export function auth(value: string): boolean {
const stores = useUserInfo();
return stores.userInfos.authBtnList.some((v: string) => v === value);
const stores = BtnPermissionStore();
return stores.data.some((v: string) => v === value);
}
/**
@@ -18,8 +17,8 @@ export function auth(value: string): boolean {
*/
export function auths(value: Array<string>): boolean {
let flag = false;
const stores = useUserInfo();
stores.userInfos.authBtnList.map((val: string) => {
const stores = BtnPermissionStore();
stores.data.map((val: string) => {
value.map((v: string) => {
if (val === v) flag = true;
});
@@ -33,6 +32,6 @@ export function auths(value: Array<string>): boolean {
* @returns 有权限,返回 `true`,反之则反
*/
export function authAll(value: Array<string>): boolean {
const stores = useUserInfo();
return judementSameArr(value, stores.userInfos.authBtnList);
const stores = BtnPermissionStore();
return judementSameArr(value, stores.data);
}

View File

@@ -1,19 +1,11 @@
import { toRaw } from 'vue';
import { DictionaryStore } from '/@/stores/dictionary';
/**
* @method 获取指定name字典
/**
* @method 获取指定name字典
*/
export const dictionary = (key: string,value:string|number|null=null) => {
export const dictionary = (name: string) => {
const dict = DictionaryStore()
const dictionary = toRaw(dict.data)
if(value!==null){
for (let item of dictionary[key]) {
if (item.value === value) {
return item.label
}
}
return []
}
return dictionary[key]
}
return dictionary[name]
}

View File

@@ -3,7 +3,7 @@ import {Session} from "/@/utils/storage";
import {getWsBaseURL} from "/@/utils/baseUrl";
// @ts-ignore
import socket from '@/types/api/socket'
import {useUserInfo} from "/@/stores/userInfo";
const websocket: socket = {
websocket: null,
connectURL: getWsBaseURL(),
@@ -42,6 +42,7 @@ const websocket: socket = {
}
websocket.websocket.onclose = (e: any) => {
websocket.socket_open = false
useUserInfo().setWebSocketState(websocket.socket_open);
// 需要重新连接
if (websocket.is_reonnect) {
websocket.reconnect_timer = setTimeout(() => {
@@ -49,6 +50,8 @@ const websocket: socket = {
if (websocket.reconnect_current > websocket.reconnect_count) {
clearTimeout(websocket.reconnect_timer)
websocket.is_reonnect = false
websocket.socket_open = false
useUserInfo().setWebSocketState(websocket.socket_open);
return
}
// 记录重连次数
@@ -60,6 +63,7 @@ const websocket: socket = {
// 连接成功
websocket.websocket.onopen = function () {
websocket.socket_open = true
useUserInfo().setWebSocketState(websocket.socket_open);
websocket.is_reonnect = true
// 开启心跳
websocket.heartbeat()
@@ -85,17 +89,21 @@ const websocket: socket = {
callback && callback()
} else {
clearInterval(websocket.hearbeat_timer)
message({
type: 'warning',
message: 'socket链接已断开',
duration: 1000,
})
// message({
// type: 'warning',
// message: 'socket链接已断开',
// duration: 1000,
// })
websocket.socket_open = false
useUserInfo().setWebSocketState(websocket.socket_open);
}
},
close: () => {
websocket.is_reonnect = false
websocket.websocket.close()
websocket.websocket = null;
websocket.socket_open = false
useUserInfo().setWebSocketState(websocket.socket_open);
},
/**
* 重新连接

View File

@@ -12,5 +12,5 @@ export const scanAndInstallPlugins = (app: any) => {
pluginNames.add(pluginsName);
}
pluginsAll = Array.from(pluginNames);
console.table('已注册插件:', pluginsAll);
console.log('已发现插件:', pluginsAll);
};

View File

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

View File

@@ -118,7 +118,7 @@
list-type="picture-card"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">选取图片后,需手动上传到服务器,并且只能上传jpg/png文件</div>
<div slot="tip" class="el-upload__tip">选取图片,并且只能上传jpg/png文件</div>
</el-upload>
<el-dialog :visible.sync="dialogImgVisible">
<img width="100%" :src="dialogImageUrl" alt="" />
@@ -150,7 +150,7 @@
list-type="picture-card"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">选取图片后,需手动上传到服务器,并且只能上传jpg/png文件</div>
<div slot="tip" class="el-upload__tip">选取图片,并且只能上传jpg/png文件</div>
</el-upload>
<el-dialog :visible.sync="dialogImgVisible">
<img width="100%" :src="dialogImageUrl" alt="" />
@@ -506,4 +506,8 @@ watch(
);
</script>
<style></style>
<style scoped>
:deep(.el-upload-list--picture-card){
text-align: center;
}
</style>

View File

@@ -19,7 +19,7 @@
<el-input v-model="deptFormData.key" />
</el-form-item>
<el-form-item label="负责人">
<el-input v-model="deptFormData.owner" />
<el-input v-model="deptFormData.owner" placeholder="请输入" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="deptFormData.description" maxlength="200" show-word-limit type="textarea" />

View File

@@ -4,6 +4,7 @@ import { request } from '/@/utils/service';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import {auth} from "/@/utils/authFunction";
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
@@ -39,9 +40,6 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
return await api.exportData(query);
};
//权限判定
const hasPermissions: any = inject('$hasPermissions');
return {
crudOptions: {
table: {
@@ -58,12 +56,12 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
actionbar: {
buttons: {
add: {
show: hasPermissions('user:Create'),
// show:true
show: auth('user:Create')
},
export: {
text: '导出', //按钮文字
title: '导出', //鼠标停留显示的信息
show: auth('user:Export'),
click() {
return exportRequest(crudExpose!.getSearchFormData());
},
@@ -89,15 +87,15 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
show: false,
},
edit: {
show: hasPermissions('user:Update'),
show: auth('user:Update'),
},
remove: {
show: hasPermissions('user:Delete'),
show: auth('user:Delete'),
},
custom: {
text: '重设密码',
type: 'primary',
show: hasPermissions('user:ResetPassword'),
show: auth('user:ResetPassword'),
tooltip: {
placement: 'top',
content: '重设密码',
@@ -266,7 +264,6 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
url: '/api/system/role/',
value: 'id',
label: 'name',
isTree: true,
getData: async ({ url }: { url: string }) => {
return request({
url: url,

View File

@@ -40,7 +40,7 @@
<el-button :icon="!showCount ? 'Hide' : 'View'" circle @click="showCount = !showCount"></el-button>
</template>
<template #actionbar-right>
<importExcel api="api/system/user/">导入 </importExcel>
<importExcel api="api/system/user/" v-auth="'user:Import'">导入 </importExcel>
</template>
</fs-crud>
@@ -251,10 +251,10 @@ const handleResetPwdSubmit = async () => {
};
onMounted(() => {
crudExpose.doRefresh();
deptCountChart = init(deptCountBar.value as HTMLElement);
deptSexChart = init(deptSexPie.value as HTMLElement);
getDeptInfo();
crudExpose.doRefresh();
});
defineExpose({

View File

@@ -3,7 +3,7 @@ import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOption
import { dictionary } from '/@/utils/dictionary';
import { inject, nextTick, ref } from 'vue';
import { successMessage } from '/@/utils/message';
import {auth} from '/@/utils/authFunction';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
@@ -19,8 +19,6 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject('$hasPermissions');
return {
crudOptions: {
@@ -40,17 +38,17 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
edit: {
iconRight: 'Edit',
type: 'text',
show: hasPermissions('dictionary:Update'),
show: auth('dictionary:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: hasPermissions('dictionary:Delete'),
show: auth('dictionary:Delete'),
},
custom: {
text: '字典配置',
type: 'text',
show: hasPermissions('dictionary:Update'),
show: auth('dictionary:Update'),
tooltip: {
placement: 'top',
content: '字典配置',

View File

@@ -1,6 +1,8 @@
<template>
<el-drawer size="70%" v-model="drawer" direction="rtl" destroy-on-close :before-close="handleClose">
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<div>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</div>
</el-drawer>
</template>
@@ -23,7 +25,6 @@ const handleClose = (done: () => void) => {
})
.then(() => {
done();
})
.catch(() => {
// catch error

View File

@@ -34,6 +34,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
//固定右侧
fixed: 'right',
width: 200,
show:false,
buttons: {
view: {
show: false,

View File

@@ -4,8 +4,8 @@
<div class="login-left-logo">
<img :src="logoMini" />
<div class="login-left-logo-text">
<span>{{ getThemeConfig.globalViceTitle }}</span>
<span class="login-left-logo-text-msg">{{ getThemeConfig.globalViceTitleMsg }}</span>
<span>{{ getSystemConfig['login.site_title']||getThemeConfig.globalViceTitle }}</span>
<span class="login-left-logo-text-msg">{{ getSystemConfig['login.site_name']||getThemeConfig.globalViceTitleMsg }}</span>
</div>
</div>
<div class="login-left-img">
@@ -42,15 +42,15 @@
</div>
<div class="login-authorization">
<p>Copyright © 2021-2022 django-vue-admin.com 版权所有</p>
<p>Copyright © {{getSystemConfig['login.copyright'] || '2021-2024 django-vue-admin.com'}} 版权所有</p>
<p class="la-other">
<a href="https://beian.miit.gov.cn" target="_blank">晋ICP备18005113号-3</a>
<a href="https://beian.miit.gov.cn" target="_blank">{{getSystemConfig['login.keep_record'] || '晋ICP备18005113号-3'}}</a>
|
<a href="https://django-vue-admin.com" target="_blank">帮助</a>
<a :href="getSystemConfig['login.help_url']?getSystemConfig['login.help_url']:'https://django-vue-admin.com'" target="_blank">帮助</a>
|
<a href="#">隐私</a>
<a :href="getSystemConfig['login.privacy_url']?getSystemConfig['login.privacy_url']:'#'">隐私</a>
|
<a href="#">条款</a>
<a :href="getSystemConfig['login.clause_url']?getSystemConfig['login.clause_url']:'#'">条款</a>
</p>
</div>
</div>
@@ -64,7 +64,7 @@ import { NextLoading } from '/@/utils/loading';
import logoMini from '/@/assets/logo-mini.svg';
import loginMain from '/@/assets/login-main.svg';
import loginBg from '/@/assets/login-bg.svg';
import {SystemConfigStore} from '/@/stores/systemConfig'
// 引入组件
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
@@ -82,6 +82,14 @@ const state = reactive({
const getThemeConfig = computed(() => {
return themeConfig.value;
});
const systemConfigStore = SystemConfigStore()
const {systemConfig} = storeToRefs(systemConfigStore)
const getSystemConfig = computed(()=>{
return systemConfig.value
})
// 页面加载时
onMounted(() => {
NextLoading.done();

View File

@@ -5,19 +5,12 @@ export const apiPrefix = '/api/system/menu/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix+"tree/",
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetChildren(id: InfoReq) {
return request({
url: apiPrefix + id + '/getChildren/',
method: 'get',
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id + '/',

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/menu_button/';
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: any) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,215 @@
import {AddReq, DelReq, EditReq, dict, CreateCrudOptionsRet, CreateCrudOptionsProps} from '@fast-crud/fast-crud';
import * as api from './api';
import {auth} from '/@/utils/authFunction'
import {request} from '/@/utils/service';
//此处为crudOptions配置
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async () => {
if (context!.selectOptions.value.id) {
return await api.GetList({menu: context!.selectOptions.value.id} as any);
} else {
return undefined;
}
};
const editRequest = async ({form, row}: EditReq) => {
return await api.UpdateObj({...form, menu: row.menu});
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
return await api.AddObj({...form, ...{menu: context!.selectOptions.value.id}});
};
return {
crudOptions: {
pagination:{
show:false
},
search: {
container: {
action: {
//按钮栏配置
col: {
span: 8,
},
},
},
},
actionbar: {
buttons: {
add: {
show: auth('btn:Create')
},
},
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
icon: '',
type: 'primary',
show: auth('btn:Update')
},
remove: {
show: auth('btn:Delete')
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
form: {
col: {span: 24},
labelWidth: '100px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: {show: false},
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
search: {
title: '关键词',
column: {show: false},
type: 'text',
search: {show: true},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
id: {
title: 'ID',
type: 'text',
column: {show: false},
search: {show: false},
form: {show: false},
},
name: {
title: '权限名称',
type: 'text',
search: {show: true},
column: {
minWidth: 120,
sortable: true,
},
form: {
rules: [{required: true, message: '权限名称必填'}],
component: {
placeholder: '输入权限名称搜索',
props: {
clearable: true,
allowCreate: true,
filterable: true,
},
},
helper: {
render() {
return <el-alert title="手动输入" type="warning"
description="页面中按钮的名称或者自定义一个名称"/>;
},
},
},
},
value: {
title: '权限值',
type: 'text',
search: {show: false},
column: {
width: 200,
sortable: true,
},
form: {
rules: [{required: true, message: '权限标识必填'}],
placeholder: '输入权限标识',
helper: {
render() {
return <el-alert title="唯一值" type="warning"
description="用于判断前端按钮权限或接口权限"/>;
},
},
},
},
method: {
title: '请求方式',
search: {show: false},
type: 'dict-select',
column: {
width: 120,
sortable: true,
},
dict: dict({
data: [
{label: 'GET', value: 0},
{label: 'POST', value: 1, color: 'success'},
{label: 'PUT', value: 2, color: 'warning'},
{label: 'DELETE', value: 3, color: 'danger'},
],
}),
form: {
rules: [{required: true, message: '必填项'}],
},
},
api: {
title: '接口地址',
search: {show: false},
type: 'dict-select',
dict: dict({
getData() {
return request({url: '/swagger.json'}).then((res: any) => {
const ret = Object.keys(res.paths);
const data = [];
for (const item of ret) {
const obj: any = {};
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column: {
minWidth: 250,
sortable: true,
},
form: {
rules: [{required: true, message: '必填项'}],
component: {
props: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
helper: {
render() {
return <el-alert title="请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/"
type="warning"/>;
},
},
},
},
},
},
};
};

View File

@@ -0,0 +1,28 @@
<template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { MenuTreeItemType } from '../../types';
// 当前选择的菜单信息
let selectOptions: any = ref({ name: null });
const { crudRef, crudBinding, crudExpose, context } = useFs({ createCrudOptions, context: { selectOptions } });
const { doRefresh, setTableData } = crudExpose;
const handleRefreshTable = (record: MenuTreeItemType) => {
if (!record.is_catalog && record.id) {
selectOptions.value = record;
doRefresh();
} else {
//清空表格数据
setTableData([]);
}
};
defineExpose({ selectOptions, handleRefreshTable });
</script>

View File

@@ -0,0 +1,240 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { inject } from 'vue';
import {auth} from "/@/utils/authFunction";
export const createCrudOptions = function ({ crudExpose, props,modelDialog,selectOptions,allModelData }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
// return await api.GetList(query);
if (selectOptions.value.id) {
return await api.GetList({ menu: selectOptions.value.id } as any);
} else {
return undefined;
}
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
form.menu = selectOptions.value.id;
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: false,
},
actionbar: {
buttons: {
add:{
show:auth('column:Create')
},
auto: {
text: '自动匹配',
type: 'success',
show:auth('column:Match'),
click: () => {
return modelDialog.value=true;
},
},
},
},
rowHandle: {
//固定右侧
fixed: 'right',
buttons: {
view: {
show: false,
},
edit: {
show: auth('column:Update')
},
remove: {
show: auth('column:Delete')
},
},
},
form: {
col: { span: 24 },
labelWidth: '110px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
//@ts-ignore
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose!.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
model: {
title: 'model',
type: 'dict-select',
dict:dict({
url:'/api/system/column/get_models/',
label:'title',
value:'key'
}),
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
},
},
},
title: {
title: '中文名',
sortable: 'custom',
search: {
show: true,
},
type: 'text',
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
placeholder: '请输入中文名',
},
},
},
field_name: {
title: '字段名',
type: 'text',
search: {
show: true,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
placeholder: '请输入字段名',
},
},
},
// is_create: {
// title: '创建时显示',
// sortable: 'custom',
// search: {
// disabled: true,
// },
// type: 'dict-switch',
// dict: dict({
// data: [
// { value: true, label: '启用' },
// { value: false, label: '禁用' },
// ],
// }),
// form: {
// value: true,
// },
// column: {
// valueChange(context){
// return api.UpdateObj(context.row)
// },
// component: {
// name: 'fs-dict-switch',
// },
// },
// },
// is_update: {
// title: '编辑时显示',
// search: {
// show: true,
// },
// type: 'dict-switch',
// dict: dict({
// data: [
// { value: true, label: '启用' },
// { value: false, label: '禁用' },
// ],
// }),
// form: {
// value: true,
// },
// column: {
// component: {
// name: 'fs-dict-switch',
// onChange: compute((context) => {
// //动态onChange方法测试
// return () => {
// console.log('onChange', context.row.switch);
// };
// }),
// },
// },
// },
// is_query: {
// title: '列表中显示',
// type: 'dict-switch',
// dict: dict({
// data: [
// { value: true, label: '启用' },
// { value: false, label: '禁用' },
// ],
// }),
// form: {
// value: true,
// },
// column: {
// component: {
// name: 'fs-dict-switch',
// onChange: compute((context) => {
// //动态onChange方法测试
// return () => {
// console.log('onChange', context.row.switch);
// };
// }),
// },
// },
// },
},
},
};
};

View File

@@ -0,0 +1,110 @@
<template>
<div>
<el-dialog ref="modelRef" v-model="modelDialog" title="选择model">
<div v-show="props.model">
<el-tag>已选择:{{ props.model }}</el-tag>
</div>
<div class="model-card">
<div v-for="(item,index) in allModelData" :value="item.key" :key="index">
<el-text :type="modelCheckIndex===index?'primary':''" @click="onModelChecked(item,index)">
{{ item.app + '--' + item.title + '(' + item.key + ')' }}
</el-text>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="modelDialog = false">取消</el-button>
<el-button type="primary" @click="handleAutomatch">
确定
</el-button>
</span>
</template>
</el-dialog>
<div style="height: 80vh">
<fs-crud ref="crudRef" v-bind="crudBinding">
</fs-crud>
</div>
</div>
</template>
<script lang="ts" setup>
import {ref, onMounted, reactive} from 'vue';
import {useFs} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
import {getModelList} from './api'
import {MenuTreeItemType} from "/@/views/system/menu/types";
import {successMessage, successNotification, warningNotification} from '/@/utils/message';
import {automatchColumnsData} from '/@/views/system/columns/components/ColumnsTableCom/api';
// 当前选择的菜单信息
let selectOptions: any = ref({name: null});
const props = reactive({
model: '',
app: '',
menu: ''
})
//model弹窗
const modelDialog = ref(false)
// 获取所有model
const allModelData = ref<any[]>([]);
const modelCheckIndex = ref(null)
const onModelChecked = (row, index) => {
modelCheckIndex.value = index
props.model = row.key
props.app = row.app
}
/**
* 菜单选中时,加载表格数据
* @param record
*/
const handleRefreshTable = (record: MenuTreeItemType) => {
if (!record.is_catalog && record.id) {
selectOptions.value = record;
crudExpose.doRefresh();
} else {
//清空表格数据
crudExpose.setTableData([]);
}
};
/**
* 自动匹配列
*/
const handleAutomatch = async () => {
props.menu = selectOptions.value.id
modelDialog.value = false
if (props.menu && props.model) {
const res = await automatchColumnsData(props);
if (res?.code === 2000) {
successNotification('匹配成功');
}
crudExpose.doSearch({form: {menu: props.menu, model: props.model}});
}else {
warningNotification('请选择角色和模型表!');
}
};
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, props, modelDialog, selectOptions,allModelData});
onMounted(async () => {
const res = await getModelList();
allModelData.value = res.data;
});
defineExpose({selectOptions, handleRefreshTable});
</script>
<style scoped lang="scss">
.model-card {
margin-top: 10px;
height: 30vh;
overflow-y: scroll;
div {
margin: 15px 0;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,279 @@
<template>
<div class="menu-form-com">
<div class="menu-form-alert">
1.红色星号表示必填;<br />
2.添加菜单如果是目录组件地址为空即可;<br />
3.添加根节点菜单父级菜单为空即可;
</div>
<el-form ref="formRef" :rules="rules" :model="menuFormData" label-width="80px" label-position="right">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="menuFormData.name" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="父级菜单" prop="parent">
<el-tree-select
v-model="menuFormData.parent"
:props="defaultTreeProps"
:data="deptDefaultList"
:cache-data="props.cacheData"
lazy
check-strictly
clearable
:load="handleTreeLoad"
placeholder="请选择父级菜单"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="路由地址" prop="web_path">
<el-input v-model="menuFormData.web_path" placeholder="请输入路由地址,请以/开头" />
</el-form-item>
<el-form-item label="图标" prop="icon">
<IconSelector clearable v-model="menuFormData.icon" />
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item required label="状态">
<el-switch v-model="menuFormData.status" width="60" inline-prompt active-text="启用" inactive-text="禁用" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="menuFormData.status" required label="侧边显示">
<el-switch v-model="menuFormData.visible" width="60" inline-prompt active-text="显示" inactive-text="隐藏" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item required label="是否目录">
<el-switch v-model="menuFormData.is_catalog" width="60" inline-prompt active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="!menuFormData.is_catalog" required label="外链接">
<el-switch v-model="menuFormData.is_link" width="60" inline-prompt active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="menuFormData.description" maxlength="200" show-word-limit type="textarea" placeholder="请输入备注" />
</el-form-item>
<el-divider></el-divider>
<div style="min-height: 184px">
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件地址" prop="component">
<el-autocomplete
class="w-full"
v-model="menuFormData.component"
:fetch-suggestions="querySearch"
:trigger-on-focus="false"
clearable
:debounce="100"
placeholder="输入组件地址"
/>
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件名称" prop="component_name">
<el-input v-model="menuFormData.component_name" placeholder="请输入组件名称" />
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="Url" prop="web_path">
<el-input v-model="menuFormData.web_path" placeholder="请输入Url" />
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog" label="缓存">
<el-switch v-model="menuFormData.cache" width="60" inline-prompt active-text="启用" inactive-text="禁用" />
</el-form-item>
</div>
<el-divider></el-divider>
</el-form>
<div class="menu-form-btns">
<el-button @click="handleSubmit" type="primary" :loading="menuBtnLoading">保存</el-button>
<el-button @click="handleCancel">取消</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive } from 'vue';
import { ElForm, FormRules } from 'element-plus';
import IconSelector from '/@/components/iconSelector/index.vue';
import { lazyLoadMenu, AddObj, UpdateObj } from '../../api';
import { successNotification } from '/@/utils/message';
import { MenuFormDataType, MenuTreeItemType, ComponentFileItem, APIResponseData } from '../../types';
import type Node from 'element-plus/es/components/tree/src/model/node';
interface IProps {
initFormData: Partial<MenuTreeItemType> | null;
treeData: MenuTreeItemType[];
cacheData: MenuTreeItemType[];
}
const defaultTreeProps: any = {
children: 'children',
label: 'name',
value: 'id',
isLeaf: (data: MenuTreeItemType[], node: Node) => {
if (node?.data.hasChild) {
return false;
} else {
return true;
}
},
};
const validateWebPath = (rule: any, value: string, callback: Function) => {
let pattern = /^\/.*?/;
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
const reg = menuFormData.is_link ? patternUrl.test(value) : pattern.test(value);
if (reg) {
callback();
} else {
callback(new Error('请输入正确的地址'));
}
};
const props = withDefaults(defineProps<IProps>(), {
initFormData: () => null,
treeData: () => [],
cacheData: () => [],
});
const emit = defineEmits(['drawerClose']);
const formRef = ref<InstanceType<typeof ElForm>>();
const rules = reactive<FormRules>({
web_path: [{ required: true, message: '请输入正确的地址', validator: validateWebPath, trigger: 'blur' }],
name: [{ required: true, message: '菜单名称必填', trigger: 'blur' }],
component: [{ required: true, message: '请输入组件地址', trigger: 'blur' }],
component_name: [{ required: true, message: '请输入组件名称', trigger: 'blur' }],
});
let deptDefaultList = ref<MenuTreeItemType[]>([]);
let menuFormData = reactive<MenuFormDataType>({
parent: '',
name: '',
component: '',
web_path: '',
icon: '',
cache: true,
status: true,
visible: true,
component_name: '',
description: '',
is_catalog: false,
is_link: false,
});
let menuBtnLoading = ref(false);
const setMenuFormData = () => {
if (props.initFormData?.id) {
menuFormData.id = props.initFormData?.id || '';
menuFormData.name = props.initFormData?.name || '';
menuFormData.parent = props.initFormData?.parent || '';
menuFormData.component = props.initFormData?.component || '';
menuFormData.web_path = props.initFormData?.web_path || '';
menuFormData.icon = props.initFormData?.icon || '';
menuFormData.status = props.initFormData?.status || true;
menuFormData.visible = props.initFormData?.visible || true;
menuFormData.cache = props.initFormData?.cache || true;
menuFormData.component_name = props.initFormData?.component_name || '';
menuFormData.description = props.initFormData?.description || '';
menuFormData.is_catalog = props.initFormData?.is_catalog || false;
menuFormData.is_link = props.initFormData?.is_link || false;
}
};
const querySearch = (queryString: string, cb: any) => {
const files: any = import.meta.glob('@views/**/*.vue');
let fileLists: Array<any> = [];
Object.keys(files).forEach((queryString: string) => {
fileLists.push({
label: queryString.replace(/(\.\/|\.vue)/g, ''),
value: queryString.replace(/(\.\/|\.vue)/g, ''),
});
});
const results = queryString ? fileLists.filter(createFilter(queryString)) : fileLists;
// 统一去掉/src/views/前缀
results.forEach((val) => {
val.label = val.label.replace('/src/views/', '');
val.value = val.value.replace('/src/views/', '');
});
cb(results);
};
const createFilter = (queryString: string) => {
return (file: ComponentFileItem) => {
return file.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1;
};
};
/**
* 树的懒加载
*/
const handleTreeLoad = (node: Node, resolve: Function) => {
if (node.level !== 0) {
lazyLoadMenu({ parent: node.data.id }).then((res: APIResponseData) => {
resolve(res.data);
});
}
};
const handleSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (!valid) return;
try {
let res;
menuBtnLoading.value = true;
if (menuFormData.id) {
res = await UpdateObj(menuFormData);
} else {
res = await AddObj(menuFormData);
}
if (res?.code === 2000) {
successNotification(res.msg as string);
handleCancel('submit');
}
} finally {
menuBtnLoading.value = false;
}
});
};
const handleCancel = (type: string = '') => {
emit('drawerClose', type);
formRef.value?.resetFields();
};
onMounted(async () => {
props.treeData.map((item) => {
deptDefaultList.value.push(item);
});
setMenuFormData();
});
</script>
<style lang="scss" scoped>
.menu-form-com {
margin: 10px;
overflow-y: auto;
.menu-form-alert {
color: #fff;
line-height: 24px;
padding: 8px 16px;
margin-bottom: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
}
.menu-form-btns {
padding-bottom: 10px;
box-sizing: border-box;
}
}
</style>

View File

@@ -0,0 +1,318 @@
<template>
<el-input v-model="filterVal" :prefix-icon="Search" placeholder="请输入菜单名称" />
<div class="menu-tree-com">
<div class="mtc-head">
<el-icon size="16" color="#606266" class="mtc-head-icon">
<Menu />
</el-icon>
菜单列表
<el-tooltip
effect="dark"
placement="right"
content="1.红色菜单代表状态禁用; 2.添加菜单,如果是目录,组件地址为空即可; 3.添加根节点菜单父级ID为空即可; 4.支持拖拽菜单;"
>
<el-icon size="16" color="var(--el-color-primary)" class="mtc-tooltip">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
<el-tree
ref="treeRef"
:data="treeData"
:props="defaultTreeProps"
:filter-node-method="filterNode"
:load="handleTreeLoad"
lazy
:indent="45"
@node-click="handleNodeClick"
highlight-current
>
<template #default="{ node, data }">
<element-tree-line :node="node" :showLabelLine="false" :indent="32">
<span v-if="data.status" class="text-center font-black font-normal">
<SvgIcon :name="node.data.icon" color="var(--el-color-primary)" />
&nbsp;{{ node.label }}
</span>
<span v-else class="text-center font-black text-red-700 font-normal"> <SvgIcon :name="node.data.icon" />&nbsp;{{ node.label }} </span>
</element-tree-line>
</template>
</el-tree>
<div class="mtc-tags">
<el-tooltip effect="dark" content="新增">
<el-icon size="16" v-auth="'menu:Create'" @click="handleUpdateMenu('create')" class="mtc-tags-icon">
<Plus />
</el-icon>
</el-tooltip>
<el-tooltip effect="dark" content="编辑">
<el-icon size="16" v-auth="'menu:Update'" @click="handleUpdateMenu('update')" class="mtc-tags-icon">
<Edit />
</el-icon>
</el-tooltip>
<el-tooltip effect="dark" content="上移">
<el-icon size="16" v-auth="'menu:MoveUp'" @click="handleSort('up')" class="mtc-tags-icon">
<Top />
</el-icon>
</el-tooltip>
<el-tooltip effect="dark" content="下移">
<el-icon size="16" v-auth="'menu:MoveDown'" @click="handleSort('down')" class="mtc-tags-icon">
<Bottom />
</el-icon>
</el-tooltip>
<el-tooltip effect="dark" content="删除">
<el-icon size="16" v-auth="'menu:Delete'" @click="handleDeleteMenu()" class="mtc-tags-icon">
<Delete />
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, toRaw, watch, h } from 'vue';
import { ElTree } from 'element-plus';
import { getElementLabelLine } from 'element-tree-line';
import { Search } from '@element-plus/icons-vue';
import SvgIcon from '/@/components/svgIcon/index.vue';
import { lazyLoadMenu, menuMoveUp, menuMoveDown } from '../../api';
import { warningNotification } from '/@/utils/message';
import { TreeTypes, MenuTreeItemType } from '../../types';
import type Node from 'element-plus/es/components/tree/src/model/node';
interface IProps {
treeData: TreeTypes[];
}
const ElementTreeLine = getElementLabelLine(h);
const defaultTreeProps: any = {
children: 'children',
label: 'name',
icon: 'icon',
isLeaf: (data: TreeTypes[], node: Node) => {
if (node.data.is_catalog) {
return false;
} else {
return true;
}
},
};
const treeRef = ref<InstanceType<typeof ElTree>>();
withDefaults(defineProps<IProps>(), {
treeData: () => [],
});
const emit = defineEmits(['treeClick', 'deleteDept', 'updateDept']);
let filterVal = ref('');
let sortDisable = ref(false);
let treeSelectMenu = ref<Partial<MenuTreeItemType>>({});
let treeSelectNode = ref<Node | null>(null);
watch(filterVal, (val) => {
treeRef.value!.filter(val);
});
/**
* 树的搜索事件
*/
const filterNode = (value: string, data: any) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
/**
* 树的懒加载
*/
const handleTreeLoad = (node: Node, resolve: Function) => {
if (node.level !== 0) {
lazyLoadMenu({ parent: node.data.id }).then((res: APIResponseData) => {
resolve(res.data);
});
}
};
/**
* 树的点击事件
*/
const handleNodeClick = (record: MenuTreeItemType, node: Node) => {
treeSelectMenu.value = record;
treeSelectNode.value = node;
emit('treeClick', record);
};
/**
* 点击左侧编辑按钮
*/
const handleUpdateMenu = (type: string) => {
if (type === 'update') {
if (!treeSelectMenu.value.id) {
warningNotification('请选择菜单!');
return;
}
emit('updateDept', type, treeSelectMenu.value);
} else {
emit('updateDept', type);
}
};
/**
* 删除菜单
*/
const handleDeleteMenu = () => {
if (!treeSelectMenu.value.id) {
warningNotification('请选择菜单!');
return;
}
emit('deleteDept', treeSelectMenu.value.id, () => {
treeSelectMenu.value = {};
});
};
/**
* 移动操作
*/
const handleSort = async (type: string) => {
if (!treeSelectMenu.value.id) {
warningNotification('请选择菜单!');
return;
}
if (sortDisable.value) return;
const parentList = treeSelectNode.value?.parent.childNodes || [];
const index = parentList.findIndex((i) => i.data.id === treeSelectMenu.value.id);
const record = parentList.find((i) => i.data.id === treeSelectMenu.value.id);
if (type === 'up') {
if (index === 0) return;
parentList.splice(index - 1, 0, record as any);
parentList.splice(index + 1, 1);
sortDisable.value = true;
await menuMoveUp({ menu_id: treeSelectMenu.value.id });
sortDisable.value = false;
}
if (type === 'down') {
parentList.splice(index + 2, 0, record as any);
parentList.splice(index, 1);
sortDisable.value = true;
await menuMoveDown({ menu_id: treeSelectMenu.value.id });
sortDisable.value = false;
}
};
defineExpose({
treeRef,
});
</script>
<style lang="scss" scoped>
.menu-tree-com {
.mtc-head {
display: flex;
align-items: center;
margin-left: -8px;
color: #606266;
font-weight: 600;
.mtc-head-icon {
margin-right: 8px;
position: relative;
top: -1px;
}
.mtc-tooltip {
margin-left: 5px;
position: relative;
top: -1px;
}
}
.mtc-tags {
height: 40px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-around;
box-sizing: border-box;
.mtc-tags-icon {
cursor: pointer;
color: var(--el-color-primary);
}
}
}
</style>
<style lang="scss">
.menu-tree-com {
height: calc(100% - 60px);
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
.el-tree-node__content {
height: 32px !important;
}
.el-tree .el-tree-node__expand-icon svg {
display: none !important;
height: 0;
width: 0;
}
.el-tree-node__expand-icon {
font-size: 16px;
}
.el-tree-node__content > .el-tree-node__expand-icon {
padding: 0;
box-sizing: border-box;
margin-right: 5px;
margin-left: 24px;
}
.el-tree .el-tree-node__expand-icon.expanded {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
.el-tree .el-tree-node__expand-icon.is-leaf {
margin-left: 0;
}
.el-tree .el-tree-node__expand-icon:before {
background: url('../../../../../assets/img/menu-tree-show-icon.png') no-repeat center / 100%;
content: '';
display: block;
width: 24px;
height: 24px;
}
.el-tree .el-tree-node__expand-icon.expanded:before {
background: url('../../../../../assets/img/menu-tree-hidden-icon.png') no-repeat center / 100%;
content: '';
display: block;
width: 24px;
height: 24px;
}
.el-tree .is-leaf.el-tree-node__expand-icon::before {
display: block;
background: none !important;
content: '';
width: 18px;
height: 18px;
border: none;
}
}
</style>

View File

@@ -1,371 +0,0 @@
import * as api from './api';
import { CreateCrudOptionsProps, CreateCrudOptionsRet, dict, useCompute } from '@fast-crud/fast-crud';
const { compute } = useCompute();
import { shallowRef } from "vue";
import IconSelector from "/@/components/IconSelector/index.vue"
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }) => {
form.id = row.id;
await api.UpdateObj(form);
if (row.parent) {
//刷新父节点的状态
reloadTreeChildren(row.parent);
}
};
const delRequest = async ({ row }:any) => {
await api.DelObj(row.id);
if (row.parent) {
//刷新父节点的状态
reloadTreeChildren(row.parent);
}
};
const addRequest = async (context:any) => {
const {form} = context;
if(form.web_path===undefined||form.web_path===null){
form.web_path='/'
}
return await api.AddObj(form);
};
//刷新父节点状态
function reloadTreeChildren(parent:string|number) {
const data = crudExpose.getBaseTableRef().store.states.treeData;
if (data.value != null) {
const item = data.value[parent];
if (item != null) {
item.loaded = false;
item.expanded = false;
}
}
}
const createFilter = (queryString: string) => {
return (file: any) => {
return file.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1;
};
};
// 获取组件地址
const getCompoent = (queryString: string, cb: any) => {
const files: any = import.meta.glob('@views/**/*.vue');
let fileLists: Array<any> = [];
Object.keys(files).forEach((queryString: string) => {
fileLists.push({
label: queryString.replace(/(\.\/|\.vue)/g, ''),
value: queryString.replace(/(\.\/|\.vue)/g, ''),
});
});
const results = queryString ? fileLists.filter(createFilter(queryString)) : fileLists;
// 统一去掉/src/views/前缀
results.forEach((val) => {
val.label = val.label.replace('/src/views/', '');
val.value = val.value.replace('/src/views/', '');
});
cb(results);
};
// 验证路由地址
const { getFormData } = crudExpose;
const validateWebPath = (rule: any, value: string, callback: Function) => {
let pattern = /^\/.*?/;
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
const reg = getFormData().is_link ? patternUrl.test(value) : pattern.test(value);
if (reg) {
callback();
} else {
callback(new Error('请输入正确的地址'));
}
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: false,
},
form: {
labelWidth: '120px',
row: { gutter: 20 },
},
table: {
lazy: true,
load: async (row: any, treeNode: unknown, resolve: (date: any[]) => void) => {
//懒加载,更新和删除后,需要刷新父节点的状态,见上方
const obj = await api.GetChildren(row.id);
resolve([...obj.data]);
},
},
columns: {
id: {
title: 'ID',
key: 'id',
type: 'number',
column: {
width: 100,
},
form: {
show: false,
},
},
menu_type: {
title: '类型',
type: 'dict-radio',
dict: dict({
data: [
{ label: '目录', value: 0 },
{ label: '菜单', value: 1 },
{ label: '按钮', value: 2 },
],
}),
form: {
value:0,
component:{
optionName: "el-radio-button"
},
valueChange({ form, value, getComponentRef }) {
if (value) {
getComponentRef("parent")?.reloadDict(); // 执行city的select组件的reloadDict()方法触发“city”重新加载字典
}
}
},
column: {
show: false,
},
},
parent: {
title: '父级',
dict: dict({
prototype: true,
url({form}){
if(form && form.menu_type===1){
return '/api/system/menu/tree/?menu_type=0'
}else{
return `/api/system/menu/tree/?menu_type=1`
}
return undefined
},
label: 'name',
value: 'id',
}),
type: 'dict-select',
form: {
show:compute(({form})=>{
return [1,2].includes(form.menu_type);
}),
rules: [{ required: true, message: '必填项' }],
component: {},
},
column: {
show: false,
},
},
name: {
title: '名称',
search: { show: true },
type: 'text',
form: {
rules: [{ required: true, message: '请输入名称' }],
component: {
placeholder: '请输入名称',
},
},
},
icon: {
title: '图标',
form:{
show:compute(({form})=>{
return [0,1].includes(form.menu_type);
}),
component:{
name: shallowRef(IconSelector),
vModel: "modelValue",
}
},
column: {
component: {
style: 'font-size:18px',
},
},
},
sort: {
title: '排序',
type: 'number',
form: {
value: 1,
show:compute(({form})=>{
return [0,1].includes(form.menu_type);
}),
},
},
is_link: {
title: '外链接',
type: 'dict-switch',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: false,
show:compute(({form})=>{
return [1].includes(form.menu_type);
}),
},
},
web_path: {
title: '路由地址',
form: {
show:compute(({form})=>{
return [1].includes(form.menu_type);
}),
helper: compute(({ form }) => {
return form.is_link ? '请输入http开头的地址' : '浏览器中url的地址,请以/开头';
}),
rules: [{ required: true, message: '请输入路由地址', validator: validateWebPath, trigger: 'blur' }],
component: {
placeholder: '请输入路由地址',
},
},
column: {
show: false,
},
},
component_name: {
title: '组件名称',
form: {
show:compute(({form})=>{
return [1].includes(form.menu_type);
}),
rules: [{ required: true, message: '请输入组件名称', trigger: 'blur' }],
component: {
placeholder: '请输入组件名称',
},
},
column: {
show: false,
},
},
component: {
title: compute(({ form }) => {
return form.menu_type === 1 ? '组件地址' : '按钮权限值';
}),
form: {
col:{
span:24,
},
show: compute(({ form }) => {
return [1,2].includes(form.menu_type)
}),
helper: compute(({ form }) => {
return form.menu_type === 1 ? 'src/views下的文件夹地址' : '按钮权限值是唯一的标识';
}),
rules: [{ required: true, message: '请输入组件地址', trigger: 'blur' }],
component: {
style: {
width: '100%',
},
disabled: compute(({ form }) => {
if(form.is_link&&form.menu_type===1){
form.component ="无"
return form.is_link
}
form.component =null
return false
}),
name: compute(({ form }) => {
return [1,2].includes(form.menu_type)? 'el-autocomplete' : 'el-input';
}),
triggerOnFocus: false,
fetchSuggestions: (query, cb) => {
return getCompoent(query, cb);
},
},
},
column: {
show: false,
},
},
visible: {
title: '侧边可见',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: true,
show:compute(({form})=>{
return [1].includes(form.menu_type)
}),
},
},
cache: {
title: '是否缓存',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: false,
show:compute(({form})=>{
return [1].includes(form.menu_type)
}),
},
},
frame_out: {
title: '主框架外展示',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: false,
show:compute(({form})=>{
return [1].includes(form.menu_type)
}),
},
},
status: {
title: '状态',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '启用', value: true },
{ label: '禁用', value: false },
],
}),
form: {
value: true,
show:compute(({form})=>{
return [0,1].includes(form.menu_type);
}),
},
},
},
},
};
}

View File

@@ -1,41 +1,154 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<fs-page>
<el-row class="menu-el-row">
<el-col :span="6">
<div class="menu-box menu-left-box">
<MenuTreeCom
ref="menuTreeRef"
:treeData="menuTreeData"
@treeClick="handleTreeClick"
@updateDept="handleUpdateMenu"
@deleteDept="handleDeleteMenu"
/>
</div>
</el-col>
</fs-crud>
<fs-form-wrapper ref="addChildrenRef" />
</fs-page>
<el-col :span="18">
<el-tabs type="border-card">
<el-tab-pane label="按钮权限配置" >
<div style="height: 80vh">
<MenuButtonCom ref="menuButtonRef" />
</div>
</el-tab-pane>
<el-tab-pane label="列权限配置">
<div style="height: 80vh">
<MenuFieldCom ref="menuFieldRef"></MenuFieldCom>
</div>
</el-tab-pane>
</el-tabs>
</el-col>
</el-row>
<el-drawer v-model="drawerVisible" title="菜单配置" direction="rtl" size="500px" :close-on-click-modal="false" :before-close="handleDrawerClose">
<MenuFormCom
v-if="drawerVisible"
:initFormData="drawerFormData"
:cacheData="menuTreeCacheData"
:treeData="menuTreeData"
@drawerClose="handleDrawerClose"
/>
</el-drawer>
</fs-page>
</template>
<script lang="ts" setup>
import {ref, onMounted, reactive, computed} from "vue";
import createCrudOptions from "./crud";
import {useExpose, useCrud, useCompute} from "@fast-crud/fast-crud";
import {AddObj} from "./api";
import {ElMessage } from "element-plus"
<script lang="ts" setup name="menuPages">
import { ref, onMounted } from 'vue';
import XEUtils from 'xe-utils';
import { ElMessageBox } from 'element-plus';
import MenuTreeCom from './components/MenuTreeCom/index.vue';
import MenuButtonCom from './components/MenuButtonCom/index.vue';
import MenuFormCom from './components/MenuFormCom/index.vue';
import MenuFieldCom from './components/MenuFieldCom/index.vue';
import { GetList, DelObj } from './api';
import { successNotification } from '/@/utils/message';
import { APIResponseData, MenuTreeItemType } from './types';
let menuTreeData = ref([]);
let menuTreeCacheData = ref<MenuTreeItemType[]>([]);
let drawerVisible = ref(false);
let drawerFormData = ref<Partial<MenuTreeItemType>>({});
let menuTreeRef = ref<InstanceType<typeof MenuTreeCom> | null>(null);
let menuButtonRef = ref<InstanceType<typeof MenuButtonCom> | null>(null);
let menuFieldRef = ref<InstanceType<typeof MenuFieldCom> | null>(null);
const getData = () => {
GetList({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
menuTreeData.value = result;
});
};
/**
* 菜单的点击事件
*/
const handleTreeClick = (record: MenuTreeItemType) => {
menuButtonRef.value?.handleRefreshTable(record);
menuFieldRef.value?.handleRefreshTable(record)
};
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
/**
* 部门的 新增 or 编辑 事件
*/
const handleUpdateMenu = (type: string, record?: MenuTreeItemType) => {
if (type === 'update' && record) {
const parentData = menuTreeRef.value?.treeRef?.currentNode.parent.data || {};
menuTreeCacheData.value = [parentData];
drawerFormData.value = record;
}
drawerVisible.value = true;
};
const handleDrawerClose = (type?: string) => {
if (type === 'submit') {
getData();
}
drawerVisible.value = false;
drawerFormData.value = {};
};
/**
* 部门的删除事件
*/
const handleDeleteMenu = (id: string, callback: Function) => {
ElMessageBox.confirm('您确认删除该菜单项吗?', '温馨提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const res: APIResponseData = await DelObj(id);
callback();
if (res?.code === 2000) {
successNotification(res.msg as string);
getData();
}
});
};
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
// 你可以调用此方法重新初始化crud配置
// resetCrudOptions(options)
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
getData();
});
</script>
<style lang="scss" scoped>
.menu-el-row {
height: 100%;
overflow: hidden;
.el-col {
height: 100%;
padding: 10px 0;
box-sizing: border-box;
}
}
.menu-box {
height: 100%;
padding: 10px;
background-color: #fff;
box-sizing: border-box;
}
.menu-left-box {
position: relative;
border-radius: 0 8px 8px 0;
margin-right: 10px;
}
.menu-right-box {
border-radius: 8px 0 0 8px;
}
</style>

View File

@@ -3,7 +3,7 @@ import { dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudO
import tableSelector from '/@/components/tableSelector/index.vue';
import {shallowRef, computed, ref, inject} from 'vue';
import manyToMany from '/@/components/manyToMany/index.vue';
import {auth} from '/@/utils/authFunction'
const { compute } = useCompute();
interface CreateCrudOptionsTypes {
@@ -36,8 +36,6 @@ export const createCrudOptions = function ({ crudExpose, tabActivted }: { crudEx
return tabActivted.value === 'receive';
});
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
@@ -47,6 +45,15 @@ export const createCrudOptions = function ({ crudExpose, tabActivted }: { crudEx
editRequest,
delRequest,
},
actionbar:{
buttons:{
add:{
show:computed(() =>{
return tabActivted.value !== 'receive' && auth('messageCenter:Create');
})
},
}
},
rowHandle: {
fixed:'right',
width:150,
@@ -58,7 +65,7 @@ export const createCrudOptions = function ({ crudExpose, tabActivted }: { crudEx
text:"查看",
type:'text',
iconRight:'View',
show:hasPermissions("messageCenter:Search"),
show:auth("messageCenter:Search"),
click({ index, row }) {
crudExpose.openView({ index, row });
if (tabActivted.value === 'receive') {
@@ -70,7 +77,7 @@ export const createCrudOptions = function ({ crudExpose, tabActivted }: { crudEx
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions('messageCenter:Delete')
show:auth('messageCenter:Delete')
},
},
},

View File

@@ -1,57 +0,0 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import XEUtils from "xe-utils";
export const apiPrefix = '/api/system/role_api_permission/';
export function GetList(query: UserPageQuery) {
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 },
});
}
/**
* 获取数据范围授权的所有部门
* @param query
* @constructor
*/
export function GetAllDeptData(query: UserPageQuery) {
return request({
url: apiPrefix+'role_to_dept_all/',
method: 'get',
params: query,
}).then((res:any)=>{
return XEUtils.toArrayTree(res.data,{ parentKey: 'parent', key: 'id', children: 'children',})
})
}

View File

@@ -1,211 +0,0 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import {inject} from "vue";
export const createCrudOptions = function ({ crudExpose,propsContext }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
form.role = row.role;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
form.role = propsContext.roleId
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
},
form: {
col: { span: 24 },
labelWidth: '110px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
//@ts-ignore
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose!.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
name:{
title: '接口名称',
type: 'text',
search:{
show:true
},
column:{
width:150
}
},
method: {
title: '请求方式',
sortable: 'custom',
search: {
show:true
},
type: 'dict-select',
dict: dict({
data: [
{
label: 'GET',
value: 0,
},
{
label: 'POST',
value: 1,
},
{
label: 'PUT',
value: 2,
},
{
label: 'DELETE',
value: 3,
},
{
label: 'PATCH',
value: 4,
},
],
}),
column:{
width: 100,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
},
},
},
api: {
title: '接口地址',
sortable: 'custom',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
async getData(dict: any) {
return request('/swagger.json').then((ret: any) => {
const res = Object.keys(ret.paths);
const data = [];
for (const item of res) {
const obj = { label: '', value: '' };
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column:{
minWidth: 200,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 24,
props: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
helper: {
position: 'label',
tooltip: {
placement: 'top-start',
},
text: '请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/',
},
},
},
data_range: {
title: '数据权限范围',
search: {
show:true
},
type: 'dict-select',
dict:dict({
url:'/api/system/role_api_permission/data_scope/'
}),
column: {
minWidth:120,
},
},
dept:{
title:'数据权限部门',
column:{
minWidth:120,
cellRender(scope){
return <div>{scope.row.dept_name}</div>
}
},
form:{
show: compute(({form})=>{
return form.data_range===4
})
}
},
description:{
title:'描述',
type:'textarea',
column:{
width:300
}
}
},
},
};
};

View File

@@ -1,59 +0,0 @@
<template>
<div style="height: 86vh">
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #form_dept="scope">
<div>
<el-tree-select
v-model="scope.form.dept"
:data="allDeptData"
:props="treeProps"
node-key="id"
multiple
:render-after-expand="false"
show-checkbox
check-strictly
check-on-click-node
>
<template #default="{ data: { name } }">
{{ name }}
</template
>
</el-tree-select>
</div>
</template>
</fs-crud>
</div>
</template>
<script lang="ts" setup>
import {ref, onMounted, reactive} from 'vue';
import {useFs} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
import {GetAllDeptData} from './api'
const propsContext = defineProps({
roleId:{
type:String||Number,
required:true
}
})
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions,propsContext});
// 获取所有部门数据
const allDeptData = ref<any[]>([]);
const treeProps ={
label: 'name',
value: 'id',
children: 'children',
}
onMounted(async () => {
const res = await GetAllDeptData({role:propsContext.roleId});
allDeptData.value = res;
crudExpose.doSearch({form:{role:propsContext.roleId}})
});
</script>
<style scoped>
</style>

View File

@@ -1,208 +0,0 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { successMessage, successNotification, warningNotification } from '/@/utils/message';
import { inject } from 'vue';
import { automatchColumnsData } from '/@/views/system/columns/components/ColumnsTableCom/api';
export const createCrudOptions = function ({ crudExpose, props }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
form.role = props.role;
form.model = props.model;
form.model = props.app;
return await api.AddObj(form);
};
/**
* 自动匹配列
*/
const handleAutomatch = async () => {
if (props.role && props.model && props.app) {
const res = await automatchColumnsData(props);
if (res?.code === 2000) {
successNotification('匹配成功');
}
crudExpose.doSearch({ form: { role: props.role, model: props.model, app: props.app } });
}
warningNotification('请选择角色和模型表!');
};
//权限判定
const hasPermissions = inject('$hasPermissions');
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: false,
},
actionbar: {
buttons: {
auto: {
text: '自动匹配',
type: 'success',
click: () => {
return handleAutomatch();
},
},
},
},
rowHandle: {
//固定右侧
fixed: 'right',
},
form: {
col: { span: 24 },
labelWidth: '110px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
//@ts-ignore
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose!.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
field_name: {
title: '字段名',
type: 'text',
search: {
show: true,
},
column: {
width: 150,
},
},
title: {
title: '中文名',
sortable: 'custom',
search: {
show: true,
},
type: 'text',
column: {
width: 100,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
},
},
},
is_create: {
title: '创建时显示',
sortable: 'custom',
search: {
disabled: true,
},
type: 'dict-switch',
dict: dict({
data: [
{ value: true, label: '启用' },
{ value: false, label: '禁用' },
],
}),
form: {
value: true,
},
column: {
valueChange(context){
return api.UpdateObj(context.row)
},
component: {
name: 'fs-dict-switch',
},
},
},
is_update: {
title: '编辑时显示',
search: {
show: true,
},
type: 'dict-switch',
dict: dict({
data: [
{ value: true, label: '启用' },
{ value: false, label: '禁用' },
],
}),
form: {
value: true,
},
column: {
component: {
name: 'fs-dict-switch',
onChange: compute((context) => {
//动态onChange方法测试
return () => {
console.log('onChange', context.row.switch);
};
}),
},
},
},
is_query: {
title: '列表中显示',
type: 'dict-switch',
dict: dict({
data: [
{ value: true, label: '启用' },
{ value: false, label: '禁用' },
],
}),
form: {
value: true,
},
column: {
component: {
name: 'fs-dict-switch',
onChange: compute((context) => {
//动态onChange方法测试
return () => {
console.log('onChange', context.row.switch);
};
}),
},
},
},
},
},
};
};

View File

@@ -1,69 +0,0 @@
<template>
<div>
<el-divider>模型表</el-divider>
<div class="model-card">
<div v-for="(item,index) in allModelData" :key="index">
<el-text @click="onModelChecked(item,index)" :type="modelCheckIndex===index?'primary':''">{{item.app + '--'+item.title + '('+item.key+')' }}</el-text>
</div>
</div>
<el-divider>字段权限</el-divider>
<div style="height: 50vh">
<fs-crud ref="crudRef" v-bind="crudBinding">
</fs-crud>
</div>
</div>
</template>
<script lang="ts" setup>
import {ref, onMounted, reactive} from 'vue';
import {useFs} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
import {getModelList} from './api'
const propsContext = defineProps({
roleId:{
type:String||Number,
required:true
}
})
const props = reactive({
role: '',
model: '',
app: '',
menu:''
})
// 获取所有model
const allModelData = ref<any[]>([]);
const modelCheckIndex=ref(-1)
const onModelChecked = (row,index)=>{
modelCheckIndex.value = index
props.model = row.key
props.app = row.app
props.role=propsContext.roleId
crudExpose.setTableData([])
crudExpose.doSearch({form:{role:propsContext.roleId,model:row.key,app:row.app}})
}
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions,props});
onMounted(async () => {
const res = await getModelList();
allModelData.value = res.data;
});
</script>
<style scoped lang="scss">
.model-card{
height: 20vh;
overflow-y: scroll;
div{
margin: 15px 0;
cursor: pointer;
}
}
</style>

View File

@@ -1,212 +0,0 @@
<template>
<div>
<MenuPermissionTree
ref="permissionTreeRef"
:tree="menuPermissionTreeData"
:default-expand-all="true"
:editable="false"
node-key="id"
show-checkbox
:props="{ label: 'title' }"></MenuPermissionTree>
<div style="margin-top: 2em">
<el-button type="primary" @click="updatePermission">确定</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import {ref, onMounted, defineProps, watch, computed, reactive,nextTick} from 'vue';
import XEUtils from 'xe-utils';
import {errorNotification} from '/@/utils/message';
import {getDataPermissionRange, getDataPermissionDept, getMenuPremissionTree, saveMenuPremission,getMenuPremissionChecked} from './api';
import {MenuDataType, DataPermissionRangeType, CustomDataPermissionDeptType} from './types';
import {ElMessage} from 'element-plus'
import MenuPermissionTree from "./menuPermissionTree.vue";
const props = defineProps({
roleId: {
type: Number,
default: -1
}
})
//获取菜单/按钮权限
const permissionTreeRef = ref();
let menuPermissionTreeData = ref<MenuDataType[]>([]);
const getMenuPremissionTreeData = async () => {
const resMenu = await getMenuPremissionTree({role: props.roleId})
menuPermissionTreeData.value = resMenu
nextTick(() => {
updateChecked(props.roleId);
});
}
// 如果勾选节点中存在非叶子节点tree组件会将其所有子节点全部勾选
// 所以要找出所有叶子节点仅勾选叶子节点tree组件会将父节点同步勾选
function getAllCheckedLeafNodeId(tree, checkedIds, temp) {
for (let i = 0; i < tree.length; i++) {
const item = tree[i];
if (item.children && item.children.length !== 0) {
getAllCheckedLeafNodeId(item.children, checkedIds, temp);
} else {
if (checkedIds.indexOf(item.id) !== -1) {
temp.push(item.id);
}
}
}
return temp;
}
async function updateChecked(roleId:string|number) {
let checkedIds = await getMenuPremissionChecked({role: roleId});
// 找出所有的叶子节点
checkedIds = getAllCheckedLeafNodeId(menuPermissionTreeData.value, checkedIds, []);
permissionTreeRef.value.setCheckedKeys(checkedIds);
}
/**
* 更新菜单权限
*/
async function updatePermission() {
const roleId = props.roleId;
const { checked, halfChecked } = permissionTreeRef.value.getChecked();
const allChecked = [...checked, ...halfChecked];
const menuIds = allChecked.filter(item=>item !== -1)
await saveMenuPremission({role: roleId, menu: menuIds})
handleDrawerClose();
ElMessage.success("授权成功");
}
const emit = defineEmits(['handleDrawerClose']);
const handleDrawerClose = () => {
emit('handleDrawerClose')
}
defineExpose({getMenuPremissionTreeData})
</script>
<style lang="scss" scoped>
.permission-com {
margin: 15px;
box-sizing: border-box;
.pc-save-btn {
margin-bottom: 15px;
}
.pc-collapse-title {
line-height: 32px;
span {
font-size: 16px;
}
}
.pc-collapse-main {
padding-top: 15px;
box-sizing: border-box;
.pccm-item {
margin-bottom: 10px;
.btn-item {
display: flex;
align-items: center;
span {
margin-left: 5px;
}
}
.columns-list {
.width-txt {
width: 200px;
}
.width-check {
width: 100px;
}
.width-icon {
cursor: pointer;
}
.columns-head {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
span {
font-weight: 900;
}
}
.columns-item {
display: flex;
align-items: center;
padding: 6px 0;
box-sizing: border-box;
.ci-checkout {
height: auto !important;
}
}
}
}
}
.pc-dialog {
.dialog-select {
width: 100%;
}
.dialog-tree {
width: 100%;
margin-top: 20px;
}
}
}
</style>
<style lang="scss">
.permission-com {
.el-collapse {
border-top: none;
border-bottom: none;
}
.el-collapse-item {
margin-bottom: 15px;
}
.el-collapse-item__header {
height: auto;
padding: 15px;
border-radius: 8px;
border-top: 1px solid #ebeef5;
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
box-sizing: border-box;
}
.el-collapse-item__header.is-active {
border-radius: 8px 8px 0 0;
background-color: #fafafa;
}
.el-collapse-item__wrap {
padding: 15px;
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
border-top: 1px solid #ebeef5;
border-radius: 0 0 8px 8px;
background-color: #fafafa;
box-sizing: border-box;
.el-collapse-item__content {
padding-bottom: 0;
}
}
}
</style>

View File

@@ -1,205 +0,0 @@
<template>
<el-tree
v-if="computedTree"
ref="treeRef"
class="fs-permission-tree"
:class="{ 'is-editable': editable }"
:data="computedTree"
:props="computedProps"
@check="onChecked"
>
<template #default="{ data }">
<div :class="'node-title-pane'">
<div class="node-title">{{ data.name }}</div>
<div v-if="editable === true" class="node-suffix">
<fs-icon v-if="actions.add !== false" :icon="ui.icons.add" @click.stop="add(data)" />
<fs-icon v-if="actions.edit !== false && data.id !== -1" :icon="ui.icons.edit" @click.stop="edit(data)" />
<fs-icon
v-if="actions.remove !== false && data.id !== -1"
:icon="ui.icons.remove"
@click.stop="remove(data)"
/>
</div>
</div>
</template>
</el-tree>
</template>
<script lang="ts">
import _ from "lodash-es";
import { useUi, utils } from "@fast-crud/fast-crud";
import { defineComponent, ref, computed, nextTick, onMounted } from "vue";
export default defineComponent({
name: "FsPermissionTree",
props: {
/**
* 树形数据
* */
tree: {},
/**
* 是否可编辑
*/
editable: {
default: true
},
actions: {
default: {}
},
props: {}
} as any,
emits: ["add", "edit", "remove"],
setup(props: any, ctx) {
const treeRef = ref();
const { ui } = useUi();
const computedTree = computed(() => {
if (props.tree == null) {
return null;
}
const clone = _.cloneDeep(props.tree);
utils.deepdash.forEachDeep(clone, (value, key, pNode, context) => {
if (value == null) {
return;
}
if (!(value instanceof Object) || value instanceof Array) {
return;
}
if (value.class === "is-leaf-node") {
//处理过,无需再次处理
return;
}
if (value.children != null && value.children.length > 0) {
return;
}
const parents = context.parents;
if (parents.length < 2) {
return;
}
const parent = parents[parents.length - 2].value;
//看parent下面的children是否全部都没有children
for (const child of parent.children) {
if (child.children != null && child.children.length > 0) {
//存在child有children
return;
}
}
// 所有的子节点都没有children
parent.class = "is-twig-node"; // 连接叶子节点的末梢枝杈节点
for (const child of parent.children) {
child.class = "is-leaf-node";
}
});
console.log("nodes ", clone);
return [
{
name: "根节点",
id: -1,
children: clone
}
];
});
function add(data) {
ctx.emit("add", data);
}
function edit(data) {
ctx.emit("edit", data);
}
function remove(data) {
ctx.emit("remove", data);
}
function onChecked(a, b, c) {
console.log("chedcked", a, b, c);
}
function getChecked() {
const checked = treeRef.value.getCheckedKeys();
const halfChecked = treeRef.value.getHalfCheckedKeys();
return {
checked,
halfChecked
};
}
function setCheckedKeys(ids) {
treeRef.value.setCheckedKeys(ids);
}
function customNodeClass(data) {
if (data.class) {
return data.class;
}
return null;
}
const computedProps = computed(() => {
return _.merge({ class: customNodeClass }, props.props);
});
return {
computedTree,
add,
edit,
remove,
treeRef,
onChecked,
getChecked,
computedProps,
setCheckedKeys,
ui
};
}
});
</script>
<style lang="scss">
.fs-permission-tree {
height: 80vh;
overflow-y: scroll;
.el-tree-node.is-expanded.is-twig-node > .el-tree-node__children {
display: flex;
flex-wrap: wrap;
}
.is-twig-node > .el-tree-node__children > :not(:first-child) .el-tree-node__content {
padding-left: 0 !important;
}
.el-tree-node__content {
box-sizing: content-box;
padding: 5px;
}
.is-leaf-node {
//&::before {
// display: none;
//}
}
.node-title-pane {
display: flex;
.node-title {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.is-editable {
.el-tree-node__content {
&:hover {
.node-suffix {
visibility: visible;
}
}
}
.node-suffix {
margin-right: 5px;
visibility: hidden;
i {
width: 20px;
height: 20px;
}
> * {
margin-left: 5px;
}
}
}
}
</style>

View File

@@ -1,49 +1,18 @@
import { request } from "/@/utils/service";
import XEUtils from 'xe-utils';
/**
*
*
* @param roleId
* @param query
*/
export function getMenuPremissionTree(query:object) {
export function getRolePremission(query:object) {
return request({
url: '/api/system/role_menu_permission/menu_permission_tree/',
url: '/api/system/role_menu_button_permission/get_role_premission/',
method: 'get',
params:query
}).then((res:any)=>{
return XEUtils.toArrayTree(res.data,{ parentKey: 'parent_id', key: 'id', children: 'children',})
})
}
/**
*
* @param query
*/
export function getMenuPremissionChecked(query:object) {
return request({
url: '/api/system/role_menu_permission/get_menu_permission_checked/',
method: 'get',
params:query
}).then((res:any)=>{
return res.data
})
}
/**
*
*/
export function saveMenuPremission(data:object) {
return request({
url:'/api/system/role_menu_permission/save_menu_permission/',
method:'post',
data
})
}
/***
*
* @param roleId

View File

@@ -1,8 +1,8 @@
<template>
<el-drawer v-model="drawerVisible" title="权限配置" direction="rtl" size="60%"
:close-on-click-modal="false"
destroy-on-close
:before-close="handleDrawerClose">
<el-drawer v-model="drawerVisible" title="权限配置" direction="rtl" size="60%" :close-on-click-modal="false"
:before-close="handleDrawerClose"
:destroy-on-close="true"
>
<template #header>
<el-row>
<el-col :span="4">
@@ -10,32 +10,118 @@
<el-tag>{{ props.roleName }}</el-tag>
</div>
</el-col>
<el-col :span="6">
<div>
<el-button size="small" type="primary" class="pc-save-btn" @click="handleSavePermission">保存菜单授权
</el-button>
</div>
</el-col>
</el-row>
</template>
<div>
<el-tabs type="border-card" v-model="permissionTab">
<el-tab-pane label="菜单/按钮授权" name="menu">
<MenuPermission ref="menuPermissionRef" :role-id="props.roleId" @handleDrawerClose="handleDrawerClose"></MenuPermission>
</el-tab-pane>
<el-tab-pane label="请求接口授权" name="api">
<ApiPermission v-if="permissionTab==='api'" :role-id="props.roleId"></ApiPermission>
</el-tab-pane>
<el-tab-pane label="表单字段授权" name="field">
<FieldPermission v-if="permissionTab==='field'" :role-id="props.roleId"></FieldPermission>
</el-tab-pane>
</el-tabs>
</div>
<div class="permission-com">
<el-collapse v-model="collapseCurrent" @change="handleCollapseChange" accordion>
<el-collapse-item v-for="(item,mIndex) in menuData" :key="mIndex" :name="mIndex"
style=" background-color: #fafafa;">
<template #title>
<div>
<div class="pc-collapse-title">
<el-checkbox v-model="item.isCheck" @click.stop="null">
<span>{{ item.name }}</span>
</el-checkbox>
</div>
<div v-show="!collapseCurrent.includes(mIndex)" @click.stop="null" style="text-align: left;">
<el-checkbox v-for="btn in item.btns" :key="btn.value" :label="btn.value" v-model="btn.isCheck">
{{ btn.name }}
</el-checkbox>
</div>
</div>
</template>
<div class="pc-collapse-main">
<div class="pccm-item">
<p>允许对这些数据有以下操作</p>
<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>
</el-checkbox>
</div>
<div class="pccm-item">
<p>对这些数据有以下字段权限</p>
<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, item, head.value)">
<span>{{ head.label }}</span>
</el-checkbox>
</div>
</li>
<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"
:before-close="handleDialogClose">
<div class="pc-dialog">
<el-select v-model="dataPermission" @change="handlePermissionRangeChange" class="dialog-select"
placeholder="请选择">
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value"/>
</el-select>
<el-tree-select
v-show="dataPermission === 4"
node-key="id"
v-model="customDataPermission"
:props="defaultTreeProps"
:data="deptData"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="dialog-tree"
/>
</div>
<template #footer>
<div>
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
<el-button @click="handleDialogClose"> 取消</el-button>
</div>
</template>
</el-dialog>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import {ref, onMounted, defineProps, watch, computed, reactive,nextTick} from 'vue';
import {ref, onMounted, defineProps, watch, computed, reactive} from 'vue';
import XEUtils from 'xe-utils';
import {errorNotification} from '/@/utils/message';
import {
getDataPermissionRange,
getDataPermissionDept,
getRolePremission,
setRolePremission,
setBtnDatarange
} from './api';
import {MenuDataType, DataPermissionRangeType, CustomDataPermissionDeptType} from './types';
import {ElMessage} from 'element-plus'
import MenuPermission from "./MenuPermission/index.vue";
import ApiPermission from "./ApiPermission/index.vue";
import FieldPermission from "./FieldPermission/index.vue";
const props = defineProps({
roleId: {
type: Number,
@@ -51,87 +137,46 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:drawerVisible'])
const menuPermissionRef = ref()
const permissionTab = ref('menu')
const drawerVisible = ref(false)
watch(
() => props.drawerVisible,
(val) => {
drawerVisible.value = val;
nextTick(()=>{
menuPermissionRef.value.getMenuPremissionTreeData()
})
// fetchData()
getMenuBtnPermission()
fetchData()
}
);
const handleDrawerClose = () => {
permissionTab.value ='menu'
emit('update:drawerVisible', false);
}
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
let menuData = ref<MenuDataType[]>([]);
let collapseCurrent = ref(['1']);
let menuCurrent = ref<Partial<MenuDataType>>({});
let menuBtnCurrent = ref<number>(-1);
let dialogVisible = ref(false);
let dataPermissionRange = ref<DataPermissionRangeType[]>([]);
const formatDataRange = computed(() => {
return function(datarange:number){
return function (datarange: number) {
const findItem = dataPermissionRange.value.find((i) => i.value === datarange);
return findItem?.label || ''
return findItem?.label || ''
}
})
let deptData = ref<CustomDataPermissionDeptType[]>([]);
let dataPermission = ref();
let customDataPermission = ref([]);
///
const permissionTreeRef = ref();
let menuPermissionTreeData = ref<MenuDataType[]>([]);
const getMenuPremissionTreeData = async () => {
const resMenu = await getMenuPremissionTree({role: props.roleId})
menuPermissionTreeData.value = resMenu
nextTick(() => {
updateChecked(props.roleId);
});
}
// tree
// tree
function getAllCheckedLeafNodeId(tree, checkedIds, temp) {
for (let i = 0; i < tree.length; i++) {
const item = tree[i];
if (item.children && item.children.length !== 0) {
getAllCheckedLeafNodeId(item.children, checkedIds, temp);
} else {
if (checkedIds.indexOf(item.id) !== -1) {
temp.push(item.id);
}
}
}
return temp;
}
async function updateChecked(roleId:string|number) {
let checkedIds = await getMenuPremissionChecked({role: roleId});
//
checkedIds = getAllCheckedLeafNodeId(menuPermissionTreeData.value, checkedIds, []);
permissionTreeRef.value.setCheckedKeys(checkedIds);
}
/**
* 更新菜单权限
*/
async function updatePermission() {
const roleId = props.roleId;
const { checked, halfChecked } = permissionTreeRef.value.getChecked();
const allChecked = [...checked, ...halfChecked];
const menuIds = allChecked.filter(item=>item !== -1)
await saveMenuPremission({role: roleId, menu: menuIds})
handleDrawerClose();
//await updateChecked(roleId);
ElMessage.success("授权成功");
//,,
const getMenuBtnPermission = async () => {
const resMenu = await getRolePremission({role: props.roleId})
menuData.value = resMenu.data
}
const fetchData = async () => {
@@ -190,7 +235,7 @@ const handleDialogConfirm = () => {
if (btn.id === menuBtnCurrent.value) {
const findItem = dataPermissionRange.value.find((i) => i.value === dataPermission.value);
btn.data_range = findItem?.value || 0;
if(btn.data_range===4){
if (btn.data_range === 4) {
btn.dept = customDataPermission.value
}
}
@@ -216,7 +261,10 @@ const handleSavePermission = () => {
}
const column = reactive({
header:[{value:'is_create',label:'新增可见'},{value:'is_update',label:'编辑可见'},{value:'is_query',label:'列表可见'}]
header: [{value: 'is_create', label: '新增可见'}, {value: 'is_update', label: '编辑可见'}, {
value: 'is_query',
label: '列表可见'
}]
})
onMounted(() => {
@@ -234,6 +282,7 @@ onMounted(() => {
.pc-collapse-title {
line-height: 32px;
text-align: left;
span {
font-size: 16px;
@@ -327,6 +376,7 @@ onMounted(() => {
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
box-sizing: border-box;
background-color: #fafafa;
}
.el-collapse-item__header.is-active {

View File

@@ -3,7 +3,7 @@ import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { columnPermission } from '../../../utils/columnPermission';
import { successMessage } from '../../../utils/message';
import {auth} from '/@/utils/authFunction'
interface CreateCrudOptionsTypes {
output: any;
crudOptions: CrudOptions;
@@ -14,12 +14,10 @@ export const createCrudOptions = function ({
crudExpose,
rolePermission,
handleDrawerOpen,
hasPermissions,
}: {
crudExpose: CrudExpose;
rolePermission: any;
handleDrawerOpen: Function;
hasPermissions: Function;
}): CreateCrudOptionsTypes {
const pageRequest = async (query: any) => {
return await api.GetList(query);
@@ -47,6 +45,13 @@ export const createCrudOptions = function ({
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('role:Create')
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
@@ -56,15 +61,19 @@ export const createCrudOptions = function ({
show: true,
},
edit: {
show: hasPermissions('role:update'),
show: auth('role:Update'),
},
remove: {
show: hasPermissions('role:delete'),
show: auth('role:Delete'),
},
customNew: {
permission: {
type: 'primary',
text: '权',
// show: hasPermissions('role:Update'),
text: '权限配置',
show: auth('role:Permission'),
tooltip: {
placement: 'top',
content: '权限配置',
},
click: (context: any): void => {
const { row } = context;
handleDrawerOpen(row);
@@ -107,9 +116,9 @@ export const createCrudOptions = function ({
sortable: 'custom',
show: columnPermission('name', 'is_query'),
},
addForm: {
show: columnPermission('name', 'is_create'),
},
// addForm: {
// show: columnPermission('name', 'is_create'),
// },
editForm: {
show: columnPermission('name', 'is_update'),
},
@@ -150,7 +159,6 @@ export const createCrudOptions = function ({
column: {
minWidth: 90,
sortable: 'custom',
show: columnPermission('sort', 'is_query'),
},
addForm: {
show: columnPermission('sort', 'is_create'),
@@ -167,9 +175,6 @@ export const createCrudOptions = function ({
title: '状态',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: dictionary('button_status_bool'),
}),
column: {
width: 100,
component: {
@@ -192,7 +197,10 @@ export const createCrudOptions = function ({
},
editForm: {
show: columnPermission('status', 'is_update'),
}
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
update_datetime: {
title: '更新时间',

View File

@@ -6,19 +6,21 @@
</template>
</fs-crud>
<permission ref="rolePermission"></permission>
<PermissionComNew v-model:drawerVisible="drawerVisible" :roleId="roleId" :roleName="roleName" @drawerClose="handleDrawerClose" />
</fs-page>
</template>
<script lang="ts" setup name="role">
import { ref, onMounted, inject } from 'vue';
import {ref, onMounted, inject, onBeforeUpdate} from 'vue';
import { useColumnPermission } from '/@/stores/columnPermission';
import { GetPermission } from './api';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import permission from './components/PermissionCom/index.vue';
import PermissionComNew from './components/index.vue';
import PermissionComNew from './components/PermissionComNew/index.vue';
import _ from "lodash-es";
import {columnPermission} from "/@/utils/columnPermission";
let drawerVisible = ref(false);
let roleId = ref(null);
let roleName = ref(null);
@@ -29,11 +31,11 @@ const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
const hasPermissions: any = inject('$hasPermissions');
const fetchColumnPermission = async () => {
const res = await GetPermission();
useColumnPermission().setPermissionData(res.data);
console.log(3333,res)
};
const handleDrawerOpen = (row: any) => {
@@ -47,22 +49,53 @@ const handleDrawerClose = () => {
};
const { crudExpose } = useExpose({ crudRef, crudBinding });
const handlecolumnPermission = async (crudOptions:any)=>{
const res = await GetPermission();
const columns = crudOptions.columns;
for(let col in columns){
for(let i in res.data){
if(res.data[i].field_name === col){
columns[col].column.show = i['is_query']
columns[col].addForm = {
show:i['is_create']
}
columns[col].editForm = {
show:i['is_update']
}
break;
}
}
}
}
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose, rolePermission, handleDrawerOpen });
// 页面打开后获取列表数据
onMounted(async () => {
await fetchColumnPermission();
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose, rolePermission, handleDrawerOpen, hasPermissions });
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
onMounted( async () => {
await handlecolumnPermission(crudOptions)
// //合并新的crudOptions
// const newOptions = _.merge(crudOptions, {
// columns: {
// text: {
// title: "追加字段",
// type: "text"
// }
// }
// });
//重置crudBinding
// resetCrudOptions(newOptions);
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
crudExpose.doRefresh();
});
defineExpose(rolePermission);
</script>

View File

@@ -3,7 +3,7 @@ import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOption
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import { inject } from 'vue';
import { auth } from '/@/utils/authFunction';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
@@ -24,8 +24,6 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
return await api.exportData(query)
}
//权限判定
const hasPermissions:any = inject('$hasPermissions');
return {
crudOptions: {
@@ -43,8 +41,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
actionbar: {
buttons: {
add: {
show: hasPermissions('user:Create')
// show:true
show: auth('user:Create')
},
export:{
text:"导出",//按钮文字
@@ -66,17 +63,17 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
edit: {
iconRight: 'Edit',
type: 'text',
show: hasPermissions('user:Update'),
show: auth('user:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: hasPermissions('user:Delete'),
show: auth('user:Delete'),
},
custom: {
text: '重设密码',
type: 'text',
show: hasPermissions('user:ResetPassword'),
show: auth('user:ResetPassword'),
tooltip: {
placement: 'top',
content: '重设密码',

View File

@@ -1,237 +1,251 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import {inject} from "vue";
import {
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
compute,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import {request} from '/@/utils/service';
import {dictionary} from '/@/utils/dictionary';
import {successMessage} from '/@/utils/message';
import {auth} from '/@/utils/authFunction'
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({form, row}: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 150,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show:hasPermissions("api_white_list:Update")
},
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions("api_white_list:Delete")
},
},
},
form: {
col: { span: 24 },
labelWidth: '110px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
//@ts-ignore
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose!.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
method: {
title: '请求方式',
sortable: 'custom',
search: {
disabled: false,
},
type: 'dict-select',
dict: dict({
data: [
{
label: 'GET',
value: 0,
},
{
label: 'POST',
value: 1,
},
{
label: 'PUT',
value: 2,
},
{
label: 'DELETE',
value: 3,
},
{
label: 'PATCH',
value: 4,
},
],
}),
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
},
itemProps: {
class: { yxtInput: true },
},
},
},
url: {
title: '接口地址',
sortable: 'custom',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
async getData(dict: any) {
return request('/swagger.json').then((ret: any) => {
const res = Object.keys(ret.paths);
const data = [];
for (const item of res) {
const obj = { label: '', value: '' };
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column:{
minWidth: 200,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 24,
props: {
elProps: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
},
itemProps: {
class: { yxtInput: true },
},
helper: {
position: 'label',
tooltip: {
placement: 'top-start',
},
text: '请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/',
},
},
},
enable_datasource: {
title: '数据权限认证',
search: {
disabled: false,
},
type: 'dict-radio',
column: {
minWidth:120,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
},
},
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('api_white_list:Create')
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 150,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: auth("api_white_list:Update")
},
remove: {
iconRight: 'Delete',
type: 'text',
show: auth("api_white_list:Delete")
},
},
},
form: {
col: {span: 24},
labelWidth: '110px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: {show: false},
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
//@ts-ignore
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose!.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
method: {
title: '请求方式',
sortable: 'custom',
search: {
disabled: false,
},
type: 'dict-select',
dict: dict({
data: [
{
label: 'GET',
value: 0,
},
{
label: 'POST',
value: 1,
},
{
label: 'PUT',
value: 2,
},
{
label: 'DELETE',
value: 3,
},
{
label: 'PATCH',
value: 4,
},
],
}),
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
},
itemProps: {
class: {yxtInput: true},
},
},
},
url: {
title: '接口地址',
sortable: 'custom',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
async getData(dict: any) {
return request('/swagger.json').then((ret: any) => {
const res = Object.keys(ret.paths);
const data = [];
for (const item of res) {
const obj = {label: '', value: ''};
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column: {
minWidth: 200,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 24,
props: {
elProps: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
},
itemProps: {
class: {yxtInput: true},
},
helper: {
position: 'label',
tooltip: {
placement: 'top-start',
},
text: '请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/',
},
},
},
enable_datasource: {
title: '数据权限认证',
search: {
disabled: false,
},
type: 'dict-radio',
column: {
minWidth: 120,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
},
},
};
};