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
This commit is contained in:
dvadmin-开发-李强
2023-11-30 22:13:42 +08:00
committed by Coding
76 changed files with 3176 additions and 2731 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
@@ -125,7 +130,7 @@ or daphne :
~~~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)

100
README.md
View File

@@ -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
- 密码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 = [

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, RoleMenuButtonPermission
RoleMenuPermission, RoleMenuButtonPermission, MenuField
)
from dvadmin.utils.serializers import CustomModelSerializer
@@ -53,6 +53,16 @@ class MenuButtonInitSerializer(CustomModelSerializer):
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文件)
@@ -60,7 +70,7 @@ class MenuInitSerializer(CustomModelSerializer):
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)
@@ -76,10 +86,18 @@ class MenuInitSerializer(CustomModelSerializer):
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:
@@ -108,12 +126,24 @@ class MenuInitSerializer(CustomModelSerializer):
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', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
'cache', 'visible', 'parent', 'children', 'menu_button', 'creator', 'dept_belong_id']
'cache', 'visible', 'parent', 'children', 'menu_button','menu_field', 'creator', 'dept_belong_id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
@@ -128,7 +158,7 @@ class RoleInitSerializer(CustomModelSerializer):
class Meta:
model = Role
fields = ['name', 'key', 'sort', 'status', 'admin',
fields = ['name', 'key', 'sort', 'status',
'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
"key": "admin",
"sort": 1,
"status": true,
"admin": true,
"remark": null
},
{
@@ -12,7 +11,6 @@
"key": "public",
"sort": 2,
"status": true,
"admin": true,
"remark": null
}
]

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

@@ -13,7 +13,6 @@ class Role(CoreModel):
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
class Meta:
db_table = table_prefix + "system_role"
@@ -179,21 +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",)

View File

@@ -16,7 +16,7 @@ from dvadmin.system.views.role_menu import RoleMenuPermissionViewSet
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)
@@ -33,7 +33,7 @@ system_url.register(r'system_config', SystemConfigViewSet)
system_url.register(r'message_center', MessageCenterViewSet)
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
system_url.register(r'column', ColumnViewSet)
system_url.register(r'column', MenuFieldViewSet)
urlpatterns = [

View File

@@ -119,8 +119,7 @@ class MenuViewSet(CustomModelViewSet):
def web_router(self, request):
"""用于前端获取当前角色的路由"""
user = request.user
is_admin = user.role.values_list('admin', flat=True)
if user.is_superuser or True in is_admin:
if user.is_superuser:
queryset = self.queryset.filter(status=1)
else:
role_list = user.role.values_list('id', flat=True)

View File

@@ -11,7 +11,7 @@ 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
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -49,12 +49,24 @@ class MenuButtonViewSet(CustomModelViewSet):
retrieve:单例
destroy:删除
"""
queryset = MenuButton.objects.all()
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):
"""
@@ -63,8 +75,7 @@ class MenuButtonViewSet(CustomModelViewSet):
:return:
"""
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
if is_superuser:
queryset = MenuButton.objects.values_list('value',flat=True)
else:
role_id = request.user.role.values_list('id', flat=True)

View File

@@ -1,57 +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')
menu = request.query_params.get('menu')
if not role_id or not model_name or not app_name or not menu:
if not menu:
return SuccessResponse([])
queryset = self.filter_queryset(self.get_queryset().filter(role_id=role_id, model=model_name, app=app_name,menu_id=menu))
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
return self.get_paginated_response(serializer.data)
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)
@@ -60,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']),

View File

@@ -11,7 +11,8 @@ 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, Columns
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
@@ -71,12 +72,41 @@ class RoleButtonPermissionSerializer(CustomModelSerializer):
model = MenuButton
fields = ['id','name','value','isCheck','data_range']
class RoleColumnsSerializer(CustomModelSerializer):
class RoleFieldPermissionSerializer(CustomModelSerializer):
class Meta:
model = Columns
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):
"""
@@ -99,9 +129,8 @@ class RoleMenuPermissionSerializer(CustomModelSerializer):
return serializer.data
def get_columns(self, instance):
params = self.request.query_params
col_list = Columns.objects.filter(role__id=params.get('role'),menu__id=instance['id'])
serializer = RoleColumnsSerializer(col_list,many=True,request=self.request)
col_list = MenuField.objects.filter(menu=instance['id'])
serializer = RoleMenuFieldSerializer(col_list,many=True,request=self.request)
return serializer.data
@@ -165,10 +194,11 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
for btn in menu.get('btns'):
if btn.get('isCheck'):
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),data_range=btn.get('data_range'))
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'):
Columns.objects.filter(id=col.get('id')).update(is_query=col.get('is_query'),is_create=col.get('is_create'),is_update=col.get('is_update'))
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="授权成功")

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

@@ -121,13 +121,12 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
role__status=1,
menu_button__api=re_api,
menu_button__method=method).values(
'data_range',
role_admin=F('role__admin')
'data_range'
)
dataScope_list = [] # 权限范围列表
for ele in role_permission_list:
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
if ele.get("data_range") == 3 or ele.get("role_admin") == True:
if ele.get("data_range") == 3:
return queryset
dataScope_list.append(ele.get("data_range"))
dataScope_list = list(set(dataScope_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

@@ -79,6 +79,5 @@ class CustomPagination(PageNumberPagination):
('total', total),
('is_next', is_next),
('is_previous', is_previous),
('data', data),
('permission', self.request.permission_fields)
('data', data)
]))

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
# 运行项目
yarn dev
# 打包发布
yarn build
# 启动服务
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)

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

@@ -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

@@ -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

@@ -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

@@ -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,8 +1,8 @@
<template>
<el-drawer size="70%" v-model="drawer" direction="rtl" destroy-on-close :before-close="handleClose">
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
<div>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</div>
</el-drawer>
</template>

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

@@ -1,202 +1,215 @@
import { AddReq, DelReq, EditReq, dict, CreateCrudOptionsRet, CreateCrudOptionsProps } from '@fast-crud/fast-crud';
import {AddReq, DelReq, EditReq, dict, CreateCrudOptionsRet, CreateCrudOptionsProps} from '@fast-crud/fast-crud';
import * as api from './api';
import { request } from '/@/utils/service';
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: {
search: {
container: {
action: {
//按钮栏配置
col: {
span: 8,
},
},
},
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
icon: '',
type: 'primary',
},
remove: {
icon: '',
type: 'primary',
},
},
},
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: 120,
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" />;
},
},
},
},
},
},
};
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

@@ -1,5 +1,5 @@
<template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</template>
<script lang="ts" setup>

View File

@@ -0,0 +1,65 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import XEUtils from "xe-utils";
import {CurrentInfoType} from "/@/views/system/columns/types";
export const apiPrefix = '/api/system/column/';
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 },
});
}
/**
* 获取所有model
*/
export function getModelList() {
return request({
url: '/api/system/column/get_models/',
method: 'get',
});
}
/**
* 自动匹配field
* @param data
*/
export function automatchColumnsData(data: CurrentInfoType) {
return request({
url: '/api/system/column/auto_match_fields/',
method: 'post',
data,
});
}

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

@@ -41,31 +41,31 @@
<div class="mtc-tags">
<el-tooltip effect="dark" content="新增">
<el-icon size="16" @click="handleUpdateMenu('create')" class="mtc-tags-icon">
<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" @click="handleUpdateMenu('update')" class="mtc-tags-icon">
<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" @click="handleSort('up')" class="mtc-tags-icon">
<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" @click="handleSort('down')" class="mtc-tags-icon">
<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" @click="handleDeleteMenu()" class="mtc-tags-icon">
<el-icon size="16" v-auth="'menu:Delete'" @click="handleDeleteMenu()" class="mtc-tags-icon">
<Delete />
</el-icon>
</el-tooltip>

View File

@@ -14,9 +14,19 @@
</el-col>
<el-col :span="18">
<div class="menu-box menu-right-box">
<MenuButtonCom ref="menuButtonRef" />
</div>
<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>
@@ -39,6 +49,7 @@ 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';
@@ -49,7 +60,7 @@ 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;
@@ -67,6 +78,7 @@ const getData = () => {
*/
const handleTreeClick = (record: MenuTreeItemType) => {
menuButtonRef.value?.handleRefreshTable(record);
menuFieldRef.value?.handleRefreshTable(record)
};
/**

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,117 +0,0 @@
import { request } from "/@/utils/service";
/**
* 获取角色所拥有的菜单
* @param params
*/
export function GetMenu(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_get_menu/',
method: 'get',
params:params
});
}
/***
* 新增权限
* @param data
* @constructor
*/
export function SaveMenuPermission(data:any) {
return request({
url: '/api/system/role_menu_permission/save_auth/',
method: 'post',
data:data
});
}
/**
* 获取菜单下的按钮
* @param params
* @constructor
*/
export function GetMenuButton(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_menu_get_button/',
method: 'get',
params:params
});
}
/***
* 根据角色获取已授权的菜单
* @param params
* @constructor
*/
export function role_to_menu (params:any={}) {
return request({
url: '/api/system/role_menu_button_permission/role_to_menu/',
method: 'get',
params: params
})
}
/***
* 根据角色获取数据权限范围
* @constructor
*/
export function GetDataScope (params:any={}) {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
params: params
})
}
/***
* 获取权限部门
* @constructor
*/
export function GetDataScopeDept (params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get',
params: params
})
}
/***
* 新增权限
* @param data
* @constructor
*/
export function CreatePermission(data:any) {
return request({
url: '/api/system/role_menu_button_permission/',
method: 'post',
data:data
});
}
/***
* 根据菜单获取菜单下按钮
* @param params
*/
export function getObj(params:any) {
return request({
url: '/api/system/role_menu_button_permission/menu_to_button/',
method: 'get',
params:params
});
}
/**
* 删除按钮权限
* @param data
* @constructor
*/
export function DeletePermission(data:any) {
return request({
url: `/api/system/role_menu_button_permission/${data.id}/`,
method: 'delete',
data:{}
});
}

View File

@@ -1,350 +0,0 @@
<template>
<el-drawer size="70%" v-model="drawer" direction="rtl" destroy-on-close :before-close="handleClose">
<template #header>
<div>
<el-tag>当前角色:{{ editedRoleInfo.name }}</el-tag>
</div>
</template>
<div style="padding: 1em">
<div style="margin-bottom: 10px">
<el-button type="primary" @click="onSaveAuth">保存菜单授权</el-button>
</div>
<vxe-table
ref="tableRef"
border
resizable
:row-config="{ keyField: 'menu_id' }"
:tree-config="{ transform: true, rowField: 'menu_id', parentField: 'parent' }"
:checkbox-config="{ labelField: 'menu_id', checkRowKeys: multipleTableData, checkStrictly: true }"
:expand-config="{ accordion: true }"
@toggle-row-expand="menuNodeClick"
:data="menuData"
>
<vxe-column type="checkbox" title="ID" width="200" tree-node></vxe-column>
<vxe-column field="name" title="目录/菜单"></vxe-column>
<vxe-column type="expand" title="已授予权限" width="120">
<template #content="{ row, rowIndex }">
<div style="padding: 10px 0px" v-if="!row.is_catalog">
<el-button type="primary" size="small" style="margin-bottom: 0.5em" @click="createBtnPermission">新增 </el-button>
<el-table size="small" :data="buttonPermissionData" border style="width: 100%">
<el-table-column prop="menu_button" label="权限名称" width="100">
<template #default="scope">
<div>{{ scope.row.menu_button__name }}</div>
</template>
</el-table-column>
<el-table-column prop="menu_button__value" label="权限值" width="150"> </el-table-column>
<el-table-column prop="data_range" label="权限范围" width="140">
<template #default="scope">
<div>{{ formatDataRange(scope.row.data_range) }}</div>
</template>
</el-table-column>
<el-table-column prop="dept" label="权限涉及部门" />
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<el-button type="danger" size="small" @click="onDeleteBtn(scope)">删除 </el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</vxe-column>
</vxe-table>
<!-- 弹窗-->
<el-dialog v-model="dialogFormVisible" append-to-body width="400px" title="配置按钮权限">
<el-form ref="buttonFormRef" :model="buttonForm" :rules="buttonRules" label-width="120px">
<el-form-item label="按钮" prop="menu_button">
<el-select v-model="buttonForm.menu_button" placeholder="请选择按钮" @change="onChangeButton">
<el-option v-for="(item, index) in buttonOptions" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="权限范围" prop="data_range">
<el-select v-model="buttonForm.data_range" placeholder="请选择按钮">
<el-option v-for="(item, index) in dataScopeOptions" :key="index" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="数据部门" prop="dept" v-show="buttonForm.data_range === 4">
<div class="dept-tree">
<el-tree
:data="deptOptions"
show-checkbox
default-expand-all
:default-checked-keys="deptCheckedKeys"
ref="deptTree"
node-key="dept_id"
:check-strictly="true"
:props="{ label: 'name' }"
></el-tree>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="onSaveButtonForm"> 确定 </el-button>
</span>
</template>
</el-dialog>
</div>
</el-drawer>
</template>
<script lang="ts" setup>
import { ref, defineExpose, reactive, toRefs } from 'vue';
import { ElMessageBox, ElTable } from 'element-plus';
import * as api from './api.ts';
import type { FormRules, FormInstance } from 'element-plus';
import { ElMessage } from 'element-plus';
import XEUtils from 'xe-utils';
import { VXETable, VxeTableInstance, VxeTableEvents } from 'vxe-table';
interface tableRow {
menu_id: number;
name: string;
}
//抽屉是否显示
const drawer = ref(false);
//当前编辑的角色信息
const editedRoleInfo = ref({});
//抽屉关闭确认
const handleClose = (done: () => void) => {
ElMessageBox.confirm('您确定要关闭?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
done();
})
.catch(() => {
// catch error
});
};
/*****菜单的配置项***/
const defaultProps = {
children: 'children',
label: 'name',
isLeaf: 'hasChild',
};
interface Tree {
name: string;
children?: Tree[];
id: number;
}
let menuData = ref<Tree>();
//获取菜单
const getMenuData = () => {
api.GetMenu({}).then((res: any) => {
const { data } = res;
menuData.value = data;
});
};
//获取已授权的菜单
const tableRef = ref<VxeTableInstance<tableRow>>();
const multipleTableData = ref();
const getRoleToMenu = () => {
api.role_to_menu({ role: editedRoleInfo.value.id }).then((res: any) => {
const { data } = res;
multipleTableData.value = data;
});
};
let isBtnPermissionShow = ref(false);
let buttonOptions = ref<[]>();
let editedMenuInfo = ref();
//菜单节点点击事件
const menuNodeClick: VxeTableEvents.ToggleRowExpand<tableRow> = ({ expanded, row }) => {
// isBtnPermissionShow.value = !node.is_catalog
if (!row.is_catalog) {
buttonOptions.value = [];
editedMenuInfo.value = row;
api.GetMenuButton({ menu: row.menu_id }).then((res: any) => {
const { data } = res;
buttonOptions.value = data;
});
api.getObj({ menu: row.menu_id, role: editedRoleInfo.value.id }).then((res: any) => {
const { data } = res;
buttonPermissionData.value = data;
});
}
};
const menuTree = ref();
/*****菜单的配置项***/
/***按钮授权的弹窗****/
//是否显示新增表单
const dialogFormVisible = ref(false);
//部门树
const deptTree = ref();
//自定义部门数据
const deptOptions = ref();
//选中的部门数据
const deptCheckedKeys = [];
//按钮表单
const buttonForm = reactive({
menu_button: null,
role: null,
menu: null,
data_range: null,
dept: [],
});
//按钮表格数据
let buttonPermissionData = ref([]);
//按钮表单验证
const buttonRules = reactive<FormRules>({
menu_button: [{ required: true, message: '必填项' }],
data_range: [{ required: true, message: '必填项' }],
});
//新增按钮
const buttonFormRef = ref<FormInstance>();
const createBtnPermission = () => {
dialogFormVisible.value = true;
buttonForm.menu_button = null;
buttonForm.menu = null;
buttonForm.role = null;
buttonForm.data_range = null;
buttonForm.dept = [];
};
//权限范围数据
const dataScopeOptions = ref<[]>();
//按钮值变化事件
const onChangeButton = (val: any) => {
dataScopeOptions.value = [];
//获取权限值范围
api.GetDataScope({ menu_button: val }).then((res: any) => {
dataScopeOptions.value = res.data;
});
//获取权限部门值
api.GetDataScopeDept({ menu_button: val }).then((res: any) => {
deptOptions.value = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
});
};
//过滤按钮名称
const formatMenuBtn = (val: any) => {
let obj: any = buttonOptions.value?.find((item: any) => {
return item.id === val;
});
return obj ? obj.name : null;
};
//过滤权限范围
const formatDataRange = (val: any) => {
let obj: any = [
{
value: 0,
label: '仅本人数据权限',
},
{
value: 1,
label: '本部门及以下数据权限',
},
{
value: 2,
label: '本部门数据权限',
},
{
value: 3,
label: '全部数据权限',
},
{
value: 4,
label: '自定义数据权限',
},
].find((item: any) => {
return item.value === val;
});
return obj ? obj.label : null;
};
//保存按钮表单
const onSaveButtonForm = async () => {
const { id: roleId } = editedRoleInfo.value;
const { id: menuId } = editedMenuInfo.value;
const form: any = Object.assign({}, buttonForm);
form.role = roleId;
form.menu = menuId;
//选中的部门
const checkedList = deptTree.value.getCheckedKeys();
form.dept = checkedList;
if (!buttonFormRef.value) return;
await buttonFormRef.value.validate((valid, fields) => {
if (valid) {
api.CreatePermission(form).then((res: any) => {
const { data } = res;
buttonPermissionData.value.push(data);
dialogFormVisible.value = false;
ElMessage({
type: 'success',
message: res.msg,
});
});
} else {
ElMessage({
type: 'error',
title: '提交错误',
message: 'F12控制台看详情',
});
console.log('提交错误', fields);
}
});
};
//删除按钮权限
const onDeleteBtn = (scope: any) => {
const { row, $index } = scope;
ElMessageBox.confirm('您是否要删除数据?', '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
api.DeletePermission({ id: row.id }).then((res: any) => {
buttonPermissionData.value.splice($index, 1);
ElMessage({
type: 'success',
message: res.msg,
});
});
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
});
});
};
/***按钮授权的弹窗****/
//初始化数据
const initGet = () => {
getMenuData();
getRoleToMenu();
};
/**
* 保存授权
*/
const onSaveAuth = () => {
const $table = tableRef.value;
if ($table) {
const selectRecords = $table.getCheckboxRecords();
const menuIdList = selectRecords.map((record: any) => record.menu_id);
const { id: roleId } = editedRoleInfo.value;
const data = {
role: roleId,
menu: menuIdList,
};
api.SaveMenuPermission(data).then((res: any) => {
ElMessage({
message: res.msg,
type: 'success',
});
});
}
};
defineExpose({ drawer, editedRoleInfo, initGet });
</script>
<style scoped></style>

View File

@@ -1,21 +0,0 @@
import { request } from "/@/utils/service";
export function getDataPermissionRange() {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
})
}
export function getDataPermissionDept() {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get'
})
}
export function getDataPermissionMenu() {
return request({
url: '/api/system/role_menu_button_permission/get_role_permissions/',
method: 'get'
})
}

View File

@@ -1,235 +0,0 @@
<template>
<div class="permission-com">
<div class="pc-item">
<p class="pc-title">数据授权</p>
<div class="pc-cell">
<el-radio-group v-model="dataPermission" class="pc-data-permission">
<el-radio v-for="item in dataPermissionRange" :key="item.label" :label="item.value" @change="handleChange">{{ item.label }}</el-radio>
</el-radio-group>
<el-tree-select
v-if="dataPermission === 4"
node-key="id"
v-model="customDataPermission"
:props="defaultTreeProps"
:data="deptData"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="pc-custom-dept"
/>
</div>
</div>
<div class="pc-item pc-menu">
<p class="pc-title">菜单授权</p>
<div>
<el-tree
:props="defaultTreeProps"
:data="menuData"
show-checkbox
node-key="id"
default-expand-all
:expand-on-click-node="false"
class="dc-menu-tree"
>
<template #default="{ node, data }">
<div class="pc-tree-node" :class="{ 'tree-node-label-border': !data.is_catalog }">
<p class="tree-node-label">{{ node.label }}</p>
<div v-if="!data.is_catalog">
<ul class="menu-permission-list">
<li v-for="m in data.menuPermission" :key="m.id" class="menu-permission-item">
<el-checkbox v-model="m.id" :label="m.name" />
</li>
</ul>
<ul class="menu-permission-list">
<li v-for="m in data.columns" :key="m.id" class="menu-permission-item">
<el-checkbox v-model="m.id" :label="m.title" />
</li>
</ul>
</div>
</div>
</template>
</el-tree>
</div>
</div>
<div class="pc-btn">
<el-button type="primary">确定</el-button>
<el-button>取消</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import XEUtils from 'xe-utils';
import { getDataPermissionRange, getDataPermissionDept, getDataPermissionMenu } from './api';
import { DataPermissionRangeType, CustomDataPermissionDeptType, CustomDataPermissionMenuType } from './types';
const defaultTreeProps = {
children: 'children',
label: 'name',
value: 'id',
};
const data: any[] = [
{
id: 1,
label: 'Level one 1',
children: [
{
id: 4,
label: 'Level two 1-1',
isPenultimate: true,
children: [
{
id: 9,
label: 'Level three 1-1-1',
},
{
id: 10,
label: 'Level three 1-1-2',
},
],
},
],
},
{
id: 2,
label: 'Level one 2',
isPenultimate: true,
children: [
{
id: 5,
label: 'Level two 2-1',
},
{
id: 6,
label: 'Level two 2-2',
},
],
},
{
id: 3,
label: 'Level one 3',
isPenultimate: true,
children: [
{
id: 7,
label: 'Level two 3-1',
},
{
id: 8,
label: 'Level two 3-2',
},
],
},
];
let dataPermission = ref();
let dataPermissionRange = ref<DataPermissionRangeType[]>([]);
let customDataPermission = ref();
let deptData = ref<CustomDataPermissionDeptType[]>([]);
let menuData = ref<CustomDataPermissionMenuType[]>([]);
const fetchData = async () => {
try {
const resRange = await getDataPermissionRange();
const resMenu = await getDataPermissionMenu();
if (resRange?.code === 2000) {
dataPermissionRange.value = resRange.data;
}
if (resMenu?.code === 2000) {
console.log(resMenu.data);
menuData.value = resMenu.data;
}
} catch {
return;
}
};
const handleChange = async () => {
if (dataPermission.value === 4) {
const res = await getDataPermissionDept();
const data = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
deptData.value = data;
}
};
const handleTestClick = (node: any, data: any) => {
console.log(node, data);
};
onMounted(() => {
fetchData();
});
</script>
<style lang="scss" scoped>
.permission-com {
width: 100%;
height: 100%;
padding: 15px;
box-sizing: border-box;
.pc-item {
width: 100%;
margin-bottom: 15px;
border-bottom: 1px #dcdfe6 solid;
}
.pc-title {
font-weight: 600;
}
.pc-cell {
display: flex;
padding: 10px;
overflow-x: auto;
.pc-data-permission {
min-width: 800px;
}
.pc-custom-dept {
min-width: 200px;
}
}
.pc-menu {
height: calc(100% - 140px);
overflow-y: auto;
}
.pc-tree-node {
width: 100%;
display: flex;
align-items: center;
.tree-node-label {
font-size: 16px;
margin-right: 20px;
}
.menu-permission-list {
display: flex;
align-items: center;
.menu-permission-item {
margin-right: 10px;
}
}
}
.tree-node-label-border {
border-bottom: 1px #dcdfe6 solid;
}
.pc-btn {
padding-bottom: 15px;
}
}
</style>
<style lang="scss">
.dc-menu-tree {
.el-tree-node__content {
height: auto;
}
}
</style>

View File

@@ -1,20 +0,0 @@
export interface DataPermissionRangeType {
label: string;
value: number;
}
export interface CustomDataPermissionDeptType {
id: number;
name: string;
patent: number;
children: CustomDataPermissionDeptType[]
}
export interface CustomDataPermissionMenuType {
id: number;
name: string;
is_catalog: boolean;
menuPermission: { id: number; name: string; value: string }[] | null;
columns: { id: number; name: string; title: string }[] | null;
children: CustomDataPermissionMenuType[]
}

View File

@@ -1,6 +1,8 @@
<template>
<el-drawer v-model="drawerVisible" title="权限配置" direction="rtl" size="60%" :close-on-click-modal="false"
:before-close="handleDrawerClose">
:before-close="handleDrawerClose"
:destroy-on-close="true"
>
<template #header>
<el-row>
<el-col :span="4">
@@ -8,7 +10,7 @@
<el-tag>{{ props.roleName }}</el-tag>
</div>
</el-col>
<el-col :span="6" :offset="8">
<el-col :span="6">
<div>
<el-button size="small" type="primary" class="pc-save-btn" @click="handleSavePermission">保存菜单授权
</el-button>
@@ -18,15 +20,16 @@
</template>
<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">
<el-collapse-item v-for="(item,mIndex) in menuData" :key="mIndex" :name="mIndex"
style=" background-color: #fafafa;">
<template #title>
<div @click.stop="null">
<p class="pc-collapse-title">
<el-checkbox v-model="item.isCheck">
<div>
<div class="pc-collapse-title">
<el-checkbox v-model="item.isCheck" @click.stop="null">
<span>{{ item.name }}</span>
</el-checkbox>
</p>
<div v-show="!collapseCurrent.includes(mIndex)">
</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>
@@ -38,7 +41,7 @@
<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 }}
{{ 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>
@@ -49,7 +52,7 @@
<div class="pccm-item">
<p>对这些数据有以下字段权限</p>
<ul class="columns-list">
<ul class="columns-list">
<li class="columns-head">
<div class="width-txt">
<span>字段</span>
@@ -57,7 +60,7 @@
<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>
<span>{{ head.label }}</span>
</el-checkbox>
</div>
</li>
@@ -109,7 +112,13 @@
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 {
getDataPermissionRange,
getDataPermissionDept,
getRolePremission,
setRolePremission,
setBtnDatarange
} from './api';
import {MenuDataType, DataPermissionRangeType, CustomDataPermissionDeptType} from './types';
import {ElMessage} from 'element-plus'
@@ -156,9 +165,9 @@ 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[]>([]);
@@ -226,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
}
}
@@ -252,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(() => {
@@ -270,6 +282,7 @@ onMounted(() => {
.pc-collapse-title {
line-height: 32px;
text-align: left;
span {
font-size: 16px;
@@ -363,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,31 +61,15 @@ export const createCrudOptions = function ({
show: true,
},
edit: {
show: hasPermissions('role:Update'),
show: auth('role:Update'),
},
remove: {
show: hasPermissions('role:Delete'),
show: auth('role:Delete'),
},
/* custom: {
permission: {
type: 'primary',
text: '权限配置',
show: hasPermissions('role:Update'),
tooltip: {
placement: 'top',
content: '权限配置',
},
click: (context: any): void => {
const { row } = context;
// eslint-disable-next-line no-mixed-spaces-and-tabs
rolePermission.value.drawer = true;
rolePermission.value.editedRoleInfo = row;
rolePermission.value.initGet();
},
}, */
customNew: {
type: 'primary',
text: '权限配置',
show: hasPermissions('role:Update'),
show: auth('role:Permission'),
tooltip: {
placement: 'top',
content: '权限配置',
@@ -127,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'),
},
@@ -170,7 +159,6 @@ export const createCrudOptions = function ({
column: {
minWidth: 90,
sortable: 'custom',
show: columnPermission('sort', 'is_query'),
},
addForm: {
show: columnPermission('sort', 'is_create'),
@@ -183,40 +171,6 @@ export const createCrudOptions = function ({
value: 1,
},
},
admin: {
title: '是否管理员',
search: { show: false },
type: 'dict-radio',
dict: dict({
data: [
{
label: '是',
value: true,
color: 'success',
},
{
label: '否',
value: false,
color: 'danger',
},
],
}),
column: {
minWidth: 130,
sortable: 'custom',
show: columnPermission('admin', 'is_query'),
},
addForm: {
show: columnPermission('admin', 'is_create'),
},
editForm: {
show: columnPermission('admin', 'is_update'),
},
form: {
rules: [{ required: true, message: '是否管理员必填' }],
value: false,
},
},
status: {
title: '状态',
search: { show: true },

View File

@@ -13,14 +13,14 @@
</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/PermissionComNew/index.vue';
import _ from "lodash-es";
import {columnPermission} from "/@/utils/columnPermission";
let drawerVisible = ref(false);
let roleId = ref(null);
let roleName = ref(null);
@@ -31,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) => {
@@ -49,21 +49,51 @@ 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();
});

View File

@@ -1,107 +0,0 @@
import { request } from "/@/utils/service";
/**
* 获取角色所拥有的菜单
* @param params
*/
export function GetMenu(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_get_menu/',
method: 'get',
params:params
});
}
/***
* 新增权限
* @param data
* @constructor
*/
export function SaveMenuPermission(data:any) {
return request({
url: '/api/system/role_menu_permission/save_auth/',
method: 'post',
data:data
});
}
/**
* 获取菜单下的按钮
* @param params
* @constructor
*/
export function GetMenuButton(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_menu_get_button/',
method: 'get',
params:params
});
}
/***
* 根据角色获取数据权限范围
* @constructor
*/
export function GetDataScope (params:any={}) {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
params: params
})
}
/***
* 获取权限部门
* @constructor
*/
export function GetDataScopeDept (params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get',
params: params
})
}
/***
* 新增权限
* @param data
* @constructor
*/
export function CreatePermission(data:any) {
return request({
url: '/api/system/role_menu_button_permission/',
method: 'post',
data:data
});
}
/***
* 根据菜单获取菜单下按钮
* @param params
*/
export function getObj(params:any) {
return request({
url: '/api/system/role_menu_button_permission/menu_to_button/',
method: 'get',
params:params
});
}
/**
* 删除按钮权限
* @param data
* @constructor
*/
export function DeletePermission(data:any) {
return request({
url: `/api/system/role_menu_button_permission/${data.id}/`,
method: 'delete',
data:{}
});
}

View File

@@ -1,432 +0,0 @@
<template>
<el-drawer
size="70%"
v-model="drawer"
direction="rtl"
destroy-on-close
:before-close="handleClose"
>
<template #header>
<div>
<el-tag size="large" type="primary">当前角色:{{ editedRoleInfo.name }}</el-tag>
</div>
</template>
<div style="padding: 1em">
<el-row :gutter="10">
<el-col :xs="24" :sm="24" :md="8" :lg="6" :xl="6">
<el-card header="菜单页面授权">
<template #header>
<div class="card-header">
<el-tooltip effect="dark" content="点击菜单项,可对菜单下的按钮/接口授权"
placement="right">
<div>
<span>菜单页面</span>
<el-icon>
<QuestionFilled/>
</el-icon>
</div>
</el-tooltip>
<el-button size="mini" type="primary" @click="onSaveAuth">保存菜单授权</el-button>
</div>
</template>
<el-tree :data="menuData"
ref="menuTree"
show-checkbox
node-key="id"
highlight-current
:expand-on-click-node="false"
:check-on-click-node="true"
:props="defaultProps"
@node-click="menuNodeClick"
/>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="16" :lg="18" :xl="18">
<!-- <el-alert title="对页面菜单下按钮授权" description="新增或删除对菜单下的按钮/接口授权" type="warning" />-->
<el-card v-if="isBtnPermissionShow">
<template #header>
<div class="card-header">
<el-tooltip effect="dark" content="新增或删除对菜单下的按钮/接口授权" placement="right">
<div>
<span>按钮/接口授权</span>
<el-icon>
<QuestionFilled/>
</el-icon>
</div>
</el-tooltip>
</div>
</template>
<div>
<el-divider content-position="left">{{ editedMenuInfo.name }}</el-divider>
<el-button type="primary" size="small" style="margin-bottom: 0.5em"
@click="createBtnPermission">新增
</el-button>
<el-table size="small" :data="buttonPermissionData" border style="width: 100%">
<el-table-column prop="menu_button" label="权限名称" width="100">
<template #default="scope">
<div>{{ formatMenuBtn(scope.row.menu_button) }}</div>
</template>
</el-table-column>
<el-table-column prop="data_range" label="权限范围" width="140">
<template #default="scope">
<div>{{ formatDataRange(scope.row.data_range) }}</div>
</template>
</el-table-column>
<el-table-column prop="dept" label="权限涉及部门"/>
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<el-button type="danger" size="small" @click="onDeleteBtn(scope)">删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- <el-divider content-position="left">字段授权</el-divider>-->
<!-- <el-table size="small" :data="crudPermissionData" border style="width: 100%">-->
<!-- <el-table-column prop="field" label="字段"></el-table-column>-->
<!-- <el-table-column prop="table" label="列表显示">-->
<!-- <template #default="scope">-->
<!-- <div>-->
<!-- <el-switch size="mini" v-model="scope.row.table"/>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column prop="view" label="表单查看">-->
<!-- <template #default="scope">-->
<!-- <div>-->
<!-- <el-switch size="mini" v-model="scope.row.view"/>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column prop="edit" label="表单编辑">-->
<!-- <template #default="scope">-->
<!-- <div>-->
<!-- <el-switch size="mini" v-model="scope.row.edit"/>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
</el-card>
</el-col>
</el-row>
<el-dialog v-model="dialogFormVisible" width="400px" title="配置按钮权限">
<el-form ref="buttonFormRef" :model="buttonForm" :rules="buttonRules" label-width="120px">
<el-form-item label="按钮" prop="menu_button">
<el-select v-model="buttonForm.menu_button" placeholder="请选择按钮" @change="onChangeButton">
<el-option v-for="(item,index) in buttonOptions" :key="index" :label="item.name"
:value="item.id"/>
</el-select>
</el-form-item>
<el-form-item label="权限范围" prop="data_range">
<el-select v-model="buttonForm.data_range" placeholder="请选择按钮">
<el-option v-for="(item,index) in dataScopeOptions" :key="index" :label="item.label"
:value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="数据部门" prop="dept" v-show="buttonForm.data_range === 4">
<div class="dept-tree">
<el-tree
:data="deptOptions"
show-checkbox
default-expand-all
:default-checked-keys="deptCheckedKeys"
ref="deptTree"
node-key="id"
:check-strictly="true"
:props="{ label: 'name' }"
></el-tree>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="onSaveButtonForm">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</el-drawer>
</template>
<script lang="ts" setup name="rolePermission">
import {ref, defineExpose, reactive, toRefs} from 'vue'
import {ElMessageBox} from 'element-plus'
import * as api from './api'
import type {FormRules, FormInstance} from 'element-plus'
import {ElMessage} from 'element-plus'
import XEUtils from 'xe-utils'
//抽屉是否显示
const drawer = ref(false)
//当前编辑的角色信息
const editedRoleInfo = ref({})
//抽屉关闭确认
const handleClose = (done: () => void) => {
ElMessageBox.confirm('您确定要关闭?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
done()
})
.catch(() => {
// catch error
})
}
/*****菜单的配置项***/
const defaultProps = {
children: 'children',
label: 'name',
isLeaf: 'hasChild'
}
interface Tree {
name: string
children?: Tree[],
id: number
}
let menuData = ref<Tree>()
//获取菜单
const getMenuData = () => {
api.GetMenu({}).then((res: any) => {
const {data} = res
const list = XEUtils.toArrayTree(data, {parentKey: "parent", key:'menu_id',strict: true})
menuData.value = list
})
}
let isBtnPermissionShow = ref(false)
let buttonOptions = ref<[]>()
let editedMenuInfo = ref()
//菜单节点点击事件
const menuNodeClick = (node: any, obj: any) => {
isBtnPermissionShow.value = !node.is_catalog
if (!node.is_catalog) {
buttonOptions.value = []
editedMenuInfo.value = node
api.GetMenuButton({menu: node.menu_id}).then((res: any) => {
const {data} = res
buttonOptions.value = data
})
api.getObj({menu: node.menu_id, role: editedRoleInfo.value.id}).then((res: any) => {
const {data} = res
buttonPermissionData.value = data
})
}
}
const menuTree = ref()
/*****菜单的配置项***/
/***按钮授权的弹窗****/
//是否显示新增表单
const dialogFormVisible = ref(false)
//部门树
const deptTree = ref()
//自定义部门数据
const deptOptions = ref()
//选中的部门数据
const deptCheckedKeys = []
//按钮表单
const buttonForm = reactive({
menu_button: null,
role: null,
menu: null,
data_range: null,
dept: []
})
//按钮表格数据
let buttonPermissionData = ref([])
//按钮表单验证
const buttonRules = reactive<FormRules>({
menu_button: [
{required: true, message: '必填项'}
],
data_range: [
{required: true, message: '必填项'}
]
})
//新增按钮
const buttonFormRef = ref<FormInstance>()
const createBtnPermission = () => {
dialogFormVisible.value = true
buttonForm.menu_button = null
buttonForm.menu = null
buttonForm.role = null
buttonForm.data_range = null
buttonForm.dept = []
}
//权限范围数据
const dataScopeOptions = ref<[]>()
//按钮值变化事件
const onChangeButton = (val: any) => {
dataScopeOptions.value = []
//获取权限值范围
api.GetDataScope({menu_button: val}).then((res: any) => {
dataScopeOptions.value = res.data
})
//获取权限部门值
api.GetDataScopeDept({menu_button: val}).then((res: any) => {
deptOptions.value = XEUtils.toArrayTree(res.data, {parentKey: 'parent', strict: false})
})
}
//过滤按钮名称
const formatMenuBtn = (val: any) => {
let obj: any = buttonOptions.value?.find((item: any) => {
return item.id === val
})
return obj ? obj.name : null
}
//过滤权限范围
const formatDataRange = (val: any) => {
let obj: any = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
].find((item: any) => {
return item.value === val
})
return obj ? obj.label : null
}
//保存按钮表单
const onSaveButtonForm = async () => {
const {id: roleId} = editedRoleInfo.value
const {id: menuId} = editedMenuInfo.value
const form: any = Object.assign({}, buttonForm)
form.role = roleId
form.menu = menuId
//选中的部门
const checkedList = deptTree.value.getCheckedKeys()
form.dept = checkedList
if (!buttonFormRef.value) return
await buttonFormRef.value.validate((valid, fields) => {
if (valid) {
api.CreatePermission(form).then((res: any) => {
const {data} = res
buttonPermissionData.value.push(data)
dialogFormVisible.value = false
ElMessage({
type: 'success',
message: res.msg,
})
})
} else {
ElMessage({
type: 'error',
title: '提交错误',
message: 'F12控制台看详情',
})
console.log('提交错误', fields)
}
})
}
//删除按钮权限
const onDeleteBtn = (scope: any) => {
const {row, $index} = scope
ElMessageBox.confirm(
'您是否要删除数据?',
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
api.DeletePermission({id: row.id}).then((res: any) => {
buttonPermissionData.value.splice($index, 1)
ElMessage({
type: 'success',
message: res.msg,
})
})
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
/***按钮授权的弹窗****/
//初始化数据
const initGet = () => {
getMenuData()
}
/**
* 保存授权
*/
const onSaveAuth = () => {
//选中的菜单
const checkedList = menuTree.value.getCheckedKeys()
//半选中的菜单
const halfCheckedList = menuTree.value.getHalfCheckedKeys()
//合并的菜单数据
const menuIdList = [...checkedList, ...halfCheckedList]
// console.log(menuIdList)
const {id: roleId} = editedRoleInfo.value
const data = {
role: roleId,
menu: menuIdList
}
api.SaveMenuPermission(data).then((res: any) => {
ElMessage({
message: res.msg,
type: 'success',
})
})
}
defineExpose({drawer, editedRoleInfo, initGet})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.dept-tree::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.dept-tree {
height: 160px;
overflow-y: scroll;
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
border: 1px solid #e1e1e1;
width: 16em;
border-radius: 2px;
}
</style>

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'),
}),
},
},
},
};
};