86 Commits

Author SHA1 Message Date
dvadmin-开发-李强
82a16545b4 Accept Merge Request #5: (develop -> master)
1. 新增: 大数据选择器组件
2. 新增: docker默认一键启动参数
3. 新增: 集成celery和Redis
4. 新增: 封装时间范围的搜索,示例在登录日志页面
5. 优化: 角色授权中的获取授权列表接口
6. 优化: 角色授权中字段权限显示判断
7. 优化: 个人中心修改密码成功后,强制退出登录状态
8. 优化: getBaseURL函数
9. 修复: 页面缓存无效问题
10. 修复: 登录页背景logo设置无效问题
11. 修复: 全局过滤器导致过滤无效问题
12. 修复: 当dept_belong_id为null时,触发接口报错
2024-03-12 22:45:36 +08:00
猿小天
39638e2e6a 功能变化: 优化个人中心修改密码后强制退出登录状态 2024-02-28 15:27:03 +08:00
猿小天
d3e5f258e5 修复BUG: 修复页面缓存问题 2024-02-25 16:30:49 +08:00
猿小天
77a27cba14 Merge remote-tracking branch 'origin/develop' into develop 2024-02-25 15:03:26 +08:00
猿小天
26bbf67da8 新功能: 角色授权字段权限判断 2024-02-25 15:03:07 +08:00
李强
d4f754976e Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	backend/application/settings.py
2024-02-21 17:49:38 +08:00
猿小天
85805e0d4b 功能变化: 优化角色授权中的获取授权列表 2024-02-16 22:41:15 +08:00
猿小天
8a646e5ef7 新功能: 新增大数据选择组件 2024-02-16 20:13:20 +08:00
猿小天
71eec9cfbd 修复BUG:
1.消息中心不加载目标数据问题;
2.地区数据加载问题
2024-01-18 22:12:55 +08:00
猿小天
9bd79b9dc6 修复BUG:
1.角色管理没有分页问题;
2.自定义指令引用问题
2024-01-18 20:25:02 +08:00
李强
e210b7dd17 Merge remote-tracking branch 'origin/develop' into develop 2024-01-12 00:28:56 +08:00
李强
4d00661163 feat: 优化getBaseURL函数 2024-01-12 00:28:52 +08:00
H0nGzA1
6798fca362 fix: 修复登录页背景logo设置 2024-01-04 19:49:40 +08:00
猿小天
6f5bbb045d 修复BUG: 全局过滤器导致的过滤无效 2024-01-04 16:53:12 +08:00
猿小天
bbf3018b20 Merge remote-tracking branch 'origin/develop' into develop 2024-01-03 23:23:24 +08:00
猿小天
5510a18280 修复BUG: 全局过滤器导致的过滤无效 2024-01-03 23:23:13 +08:00
李强
97737c3ef1 feat: 更新docker默认一键启动参数 2024-01-02 23:17:39 +08:00
李强
0b554f3669 feat: 默认集成celery与redis 2024-01-02 22:10:57 +08:00
猿小天
63453450ad 修复BUG: 当dept_belong_id为null时,触发接口报错 2024-01-02 14:39:16 +08:00
猿小天
fea6ebd98e Merge remote-tracking branch 'origin/develop' into develop 2024-01-02 13:50:52 +08:00
猿小天
d4e43e28e8 新功能: 封装时间范围的搜索,示例在登录日志页面 2024-01-02 13:50:43 +08:00
dvadmin-开发-李强
9973688675 Accept Merge Request #4: (develop -> master)
Merge Request: 正式发布v3.0.1版本

Created By: @dvadmin-开发-李强
Accepted By: @dvadmin-开发-李强
URL: https://dvadmin-private.coding.net/p/code/d/dvadmin3/git/merge/4?initial=true
2024-01-01 22:56:43 +08:00
李强
98a5bbd60a feat: 文档更新 2024-01-01 22:41:01 +08:00
李强
ac7e8a7764 feat: CustomModelSerializer序列化器中dept_belong_id 添加默认值 2024-01-01 22:31:43 +08:00
猿小天
26ed59d1ff 功能变化: 修复部分bug 2024-01-01 15:30:48 +08:00
猿小天
369157fa4f 修复BUG: 当部门没有返回顶级部门时,部门接口为空情况 2024-01-01 15:00:17 +08:00
猿小天
8961733025 功能变化: 修复部分bug 2024-01-01 14:47:53 +08:00
猿小天
577b88332f 功能变化: 菜单授权返回包含父级的完整名称 2023-12-28 01:25:58 +08:00
猿小天
411f065bfc 功能变化: 菜单授权返回包含父级的完整名称 2023-12-28 00:41:07 +08:00
猿小天
8d37d929b2 功能变化: 菜单按钮排序 2023-12-27 23:35:28 +08:00
猿小天
c0d6cd54c3 Merge remote-tracking branch 'origin/develop' into develop 2023-12-27 23:29:22 +08:00
猿小天
ae8366f097 功能变化: 部门允许选择父级 2023-12-27 23:29:15 +08:00
猿小天
4baf5da0ff 新功能: 框架外路由 2023-12-27 23:22:25 +08:00
猿小天
c00f5f2beb 新功能: 框架外路由 2023-12-27 23:22:05 +08:00
李强
5143afdd85 feat: 列权限初始化 2023-12-27 21:26:21 +08:00
李强
b4153d1848 feat: 优化commonCrudConfig 2023-12-27 19:14:46 +08:00
猿小天
d2c0c54080 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	web/src/utils/commonCrud.ts
2023-12-27 19:10:53 +08:00
猿小天
7f184b2a9a 新功能:
1.部门显示组件
2023-12-27 19:08:55 +08:00
李强
87f9784445 feat: 优化commonCrudConfig顺序 2023-12-26 15:39:04 +08:00
李强
e900cc2280 feat: 优化所有类型的table居中显示 2023-12-26 15:38:09 +08:00
李强
4a26257a61 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	web/src/settings.ts
2023-12-26 15:37:08 +08:00
李强
06476c182e feat: 优化所有类型的table居中显示 2023-12-26 15:36:26 +08:00
猿小天
6146e6edb6 修复BUG:
1.新增用户时,默认密码
2023-12-26 12:46:19 +08:00
猿小天
85f5ff1935 修复BUG:
1.允许table表头被选中
2023-12-26 11:50:52 +08:00
猿小天
86e33e44e1 功能变化: 将网络字体改为本地字体 2023-12-26 11:33:37 +08:00
猿小天
217f3c8e14 Merge remote-tracking branch 'origin/develop' into develop 2023-12-26 10:49:44 +08:00
猿小天
d0feaa23e5 功能变化: 修改baseUrl函数 2023-12-26 10:49:09 +08:00
李强
fa67a27908 feat: 删除log打印 2023-12-26 09:51:45 +08:00
李强
56145dfe61 feat: 删除log打印 2023-12-26 09:50:49 +08:00
李强
3def205e6e Merge remote-tracking branch 'origin/develop' into develop 2023-12-26 09:48:06 +08:00
李强
873400bcce feat: 新增 placeholder 的默认值 2023-12-26 09:47:59 +08:00
猿小天
0c1cf1218b 新功能: 1. 引入fa 图标,兼容dvadmin2的图标 2023-12-25 20:35:33 +08:00
猿小天
d8e5ad9057 新功能: 加入隐私和条款文件 2023-12-25 20:13:05 +08:00
猿小天
3639cad58c 新功能: 1. 封装所有的更新时间、更新数据、部门等,类似dvadmin2的,一键加入 2023-12-25 20:02:43 +08:00
猿小天
c99eb1a9bc 修复BUG:
1.系统配置没有在刷新页面的时候获取;
2.登录页面网站标题等信息更新名字不刷新;
3.列权限排序,且需可以搜索
2023-12-25 18:48:11 +08:00
猿小天
7ae15022c0 新功能:
1.菜单新增框架外显示字段
2023-12-24 23:05:00 +08:00
猿小天
68b3d479ca 新功能:
1.菜单新增是否固定配置;
2.菜单新增框外显示配置
2023-12-24 12:09:15 +08:00
猿小天
2826cafa75 修复BUG:
1.验证码输入错误不刷新问题
2023-12-24 11:04:28 +08:00
猿小天
ba4a580bb0 修复BUG:
1.修复更新提示,拒绝后仍然弹出问题
2023-12-20 23:56:09 +08:00
猿小天
f322d38ab8 Merge remote-tracking branch 'origin/develop' into develop 2023-12-20 23:45:49 +08:00
猿小天
7d315a7d28 修复BUG:
1.文件上传返回值缺少问题
2023-12-20 23:44:45 +08:00
猿小天
916071814a 修复BUG:
1.接口报错后,继续通过的情况
2023-12-20 23:43:32 +08:00
李强
acf113e5e6 feat: 升级中心弹窗bug 2023-12-19 01:06:18 +08:00
李强
083eef9df1 Merge branch 'master' into develop 2023-12-19 00:54:05 +08:00
李强
e8c64d6d11 feat: 新增存活检测中间件 2023-12-19 00:50:42 +08:00
李强
d1abc58b40 feat: add __init__.py 2023-12-19 00:34:14 +08:00
李强
0c8ce0ce27 feat: 更新celery.py 添加重试机制 2023-12-18 18:14:18 +08:00
猿小天
23d320434e Merge remote-tracking branch 'origin/develop' into develop 2023-12-06 18:28:09 +08:00
猿小天
14907e140d 修复BUG:
1.修复加入列权限后没有分页问题
2023-12-06 18:28:00 +08:00
猿小天
1132e04080 修复BUG:
1.修复加入列权限后没有分页问题
2023-12-06 18:15:18 +08:00
猿小天
39fee0875d 修复BUG:
1.修复加入列权限后没有分页问题
2023-12-06 18:00:21 +08:00
符锦辉
5898ae85de chore: 后端降低channels版本为3 2023-12-05 17:56:43 +08:00
猿小天
172a867bad 修复BUG:
1.部门信息递归获取子部门
2023-12-04 23:57:40 +08:00
猿小天
5a308695fd 修复BUG:
1.个人中心密码修改,未对密码进行md5加密
2023-12-04 23:53:22 +08:00
猿小天
0c5c3c4ecd 修复BUG:
1.修复字段权限不显示序号的问题
2023-12-04 18:19:58 +08:00
猿小天
9a8241023b 修复BUG:
1.个人中心密码修改,未对密码进行md5加密
2023-12-03 23:28:53 +08:00
猿小天
27be35616b 修复BUG:
1.菜单管理中状态在编辑时显示不正确的问题
2023-12-03 23:24:32 +08:00
猿小天
e6f0a5e0ea 修复BUG:
1.修复角色相关代码中存在is_admin引起的错误
2023-12-03 23:07:18 +08:00
猿小天
cbebd8305c 修复BUG:
1.自定义权限bug
2023-12-03 23:04:26 +08:00
猿小天
66b68523f5 功能变化:
1.优化字典配置的表格显示;
2023-12-03 22:51:58 +08:00
猿小天
95b14e5a42 功能变化:
1.优化角色管理中的列权限;
2023-12-03 22:51:42 +08:00
猿小天
583b172a37 功能变化:
1.优化对字段权限的处理
2023-12-03 22:42:37 +08:00
猿小天
2227564c6f 功能变化:
1.优化对字段权限的处理
2023-12-03 15:19:42 +08:00
猿小天
5e3ecfc0a6 功能变化:
1.优化对字段权限的处理
2023-12-03 15:03:18 +08:00
猿小天
d467915f97 Merge remote-tracking branch 'origin/develop' into develop 2023-12-01 17:31:47 +08:00
猿小天
197714e845 修复BUG:
1.修复settings中上传文件的bug;
2.优化字典展示
2023-12-01 17:31:37 +08:00
91 changed files with 5818 additions and 1765 deletions

View File

@@ -1,175 +0,0 @@
# 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」**
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.
## 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.
* 👬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:[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)👩‍👦‍👦
github[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩‍👦‍👦
## core function
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 🔌
Updating...
## Repository Branch Explanation 💈
Main Branch: master (stable version)
Development Branch: develop
## before start project you need:
~~~
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/huge-dream/django-vue3-admin.git
# enter code dir
cd web
# install dependence
npm install yarn
yarn install --registry=https://registry.npm.taobao.org
# Start service
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
# yarn 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 uvicorn :
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
~~~
### 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 dvadmin3-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://foruda.gitee.com/images/1701348994587355489/1bc749e7_5074988.png)
![image-02](https://foruda.gitee.com/images/1701349037811908960/80d361db_5074988.png)
![image-03](https://foruda.gitee.com/images/1701349224478845203/954f0a7b_5074988.png)
![image-04](https://foruda.gitee.com/images/1701349248928658877/64926724_5074988.png)
![image-05](https://foruda.gitee.com/images/1701349259068943299/1306ba40_5074988.png)
![image-06](https://foruda.gitee.com/images/1701349294894429495/e3b3a8cf_5074988.png)
![image-07](https://foruda.gitee.com/images/1701350432536247561/3b26685e_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)

219
README.md
View File

@@ -1,171 +1,135 @@
# 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)
[预 览](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)
[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」**
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.
## 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.
我们是一群热爱代码的青年在这个炙热的时代下我们希望静下心来通过Code带来一点我们的色彩和颜色
* 🧑🤝🧑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.
* 👬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:[https://demo.dvadmin.com](https://demo.dvadmin.com)
## 平台简介
* demo accountsuperadmin
💡 [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
* demo passwordadmin123456
👩👦👦docs:[https://django-vue-admin.com](https://django-vue-admin.com)
## communication
* Communication community:[click here](https://bbs.django-vue-admin.com)👩‍👦‍👦
* 🧑‍🤝‍🧑前端采用 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 授权。
* plugins market:[click here](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦
#### 🏭 环境支持
## source code url:
| Edge | Firefox | Chrome | Safari |
| --------- | ------------ | ----------- | ----------- |
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
gitee(Main push)[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩‍👦‍👦
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
github[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩‍👦‍👦
## core function
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 🔌
👩‍👧‍👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
Updating...
- 账号superadmin
## Repository Branch Explanation 💈
Main Branch: master (stable version)
Development Branch: develop
- 密码admin123456
## before start project you need:
👩‍👦‍👦文档地址:[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地址[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩‍👦‍👦
## 内置功能
1. 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
2. 🧑‍⚕️部门管理:配置系统组织机构(公司、部门、角色)。
3. 👩‍⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
4. 🧑‍🎓按钮权限控制:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
5. 🧑‍🎓字段列权限控制:授权页面的字段显示权限,具体到某一列的显示权限。
7. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
8. 👬接口白名单:配置不需要进行权限校验的接口。
9. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
10. 🧑‍🔧地区管理:对省市县区域进行管理。
11. 📁附件管理:对平台上所有文件、图片等进行统一管理。
12. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌
更新中...
## 仓库分支说明 💈
主分支master稳定版本
开发分支develop
## 准备工作
~~~
Python >= 3.11.0 (最低3.9+版本)
nodejs >= 16.0
Mysql >= 8.0 (可选默认数据库sqlite3支持5.7+,推荐8.0版本)
Redis (可选,最新版)
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/huge-dream/django-vue3-admin.git
# 进入项目目录
# enter code dir
cd web
# 安装依赖
# install dependence
npm install yarn
yarn install --registry=https://registry.npm.taobao.org
# 启动服务
yarn build
# 浏览器访问 http://localhost:8080
# .env.development 文件中可配置启动端口等参数
# 构建生产环境
# Start service
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
# yarn run build
```
## 后端💈
## backend💈
~~~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
或使用 uvicorn :
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 uvicorn :
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
~~~
## 开发建议
前后端backend与web各自单独一个窗口打开进行开发
### 访问项目
### visit backend swagger
- 访问地址[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
- 账号`superadmin` 密码`admin123456`
* visit url[http://localhost:8080](http://localhost:8080) (The default address is this one. If you want to change it, follow the configuration file)
* account`superadmin` password`admin123456`
### docker-compose 运行
### docker-compose
~~~shell
# 先安装docker-compose (自行百度安装),执行此命令等待安装如有使用celery插件请打开docker-compose.yml中celery 部分注释
docker-compose up -d
# 初始化后端数据(第一次执行即可)
# Initialize backend data (first execution only)
docker exec -ti dvadmin3-django bash
python manage.py makemigrations
python manage.py migrate
@@ -173,22 +137,20 @@ 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
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 停止
# docker-compose stop
docker-compose down
# docker-compose 重启
# docker-compose restart
docker-compose restart
# docker-compose 启动时重新进行 build
# docker-compose on start build
docker-compose up -d --build
~~~
## 演示图✅
## Demo screenshot✅
![image-01](https://foruda.gitee.com/images/1701348994587355489/1bc749e7_5074988.png)
@@ -211,4 +173,3 @@ docker-compose up -d --build
![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png)

214
README.zh.md Normal file
View File

@@ -0,0 +1,214 @@
# 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)
[预 览](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 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus
* 🧑‍🤝‍🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/))
* 👭后端采用 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 授权。
#### 🏭 环境支持
| Edge | Firefox | Chrome | Safari |
| --------- | ------------ | ----------- | ----------- |
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
## 在线体验
👩‍👧‍👦演示地址:[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地址[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩‍👦‍👦
## 内置功能
1. 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
2. 🧑‍⚕️部门管理:配置系统组织机构(公司、部门、角色)。
3. 👩‍⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
4. 🧑‍🎓按钮权限控制:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
5. 🧑‍🎓字段列权限控制:授权页面的字段显示权限,具体到某一列的显示权限。
7. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
8. 👬接口白名单:配置不需要进行权限校验的接口。
9. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
10. 🧑‍🔧地区管理:对省市县区域进行管理。
11. 📁附件管理:对平台上所有文件、图片等进行统一管理。
12. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌
更新中...
## 仓库分支说明 💈
主分支master稳定版本
开发分支develop
## 准备工作
~~~
Python >= 3.11.0 (最低3.9+版本)
nodejs >= 16.0
Mysql >= 8.0 (可选默认数据库sqlite3支持5.7+推荐8.0版本)
Redis (可选,最新版)
~~~
## 前端♝
```bash
# 克隆项目
git clone https://gitee.com/huge-dream/django-vue3-admin.git
# 进入项目目录
cd web
# 安装依赖
npm install yarn
yarn install --registry=https://registry.npm.taobao.org
# 启动服务
yarn build
# 浏览器访问 http://localhost:8080
# .env.development 文件中可配置启动端口等参数
# 构建生产环境
# yarn 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
或使用 uvicorn :
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
~~~
## 开发建议
前后端backend与web各自单独一个窗口打开进行开发
### 访问项目
- 访问地址:[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 dvadmin3-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://foruda.gitee.com/images/1701348994587355489/1bc749e7_5074988.png)
![image-02](https://foruda.gitee.com/images/1701349037811908960/80d361db_5074988.png)
![image-03](https://foruda.gitee.com/images/1701349224478845203/954f0a7b_5074988.png)
![image-04](https://foruda.gitee.com/images/1701349248928658877/64926724_5074988.png)
![image-05](https://foruda.gitee.com/images/1701349259068943299/1306ba40_5074988.png)
![image-06](https://foruda.gitee.com/images/1701349294894429495/e3b3a8cf_5074988.png)
![image-07](https://foruda.gitee.com/images/1701350432536247561/3b26685e_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)

View File

@@ -1,9 +1,11 @@
import functools
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
from django.conf import settings
from celery import platforms
# 租户模式
if "django_tenants" in settings.INSTALLED_APPS:
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
@@ -16,3 +18,23 @@ else:
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
platforms.C_FORCE_ROOT = True
def retry_base_task_error():
"""
celery 失败重试装饰器
:return:
"""
def wraps(func):
@app.task(bind=True, retry_delay=180, max_retries=3)
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as exc:
raise self.retry(exc=exc)
return wrapper
return wraps

View File

@@ -63,6 +63,7 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
"dvadmin.utils.middleware.HealthCheckMiddleware",
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
@@ -154,6 +155,11 @@ STATICFILES_DIRS = [
MEDIA_ROOT = "media" # 项目下的目录
MEDIA_URL = "/media/" # 跟STATIC_URL类似指定用户可以通过这个url找到文件
#添加以下代码以后就不用写{% load staticfiles %},可以直接引用
STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder"
)
# 收集静态文件,必须将 MEDIA_ROOT,STATICFILES_DIRS先注释
# python manage.py collectstatic
# STATIC_ROOT=os.path.join(BASE_DIR,'static')
@@ -402,5 +408,7 @@ PLUGINS_URL_PATTERNS = []
# from dvadmin_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理
#from dvadmin_social_auth.settings import *
#from dvadmin_uniapp.settings import *
# ...
# ********** 一键导入插件配置结束 **********

View File

@@ -7,30 +7,30 @@ from application.settings import BASE_DIR
# ================================================= #
# 数据库 ENGINE ,默认演示使用 sqlite3 数据库,正式环境建议使用 mysql 数据库
# sqlite3 设置
DATABASE_ENGINE = "django.db.backends.sqlite3"
DATABASE_NAME = os.path.join(BASE_DIR, "db.sqlite3")
# DATABASE_ENGINE = "django.db.backends.sqlite3"
# DATABASE_NAME = os.path.join(BASE_DIR, "db.sqlite3")
# 使用mysql时改为此配置
# DATABASE_ENGINE = "django.db.backends.mysql"
# DATABASE_NAME = 'django-vue-admin' # mysql 时使用
DATABASE_ENGINE = "django.db.backends.mysql"
DATABASE_NAME = 'django-vue3-admin' # mysql 时使用
# 数据库地址 改为自己数据库地址
DATABASE_HOST = "127.0.0.1"
DATABASE_HOST = '127.0.0.1'
# # 数据库端口
DATABASE_PORT = 3306
# # 数据库用户名
DATABASE_USER = "root"
# # 数据库密码
DATABASE_PASSWORD = "123456"
DATABASE_PASSWORD = "DVADMIN3"
# 表前缀
TABLE_PREFIX = "dvadmin_"
# ================================================= #
# ******** redis配置无redis 可不进行配置 ******** #
# ================================================= #
# REDIS_PASSWORD = ''
# REDIS_HOST = '127.0.0.1'
# REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6380'
REDIS_PASSWORD = 'DVADMIN3'
REDIS_HOST = '127.0.0.1'
REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379'
# ================================================= #
# ****************** 功能 启停 ******************* #
# ================================================= #

View File

@@ -5,6 +5,7 @@ from rest_framework import serializers
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
import django
django.setup()
from dvadmin.system.models import (
Role, Dept, Users, Menu, MenuButton,
@@ -60,9 +61,10 @@ class MenuFieldInitSerializer(CustomModelSerializer):
class Meta:
model = MenuField
fields = ['id', 'menu','field_name','title','model']
fields = ['id', 'menu', 'field_name', 'title', 'model']
read_only_fields = ["id"]
class MenuInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
@@ -71,6 +73,7 @@ class MenuInitSerializer(CustomModelSerializer):
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)
@@ -90,7 +93,7 @@ class MenuInitSerializer(CustomModelSerializer):
data = []
instance = obj.menufield_set.order_by('field_name')
if instance:
data = list(instance.values('field_name', 'title','model'))
data = list(instance.values('field_name', 'title', 'model'))
return data
def save(self, **kwargs):
@@ -131,8 +134,9 @@ class MenuInitSerializer(CustomModelSerializer):
for field_data in menu_field:
field_data['menu'] = instance.id
filter_data = {
'menu':field_data['menu'],
'field_name':field_data['field_name']
'menu': field_data['menu'],
'field_name': field_data['field_name'],
'model': field_data['model']
}
instance_obj = MenuField.objects.filter(**filter_data).first()
serializer = MenuFieldInitSerializer(instance_obj, data=field_data, request=self.request)
@@ -143,7 +147,7 @@ class MenuInitSerializer(CustomModelSerializer):
class Meta:
model = Menu
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
'cache', 'visible', 'parent', 'children', 'menu_button','menu_field', 'creator', 'dept_belong_id']
'cache', 'visible', 'parent', 'children', 'menu_button', 'menu_field', 'creator', 'dept_belong_id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}

View File

@@ -182,7 +182,7 @@
"parent": 1,
"title": "隐私链接",
"key": "privacy_url",
"value": "#",
"value": "/api/system/clause/privacy.html",
"sort": 7,
"status": true,
"data_options": null,
@@ -196,7 +196,7 @@
"parent": 1,
"title": "条款链接",
"key": "clause_url",
"value": "#",
"value": "/api/system/clause/terms_service.html",
"sort": 8,
"status": true,
"data_options": null,

View File

@@ -162,6 +162,7 @@ class Menu(CoreModel):
(1, ""),
)
is_link = models.BooleanField(default=False, verbose_name="是否外链", help_text="是否外链")
link_url = models.CharField(max_length=255, verbose_name="链接地址", null=True, blank=True, help_text="链接地址")
is_catalog = models.BooleanField(default=False, verbose_name="是否目录", help_text="是否目录")
web_path = models.CharField(max_length=128, verbose_name="路由地址", null=True, blank=True, help_text="路由地址")
component = models.CharField(max_length=128, verbose_name="组件地址", null=True, blank=True, help_text="组件地址")
@@ -171,7 +172,29 @@ class Menu(CoreModel):
cache = models.BooleanField(default=False, blank=True, verbose_name="是否页面缓存", help_text="是否页面缓存")
visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示",
help_text="侧边栏中是否显示")
is_iframe = models.BooleanField(default=False, blank=True, verbose_name="框架外显示", help_text="框架外显示")
is_affix = models.BooleanField(default=False, blank=True, verbose_name="是否固定", help_text="是否固定")
@classmethod
def get_all_parent(cls, id: int, all_list=None, nodes=None):
"""
递归获取给定ID的所有层级
:param id: 参数ID
:param all_list: 所有列表
:param nodes: 递归列表
:return: nodes
"""
if not all_list:
all_list = Menu.objects.values("id", "name", "parent")
if nodes is None:
nodes = []
for ele in all_list:
if ele.get("id") == id:
parent_id = ele.get("parent")
if parent_id is not None:
cls.get_all_parent(parent_id, all_list, nodes)
nodes.append(ele)
return nodes
class Meta:
db_table = table_prefix + "system_menu"
verbose_name = "菜单表"

View File

@@ -1 +1,56 @@
from functools import wraps
from django.db.models import Func, F, OuterRef, Exists
from django.test import TestCase
import django
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings")
django.setup()
from dvadmin.system.models import Menu, RoleMenuPermission, RoleMenuButtonPermission, MenuButton
import time
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
run_time = end_time - start_time
print(f"{func.__name__} ran in {run_time:.6f} seconds")
return result
return wrapper
@timing_decorator
def getMenu():
data = []
queryset = Menu.objects.filter(status=1, is_catalog=False).values('name', 'id')
for item in queryset:
parent_list = Menu.get_all_parent(item['id'])
names = [d["name"] for d in parent_list]
completeName = "/".join(names)
isCheck = RoleMenuPermission.objects.filter(
menu__id=item['id'],
role__id=1,
).exists()
mbCheck = RoleMenuButtonPermission.objects.filter(
menu_button = OuterRef("pk"),
role__id=1,
)
btns = MenuButton.objects.filter(
menu__id=item['id'],
).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',data_range=F('menu_button_permission__data_range'))
# print(b)
dicts = {
'name': completeName,
'id': item['id'],
'isCheck': isCheck,
'btns':btns
}
print(dicts)
data.append(dicts)
# print(data)
if __name__ == '__main__':
getMenu()

View File

@@ -3,6 +3,7 @@ from rest_framework import routers
from dvadmin.system.views.api_white_list import ApiWhiteListViewSet
from dvadmin.system.views.area import AreaViewSet
from dvadmin.system.views.clause import PrivacyView, TermsServiceView
from dvadmin.system.views.dept import DeptViewSet
from dvadmin.system.views.dictionary import DictionaryViewSet
from dvadmin.system.views.file_list import FileViewSet
@@ -46,5 +47,7 @@ urlpatterns = [
path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
path('clause/privacy.html', PrivacyView.as_view()),
path('clause/terms_service.html', TermsServiceView.as_view()),
]
urlpatterns += system_url.urls

View File

@@ -60,12 +60,9 @@ class AreaViewSet(CustomModelViewSet):
del params['page']
if limit:
del params['limit']
if params:
if pcode:
queryset = self.queryset.filter(enable=True, pcode=pcode)
else:
queryset = self.queryset.filter(enable=True)
if params and pcode:
queryset = self.queryset.filter(enable=True, pcode=pcode)
else:
queryset = self.queryset.filter(enable=True, pcode__isnull=True)
queryset = self.queryset.filter(enable=True)
return queryset

View File

@@ -0,0 +1,23 @@
from rest_framework.views import APIView
from django.shortcuts import render
class PrivacyView(APIView):
"""
后台隐私政策
"""
permission_classes = []
def get(self, request, *args, **kwargs):
return render(request, 'privacy.html')
class TermsServiceView(APIView):
"""
后台服务条款
"""
permission_classes = []
def get(self, request, *args, **kwargs):
return render(request, 'terms_service.html')

View File

@@ -115,13 +115,10 @@ class DeptViewSet(CustomModelViewSet):
del params['page']
if limit:
del params['limit']
if params:
if parent:
queryset = self.queryset.filter(status=True, parent=parent)
else:
queryset = self.queryset.filter(status=True)
if params and parent:
queryset = self.queryset.filter(status=True, parent=parent)
else:
queryset = self.queryset.filter(status=True, parent__isnull=True)
queryset = self.queryset.filter(status=True)
queryset = self.filter_queryset(queryset)
serializer = DeptSerializer(queryset, many=True, request=request)
data = serializer.data
@@ -143,7 +140,7 @@ class DeptViewSet(CustomModelViewSet):
if item in [0, 2]:
dept_list = [user_dept_id]
elif item == 1:
dept_list = Dept.recursion_dept_info(dept_id=user_dept_id)
dept_list = Dept.recursion_all_dept(dept_id=user_dept_id)
elif item == 3:
dept_list = Dept.objects.values_list('id', flat=True)
elif item == 4:

View File

@@ -71,8 +71,8 @@ class WebRouterSerializer(CustomModelSerializer):
class Meta:
model = Menu
fields = (
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible', 'status')
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link','link_url', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible','is_iframe','is_affix', 'status')
read_only_fields = ["id"]

View File

@@ -63,7 +63,7 @@ class MenuButtonViewSet(CustomModelViewSet):
:param kwargs:
:return:
"""
queryset = self.filter_queryset(self.get_queryset())
queryset = self.filter_queryset(self.get_queryset()).order_by('name')
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(serializer.data,msg="获取成功")

View File

@@ -26,7 +26,7 @@ class MenuFieldViewSet(CustomModelViewSet):
"""
列权限视图集
"""
queryset = MenuField.objects.all()
queryset = MenuField.objects.order_by('-model')
serializer_class = MenuFieldSerializer
def list(self, request, *args, **kwargs):

View File

@@ -47,12 +47,12 @@ class RoleCreateUpdateSerializer(CustomModelSerializer):
def validate(self, attrs: dict):
return super().validate(attrs)
def save(self, **kwargs):
is_superuser = self.request.user.is_superuser
if not is_superuser:
self.validated_data.pop('admin')
data = super().save(**kwargs)
return data
# def save(self, **kwargs):
# is_superuser = self.request.user.is_superuser
# if not is_superuser:
# self.validated_data.pop('admin')
# data = super().save(**kwargs)
# return data
class Meta:
model = Role

View File

@@ -112,10 +112,15 @@ class RoleMenuPermissionSerializer(CustomModelSerializer):
"""
菜单和按钮权限
"""
name = serializers.SerializerMethodField()
isCheck = serializers.SerializerMethodField()
btns = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField()
def get_name(self, instance):
parent_list = Menu.get_all_parent(instance['id'])
names = [d["name"] for d in parent_list]
return "/".join(names)
def get_isCheck(self, instance):
params = self.request.query_params
return RoleMenuPermission.objects.filter(
@@ -166,14 +171,46 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
if role is None:
return ErrorResponse(msg="未获取到角色信息")
is_superuser = request.user.is_superuser
# if is_superuser:
# queryset = Menu.objects.filter(status=1,is_catalog=False).values('name', 'id').all()
# else:
# role_id = request.user.role.values_list('id', flat=True)
# menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True)
# queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id').all()
# serializer = RoleMenuPermissionSerializer(queryset,many=True,request=request)
# data = serializer.data
# return DetailResponse(data=data)
data = []
if is_superuser:
queryset = Menu.objects.filter(status=1,is_catalog=False).values('name', 'id').all()
else:
role_id = request.user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True)
queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id').all()
serializer = RoleMenuPermissionSerializer(queryset,many=True,request=request)
data = serializer.data
queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id')
for item in queryset:
parent_list = Menu.get_all_parent(item['id'])
names = [d["name"] for d in parent_list]
completeName = "/".join(names)
isCheck = RoleMenuPermission.objects.filter(
menu__id=item['id'],
role__id=role,
).exists()
mbCheck = RoleMenuButtonPermission.objects.filter(
menu_button=OuterRef("pk"),
role__id=role,
)
btns = MenuButton.objects.filter(
menu__id=item['id'],
).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',
data_range=F('menu_button_permission__data_range'))
dicts = {
'name': completeName,
'id': item['id'],
'isCheck': isCheck,
'btns': btns,
}
data.append(dicts)
return DetailResponse(data=data)
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
@@ -189,9 +226,12 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
RoleMenuButtonPermission.objects.filter(role=pk).delete()
for menu in body:
if menu.get('isCheck'):
menu_parent = Menu.objects.filter(id=menu.get('id')).values('parent').first()
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu_parent.get('parent'))
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
menu_parent = Menu.get_all_parent(menu.get('id'))
role_menu_permission_list = []
for d in menu_parent:
role_menu_permission_list.append(RoleMenuPermission(role_id=pk, menu_id=d["id"]))
RoleMenuPermission.objects.bulk_create(role_menu_permission_list)
# RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
for btn in menu.get('btns'):
if btn.get('isCheck'):
data_range = btn.get('data_range',0) or 0
@@ -213,8 +253,7 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
if params := request.query_params:
if menu_id := params.get('menu', None):
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
if is_superuser:
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
else:
role_list = request.user.role.values_list('id', flat=True)
@@ -324,8 +363,7 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
"""
params = request.query_params
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
if is_superuser:
queryset = Dept.objects.values('id', 'name', 'parent')
else:
if not params:
@@ -353,8 +391,7 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
if menu_id is None:
return ErrorResponse(msg="未获取到参数")
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
if is_superuser:
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
'id',
'data_range',

View File

@@ -315,7 +315,7 @@ class UserViewSet(CustomModelViewSet):
serializer.save()
return DetailResponse(data=None, msg="修改成功")
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
@action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated])
def change_password(self, request, *args, **kwargs):
"""密码修改"""
data = request.data
@@ -330,7 +330,7 @@ class UserViewSet(CustomModelViewSet):
if not verify_password:
verify_password = check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest(), self.request.user.password)
if verify_password:
request.user.password = make_password(new_pwd)
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.save()
return DetailResponse(data=None, msg="修改成功")
else:

View File

@@ -12,16 +12,49 @@ from collections import OrderedDict
from functools import reduce
import six
from django.db import models
from django.db.models import Q, F
from django.db.models.constants import LOOKUP_SEP
from django_filters import utils
from django_filters.filters import CharFilter
from django_filters import utils, FilterSet
from django_filters.constants import ALL_FIELDS
from django_filters.filters import CharFilter, DateTimeFromToRangeFilter
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend
from django_filters.conf import settings
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
from dvadmin.utils.models import CoreModel
class CoreModelFilterBankend(BaseFilterBackend):
"""
自定义时间范围过滤器
"""
def filter_queryset(self, request, queryset, view):
create_datetime_after = request.query_params.get('create_datetime_after', None)
create_datetime_before = request.query_params.get('create_datetime_before', None)
update_datetime_after = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_after', None)
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q()
if create_datetime_after and create_datetime_before:
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=create_datetime_before)
elif create_datetime_after:
create_filter &= Q(create_datetime__gte=create_datetime_after)
elif create_datetime_before:
create_filter &= Q(create_datetime__lte=create_datetime_before)
# 更新时间范围过滤条件
update_filter = Q()
if update_datetime_after and update_datetime_before:
update_filter &= Q(update_datetime__gte=update_datetime_after) & Q(update_datetime__lte=update_datetime_before)
elif update_datetime_after:
update_filter &= Q(update_datetime__gte=update_datetime_after)
elif update_datetime_before:
update_filter &= Q(update_datetime__lte=update_datetime_before)
# 结合两个时间范围过滤条件
queryset = queryset.filter(create_filter & update_filter)
return queryset
return queryset
def get_dept(dept_id: int, dept_all_list=None, dept_list=None):
"""
@@ -150,10 +183,14 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
elif ele == 2:
dept_list.append(user_dept_id)
elif ele == 4:
dept_ids = RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
data_range=4).values_list(
'dept__id',flat=True
)
dept_list.extend(
request.user.role.filter(status=1).values_list(
"dept__id", flat=True
)
dept_ids
)
if queryset.model._meta.model_name == 'dept':
return queryset.filter(id__in=list(set(dept_list)))
@@ -168,6 +205,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
"$": "iregex",
"~": "icontains",
}
filter_fields = "__all__"
def construct_search(self, field_name, lookup_expr=None):
lookup = self.lookup_prefixes.get(field_name[0])
@@ -175,14 +213,16 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
field_name = field_name[1:]
else:
lookup = lookup_expr
if field_name.endswith(lookup):
return field_name
return LOOKUP_SEP.join([field_name, lookup])
if lookup:
if field_name.endswith(lookup):
return field_name
return LOOKUP_SEP.join([field_name, lookup])
return field_name
def find_filter_lookups(self, orm_lookups, search_term_key):
for lookup in orm_lookups:
# if lookup.find(search_term_key) >= 0:
new_lookup = lookup.split("__")[0]
new_lookup = LOOKUP_SEP.join(lookup.split(LOOKUP_SEP)[:-1]) if len(lookup.split(LOOKUP_SEP)) > 1 else lookup
# 修复条件搜索错误 bug
if new_lookup == search_term_key:
return lookup
@@ -198,18 +238,22 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# TODO: remove assertion in 2.1
if filterset_class is None and hasattr(view, "filter_class"):
utils.deprecate(
"`%s.filter_class` attribute should be renamed `filterset_class`."
% view.__class__.__name__
"`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__
)
filterset_class = getattr(view, "filter_class", None)
# TODO: remove assertion in 2.1
if filterset_fields is None and hasattr(view, "filter_fields"):
utils.deprecate(
"`%s.filter_fields` attribute should be renamed `filterset_fields`."
% view.__class__.__name__
"`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__
)
filterset_fields = getattr(view, "filter_fields", None)
self.filter_fields = getattr(view, "filter_fields", None)
if isinstance(self.filter_fields, (list, tuple)):
filterset_fields = [
field[1:] if field[0] in self.lookup_prefixes.keys() else field for field in self.filter_fields
]
else:
filterset_fields = self.filter_fields
if filterset_class:
filterset_model = filterset_class._meta.model
@@ -229,6 +273,51 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
MetaBase = getattr(self.filterset_base, "Meta", object)
class AutoFilterSet(self.filterset_base):
@classmethod
def get_all_model_fields(cls, model):
opts = model._meta
return [
f.name
for f in sorted(opts.fields + opts.many_to_many)
if (f.name == "id")
or not isinstance(f, models.AutoField)
and not (getattr(f.remote_field, "parent_link", False))
]
@classmethod
def get_fields(cls):
"""
Resolve the 'fields' argument that should be used for generating filters on the
filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
"""
model = cls._meta.model
fields = cls._meta.fields
exclude = cls._meta.exclude
assert not (fields is None and exclude is None), (
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' "
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit "
"'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
)
# Setting exclude with no fields implies all other fields.
if exclude is not None and fields is None:
fields = ALL_FIELDS
# Resolve ALL_FIELDS into all fields for the filterset's model.
if fields == ALL_FIELDS:
fields = cls.get_all_model_fields(model)
# Remove excluded fields
exclude = exclude or []
if not isinstance(fields, dict):
fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude]
else:
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
return OrderedDict(fields)
@classmethod
def get_filters(cls):
"""
@@ -257,9 +346,12 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
if field is None:
undefined.append(field_name)
# 更新默认字符串搜索为模糊搜索
if isinstance(field, (models.CharField)) and filterset_fields == '__all__' and lookups == [
'exact']:
lookups = ['icontains']
if (
isinstance(field, (models.CharField))
and filterset_fields == "__all__"
and lookups == ["exact"]
):
lookups = ["icontains"]
for lookup_expr in lookups:
filter_name = cls.get_filter_name(field_name, lookup_expr)
@@ -269,20 +361,15 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
continue
if field is not None:
filters[filter_name] = cls.filter_for_field(
field, field_name, lookup_expr
)
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
# Allow Meta.fields to contain declared filters *only* when a list/tuple
if isinstance(cls._meta.fields, (list, tuple)):
undefined = [
f for f in undefined if f not in cls.declared_filters
]
undefined = [f for f in undefined if f not in cls.declared_filters]
if undefined:
raise TypeError(
"'Meta.fields' must not contain non-model field names: %s"
% ", ".join(undefined)
"'Meta.fields' must not contain non-model field names: %s" % ", ".join(undefined)
)
# Add in declared filters. This is necessary since we don't enforce adding
@@ -304,22 +391,31 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
return queryset
if filterset.__class__.__name__ == "AutoFilterSet":
queryset = filterset.queryset
orm_lookups = []
for search_field in filterset.filters:
if isinstance(filterset.filters[search_field], CharFilter):
orm_lookups.append(
self.construct_search(six.text_type(search_field), filterset.filters[search_field].lookup_expr)
)
else:
orm_lookups.append(search_field)
filter_fields = filterset.filters if self.filter_fields == "__all__" else self.filter_fields
orm_lookup_dict = dict(
zip(
[field for field in filter_fields],
[filterset.filters[lookup].lookup_expr for lookup in filterset.filters.keys()],
)
)
orm_lookups = [
self.construct_search(lookup, lookup_expr) for lookup, lookup_expr in orm_lookup_dict.items()
]
# print(orm_lookups)
conditions = []
queries = []
for search_term_key in filterset.data.keys():
orm_lookup = self.find_filter_lookups(orm_lookups, search_term_key)
if not orm_lookup:
if not orm_lookup or filterset.data.get(search_term_key) == '':
continue
query = Q(**{orm_lookup: filterset.data[search_term_key]})
queries.append(query)
filterset_data_len = len(filterset.data.getlist(search_term_key))
if filterset_data_len == 1:
query = Q(**{orm_lookup: filterset.data[search_term_key]})
queries.append(query)
elif filterset_data_len == 2:
orm_lookup += '__range'
query = Q(**{orm_lookup: filterset.data.getlist(search_term_key)})
queries.append(query)
if len(queries) > 0:
conditions.append(reduce(operator.and_, queries))
queryset = queryset.filter(reduce(operator.and_, conditions))

View File

@@ -2,9 +2,11 @@
日志 django中间件
"""
import json
import logging
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http import HttpResponse, HttpResponseServerError
from django.utils.deprecation import MiddlewareMixin
from dvadmin.system.models import OperationLog
@@ -87,3 +89,58 @@ class ApiLoggingMiddleware(MiddlewareMixin):
if self.methods == 'ALL' or request.method in self.methods:
self.__handle_response(request, response)
return response
logger = logging.getLogger("healthz")
class HealthCheckMiddleware(object):
"""
存活检查中间件
"""
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
if request.method == "GET":
if request.path == "/readiness":
return self.readiness(request)
elif request.path == "/healthz":
return self.healthz(request)
return self.get_response(request)
def healthz(self, request):
"""
Returns that the server is alive.
"""
return HttpResponse("OK")
def readiness(self, request):
# Connect to each database and do a generic standard SQL query
# that doesn't write any data and doesn't depend on any tables
# being present.
try:
from django.db import connections
for name in connections:
cursor = connections[name].cursor()
cursor.execute("SELECT 1;")
row = cursor.fetchone()
if row is None:
return HttpResponseServerError("db: invalid response")
except Exception as e:
logger.exception(e)
return HttpResponseServerError("db: cannot connect to database.")
# Call get_stats() to connect to each memcached instance and get it's stats.
# This can effectively check if each is online.
try:
from django.core.cache import caches
from django.core.cache.backends.memcached import BaseMemcachedCache
for cache in caches.all():
if isinstance(cache, BaseMemcachedCache):
stats = cache._cache.get_stats()
if len(stats) != len(cache._servers):
return HttpResponseServerError("cache: cannot connect to cache.")
except Exception as e:
logger.exception(e)
return HttpResponseServerError("cache: cannot connect to cache.")
return HttpResponse("OK")

View File

@@ -26,6 +26,7 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
# 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
modifier_field_id = "modifier"
modifier_name = serializers.SerializerMethodField(read_only=True)
dept_belong_id = serializers.IntegerField(required=False, allow_null=True)
def get_modifier_name(self, instance):
if not hasattr(instance, "modifier"):

View File

@@ -7,16 +7,18 @@
@Remark: 自定义视图集
"""
from django.db import transaction
from django_filters import DateTimeFromToRangeFilter
from django_filters.rest_framework import FilterSet
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.viewsets import ModelViewSet
from dvadmin.utils.filters import DataLevelPermissionsFilter
from dvadmin.utils.filters import DataLevelPermissionsFilter, CoreModelFilterBankend
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
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.utils.models import get_custom_app_models, CoreModel
from dvadmin.system.models import FieldPermission, MenuField
from django_restql.mixins import QueryArgumentsMixin
@@ -37,7 +39,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
update_serializer_class = None
filter_fields = '__all__'
search_fields = ()
extra_filter_class = [DataLevelPermissionsFilter]
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionsFilter]
permission_classes = [CustomPermission]
import_field_dict = {}
export_field_label = {}

0
backend/logs/__init__.py Normal file
View File

View File

@@ -17,7 +17,7 @@ openpyxl==3.1.2
requests==2.31.0
typing-extensions==4.8.0
tzlocal==5.1
channels==4.0.0
channels==3.0.5
channels-redis==4.1.0
websockets==11.0.3
user-agents==2.2.0
@@ -28,3 +28,4 @@ uvicorn==0.23.2
gunicorn==21.2.0
gevent==23.9.1
Pillow==10.1.0
dvadmin-celery==1.0.5

View File

@@ -0,0 +1,34 @@
html,body{width:100%;overflow-x: hidden;margin: 0;padding: 0;font-size:1rem;font-family: "PingFang SC","SF Pro SC","SF Pro Text","SF Pro Icons","Helvetica Neue","Helvetica","Arial",sans-serif;
-webkit-font-smoothing: antialiased;}
a{text-decoration:none;}
ul,li{list-style:none;padding:0;margin:0;}
.bg{background-repeat:no-repeat;background-position:center center;background-size:100%;}
.center{margin:0 auto;position:relative;}
.abs_center{position:absolute;left: 50%;transform: translateX(-50%);-webkit-transform:translateX(-50%);}
/*滚动条统一样式*/
.scrollbar{overflow-x:hidden;scrollbar-track-color:none;}
.scrollbar::-webkit-scrollbar{width:8px;background-color:transparent;} /*滚动条整体部分*/
.scrollbar::-webkit-scrollbar-track{border-radius:4px;-webkit-box-shadow: inset 0 0 0 rgba(0,0,0,0);background-color:transparent;display:none;} /* 滚动条的轨道*/
.scrollbar::-webkit-scrollbar-track-piece{background-color:transparent;display:none;}/* 滚动条的内层轨道*/
.scrollbar::-webkit-scrollbar-thumb{border-radius:4px;background: #313131;}/*滚动条里面的小方块,能向上向下移动(或往左往右移动,取决于是垂直滚动条还是水平滚动条)*/
h3,h4,h5{ font-weight: bold;}
h3{ font-size: .4rem; color:#000;}
h4{ font-size: .35rem; color: #000; margin:.2rem 0 0 0;}
p{ line-height: .5rem; margin:.2rem 0;word-break: break-all;text-align:justify;}
.app .main{width:90%;overflow:hidden;font-size:.30rem;box-sizing: border-box; margin: 0 auto; color: #484d57;}
.app ul { padding-left: .1rem; font-size: .26rem;}
.app ul p{ line-height: .4rem;}
.app .mainbg{position:absolute;left:50%;transform:translateX(-50%);background-repeat:no-repeat;background-size:100%;}
.app .poster{width:100%;height:31.85rem;background-image:url(../images/poster_bg.jpg);}
.pc .main{font-size:.16rem;}

View File

@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html data-dpr="1" style="font-size: 50px;">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>隐私政策</title>
<link href="/static/clause/privacy.css" rel="stylesheet">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
</head>
<body class="app" style="font-size: 0.28rem;">
<div class="main">
<h3>隐私政策</h3>
<br>更新日期2024年01月01日
<br>生效日期2024年01月01日
<br>
<br>欢迎您使用DvAdmin产品DvAdmin非常重视用户的个人信息和隐私保护为同时给您提供更准确有个性化的服务和更安全的互联网环境我们依据中华人民共和国网络安全法信息安全技术个人信息安全规范GB/T35273-2017以及其他相关法律法规和技术规范制定了隐私政策阐述了关于您个人信息的相关权利等
<br>
<br>本政策适用于我们的所有DvAdmin系列产品及服务本政策与您所使用的我们的产品与/或服务息息相关您在下载安装启动浏览注册登录使用巨梦科技的产品及/或服务时我们将按照本政策的约定处理和保护您的个人信息我们将尽量以简明扼要的表述向您解释本政策所涉及的技术词汇以便于您理解
<br>
<br>您在使用DvAdmin的产品及/或服务时巨梦科技可能会收集和使用您的相关信息为此希望通过本隐私政策向您说明巨梦科技如何取得使用保存和管理这些信息请在使用/继续使用我们的各项产品与服务前仔细阅读并充分理解本政策特别应重点阅读我们以粗体/粗体下划线标识的条款并在需要时按照本政策的指引作出适当的选择如果您不同意本政策的内容将可能导致我们的产品与/或服务无法正常运行或者无法达到我们拟达到的服务效果您应立即停止访问/使用我们的产品与/或服务您使用或继续使用我们提供的产品与/或服务的行为都表示您充分理解和同意本隐私政策的全部内容
<br>
<br>隐私政策将帮助您了解以下内容
<br><a href="#1_chapter">第一章 如何收集您的个人信息</a>
<br><a href="#2_chapter">第二章 如何使用您的个人信息</a>
<br><a href="#3_chapter">第三章 如何使用Cookie和同类技术</a>
<br><a href="#4_chapter">第四章 如何共享转让公开披露您的个人信息</a>
<br><a href="#5_chapter">第五章 如何保护您的个人信息</a>
<br><a href="#6_chapter">第六章 如何保存您的个人信息</a>
<br><a href="#7_chapter">第七章 管理查看或删除您的个人信息</a>
<br><a href="#8_chapter">第八章 如何处理儿童的个人信息</a>
<br><a href="#9_chapter">第九章 本政策如何更新</a>
<br><a href="#10_chapter">第十章 投诉及建议</a>
<br><a href="#11_chapter">第十一章 其他</a>
<br>
<h1 id="1_chapter">第一章 如何收集您的个人信息</h1>
<br>
<br>在您使用我们的产品/服务时我们需要/可能需要收集和使用的您的个人信息包括如下两种
<br>第一种为实现向您提供我们产品及/或服务的基本功能您须授权我们收集使用的必要的信息如您拒绝提供相应信息您将无法正常使用我们的产品及/或服务
<br>第二种为实现向您提供我们产品及/或服务的附加功能您可选择单独同意或不同意我们收集使用的信息如您拒绝提供您将无法正常使用相关附加功能或无法达到我们拟达到的功能效果不会影响您使用我们的基本功能
<br>
<br>个人敏感信息是指一旦泄露非法提供或滥用可能危害人身和财产安全极易导致个人名誉身心健康受到损害或歧视性待遇等的个人信息如个人财产信息个人健康生理信息个人生物识别信息个人身份信息网络身份标识信息及其他信息
<br>上述信息所包含的内容均出自于GB/T35273信息安全技术 个人信息安全规范
<br>
<h3>1.1 您向巨梦科技提供相关个人信息</h3>
<h4>1.1.1
为使用巨梦科技的产品及/或服务您首先需要注册账号并登录您可以选择通过微信等方式创建巨梦科技产品及/或服务的账号并登录"账号登录"</h4>
<h5>1.1.1.1 通过微信第三方账户登录巨梦科技产品及/或服务时您同意我们从微信获取您授权共享的信息包括手机号位置信息昵称头像等
并且您同意将您的第三方账户与您的巨梦科技产品及/或服务账号绑定使您可以通过第三方账户直接登录并使用巨梦科技的产品及/或服务并借助该账户实现数据在不同设备之间的同步
我们会依据与第三方的约定并在符合相关法律法规的前提下使用您授权共享的个人信息如果您拒绝同意巨梦科技从第三方获取您授权共享的信息您将无法通过微信的方式注册账号并登录也无法借助该第三方账户实现数据的同步</h5>
<h4>1.1.2 在您使用巨梦科技产品及/或服务时您可能会因账号管理产品使用等问题联系巨梦科技客服为此您可能需要向巨梦科技提供姓名手机号码系统操作记录照片银行账号第三方支付平台的账号
与巨梦科技进行联系的通信记录及内容等与您使用巨梦科技产品及/或服务相关的信息以便巨梦科技的客服人员您联系并帮助您解决相关问题
如果您拒绝提供相关信息巨梦科技可能无法帮助您解决相关问题</h4>
<h3>1.2 巨梦科技收集您所使用的相关设备及网络信息</h3>
<h4>1.2.1
当您使用巨梦科技的产品及/或服务时巨梦科技将收集您所使用的相关设备信息以便为您提供持续稳定的服务支持使您在使用巨梦科技的产品及/或服务过程中获得最优体验巨梦科技收集的您所使用的相关设备信息包括浏览器信息网络信息IP地址日志信息如操作日志服务日志等等</h4>
<h3>1.3 巨梦科技向您收集的信息</h3>
<h4>1.3.1
为提升巨梦科技产品及/或服务的使用体验和便利程度巨梦科技可能会通过调用设备权限的方式收集您的如下个人信息如果您不同意提供这些个人信息您可能无法使用与提升用户体验相关的功能您也可能需要在使用巨梦科技产品及/或服务过程中手动重复填写一些信息</h4>
<h4>1.3.2
若您希望向巨梦科技上传您的头像及/或其他图片信息您可以选择授权巨梦科技调用您所使用设备中的摄像头或者相册权限</h4>
<h4>1.3.3
若您希望快速便捷地通过第三方移动运营商完成支付在您的设备支持的情况下您可以选择授权调用您所使用设备中的移动运营相关权限如短信以便您快速便捷地完成付款</h4>
<h4>1.3.4
若您希望实现或体验上述功能您可能需要在您的设备中向巨梦科技开启您的相应访问权限您也可以随时选择关闭相应访问权限以取消相应授权在不同设备中权限显示方式及关闭方式可能有所不同具体请参考设备及系统开发方的说明或指引</h4>
<h3>1.4 第三方向您收集的信息</h3>
<h4>1.4.1 第三方付款服务提供商收集相关个人信息</h4>
<h5>1.4.1.1
您可以通过向巨梦科技付款的方式获取巨梦科技产品及/或服务相关的产品上述付款服务可能由巨梦科技以外的第三方提供巨梦科技并不会获取与您付款相关的密码等信息但可能从您或第三方支付平台获得您的第三方支付账户微信账号若您希望了解第三方服务提供商收集个人信息的具体规则请查阅您选择的第三方付款服务提供商的隐私政策</h5>
<h3>1.5 您充分知晓在以下情形下我们收集使用个人信息无需征得您的授权同意</h3>
<h4>1.5.1 与国家安全国防安全有关的</h4>
<h4>1.5.2 与公共安全公共卫生重大公共利益有关的</h4>
<h4>1.5.3 与犯罪侦查起诉审判和判决执行等有关的</h4>
<h4>1.5.4 出于维护个人信息主体或其他个人的生命财产等重大合法权益但又很难得到本人同意的</h4>
<h4>1.5.5 所收集的个人信息是个人信息主体自行向社会公众公开的</h4>
<h4>1.5.6 从合法公开披露的信息中收集的您的个人信息的如合法的新闻报道政府信息公开等渠道</h4>
<h4>1.5.7 根据您要求签订和履行合同所必需的</h4>
<h4>1.5.8 用于维护所提供的产品及/或服务的安全稳定运行所必需的例如发现处置产品及/或服务的故障</h4>
<h4>1.5.9 法律法规规定的其他情形</h4>
<h3>1.6
根据法律规定向第三方提供经去标识化处理的个人信息且确保数据接收方无法复原并重新识别个人信息主体的不属于个人信息的对外共享转让及公开披露行为对此类数据的保存及处理将无需另行向您通知并征得您的同意</h3>
<br>
<h1 id="2_chapter">第二章 如何使用您的个人信息</h1>
<br>
<h2>在收集您的个人信息后巨梦科技将根据如下规则使用您的个人信息</h2>
<br>
<h3>2.1 会根据本隐私政策的约定并为实现巨梦科技的产品及/或服务功能对所收集的个人信息进行使用</h3>
<h3>2.2
请您注意对于您在使用巨梦科技的产品及/或服务时所提供的所有个人信息除非您删除或通过相关设置拒绝我们收集否则您将在使用产品及/或服务期间账号注销前持续授权我们使用</h3>
<h3>2.3
巨梦科技系列产品或服务对使用情况进行统计并可能会与公众或第三方共享这些统计信息以展示巨梦科技产品及/或服务的整体使用趋势但这些统计信息不会包含您的任何身份识别信息</h3>
<h3>2.4
当巨梦科技要将您的个人信息用于本政策未载明的其它用途时或将基于特定目的收集而来的信息用于其他目的时巨梦科技会主动事先征求您的明示同意</h3>
<br>
<h1 id="3_chapter">第三章 如何使用Cookie和同类技术</h1>
<h2>巨梦科技暂未使用任何信息收集工具</h2>
<br>
<h1 id="4_chapter">第四章 如何共享转让公开披露您的个人信息</h1>
<h2>共享</h2>
<h3>4.1 巨梦科技将严格遵守相关法律法规对您的个人信息予以保密除以下情况外我们不会向其他人共享您的个人信息</h3>
<h4>4.1.1 事先获得您明确的同意或授权</h4>
<h4>4.1.2 根据适用的法律法规规定或基于司法或行政主管部门的强制性要求进行提供</h4>
<h4>4.1.3
在法律法规允许的范围内为维护您或其他巨梦科技用户或其他个人的生命财产等合法权益或是社会公共利益而有必要提供</h4>
<h4>4.1.4 应您的监护人的合法要求而提供您的信息</h4>
<h4>4.1.5 根据与您签署的相关协议包括在线签署的电子协议以及相应的平台规则或其他的法律文件约定而提供</h4>
<h4>4.1.6 根据本隐私政策所述与第三方进行共享</h4>
<h4>4.1.7
巨梦科技可能会基于您的相应授权将您的个人信息与公司的关联方共享但巨梦科技只会共享必要的个人信息且受本隐私政策所述目的之约束 如果巨梦科技的关联方要改变个人信息的处理目的将适时向您征得明示同意</h4>
<h3>4.2
对巨梦科技与之共享个人信息的公司组织和个人我们将尽合理的努力要求其处理您的个人信息时遵守相关法律法规尽力要求其采取相关的保密和安全措施以保障您的个人信息安全</h3>
<h2>转让</h2>
<h3>4.3 巨梦科技不会将您的个人信息转让给任何公司组织和个人但以下情况除外</h3>
<h4>4.3.1 事先获得您明确的同意或授权</h4>
<h4>4.3.2 根据适用的法律法规法律程序的要求强制性的行政或司法要求而必须进行提供</h4>
<h4>4.3.3
涉及收购兼并破产清算重组等重大变更时如涉及到个人信息转让的巨梦科技会要求新的持有您个人信息的公司或组织继续履行本隐私政策项下的责任和义务如变更后的主体需变更个人信息使用目的我们会要求其事先获得您的明示同意</h4>
<h2>公开披露</h2>
<h3>4.4 巨梦科技仅会在以下情况下且在采取符合业界标准的安全防护措施的前提下才会公开披露您的个人信息</h3>
<h4>4.4.1根据您的需求在您明确同意的披露方式下披露您所指定的个人信息</h4>
<h4>4.4.2
根据法律法规的要求行政或司法机关的强制性要求我们可能会公开披露您的个人信息当巨梦科技收到上述披露请求时巨梦科技会依法要求请求方出具相关法律文件如传票或协助调查函等巨梦科技会慎重审查每一个披露请求以确保该等披露请求符合相关法律规定在法律法规许可的前提下巨梦科技会对包含披露信息的文件采取加密保护等措施</h4>
<br>
<h1 id="5_chapter">第五章 如何保护您的个人信息</h1>
<h3>5.1
巨梦科技非常重视个人信息安全并会采取一切合理可行的措施持续保护您的个人信息以防其他人在未经授权的情况下访问篡改或披露巨梦科技收集的您的个人信息</h3>
<h4>5.1.1
巨梦科技已采用符合行业标准的安全防护措施来保护您的个人信息防止数据遭到未经授权的访问公开披露使用修改损坏或丢失我们会采取一切合理可行的措施保护您的个人信息我们会使用受信赖的保护机制防止数据遭到恶意攻击</h4>
<h4>5.1.2
巨梦科技仅允许有必要知晓的人员访问相关个人信息并为此设置了严格的访问权限控制和监控机制巨梦科技同时要求可能接触到您个人信息的所有人员履行相应的保密义务如果未能履行这些义务可能会被追究法律责任或被终止与巨梦科技的相应法律关系</h4>
<h3>5.2 巨梦科技会采取一切合理可行的措施确保未收集无关的信息</h3>
<h3>5.3
您理解由于技术的限制以及可能存在的各种恶意手段在互联网行业即便我们竭尽所能加强安全措施但也不可能始终保证信息百分之百的绝对安全您需要了解和知悉您接入巨梦科技的产品及/或服务所用的系统和通讯网络有可能因我们可控范围外的其他因素而出现问题在此情形下我们会依法尽力协助解决</h3>
<h3>5.4
如不幸发生信息安全事故我们将按照法律法规的要求及时向您告知安全事件的基本情况和可能的影响我们已采取或将要采取的处置措施您可自主防范和降低风险的建议等我们同时将及时将事件相关情况以邮件信函电话推送通知等适合的方式告知您难以逐一告知信息主体时我们会采取合理有效的方式发布公告同时我们还将按照监管部门要求主动上报信息安全事件的处置情况</h3>
<br>
<h1 id="6_chapter">第六章 如何保存您的个人信息</h1>
<h2>保存期限</h2>
<h3>6.1 在用户使用巨梦科技产品及/或服务期间巨梦科技会持续保存用户的个人信息</h3>
<h3>6.2
巨梦科技承诺始终按照法律的规定在合理必要期限内在存储您个人信息对于日志信息记录备份等信息为您注销账号后180天但根据法律规定有最短保存期限要求的则我们会保存法律要求的最短期限交易信息为交易完成日起三年或您注销账号后180天以较长者为准在超出上述期限后我们会对您的相关信息进行删除或匿名化处理</h3>
<h2>保存地域</h2>
<h3>6.3 您的个人信息将全部被存储于中华人民共和国境内</h3>
<h3>6.4 目前我们不存在向境外提供个人信息的场景</h3>
<br>
<h1 id="7_chapter">第七章 管理查看或删除您的个人信息</h1>
<h2>巨梦科技非常尊重您对自己的个人信息所享有的权利我们保障您对个人信息所享有的访问更正删除管理等权利</h2>
<br>
<h2>访问和更正您的个人信息</h2>
<h3>7.1
除法律法规另有规定之外您有权行使数据访问权当您发现我们处理关于您的个人信息有错误或者您有其他修改补充需求时您也有权要求我们或自行予以更正您行使数据访问和更正权的方式包括但不限于</h3>
<h4>7.1.1
如果您希望访问或修改您在巨梦科技产品及/或服务中的账号信息包括头像昵称性别生日等您可以通过访问我们的产品及/或服务进行操作可通过点击我的功能后进行操作</h4>
<h4>7.1.2
如巨梦科技的产品及/或服务中提供发表话题参与讨论留言等功能并使得您有机会通过这些服务公开或提供的个人信息的则巨梦科技在提供前述服务的同时会提供相应的功能确保您可以再次访问或删除您在前述服务过程中公开或提供的个人信息</h4>
<h3>7.2
您有权知悉通过巨梦科技获得您的个人信息的第三方的身份或类型您可以通过本政策第一章和第四章了解第三方的身份或类型</h3>
<br>
<h2>删除您的个人信息及撤回已同意的授权</h2>
<h3>7.3 在以下情形中您可以向巨梦科技提出删除个人信息的请求</h3>
<h4>7.3.1 如果巨梦科技处理个人信息的行为违反相关的法律法规</h4>
<h4>7.3.2 如果巨梦科技收集使用您的个人信息却未征得您的同意</h4>
<h4>7.3.3 如果巨梦科技处理个人信息的行为违反了与您的约定或法律的规定</h4>
<h4>7.3.4 如果您不再使用巨梦科技的产品及/或服务或者您注销了相关账号</h4>
<h4>7.3.5 如果巨梦科技不再为您提供产品及/或服务</h4>
<h3>7.4
您有权向巨梦科技撤回您此前作出的有关同意收集使用您的个人信息的授权当您撤回同意后我们将不再处理您的相关个人信息但您撤回同意的决定不会影响此前基于您的授权而开展的个人信息处理活动</h3>
<h3>7.5 您可以通过删除相关个人信息的方式撤回您此前就特定个人信息而对我们作出的同意授权</h3>
<h2>注销账号</h2>
<h3>7.6
您有权随时提出申请注销您在巨梦科技产品及/或服务中注册的账号为保障账号及财产安全您需要通过客户服务或本隐私条款第十章载明的联系方式向巨梦科技提出您的账号注销请求巨梦科技将在与您核实相关信息后的15个工作日内为您注销账号</h3>
<h3>7.7
请您注意注销巨梦科技相关产品及/或服务账号是不可恢复的操作在注销账号之后我们将停止为您提供产品及/或服务并将删除该账号项下的您的个人信息除非法律法规另有规定</h3>
<h2>约束信息系统自动决策</h2>
<h3>7.8
在巨梦科技仅依据信息系统算法等在内的非人工自动决策机制做出决定并且这些决定显著影响您的合法权益的情况下您有权要求巨梦科技做出解释巨梦科技也将提供适当的救济方式</h3>
<h2>获取个人信息副本</h2>
<h3>7.9
根据您的请求巨梦科技可以向您提供巨梦科技持有的有关您的个人信息副本如个人基本资料您可以通过客户服务或本隐私条款第十章载明的联系方式向我们提出请求</h3>
<h2>响应您的上述请求</h2>
<h3>
7.10如果您对巨梦科技在以上列明的有关访问更正删除您的个人信息以及撤回同意注销账号约束信息系统自动决策方法有任何疑问您可以通过客户服务或本隐私政策第十章载明的联系方式与我们取得联系</h3>
<h3>7.11
对于您合理的请求巨梦科技原则上不收取费用但对多次重复超出合理限度的请求巨梦科技将视情况收取一定成本费用对于那些无端重复需要过多技术手段给他人合法权益带来风险或者非常不切实际的请求巨梦科技可能会予以拒绝</h3>
<h3>7.12 在以下情形下我们可能无法响应您的请求</h3>
<h4>7.12.1 与国家安全国防安全有关的</h4>
<h4>7.12.2 与公共安全公共卫生重大公共利益有关的</h4>
<h4>7.12.3 与犯罪侦查起诉和审判等有关的</h4>
<h4>7.12.4 有证据表明您存在主观恶意或滥用权利的</h4>
<h4>7.12.5 响应您的请求将导致其他个人组织的合法权益受到严重损害的</h4>
<h4>7.12.6 涉及商业秘密的</h4>
<h2>申诉机制</h2>
<h3>7.13
巨梦科技已经建立申诉管理机制包括跟踪流程等为了保障账号及财产安全巨梦科技可能会与您核实相关信息巨梦科技将在收到您的反馈后尽快受理并处理您的请求最长不超过15个工作日若您对答复意见不满意您可以再次通过客户服务进行申诉申诉的联系方式及相关具体信息请详见第十章</h3>
<br>
<br>
<h1 id="8_chapter">第八章 儿童信息的保护</h1>
<h3>8.1
我们的产品及服务主要面向成人巨梦科技非常重视对未成年人信息的保护如果您是未满18周岁的未成年人应在监护人监护指导并获得监护人同意情况下仔细阅读本隐私政策和使用巨梦科技的产品及/或服务</h3>
<h3>8.2 如果您/您的监护人不同意本隐私政策的任何内容您应该立即停止使用我们的产品及/或服务</h3>
<h3>8.3
若您是未成年人的监护人当您对您所监护的未成年人使用我们的产品及/或服务或其向我们提供的用户信息有任何疑问时请您及时与我们联系我们将根据国家相关法律法规及本政策的规定保护未成年人用户信息的保密性及安全性如果我们发现自己在未事先获得可证实的监护人同意的情况下收集了未成年人的个人信息则会设法尽快删除相关数据</h3>
<br>
<h1 id="9_chapter">第九章 本政策如何更新</h1>
<h3>9.1 如巨梦科技产品及/或服务发生以下变化巨梦科技将及时对本隐私政策进行相应的修订</h3>
<h4>9.1.1 巨梦科技产品及/或服务所涉业务功能发生变更导致处理个人信息的目的类型使用方式发生变更</h4>
<h4>9.1.2 您参与个人信息处理方面的权利及其行使方式发生重大变化</h4>
<h4>9.1.3 巨梦科技负责处理您的个人信息安全的部门的联络方式发生变更</h4>
<h4>9.1.4 发生其他可能影响用户个人信息安全或影响用户隐私权利的变更等</h4>
<h3>9.2
隐私政策修订后巨梦科技会在巨梦科技产品及/或服务相关界面发布最新版本并以弹窗推送通知等合理的方式告知用户以便用户及时了解隐私政策的最新版本</h3>
<h3>9.3 未经您的明确同意巨梦科技不会削减您基于本隐私政策所享有的权利</h3>
<h3>9.4 如无特殊说明修订后的隐私政策自公布之日起生效</h3>
<br>
<h1 id="10_chapter">第十章 投诉及建议</h1>
<h3>10.1
您在使用巨梦科技产品及/或服务的过程中如果对本隐私政策有任何的疑义或建议或您认为您的个人信息没有得到本隐私政策规定的保护您可以通过以下方式联系我们我们将真诚地处理您的投诉及建议</h3>
<h4>公司名称北京巨梦科技信息技术有限公司</h4>
<h4>联系邮箱qiangli@django-vue-admin.com</h4>
<h4>微信公众号巨梦科技9:00-22:00</h4>
<br>
<h1 id="11_chapter">第十一章 其他</h1>
<h3>11.1 因本政策以及我们处理您个人信息事宜引起的任何争议您可诉至有管辖权的人民法院</h3>
<h3>11.2 如果您认为我们的个人信息处理行为损害了您的合法权益您也可向有关政府部门进行反映</h3>
<h3>11.3
在巨梦科技发布本隐私政策或向您提供产品及/或服务均视为本隐私政策生效巨梦科技停止运营或永久停止提供产品及/或服务时本隐私政策失效</h3>
<br>
</div>
</body>
</html>

View File

@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html data-dpr="1" style="font-size: 50px;">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户服务协议</title>
<link href="/static/clause/privacy.css" rel="stylesheet">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
</head>
<body class="app" style="font-size: 0.28rem;">
<div class="main">
<h3>服务使用协议</h3>
<br>更新日期2024年01月01日
<br>生效日期2024年01月01日
<br>DvAdmin服务使用协议是您下称"用户"与北京巨梦科技科技有限公司之间在使用其DvAdmin产品中注册或登录时签署的协议巨梦科技产品包括django-vue-admin产品请仔细阅读以下条款点击同意按钮即表示对相关条款的同意如果您阅读这份协议后有任何疑问或意见请与巨梦科技客服人员取得联系联系邮箱qiangli@django-vue-admin.cn微信公众号巨梦科技9:00-18:00
<br>
<h2>重要须知</h2>
<h3>
1用户应认真阅读未成年人应当在监护人陪同下阅读充分理解本协议中各条款除非用户接受本协议用户应当立即停止注册及使用行为</h3>
<h3>2用户在进行注册程序过程中点击同意按钮即表示用户完全接受本协议项下的全部条款</h3>
<br>
<h2>服务内容</h2>
<h3>1巨梦科技服务的具体内容由巨梦科技根据实际情况提供</h3>
<h3>2用户理解巨梦科技仅提供软件相关服务
除此之外与相关软件服务有关的设备如手机个人电脑及其他与接入互联网或移动网有关的装置及所需的费用如为接入互联网而支付的电话费及上网费为使用移动网而支付的手机费均应由用户自行负担</h3>
<h3>
3用户同意巨梦科技为本协议履行或相关研究的目的收集使用或授权第三方且该等第三方同意承担与巨梦科技同等的个人信息保护义务非商业地使用通过服务获取的用户信息包括但不限于身份信息性别职业兴趣爱好等资料巨梦科技承诺除非另行取得用户的事先同意否则巨梦科技不会将用户信息授权给与履行本协议无关的第三方用于商业目的</h3>
<h3>5根据相关法律法规及国家标准在以下情形中巨梦科技可能会依法收集并使用用户的个人信息并且无需征得用户的同意:</h3>
<h4>1与国家安全国防安全直接相关的</h4>
<h4>2与公共安全公共卫生重大公共利益直接相关的</h4>
<h4>3与犯罪侦查起诉审判和判决执行等直接相关的</h4>
<h4>4出于维护用户或他人的生命财产等重大合法权益但又很难得到用户本人同意的</h4>
<h4>5所收集的个人信息是用户自行向社会公众公开的</h4>
<h4>6从合法公开披露的信息中收集个人信息例如合法的新闻报道政府信息公开等渠道</h4>
<h4>7根据用户的要求签订和履行合同所必需的</h4>
<h4>8用于维护所提供的服务的安全稳定运行所必需的例如发现处置产品或服务的故障</h4>
<h4>9为合法的新闻报道所必需的</h4>
<h4>
10学术研究机构基于公共利益开展统计或学术研究所必要且对外提供学术研究或描述的结果时对结果中所包含的个人信息进行去标识化处理的</h4>
<h4>11法律法规规定的其他情形</h4>
<h3>6在以下情形中巨梦科技向第三方提供用户的个人信息无需事先征得用户的授权同意</h3>
<h4>1与国家安全国防安全有关的</h4>
<h4>2与公共安全公共卫生重大公共利益有关的</h4>
<h4>3与犯罪侦查起诉审判和判决执行等有关的</h4>
<h4>4出于维护用户或其他个人的生命财产等重大合法权益但又很难得到本人同意的</h4>
<h4>5用户自行向社会公众公开的个人信息</h4>
<h4>6从合法公开披露的信息中收集个人信息的如合法的新闻报道政府信息公开等渠道</h4>
<h3>
根据法律规定向第三方提供经去标识化处理的个人信息且确保数据接收方无法复原并重新识别个人信息主体的不属于个人信息的对外共享转让及公开披露行为对此类数据的保存及处理将无需另行向用户通知并征得用户的同意</h3>
<h3>本协议中涉及巨梦科技对于用户提供的个人信息的使用如与隐私政策不符的隐私政策为准</h3>
<br>
<h2>用户使用规则</h2>
<h3>1用户授权微信登录成功后用户有权使用巨梦科技软件功能包括参与活动终端管理经销商业务管理等</h3>
<h3>2用户有权使用巨梦科技提供的功能进行照片上传分享个人信息</h3>
<h3>3用户在使用巨梦科技时须遵守国家相关法律法规内容不得包含有下列内容之一的信息</h3>
<h4>a) 反对宪法所确定的基本原则的</h4>
<h4>b) 危害国家安全泄露国家秘密颠覆国家政权破坏国家统一的</h4>
<h4>c) 损害国家荣誉和利益的</h4>
<h4>d) 煽动民族仇恨民族歧视破坏民族团结的</h4>
<h4>e) 破坏国家宗教政策宣扬邪教和封建迷信的</h4>
<h4>f) 散布谣言扰乱社会秩序破坏社会稳定的</h4>
<h4>g) 散布淫秽色情赌博暴力凶杀恐怖或者教唆犯罪的</h4>
<h4>h) 侮辱或者诽谤他人侵害他人合法权利的</h4>
<h4>i) 含有虚假有害胁迫侵害他人隐私骚扰侵害中伤粗俗猥亵或其它道德上令人反感的内容</h4>
<h4>j) 含有中国法律法规规章条例以及任何具有法律效力之规范所限制或禁止的其它内容的</h4>
<h4>k) 含有巨梦科技认为不利于巨梦科技运营的内容</h4>
<h3>
4用户保证在使用巨梦科技时发布传播的信息的真实性准确性保证不得发布谣言或其他与事实不符引起他人不适的言论信息</h3>
<h3>
5用户保证在使用巨梦科技不得发布传播侵犯知识产权或其他合法权益的信息用户传播内容中涉及第三方拥有知识产权内容的相关授权或其他内容的合法性由用户自行负责巨梦科技对相关内容的知识产权权属或是否侵犯他人民事权益等情况不进行审查或监督但将按相关法规对涉嫌侵权的内容履行删除或断开链接等职责</h3>
<h3>
6若用户发生前述3-6的行为时由用户承担所有的违法侵权责任若因此给巨梦科技造成任何损失巨梦科技有权向责任用户主张相关责任同时巨梦科技有权对违法侵权违规的用户终止提供服务如果政府或者司法机关要求巨梦科技告知进行侵权行为用户的具体信息的巨梦科技有权根据现行法规向其告知用户信息</h3>
<h3>
7用户同意巨梦科技有权在提供服务过程中以各种方式投放各种商业性广告或其他任何类型的商业信息并且用户同意接受巨梦科技通过电子邮件或其他方式向用户发送商品促销或其他相关商业信息</h3>
<h3>
8用户承诺提供的注册信息的真实性合法性有效性承担全部责任用户不得冒充他人不得利用他人的名义发布任何信息或享受巨梦科技供的任何服务不得恶意使用注册帐号导致其他用户误认否则巨梦科技有权立即停止提供服务收回其帐号并由用户独自承担由此而产生的一切法律责任</h3>
<br>
<h2>服务变更中断或终止</h2>
<h3>
1鉴于服务的特殊性用户同意巨梦科技有权随时变更中断或终止部分或全部的服务如变更中断或终止的服务属于免费服务巨梦科技无需通知用户也无需对任何用户或任何第三方承担任何责任</h3>
<br>
<h2>巨梦科技企业客户服务说明</h2>
<h3>
1巨梦科技平台提供给多家企业客户使用企业客户通过巨梦科技平台进行发布用户活动等如果功夫企火星企业用户违反了隐私政策或发生其它侵犯用户权益的行为用户可要求巨梦科技提供必要的配合与巨梦科技企业用户进行沟通和维权维权过程中产生的费用由用户自行承担</h3>
<br>
<h2>其他事宜</h2>
<h3>
1巨梦科技有权根据法律法规的变化网站以及相应的巨梦科技运营的需要不时地对本协议及本站的内容进行修改,并在网站以及巨梦科技醒目位置张贴修改后的协议一旦被张贴在本站上即生效,并代替原来的协议,用户可随时登录查阅最新协议用户有义务及时关注并阅读最新版的协议及网站公告如用户不同意更新后的协议可以立即向巨梦科技进行反馈且应立即停止接受巨梦科技依据本协议提供的服务如用户继续使用本站提供的服务的,即视为同意更新后的协议</h3>
<h3>2本协议自用户点击同意接受后生效</h3>
<h3>3本协议所有条款的标题仅为方便而设,不具法律或契约效果</h3>
<h3>4在法律允许范围内 本协议的解释权归巨梦科技所有</h3>
</div>
</body>
</html>

View File

@@ -13,6 +13,7 @@ services:
- ./docker_env/nginx/my.conf:/etc/nginx/conf.d/my.conf
expose:
- "8080"
restart: always
networks:
network:
ipv4_address: 177.10.0.11
@@ -23,9 +24,8 @@ services:
dockerfile: ./docker_env/django/Dockerfile
container_name: dvadmin3-django
working_dir: /backend
# 打开mysql 时,打开此选项
# depends_on:
# - dvadmin3-mysql
depends_on:
- dvadmin3-mysql
environment:
PYTHONUNBUFFERED: 1
DATABASE_HOST: dvadmin3-mysql
@@ -42,74 +42,70 @@ services:
network:
ipv4_address: 177.10.0.12
# dvadmin3-mysql:
# image: mysql:5.7
# container_name: dvadmin3-mysql
# #使用该参数container内的root拥有真正的root权限否则container内的root只是外部的一个普通用户权限
# #设置为true不然数据卷可能挂载不了启动不起
## privileged: true
# restart: always
# ports:
# - "3306:3306"
# environment:
# MYSQL_ROOT_PASSWORD: "123456"
# MYSQL_DATABASE: "dvadmin3_pro"
# TZ: Asia/Shanghai
# command:
# --wait_timeout=31536000
# --interactive_timeout=31536000
# --max_connections=1000
# --default-authentication-plugin=mysql_native_password
# volumes:
# - "./docker_env/mysql/data:/var/lib/mysql"
# - "./docker_env/mysql/conf.d:/etc/mysql/conf.d"
# - "./docker_env/mysql/logs:/logs"
# networks:
# network:
# ipv4_address: 177.10.0.13
dvadmin3-mysql:
image: mysql:8.0
container_name: dvadmin3-mysql
privileged: true
restart: always
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "DVADMIN3"
MYSQL_DATABASE: "django-vue3-admin"
TZ: Asia/Shanghai
command:
--wait_timeout=31536000
--interactive_timeout=31536000
--max_connections=1000
--default-authentication-plugin=mysql_native_password
volumes:
- "./docker_env/mysql/data:/var/lib/mysql"
- "./docker_env/mysql/conf.d:/etc/mysql/conf.d"
- "./docker_env/mysql/logs:/logs"
networks:
network:
ipv4_address: 177.10.0.13
# 如果使用celery 插件,请自行打开此注释
# dvadmin3-celery:
# build:
# context: .
# dockerfile: ./docker_env/celery/Dockerfile
# # image: django:2.2
# container_name: dvadmin3-celery
# working_dir: /backend
# depends_on:
# - dvadmin3-mysql
# environment:
# PYTHONUNBUFFERED: 1
# DATABASE_HOST: dvadmin3-mysql
# TZ: Asia/Shanghai
# volumes:
# - ./backend:/backend
# - ./logs/log:/var/log
# restart: always
# networks:
# network:
# ipv4_address: 177.10.0.14
dvadmin3-celery:
build:
context: .
dockerfile: ./docker_env/celery/Dockerfile
container_name: dvadmin3-celery
working_dir: /backend
depends_on:
- dvadmin3-mysql
environment:
PYTHONUNBUFFERED: 1
DATABASE_HOST: dvadmin3-mysql
TZ: Asia/Shanghai
volumes:
- ./backend:/backend
- ./logs/log:/var/log
restart: always
networks:
network:
ipv4_address: 177.10.0.14
# dvadmin3-redis:
# image: redis:6.2.6-alpine # 指定服务镜像最好是与之前下载的redis配置文件保持一致
# container_name: dvadmin3-redis # 容器名称
# restart: on-failure # 重启方式
# environment:
# - TZ=Asia/Shanghai # 设置时区
# volumes: # 配置数据卷
# - ./docker_env/redis/data:/data
# - ./docker_env/redis/redis.conf:/etc/redis/redis.conf
# ports: # 映射端口
# - "6379:6379"
# sysctls: # 设置容器中的内核参数
# - net.core.somaxconn=1024
# command: /bin/sh -c "echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf && redis-server /etc/redis/redis.conf --appendonly yes" # 指定配置文件并开启持久化
# privileged: true # 使用该参数container内的root拥有真正的root权限。否则container内的root只是外部的一个普通用户权限
# networks:
# network:
# ipv4_address: 177.10.0.15
dvadmin3-redis:
image: redis:6.2.6-alpine # 指定服务镜像最好是与之前下载的redis配置文件保持一致
container_name: dvadmin3-redis # 容器名称
restart: always
environment:
- TZ=Asia/Shanghai # 设置时区
volumes: # 配置数据卷
- ./docker_env/redis/data:/data
- ./docker_env/redis/redis.conf:/etc/redis/redis.conf
ports: # 映射端口
- "6379:6379"
sysctls: # 设置容器中的内核参数
- net.core.somaxconn=1024
command: /bin/sh -c "echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf && redis-server /etc/redis/redis.conf --appendonly yes --requirepass DVADMIN3" # 指定配置文件并开启持久化
privileged: true # 使用该参数container内的root拥有真正的root权限。否则container内的root只是外部的一个普通用户权限
networks:
network:
ipv4_address: 177.10.0.15
networks:

View File

@@ -1,6 +1,9 @@
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-backend:latest
WORKDIR /backend
COPY ./backend/ .
RUN ls ./conf/
RUN awk 'BEGIN { cmd="cp -i ./conf/env.example.py ./conf/env.py "; print "n" |cmd; }'
RUN sed -i "s|DATABASE_HOST = "127.0.0.1"|DATABASE_HOST = '177.10.0.1'|g" ./conf/env.py
RUN sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.1'|g" ./conf/env.py
RUN python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt
CMD ["/backend/docker_start.sh"]

View File

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

View File

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

View File

@@ -14,6 +14,7 @@
"@fast-crud/fast-extends": "^1.19.2",
"@fast-crud/ui-element": "^1.19.2",
"@fast-crud/ui-interface": "^1.19.2",
"@types/lodash": "^4.14.202",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
@@ -25,7 +26,7 @@
"echarts": "^5.4.1",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.3.9",
"element-plus": "^2.5.5",
"element-tree-line": "^0.2.1",
"font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",

View File

@@ -0,0 +1,29 @@
<template>
<div>
{{ data }}
</div>
</template>
<script setup lang="ts">
import {defineProps,ref,watch} from 'vue'
import {useDeptInfoStore} from '/@/stores/modules/dept'
const props = defineProps({
modelValue:{
type: Number || String
}
})
const data = ref()
watch(()=>{
return props.modelValue
},async (newVal)=>{
const deptInfoStore = useDeptInfoStore()
const result = await deptInfoStore.getParentDeptById(newVal)
if(result?.nodes){
let name = ""
console.log(result)
result.nodes.forEach((item:any,index:number)=>{
name += index>0?`/${item.name}`:item.name
})
data.value = name
}
},{immediate: true} )
</script>

View File

@@ -0,0 +1,141 @@
<template>
<!-- 你的自定义受控组件-->
<el-select-v2
v-model="data"
:options="options"
style="width: 100%;"
:clearable="true"
:props="selectProps"
@change="onDataChange"
/>
</template>
<script lang="ts" setup>
import {ref, defineComponent, watch, computed, toRefs, toRaw, onMounted} from 'vue'
import {useUi} from "@fast-crud/fast-crud";
import {request} from "/@/utils/service";
const props = defineProps({
dict: { // 接收来自FastCrud配置中的dict数据
type: Array,
required: true,
},
modelValue: {}
})
const emit = defineEmits(['update:modelValue'])
// 获取数据
const dataList = ref([])
function getData(params) {
request({
url: props.dict.url,
params: params
}).then(res => {
dataList.value = res.data
})
}
// template上使用data
const data = ref()
// const data = computed({
// get: () => {
// console.log("有默认值", props.modelValue)
// //getData({id:props.modelValue})
//
// console.log(11, dataList)
// // const {data} = res
// // console.log("get",data[0][selectProps.value.label])
// if (dataList && dataList.length === 1) {
// return dataList[0][selectProps.value.value]
// } else {
// // console.log("aa",res.data)
// return props.modelValue
// }
// // return props.modelValue
// },
// set: (val) => {
// //data.value = val
// return val
// }
// })
const options = ref([])
const selectProps = ref({
label: 'label',
value: 'value'
})
watch(
() => {
return props.modelValue
}, // 监听modelValue的变化
(value) => {
// data.value = value
request({
url: props.dict.url,
params: {
id: props.modelValue
}
}).then(res => {
const dataList = res.data
console.log(dataList)
if (dataList && dataList.length === 1) {
data.value = dataList[0][selectProps.value.label]
}else{
data.value = null
}
})
}, // 当modelValue值触发后同步修改data.value的值
{immediate: true} // 立即触发一次给data赋值初始值
)
//获取表单校验上下文
const {ui} = useUi()
const formValidator = ui.formItem.injectFormItemContext();
// 当data需要变化时上报给父组件
// 父组件监听到update:modelValue事件后会更新props.modelValue的值
// 然后watch会被触发修改data.value的值。
function onDataChange(value) {
emit('update:modelValue', value)
data.value = value
//触发校验
formValidator.onChange()
formValidator.onBlur()
}
if (props.dict.url instanceof Function) {
request(props.dict.url).then((res) => {
options.value = res.data
})
} else {
selectProps.value.label = props.dict.label
selectProps.value.value = props.dict.value
request({
url: props.dict.url
}).then((res) => {
options.value = res.data
})
}
// onMounted(() => {
// getData({id: props.modelValue})
// })
</script>
<style scoped lang="scss">
.el-select .el-input__wrapper .el-input__inner::placeholder {
//color: #a8abb2;
color: #0d84ff;
}
.el-select-v2 {
.el-select-v2__wrapper {
.el-select-v2__placeholder.is-transparent {
//color: #a8abb2;
color: #0d84ff;
}
}
}
</style>

View File

@@ -43,7 +43,7 @@
import {defineProps, onMounted, reactive, ref, toRaw, watch} from 'vue'
import {dict} from '@fast-crud/fast-crud'
import XEUtils from 'xe-utils'
import {request} from '/@/utils/service'
const props = defineProps({
modelValue: {},
tableConfig: {
@@ -71,6 +71,7 @@ watch(multipleSelection, // 监听multipleSelection的变化
if (!tableConfig.isMultiple) {
data.value = value ? value[tableConfig.label] : null
} else {
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
@@ -123,12 +124,12 @@ const getDict = async () => {
const params = {
page: pageConfig.page,
limit: pageConfig.limit,
search: search
search: search.value
}
const dicts = dict({url: url, params: params})
await dicts.reloadDict()
const dictData: any = dicts.data
const {data, page, limit, total} = dictData
const {data, page, limit, total} = await request({
url:url,
params:params
})
pageConfig.page = page
pageConfig.limit = limit
pageConfig.total = total

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,7 +1,7 @@
import type { App } from 'vue';
import { authDirective } from '/@/directive/authDirective';
import { wavesDirective, dragDirective } from '/@/directive/customDirective';
import {resizeObDirective} from '/@/directive/sizeDirective'
/**
* 导出指令方法v-xxx
* @methods authDirective 用户权限指令用法v-auth
@@ -15,4 +15,6 @@ export function directive(app: App) {
wavesDirective(app);
// 自定义拖动指令
dragDirective(app);
// 监听窗口大小变化
resizeObDirective(app)
}

View File

@@ -0,0 +1,23 @@
import {App} from "vue/dist/vue";
const map = new WeakMap()
const ob = new ResizeObserver((entries) => {
for(const entry of entries){
const handler = map.get(entry.target);
handler && handler({
width: entry.borderBoxSize[0].inlineSize,
height: entry.borderBoxSize[0].blockSize
});
}
});
export function resizeObDirective(app: App){
app.directive('resizeOb', {
mounted(el,binding) {
map.set(el,binding.value);
ob.observe(el); // 监听目标元素
},
unmounted(el) {
ob.unobserve(el); // 停止监听
},
})
}

View File

@@ -1,10 +1,10 @@
<template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img :src="logoMini" class="layout-logo-medium-img" />
<span style="font-size: x-large">{{ themeConfig.globalTitle }}</span>
<img :src="siteLogo" class="layout-logo-medium-img" />
<span style="font-size: x-large">{{ getSystemConfig['login.site_title'] || themeConfig.globalTitle }}</span>
</div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
<img :src="logoMini" class="layout-logo-size-img" />
<img :src="siteLogo" class="layout-logo-size-img" />
</div>
</template>
@@ -13,7 +13,8 @@ import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import logoMini from '/@/assets/logo-mini.svg';
import { SystemConfigStore } from "/@/stores/systemConfig";
import _ from "lodash";
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
@@ -28,6 +29,20 @@ const onThemeConfigChange = () => {
if (themeConfig.value.layout === 'transverse') return false;
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
};
const systemConfigStore = SystemConfigStore()
const { systemConfig } = storeToRefs(systemConfigStore)
const getSystemConfig = computed(() => {
return systemConfig.value
})
const siteLogo = computed(() => {
if (!_.isEmpty(getSystemConfig.value['login.site_logo'])) {
return getSystemConfig.value['login.site_logo']
}
return logoMini
});
</script>
<style scoped lang="scss">
@@ -42,30 +57,36 @@ const onThemeConfigChange = () => {
font-size: 16px;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
span {
white-space: nowrap;
display: inline-block;
}
&:hover {
span {
color: var(--color-primary-light-2);
}
}
&-medium-img {
width: 40px;
margin-right: 5px;
}
}
.layout-logo-size {
width: 100%;
height: 50px;
display: flex;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
&-img {
width: 40px;
margin: auto;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;

View File

@@ -3,9 +3,7 @@
<router-view v-slot="{ Component }">
<transition :name="setTransitionName" mode="out-in">
<keep-alive :include="getKeepAliveNames" v-if="showView">
<div>
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" v-show="!isIframePage" />
</div>
</keep-alive>
</transition>
</router-view>
@@ -61,6 +59,7 @@ const setTransitionName = computed(() => {
});
// 获取组件缓存列表(name值)
const getKeepAliveNames = computed(() => {
console.log(cachedViews.value)
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
});
// 设置 iframe 显示/隐藏

View File

@@ -36,7 +36,7 @@ import { reactive, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { Local } from '/@/utils/storage';
import { Local,Session } from '/@/utils/storage';
// 定义变量内容
const { t } = useI18n();
@@ -57,6 +57,7 @@ const getThemeConfig = computed(() => {
// 残忍拒绝
const onCancel = () => {
state.isUpgrade = false;
Session.set('isUpgrade', false)
};
// 马上更新
const onUpgrade = () => {
@@ -66,13 +67,17 @@ const onUpgrade = () => {
Local.clear();
window.location.reload();
Local.set('version', state.version);
Session.set('isUpgrade', false)
}, 2000);
};
// 延迟显示,防止刷新时界面显示太快
const delayShow = () => {
setTimeout(() => {
state.isUpgrade = true;
}, 2000);
const isUpgrade = Session.get('isUpgrade')===false?Session.get('isUpgrade'):true
if(isUpgrade){
setTimeout(() => {
state.isUpgrade = true;
}, 2000);
}
};
// 页面加载时
onMounted(() => {

View File

@@ -1,7 +1,7 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { directive } from '/@/utils/directive';
import { directive } from '/@/directive/index';
import { i18n } from '/@/i18n';
import other from '/@/utils/other';
import '/@/assets/style/tailwind.css'; // 先引入tailwind css, 以免element-plus冲突

View File

@@ -12,14 +12,16 @@ import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useMenuApi } from '/@/api/menu/index';
import { handleMenu } from '../utils/menu';
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
import {SystemConfigStore} from "/@/stores/systemConfig";
import {useDeptInfoStore} from "/@/stores/modules/dept";
import {DictionaryStore} from "/@/stores/dictionary";
import {useFrontendMenuStore} from "/@/stores/frontendMenu";
import {toRaw} from "vue";
const menuApi = useMenuApi();
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
// 后端控制路由
/**
* 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob
@@ -45,15 +47,36 @@ export async function initBackEndControlRoutes() {
await useUserInfo().setUserInfos();
// 获取路由菜单数据
const res = await getBackEndControlRoutes();
// 无登录权限时,添加判断
// https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO
if (res.data.length <= 0) return Promise.resolve(true);
// 处理路由component替换 dynamicRoutes/@/router/route第一个顶级 children 的路由
dynamicRoutes[0].children = await backEndComponent(handleMenu(res.data));
const {frameIn,frameOut} = handleMenu(res.data)
dynamicRoutes[0].children = await backEndComponent(frameIn);
// 添加动态路由
await setAddRoute();
// 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
await setFilterMenuAndCacheTagsViewRoutes();
}
export async function setRouters(){
const {frameInRoutes,frameOutRoutes} = await useFrontendMenuStore().getRouter()
const frameInRouter = toRaw(frameInRoutes)
const frameOutRouter = toRaw(frameOutRoutes)
dynamicRoutes[0].children = frameInRouter
dynamicRoutes.forEach((item:any)=>{
router.addRoute(item)
})
frameOutRouter.forEach((item:any)=>{
router.addRoute(item)
})
const storesRoutesList = useRoutesList(pinia);
storesRoutesList.setRoutesList([...dynamicRoutes[0].children,...frameOutRouter]);
const storesTagsView = useTagsViewRoutes(pinia);
storesTagsView.setTagsViewRoutes([...dynamicRoutes[0].children,...frameOutRouter])
}
/**
* 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
* @description 用于左侧菜单、横向菜单的显示
@@ -81,6 +104,8 @@ export function setCacheTagsViewRoutes() {
*/
export function setFilterRouteEnd() {
let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
// notFoundAndNoPower 防止 404、401 不在 layout 布局中不设置的话404、401 界面将全屏显示
// 关联问题 No match found for location with path 'xxx'
filterRouteEnd[0].children = [...filterRouteEnd[0].children, ...notFoundAndNoPower];
return filterRouteEnd;
}
@@ -105,6 +130,12 @@ export async function setAddRoute() {
export function getBackEndControlRoutes() {
//获取所有的按钮权限
BtnPermissionStore().getBtnPermissionStore();
// 获取系统配置
SystemConfigStore().getSystemConfigs()
// 获取所有部门信息
useDeptInfoStore().requestDeptInfo()
// 获取字典信息
DictionaryStore().getSystemDictionarys()
return menuApi.getSystemMenu();
}
@@ -126,6 +157,32 @@ export function backEndComponent(routes: any) {
if (!routes) return;
return routes.map((item: any) => {
if (item.component) item.component = dynamicImport(dynamicViewsModules, item.component as string);
if(item.is_catalog){
// 对目录的处理
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/parent')
}
if(item.is_link){
// 对外链接的处理
if(item.is_iframe){
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/iframes')
}else {
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/link')
}
}else{
if(item.is_iframe){
// const iframeRoute:RouteRecordRaw = {
// ...item
// }
// router.addRoute(iframeRoute)
item.meta.isLink = item.link_url
// item.path = `${item.path}Link`
// item.name = `${item.name}Link`
// item.meta.isIframe = item.is_iframe
// item.meta.isKeepAlive = false
// item.meta.isIframeOpen = true
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/link.vue')
}
}
item.children && backEndComponent(item.children);
return item;
});

View File

@@ -7,9 +7,12 @@ import {useKeepALiveNames} from '/@/stores/keepAliveNames';
import {useRoutesList} from '/@/stores/routesList';
import {useThemeConfig} from '/@/stores/themeConfig';
import {Session} from '/@/utils/storage';
import {staticRoutes} from '/@/router/route';
import {dynamicRoutes, notFoundAndNoPower, staticRoutes} from '/@/router/route';
import {initFrontEndControlRoutes} from '/@/router/frontEnd';
import {initBackEndControlRoutes} from '/@/router/backEnd';
import {initBackEndControlRoutes, setRouters} from '/@/router/backEnd';
import {useFrontendMenuStore} from "/@/stores/frontendMenu";
import {useTagsViewRoutes} from "/@/stores/tagsViewRoutes";
import {toRaw} from "vue";
/**
* 1、前端控制路由时isRequestRoutes 为 false需要写 roles需要走 setFilterRoute 方法。
@@ -32,7 +35,13 @@ const {isRequestRoutes} = themeConfig.value;
*/
export const router = createRouter({
history: createWebHashHistory(),
routes: staticRoutes,
/**
* 说明:
* 1、notFoundAndNoPower 默认添加 404、401 界面,防止一直提示 No match found for location with path 'xxx'
* 2、backEnd.ts(后端控制路由)、frontEnd.ts(前端控制路由) 中也需要加 notFoundAndNoPower 404、401 界面。
* 防止 404、401 不在 layout 布局中不设置的话404、401 界面将全屏显示
*/
routes: [...notFoundAndNoPower, ...staticRoutes]
});
/**
@@ -63,14 +72,7 @@ export function formatTwoStageRoutes(arr: any) {
const cacheList: Array<string> = [];
arr.forEach((v: any) => {
if (v.path === '/') {
newArr.push({
component: v.component,
name: v.name,
path: v.path,
redirect: v.redirect,
meta: v.meta,
children: []
});
newArr.push({component: v.component,name: v.name,path: v.path,redirect: v.redirect,meta: v.meta,children: []});
} else {
// 判断是否是动态路由xx/:id/:name用于 tagsView 等中使用
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
@@ -108,19 +110,21 @@ router.beforeEach(async (to, from, next) => {
next('/home');
NProgress.done();
} else {
const storesRoutesList = useRoutesList(pinia);
const {routesList} = storeToRefs(storesRoutesList);
if (routesList.value.length === 0) {
if (isRequestRoutes) {
// 后端控制路由:路由数据初始化,防止刷新时丢失
await initBackEndControlRoutes();
// 动态添加路由:防止非首页刷新时跳转回首页的问题
// 确保 addRoute() 时动态添加的路由已经被完全加载上去
next({...to, replace: true});
// 解决刷新时,一直跳 404 页面问题,关联问题 No match found for location with path 'xxx'
// to.query 防止页面刷新时普通路由带参数时参数丢失。动态路由xxx/:id/:name"isDynamic 无需处理
next({ path: to.path, query: to.query });
} else {
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
await initFrontEndControlRoutes();
next({...to, replace: true});
next({ path: to.path, query: to.query });
}
} else {
next();

View File

@@ -88,4 +88,12 @@ export const staticRoutes: Array<RouteRecordRaw> = [
title: '登录',
},
},
{
path: '/demo',
name: 'demo',
component: () => import('/@/views/system/demo/index.vue'),
meta: {
title: 'message.router.personal'
},
}
];

View File

@@ -1,118 +1,158 @@
// 引入fast-crud
import { FastCrud, useTypes } from '@fast-crud/fast-crud';
const { getType } = useTypes();
import {FastCrud, useTypes} from '@fast-crud/fast-crud';
const {getType} = useTypes();
import '@fast-crud/fast-crud/dist/style.css';
import { setLogger } from '@fast-crud/fast-crud';
import {setLogger} from '@fast-crud/fast-crud';
import {getBaseURL} from '/@/utils/baseUrl';
// element
import ui from '@fast-crud/ui-element';
import { request } from '/@/utils/service';
import {request} from '/@/utils/service';
//扩展包
import { FsExtendsEditor,FsExtendsUploader } from '@fast-crud/fast-extends';
import {FsExtendsEditor, FsExtendsUploader,FsCropperUploader} from '@fast-crud/fast-extends';
import '@fast-crud/fast-extends/dist/style.css';
import { successMessage, successNotification } from '/@/utils/message';
import {successNotification} from '/@/utils/message';
import XEUtils from "xe-utils";
export default {
async install(app: any, options: any) {
// 先安装ui
app.use(ui);
// 然后安装FastCrud
app.use(FastCrud, {
//i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件
// 此处配置公共的dictRequest字典请求
async dictRequest({ dict }: any) {
//根据dict的url异步返回一个字典数组
return await request({ url: dict.url, params: dict.params || {} }).then((res:any)=>{
return res.data
});
},
//公共crud配置
commonOptions() {
return {
request: {
//接口请求配置
//你项目后台接口大概率与fast-crud所需要的返回结构不一致所以需要配置此项
//请参考文档http://fast-crud.docmirror.cn/api/crud-options/request.html
transformQuery: ({ page, form, sort }: any) => {
if (sort.asc !== undefined) {
form['ordering'] = `${sort.asc ? '' : '-'}${sort.prop}`;
}
//转换为你pageRequest所需要的请求参数结构
return { page: page.currentPage, limit: page.pageSize, ...form };
},
transformRes: ({ res }: any) => {
//将pageRequest的返回数据转换为fast-crud所需要的格式
//return {records,currentPage,pageSize,total};
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total };
},
},
form: {
afterSubmit(ctx: any) {
// 增加crud提示
if (ctx.res.code == 2000) {
successNotification(ctx.res.msg);
}
},
},
/* search: {
layout: 'multi-line',
collapse: true,
col: {
span: 4,
},
options: {
labelCol: {
style: {
width: '100px',
},
},
},
}, */
};
},
});
//富文本
app.use(FsExtendsEditor, {
wangEditor: {
width: 300,
},
});
// 文件上传
app.use(FsExtendsUploader, {
defaultType: "form",
form: {
action: `/api/system/file/`,
name: "file",
withCredentials: false,
uploadRequest: async ({ action, file, onProgress }) => {
// @ts-ignore
const data = new FormData();
data.append("file", file);
return await request({
url: action,
method: "post",
timeout: 60000,
headers: {
"Content-Type": "multipart/form-data"
},
data,
onUploadProgress: (p) => {
onProgress({ percent: Math.round((p.loaded / p.total) * 100) });
}
});
},
successHandle(ret) {
// 上传完成后的结果处理, 此处应返回格式为{url:xxx,key:xxx}
return {
url: getBaseURL() + ret.data.url,
key: ret.data.id
};
}
}
})
setLogger({ level: 'error' });
// 设置自动染色
const dictComponentList = ['dict-cascader', 'dict-checkbox', 'dict-radio', 'dict-select', 'dict-switch', 'dict-tree'];
dictComponentList.forEach((val) => {
getType(val).column.component.color = 'auto';
});
},
async install(app: any, options: any) {
// 先安装ui
app.use(ui);
// 然后安装FastCrud
app.use(FastCrud, {
//i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件
// 此处配置公共的dictRequest字典请求
async dictRequest({dict}: any) {
const {isTree} = dict
//根据dicturl,异步返回一个字典数组
return await request({url: dict.url, params: dict.params || {}}).then((res: any) => {
if (isTree) {
return XEUtils.toArrayTree(res.data, {parentKey: 'parent'})
}
return res.data
});
},
//公共crud配置
commonOptions() {
return {
request: {
//接口请求配置
//你项目后台接口大概率与fast-crud所需要的返回结构不一致所以需要配置此项
//请参考文档http://fast-crud.docmirror.cn/api/crud-options/request.html
transformQuery: ({page, form, sort}: any) => {
if (sort.asc !== undefined) {
form['ordering'] = `${sort.asc ? '' : '-'}${sort.prop}`;
}
//转换为你pageRequest所需要的请求参数结构
return {page: page.currentPage, limit: page.pageSize, ...form};
},
transformRes: ({res}: any) => {
//将pageRequest的返回数据转换为fast-crud所需要的格式
//return {records,currentPage,pageSize,total};
return {records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total};
},
},
form: {
afterSubmit(ctx: any) {
// 增加crud提示
if (ctx.res.code == 2000) {
successNotification(ctx.res.msg);
}
},
},
/* search: {
layout: 'multi-line',
collapse: true,
col: {
span: 4,
},
options: {
labelCol: {
style: {
width: '100px',
},
},
},
}, */
};
},
});
//富文本
app.use(FsExtendsEditor, {
wangEditor: {
width: 300,
},
});
// 文件上传
app.use(FsExtendsUploader, {
defaultType: "form",
form: {
action: `/api/system/file/`,
name: "file",
withCredentials: false,
uploadRequest: async ({action, file, onProgress}) => {
// @ts-ignore
const data = new FormData();
data.append("file", file);
return await request({
url: action,
method: "post",
timeout: 60000,
headers: {
"Content-Type": "multipart/form-data"
},
data,
onUploadProgress: (p) => {
onProgress({percent: Math.round((p.loaded / p.total) * 100)});
}
});
},
successHandle(ret) {
// 上传完成后的结果处理, 此处应返回格式为{url:xxx,key:xxx}
return {
url: getBaseURL(ret.data.url),
key: ret.data.id,
...ret.data
};
}
},
valueBuilder(context){
const {row,key} = context
return getBaseURL(row[key])
}
})
setLogger({level: 'error'});
// 设置自动染色
const dictComponentList = ['dict-cascader', 'dict-checkbox', 'dict-radio', 'dict-select', 'dict-switch', 'dict-tree'];
dictComponentList.forEach((val) => {
getType(val).column.component.color = 'auto';
getType(val).column.align = 'center';
});
// 设置placeholder 的默认值
const placeholderComponentList = [
{key: 'text', placeholder: "请输入"},
{key: 'textarea', placeholder: "请输入"},
{key: 'input', placeholder: "请输入"},
{key: 'password', placeholder: "请输入"}
]
placeholderComponentList.forEach((val) => {
if (getType(val.key)?.search?.component) {
getType(val.key).search.component.placeholder = val.placeholder;
} else if (getType(val.key)?.search) {
getType(val.key).search.component = {placeholder: val.placeholder};
}
if (getType(val.key)?.form?.component) {
getType(val.key).form.component.placeholder = val.placeholder;
} else if (getType(val.key)?.form) {
getType(val.key).form.component = {placeholder: val.placeholder};
}
if (getType(val.key)?.column?.align) {
getType(val.key).column.align = 'center'
} else if (getType(val.key)?.column) {
getType(val.key).column = {align: 'center'};
} else if (getType(val.key)) {
getType(val.key).column = {align: 'center'};
}
});
},
};

View File

@@ -0,0 +1,169 @@
import {defineStore} from 'pinia';
import {FrontendMenu} from './interface';
import {Session} from '/@/utils/storage';
import {request} from '../utils/service';
import XEUtils from "xe-utils";
import {RouteRecordRaw} from "vue-router";
import {useKeepALiveNames} from "/@/stores/keepAliveNames";
import pinia from "/@/stores/index";
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
/**
* 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json
*/
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules });
/**
* 后端路由 component 转换函数
* @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件
* @param component 当前要处理项 component
* @returns 返回处理成函数后的 component
*/
export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];
return dynamicViewsModules[matchKey];
}
if (matchKeys?.length > 1) {
return false;
}
}
/**
* @description: 处理后端菜单数据格式
* @param {Array} menuData
* @return {*}
*/
export const handleMenu = (menuData: Array<any>) => {
// 框架内路由
const frameInRoutes:Array<any> = []
// 框架外路由
const frameOutRoutes:Array<any> = []
// 需要缓存的路由
const cacheList:Array<any> = []
// 先处理menu meta数据转换
const handleMeta = (item: any) => {
item.path = item.web_path
item.meta = {
title: item.title,
isLink: item.link_url,
isHide: !item.visible,
isKeepAlive: item.cache,
isAffix: item.is_affix,
isIframe: item.is_iframe,
roles: ['admin'],
icon: item.icon
}
item.component = dynamicImport(dynamicViewsModules, item.component as string)
if(item.is_catalog){
// 对目录的处理
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/parent')
}
if(item.is_link){
// 对外链接的处理
item.meta.isIframe = !item.is_iframe
if(item.is_iframe){
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/link')
}else {
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/iframes')
}
}else{
if(item.is_iframe){
const route = JSON.parse(JSON.stringify(item))
route.meta.isLink = ''
route.path = `${item.web_path}`
route.name = `${item.name}`
route.meta.isIframe = true
route.meta.isKeepAlive = false
route.meta.isIframeOpen = true
route.component = item.component
frameOutRoutes.push(route)
item.path = `${item.web_path}FrameOut`
item.name = `${item.name}FrameOut`
item.meta.isLink = item.web_path
item.meta.isIframe = !item.is_iframe
//item.meta.isIframeOpen = true
item.component = dynamicImport(dynamicViewsModules, 'layout/routerView/link.vue')
}
}
item.children && handleMeta(item.children);
if (item.meta.isKeepAlive && item.meta.isKeepAlive && item.component_name != "") {
cacheList.push(item.name);
}
return item
}
menuData.forEach((val) => {
frameInRoutes.push(handleMeta(val))
})
const stores = useKeepALiveNames(pinia);
stores.setCacheKeepAlive(cacheList);
const data = XEUtils.toArrayTree(frameInRoutes, {
parentKey: 'parent',
strict: true,
})
const dynamicRoutes = [
{
path: '/home', name: 'home',
component: dynamicImport(dynamicViewsModules, '/system/home/index'),
meta: {
title: 'message.router.home',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: true,
isIframe: false,
roles: ['admin'],
icon: 'iconfont icon-shouye'
}
},
...data
]
return {frameIn:dynamicRoutes,frameOut:frameOutRoutes}
}
export const useFrontendMenuStore = defineStore('frontendMenu',{
state: (): FrontendMenu => ({
arrayRouter: [],
treeRouter: [],
frameInRoutes:[],
frameOutRoutes:[]
}),
actions:{
async requestMenu(){
return request({
url: '/api/system/menu/web_router/',
method: 'get',
params:{},
}).then((res:any)=>{
return res.data
});
},
async handleRouter(){
const menuData = await this.requestMenu();
this.arrayRouter = menuData
const {frameIn,frameOut} = handleMenu(menuData);
this.treeRouter = [...frameIn,...frameOut]
this.frameInRoutes=frameIn
this.frameOutRoutes=frameOut
},
async getRouter(){
await this.handleRouter()
return {
frameInRoutes:this.frameInRoutes,
frameOutRoutes:this.frameOutRoutes,
treeRouter:this.treeRouter
}
}
}
})

View File

@@ -2,6 +2,7 @@
* 定义接口来定义对象的类型
* `stores` 全部类型定义在这里
*/
import {useFrontendMenuStore} from "/@/stores/frontendMenu";
// 用户信息
export interface UserInfosState {
@@ -102,3 +103,12 @@ export interface DictionaryStates {
export interface ConfigStates {
systemConfig: any;
}
export interface FrontendMenu {
arrayRouter: Array<any>;
treeRouter:Array<any>;
frameOutRoutes:Array<any>;
frameInRoutes:Array<any>;
}

View File

@@ -0,0 +1,30 @@
import {defineStore} from "pinia";
import {request} from "/@/utils/service";
import XEUtils from "xe-utils";
import {toRaw} from 'vue'
export const useDeptInfoStore = defineStore('deptInfo', {
state:()=>(
{
list:[],
tree:[],
}
),
actions:{
async requestDeptInfo() {
// 请求部门信息
const ret = await request({
url: '/api/system/dept/all_dept/'
})
this.list = ret.data
this.tree = XEUtils.toArrayTree(ret.data,{parentKey:'parent',strict:true})
},
async getDeptById(id:any){
},
async getParentDeptById(id: any){
const tree = toRaw(this.tree)
const obj = XEUtils.findTree(tree, item => item.id == id)
return obj
}
}
})

View File

@@ -22,5 +22,8 @@ export const useRoutesList = defineStore('routesList', {
async setColumnsNavHover(bool: Boolean) {
this.isColumnsNavHover = bool;
},
async addRoutesList(data: Array<string>) {
this.routesList.push(data);
}
},
});

View File

@@ -256,6 +256,13 @@
.el-button.is-text {
padding: 0;
}
th.el-table__cell{
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select:text !important;
}
}
/* scrollbar

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,3 +5,4 @@
@import './media/media.scss';
@import './waves.scss';
@import './dark.scss';
@import './fa/css/font-awesome.min.css';

View File

@@ -1,39 +0,0 @@
import type { App } from 'vue';
import { judementSameArr } from '/@/utils/arrayOperation';
import {BtnPermissionStore} from "/@/stores/btnPermission";
/**
* 用户权限指令
* @directive 单个权限验证v-auth="xxx"
* @directive 多个权限验证满足一个则显示v-auths="[xxx,xxx]"
* @directive 多个权限验证全部满足则显示v-auth-all="[xxx,xxx]"
*/
export function authDirective(app: App) {
// 单个权限验证v-auth="xxx"
app.directive('auth', {
mounted(el, binding) {
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 = BtnPermissionStore();
stores.data.map((val: string) => {
binding.value.map((v: string) => {
if (val === v) flag = true;
});
});
if (!flag) el.parentNode.removeChild(el);
},
});
// 多个权限验证全部满足则显示v-auth-all="[xxx,xxx]"
app.directive('auth-all', {
mounted(el, binding) {
const stores = BtnPermissionStore();
const flag = judementSameArr(binding.value, stores.data);
if (!flag) el.parentNode.removeChild(el);
},
});
}

View File

@@ -3,9 +3,13 @@ import { pluginsAll } from '/@/views/plugins/index';
/**
* @description 校验是否为租户模式。租户模式把域名替换成 域名 加端口
*/
export const getBaseURL = function () {
var baseURL = import.meta.env.VITE_API_URL as any;
var param = baseURL.split('/')[3] || '';
export const getBaseURL = function (url: null | string = null, isHost: null | boolean = null) {
let baseURL = import.meta.env.VITE_API_URL as any;
// 如果需要host返回返回地址前缀加http地址
if (isHost && !baseURL.startsWith('http')) {
baseURL = window.location.protocol + '//' + window.location.host + baseURL
}
let param = baseURL.split('/')[3] || '';
// @ts-ignore
if (pluginsAll && pluginsAll.indexOf('dvadmin3-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
// 1.把127.0.0.1 替换成和前端一样域名
@@ -26,6 +30,15 @@ export const getBaseURL = function () {
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL;
}
}
if (url) {
const regex = /^(http|https):\/\//;
if (regex.test(url)) {
return url
} else {
// js判断是否是斜杠结尾
return baseURL.replace(/\/$/, '') + '/' + url.replace(/^\//, '');
}
}
if (!baseURL.endsWith('/')) {
baseURL += '/';
}

View File

@@ -1,4 +1,4 @@
import { useColumnPermission } from '/@/stores/columnPermission';
import {useColumnPermission} from '/@/stores/columnPermission';
type permissionType = 'is_create' | 'is_query' | 'is_update';
@@ -7,3 +7,54 @@ export const columnPermission = (key: string, type: permissionType): boolean =>
return !!permissions.some((i) => i.field_name === key && i[type]);
};
/**
* 处理字段信息权限
* @param func 获取字段信息的接口函数
* @param crudOptions 原始的crudOptions信息
* @param excludeColumn 需要排除的列
*/
export const handleColumnPermission = async (func: Function, crudOptions: any,excludeColumn:string[]=[]) => {
const res = await func();
if(crudOptions.pagination==undefined){
crudOptions['pagination'] = {
show:true
}
}
const columns = crudOptions.columns;
const excludeColumns = ['_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn)
for (let col in columns) {
if (excludeColumns.includes(col)) {
continue
}else{
if (columns[col].column) {
columns[col].column.show = false
} else {
columns[col]['column'] = {
show: false
}
}
columns[col].addForm = {
show: false
}
columns[col].editForm = {
show: false
}
}
for (let item of res.data) {
if (excludeColumns.includes(item.field_name)) {
continue
} else if(item.field_name === col) {
columns[col].column.show = item['is_query']
columns[col].addForm = {
show: item['is_create']
}
columns[col].editForm = {
show: item['is_update']
}
}
}
}
return crudOptions
}

259
web/src/utils/commonCrud.ts Normal file
View File

@@ -0,0 +1,259 @@
import {dict} from "@fast-crud/fast-crud";
import {shallowRef} from 'vue'
import deptFormat from "/@/components/dept-format/index.vue";
export const commonCrudConfig = (options = {
create_datetime: {
form: false,
table: false,
search: false
},
update_datetime: {
form: false,
table: false,
search: false
},
creator_name: {
form: false,
table: false,
search: false
},
modifier_name: {
form: false,
table: false,
search: false
},
dept_belong_id: {
form: false,
table: false,
search: false
},
description: {
form: false,
table: false,
search: false
},
}) => {
return {
dept_belong_id: {
title: '所属部门',
type: 'dict-tree',
search: {
show: options.dept_belong_id?.search || false
},
dict: dict({
url: '/api/system/dept/all_dept/',
isTree: true,
value: 'id',
label: 'name',
children: 'children',
}),
column: {
align: 'center',
width: 300,
show: options.dept_belong_id?.table || false,
component: {
name: shallowRef(deptFormat),
vModel: "modelValue",
}
},
form: {
show: options.dept_belong_id?.form || false,
component: {
multiple: false,
clearable: true,
props: {
checkStrictly: true,
props: {
// 为什么这里要写两层props
// 因为props属性名与fs的动态渲染的props命名冲突所以要多写一层
label: "name",
value: "id",
}
}
},
helper: "默认不填则为当前创建用户的部门ID"
}
},
description: {
title: '备注',
search: {
show: options.description?.search || false
},
type: 'textarea',
column: {
width: 100,
show: options.description?.table || false,
},
form: {
show: options.description?.form || false,
component: {
placeholder: '请输入内容',
showWordLimit: true,
maxlength: '200',
}
},
viewForm: {
show: true
}
},
modifier_name: {
title: '修改人',
search: {
show: options.modifier_name?.search || false
},
column: {
width: 100,
show: options.modifier_name?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
creator_name: {
title: '创建人',
search: {
show: options.creator_name?.search || false
},
column: {
width: 100,
show: options.creator_name?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
update_datetime: {
title: '更新时间',
type: 'datetime',
search: {
show: options.update_datetime?.search || false,
col: {span: 8},
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
valueResolve(context: any) {
const {key, value} = context
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
if (value) {
context.form.update_datetime_after = value[0]
context.form.update_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
}
},
column: {
width: 160,
show: options.update_datetime?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
create_datetime: {
title: '创建时间',
type: 'datetime',
search: {
show: options.create_datetime?.search || false,
col: {span: 8},
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
valueResolve(context: any) {
const {key, value} = context
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
if (value) {
context.form.create_datetime_after = value[0]
context.form.create_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
}
},
column: {
width: 160,
show: options.create_datetime?.table || false,
},
form: {
show: false
},
viewForm: {
show: true
}
}
}
}

View File

@@ -1,178 +0,0 @@
import type { App } from 'vue';
/**
* 按钮波浪指令
* @directive 默认方式v-waves如 `<div v-waves></div>`
* @directive 参数方式v-waves=" |light|red|orange|purple|green|teal",如 `<div v-waves="'light'"></div>`
*/
export function wavesDirective(app: App) {
app.directive('waves', {
mounted(el, binding) {
el.classList.add('waves-effect');
binding.value && el.classList.add(`waves-${binding.value}`);
function setConvertStyle(obj: { [key: string]: unknown }) {
let style: string = '';
for (let i in obj) {
if (obj.hasOwnProperty(i)) style += `${i}:${obj[i]};`;
}
return style;
}
function onCurrentClick(e: { [key: string]: unknown }) {
let elDiv = document.createElement('div');
elDiv.classList.add('waves-ripple');
el.appendChild(elDiv);
let styles = {
left: `${e.layerX}px`,
top: `${e.layerY}px`,
opacity: 1,
transform: `scale(${(el.clientWidth / 100) * 10})`,
'transition-duration': `750ms`,
'transition-timing-function': `cubic-bezier(0.250, 0.460, 0.450, 0.940)`,
};
elDiv.setAttribute('style', setConvertStyle(styles));
setTimeout(() => {
elDiv.setAttribute(
'style',
setConvertStyle({
opacity: 0,
transform: styles.transform,
left: styles.left,
top: styles.top,
})
);
setTimeout(() => {
elDiv && el.removeChild(elDiv);
}, 750);
}, 450);
}
el.addEventListener('mousedown', onCurrentClick, false);
},
unmounted(el) {
el.addEventListener('mousedown', () => {});
},
});
}
/**
* 自定义拖动指令
* @description 使用方式v-drag="[dragDom,dragHeader]",如 `<div v-drag="['.drag-container .el-dialog', '.drag-container .el-dialog__header']"></div>`
* @description dragDom 要拖动的元素dragHeader 要拖动的 Header 位置
* @link 注意https://github.com/element-plus/element-plus/issues/522
* @lick 参考https://blog.csdn.net/weixin_46391323/article/details/105228020?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-10&spm=1001.2101.3001.4242
*/
export function dragDirective(app: App) {
app.directive('drag', {
mounted(el, binding) {
if (!binding.value) return false;
const dragDom = document.querySelector(binding.value[0]) as HTMLElement;
const dragHeader = document.querySelector(binding.value[1]) as HTMLElement;
dragHeader.onmouseover = () => (dragHeader.style.cursor = `move`);
function down(e: any, type: string) {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = type === 'pc' ? e.clientX - dragHeader.offsetLeft : e.touches[0].clientX - dragHeader.offsetLeft;
const disY = type === 'pc' ? e.clientY - dragHeader.offsetTop : e.touches[0].clientY - dragHeader.offsetTop;
// body当前宽度
const screenWidth = document.body.clientWidth;
// 可见区域高度(应为body高度可某些环境下无法获取)
const screenHeight = document.documentElement.clientHeight;
// 对话框宽度
const dragDomWidth = dragDom.offsetWidth;
// 对话框高度
const dragDomheight = dragDom.offsetHeight;
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
// 获取到的值带px 正则匹配替换
let styL: any = getComputedStyle(dragDom).left;
let styT: any = getComputedStyle(dragDom).top;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (styL.includes('%')) {
styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100);
styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100);
} else {
styL = +styL.replace(/\px/g, '');
styT = +styT.replace(/\px/g, '');
}
return {
disX,
disY,
minDragDomLeft,
maxDragDomLeft,
minDragDomTop,
maxDragDomTop,
styL,
styT,
};
}
function move(e: any, type: string, obj: any) {
let { disX, disY, minDragDomLeft, maxDragDomLeft, minDragDomTop, maxDragDomTop, styL, styT } = obj;
// 通过事件委托,计算移动的距离
let left = type === 'pc' ? e.clientX - disX : e.touches[0].clientX - disX;
let top = type === 'pc' ? e.clientY - disY : e.touches[0].clientY - disY;
// 边界处理
if (-left > minDragDomLeft) {
left = -minDragDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDragDomTop) {
top = -minDragDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
}
/**
* pc端
* onmousedown 鼠标按下触发事件
* onmousemove 鼠标按下时持续触发事件
* onmouseup 鼠标抬起触发事件
*/
dragHeader.onmousedown = (e) => {
const obj = down(e, 'pc');
document.onmousemove = (e) => {
move(e, 'pc', obj);
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
};
/**
* 移动端
* ontouchstart 当按下手指时触发ontouchstart
* ontouchmove 当移动手指时触发ontouchmove
* ontouchend 当移走手指时触发ontouchend
*/
dragHeader.ontouchstart = (e) => {
const obj = down(e, 'app');
document.ontouchmove = (e) => {
move(e, 'app', obj);
};
document.ontouchend = () => {
document.ontouchmove = null;
document.ontouchend = null;
};
};
},
});
}

View File

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

View File

@@ -1,18 +0,0 @@
import type { App } from 'vue';
import { authDirective } from '/@/utils/authDirective';
import { wavesDirective, dragDirective } from '/@/utils/customDirective';
/**
* 导出指令方法v-xxx
* @methods authDirective 用户权限指令用法v-auth
* @methods wavesDirective 按钮波浪指令用法v-waves
* @methods dragDirective 自定义拖动指令用法v-drag
*/
export function directive(app: App) {
// 用户权限指令
authDirective(app);
// 按钮波浪指令
wavesDirective(app);
// 自定义拖动指令
dragDirective(app);
}

View File

@@ -50,6 +50,17 @@ const getAwesomeIconfont = () => {
const styles: any = document.styleSheets;
let sheetsList = [];
let sheetsIconList = [];
// 判断fontFamily是否是本地加载
for (let i = 0; i < styles.length; i++) {
const rules = styles[i].cssRules || styles[i].rules;
if (rules) {
for (let j = 0; j < rules.length; j++) {
if (rules[j].style && rules[j].style.fontFamily === 'FontAwesome') {
sheetsList.push(styles[i])
}
}
}
}
for (let i = 0; i < styles.length; i++) {
if (styles[i].href && styles[i].href.indexOf('netdna.bootstrapcdn.com') > -1) {
sheetsList.push(styles[i]);

View File

@@ -1,4 +1,5 @@
import XEUtils from "xe-utils"
import {dynamicRoutes, staticRoutes} from "/@/router/route";
/**
* @description: 处理后端菜单数据格式
@@ -10,27 +11,56 @@ export const handleMenu = (menuData: Array<any>) => {
const handleMeta = (item: any) => {
item.meta = {
title: item.title,
isLink: item.is_link,
isLink: item.link_url,
isHide: !item.visible,
isKeepAlive: item.cache,
isAffix: false,
isIframe: false,
isAffix: item.is_affix,
isIframe: item.is_iframe,
roles: ['admin'],
icon: item.icon
}
item.name = item.component_name
item.path = item.web_path
return item
}
menuData.forEach((val) => {
handleMeta(val)
val.path = val.web_path
})
const data = XEUtils.toArrayTree(menuData, {
// 处理框架外的路由
const handleFrame = (item: any) => {
if (item.is_iframe) {
item.meta = {
title: item.title,
isLink: item.link_url,
isHide: !item.visible,
isKeepAlive: item.cache,
isAffix: item.is_affix,
isIframe: item.is_iframe,
roles: ['admin'],
icon: item.icon
}
item.name = item.component_name
item.path = item.web_path
}
return item
}
// 框架内路由
const defaultRoutes:Array<any> = []
// 框架外路由
const iframeRoutes:Array<any> = []
menuData.forEach((val) => {
// if (val.is_iframe) {
// // iframeRoutes.push(handleFrame(val))
// } else {
// defaultRoutes.push(handleMeta(val))
// }
defaultRoutes.push(handleMeta(val))
})
const data = XEUtils.toArrayTree(defaultRoutes, {
parentKey: 'parent',
strict: true,
})
const menu = [
const dynamicRoutes = [
{
path: '/home', name: 'home', component: '/system/home/index', meta: {
title: 'message.router.home',
@@ -45,5 +75,5 @@ export const handleMenu = (menuData: Array<any>) => {
},
...data
]
return menu
return {frameIn:dynamicRoutes,frameOut:iframeRoutes}
}

View File

@@ -76,7 +76,7 @@ function createService() {
// window.location.reload();
break;
case 401:
Local.clear();
// Local.clear();
Session.clear();
dataAxios.msg = '登录认证失败,请重新登录';
ElMessageBox.alert(dataAxios.msg, '提示', {
@@ -96,12 +96,13 @@ function createService() {
return dataAxios;
case 4000:
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
return dataAxios;
break;
default:
// 不是正确的 code
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
return dataAxios;
break;
}
return Promise.reject(dataAxios);
}
},
(error) => {
@@ -111,7 +112,7 @@ function createService() {
error.message = '请求错误';
break;
case 401:
Local.clear();
// Local.clear();
Session.clear();
error.message = '登录授权过期,请重新登录';
ElMessageBox.alert(error.message, '提示', {

View File

@@ -2,6 +2,7 @@
const cssCdnUrlList: Array<string> = [
'//at.alicdn.com/t/font_2298093_y6u00apwst.css',
'//at.alicdn.com/t/c/font_3882322_9ah7y8m9175.css', //dvadmin3项目用icon
//'//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'
];
// 第三方 js url
const jsCdnUrlList: Array<string> = [];

View File

@@ -0,0 +1,80 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #header-top>
<div id="myEcharts" v-show="isEcharts" v-resize-ob="handleResize" :style="{width: '100%', height: '300px'}"></div>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="loginLog">
import { ref, onMounted } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import * as echarts from "echarts";
const isEcharts = ref(true)
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions,isEcharts,initChart });
const myEcharts = echarts
function initChart() {
let chart = myEcharts.init(document.getElementById("myEcharts"), "purple-passion");
// 在这里请求API,例如:
/***
* request({url:'xxxx'}).then(res=>{
* // 把chart.setOption写在这里面
*
* })
*
*/
chart.setOption({
title: {
text: "2021年各月份销售量单位",
left: "center",
},
xAxis: {
type: "category",
data: [
"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"
]
},
tooltip: {
trigger: "axis"
},
yAxis: {
type: "value"
},
series: [
{
data: [
606, 542, 985, 687, 501, 787, 339, 706, 383, 684, 669, 737
],
type: "line",
smooth: true,
itemStyle: {
normal: {
label: {
show: true,
position: "top",
formatter: "{c}"
}
}
}
}
]
});
window.onresize = function () {
chart.resize();
};
}
function handleResize(size) {
console.log(size)
}
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
initChart()
});
</script>

View File

@@ -13,12 +13,12 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
/**
* 处理crud警告Invalid prop: type check failed for prop "name". Expected String with value "2", got Number with value 2.
*/
res.data.forEach((item: any) => {
item.dept = String(item.dept);
if (item.role && Array.isArray(item.role) && item.role.length > 0) {
item.role = item.role.map((r: number) => String(r));
}
});
// res.data.forEach((item: any) => {
// item.dept = String(item.dept);
// if (item.role && Array.isArray(item.role) && item.role.length > 0) {
// item.role = item.role.map((r: number) => String(r));
// }
// });
return res;
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -218,13 +218,6 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name',
getData: async ({ url }: { url: string }) => {
return request({
url: url,
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 150, //最小列宽
@@ -264,17 +257,6 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
url: '/api/system/role/',
value: 'id',
label: 'name',
getData: async ({ url }: { url: string }) => {
return request({
url: url,
params: {
page: 1,
limit: 10,
},
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 100, //最小列宽

View File

@@ -57,7 +57,7 @@ const getData = async () => {
const result = XEUtils.toArrayTree(res.data, {
parentKey: 'parent',
children: 'children',
strict: true,
//strict: true,
});
deptTreeData.value = result;

View File

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

View File

@@ -13,7 +13,7 @@
</div>
<div class="right">
<img
src="https://img-blog.csdnimg.cn/9eb1d85a417f4ed1ba7107f149ce3da1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbHl0LXRvcA==,size_16,color_FFFFFF,t_70,g_se,x_16"
src="./img404.png"
/>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,5 +1,6 @@
import * as api from './api';
import { UserPageQuery, AddReq, DelReq, EditReq, CreateCrudOptionsProps, CreateCrudOptionsRet, dict } from '@fast-crud/fast-crud';
import {commonCrudConfig} from "/@/utils/commonCrud";
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
@@ -325,6 +326,11 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
},
},
},
...commonCrudConfig({
create_datetime: {
search: true
}
})
},
},
};

View File

@@ -126,6 +126,7 @@ export default defineComponent({
});
};
const refreshCaptcha = async () => {
state.ruleForm.captcha=''
loginApi.getCaptcha().then((ret: any) => {
state.ruleForm.captchaImgBase = ret.data.image_base;
state.ruleForm.captchaKey = ret.data.key;
@@ -150,11 +151,11 @@ export default defineComponent({
// 执行完 initBackEndControlRoutes再执行 signInSuccess
loginSuccess();
}
} else if (res.code === 4000) {
// 登录错误之后,刷新验证码
refreshCaptcha();
}
});
}).catch((err: any) => {
// 登录错误之后,刷新验证码
refreshCaptcha();
});
} else {
errorMessage("请填写登录信息")
}

View File

@@ -1,11 +1,12 @@
<template>
<div class="login-container flex">
<div class="login-container flex z-10">
<div class="login-left">
<div class="login-left-logo">
<img :src="logoMini" />
<img :src="siteLogo" />
<div class="login-left-logo-text">
<span>{{ getSystemConfig['login.site_title']||getThemeConfig.globalViceTitle }}</span>
<span class="login-left-logo-text-msg">{{ getSystemConfig['login.site_name']||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">
@@ -13,12 +14,13 @@
</div>
<img :src="loginBg" class="login-left-waves" />
</div>
<div class="login-right flex">
<div class="login-right flex z-10">
<div class="login-right-warp flex-margin">
<span class="login-right-warp-one"></span>
<span class="login-right-warp-two"></span>
<div class="login-right-warp-mian">
<div class="login-right-warp-main-title">{{ getThemeConfig.globalTitle }} 欢迎您</div>
<div class="login-right-warp-main-title">{{ getSystemConfig['login.site_title'] ||
getThemeConfig.globalTitle }} 欢迎您</div>
<div class="login-right-warp-main-form">
<div v-if="!state.isScan">
<el-tabs v-model="state.tabsActiveName">
@@ -41,19 +43,26 @@
</div>
</div>
<div class="login-authorization">
<p>Copyright © {{getSystemConfig['login.copyright'] || '2021-2024 django-vue-admin.com'}} 版权所有</p>
<div class="login-authorization z-10">
<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">{{getSystemConfig['login.keep_record'] || '晋ICP备18005113号-3'}}</a>
<a href="https://beian.miit.gov.cn" target="_blank">{{ getSystemConfig['login.keep_record'] ||
'晋ICP备18005113号-3' }}</a>
|
<a :href="getSystemConfig['login.help_url']?getSystemConfig['login.help_url']:'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="getSystemConfig['login.privacy_url']?getSystemConfig['login.privacy_url']:'#'">隐私</a>
<a
:href="getSystemConfig['login.privacy_url'] ? getBaseURL(getSystemConfig['login.privacy_url']) : '#'">隐私</a>
|
<a :href="getSystemConfig['login.clause_url']?getSystemConfig['login.clause_url']:'#'">条款</a>
<a
:href="getSystemConfig['login.clause_url'] ? getBaseURL(getSystemConfig['login.clause_url']) : '#'">条款</a>
</p>
</div>
</div>
<div v-if="siteBg">
<img :src="siteBg" class="fixed inset-0 z-1 w-full h-full" />
</div>
</template>
<script setup lang="ts" name="loginIndex">
@@ -64,11 +73,13 @@ 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'
import { SystemConfigStore } from '/@/stores/systemConfig'
import { getBaseURL } from "/@/utils/baseUrl";
// 引入组件
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
import _ from "lodash";
// 定义变量内容
const storesThemeConfig = useThemeConfig();
@@ -84,11 +95,23 @@ const getThemeConfig = computed(() => {
});
const systemConfigStore = SystemConfigStore()
const {systemConfig} = storeToRefs(systemConfigStore)
const getSystemConfig = computed(()=>{
return systemConfig.value
const { systemConfig } = storeToRefs(systemConfigStore)
const getSystemConfig = computed(() => {
return systemConfig.value
})
const siteLogo = computed(() => {
if (!_.isEmpty(getSystemConfig.value['login.site_logo'])) {
return getSystemConfig.value['login.site_logo']
}
return logoMini
});
const siteBg = computed(() => {
if (!_.isEmpty(getSystemConfig.value['login.login_background'])) {
return getSystemConfig.value['login.login_background']
}
});
// 页面加载时
onMounted(() => {

View File

@@ -103,6 +103,9 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
label:'title',
value:'key'
}),
column:{
sortable: true,
},
form: {
rules: [
// 表单校验规则
@@ -113,6 +116,13 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
],
component: {
span: 12,
showSearch: true,
filterable: true,
//默认的filterOption仅支持value的过滤label并不会加入查询
//所以需要自定义filterOption
filterOption(inputValue, option) {
return option.label.indexOf(inputValue) >= 0 || option.value.indexOf(inputValue) >= 0;
}
},
},
},
@@ -143,6 +153,9 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
search: {
show: true,
},
column:{
sortable: true,
},
form: {
rules: [
// 表单校验规则
@@ -157,83 +170,6 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
},
},
},
// 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

@@ -24,7 +24,7 @@
/>
</el-form-item>
<el-form-item label="路由地址" prop="web_path">
<el-form-item label="路由地址" prop="web_path">
<el-input v-model="menuFormData.web_path" placeholder="请输入路由地址,请以/开头" />
</el-form-item>
@@ -56,6 +56,16 @@
<el-switch v-model="menuFormData.is_link" width="60" inline-prompt active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item required v-if="!menuFormData.is_catalog" label="是否固定">
<el-switch v-model="menuFormData.is_affix" width="60" inline-prompt active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" required label="是否内嵌">
<el-switch v-model="menuFormData.is_iframe" width="60" inline-prompt active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
@@ -81,8 +91,8 @@
<el-input v-model="menuFormData.component_name" placeholder="请输入组件名称" />
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="Url" prop="web_path">
<el-input v-model="menuFormData.web_path" placeholder="请输入Url" />
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="外链接" prop="link_url">
<el-input v-model="menuFormData.link_url" placeholder="请输入外链接地址" />
</el-form-item>
<el-form-item v-if="!menuFormData.is_catalog" label="缓存">
@@ -129,8 +139,7 @@ const defaultTreeProps: any = {
};
const validateWebPath = (rule: any, value: string, callback: Function) => {
let pattern = /^\/.*?/;
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
const reg = menuFormData.is_link ? patternUrl.test(value) : pattern.test(value);
const reg = pattern.test(value);
if (reg) {
callback();
} else {
@@ -138,6 +147,17 @@ const validateWebPath = (rule: any, value: string, callback: Function) => {
}
};
const validateLinkUrl = (rule: any, value: string, callback: Function) => {
let pattern = /^\/.*?/;
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
const reg = pattern.test(value) || patternUrl.test(value)
if (reg) {
callback();
} else {
callback(new Error('请输入正确的地址'));
}
};
const props = withDefaults(defineProps<IProps>(), {
initFormData: () => null,
treeData: () => [],
@@ -152,6 +172,7 @@ const rules = reactive<FormRules>({
name: [{ required: true, message: '菜单名称必填', trigger: 'blur' }],
component: [{ required: true, message: '请输入组件地址', trigger: 'blur' }],
component_name: [{ required: true, message: '请输入组件名称', trigger: 'blur' }],
link_url: [{ required: true, message: '请输入外链接地址',validator:validateLinkUrl, trigger: 'blur' }],
});
let deptDefaultList = ref<MenuTreeItemType[]>([]);
@@ -168,6 +189,9 @@ let menuFormData = reactive<MenuFormDataType>({
description: '',
is_catalog: false,
is_link: false,
is_iframe: false,
is_affix: false,
link_url:''
});
let menuBtnLoading = ref(false);
@@ -179,13 +203,16 @@ const setMenuFormData = () => {
menuFormData.component = props.initFormData?.component || '';
menuFormData.web_path = props.initFormData?.web_path || '';
menuFormData.icon = props.initFormData?.icon || '';
menuFormData.status = props.initFormData?.status || true;
menuFormData.visible = props.initFormData?.visible || true;
menuFormData.cache = props.initFormData?.cache || true;
menuFormData.status = !!props.initFormData.status;
menuFormData.visible = !!props.initFormData.visible;
menuFormData.cache = !!props.initFormData.cache;
menuFormData.component_name = props.initFormData?.component_name || '';
menuFormData.description = props.initFormData?.description || '';
menuFormData.is_catalog = props.initFormData?.is_catalog || false;
menuFormData.is_link = props.initFormData?.is_link || false;
menuFormData.is_catalog = !!props.initFormData.is_catalog;
menuFormData.is_link = !!props.initFormData.is_link;
menuFormData.is_iframe =!!props.initFormData.is_iframe;
menuFormData.is_affix =!!props.initFormData.is_affix;
menuFormData.link_url =props.initFormData.link_url;
}
};

View File

@@ -44,6 +44,9 @@ export interface MenuTreeItemType {
visible: boolean;
creator: string;
parent: number | string;
is_iframe:boolean;
is_affix:boolean;
link_url:string;
}
export interface MenuFormDataType {
@@ -60,4 +63,7 @@ export interface MenuFormDataType {
description: string;
is_catalog: boolean;
is_link: boolean;
}
is_iframe:boolean;
is_affix:boolean;
link_url: string;
}

View File

@@ -1,40 +1,45 @@
import * as api from './api';
import { dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import {dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions} from '@fast-crud/fast-crud';
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();
import {createCrudOptions as userCrudOptions } from "/@/views/system/user/crud";
import {request} from '/@/utils/service'
const {compute} = useCompute();
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
crudOptions: CrudOptions;
}
export const createCrudOptions = function ({ crudExpose, tabActivted }: { crudExpose: CrudExpose; tabActivted: any }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
if (tabActivted.value === 'receive') {
return await api.GetSelfReceive(query);
}
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
export const createCrudOptions = function ({
crudExpose,
tabActivted
}: { crudExpose: CrudExpose; tabActivted: any }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
if (tabActivted.value === 'receive') {
return await api.GetSelfReceive(query);
}
return await api.GetList(query);
};
const editRequest = async ({form, row}: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
return await api.AddObj(form);
};
const viewRequest = async ({ row }: { row: any }) => {
return await api.GetObj(row.id);
};
const viewRequest = async ({row}: { row: any }) => {
return await api.GetObj(row.id);
};
const IsReadFunc = computed(() => {
return tabActivted.value === 'receive';
});
const IsReadFunc = computed(() => {
return tabActivted.value === 'receive';
});
return {
@@ -292,6 +297,7 @@ export const createCrudOptions = function ({ crudExpose, tabActivted }: { crudEx
{
prop: 'name',
label: '部门名称',
width: 150,
},
{
prop: 'status_label',

View File

@@ -182,6 +182,7 @@ import { useRouter } from 'vue-router';
import { useUserInfo } from '/@/stores/userInfo';
import { successMessage } from '/@/utils/message';
import {dictionary} from "/@/utils/dictionary";
const router = useRouter();
// 头像裁剪组件
const avatarSelector = defineAsyncComponent(() => import('/@/components/avatarSelector/index.vue'));
@@ -333,6 +334,10 @@ const settingPassword = () => {
if (valid) {
api.UpdatePassword(userPasswordInfo).then((res: any) => {
ElMessage.success('密码修改成功');
setTimeout(() => {
Session.remove('token');
router.push('/login');
}, 1000);
});
} else {
// 校验失败

View File

@@ -6,7 +6,7 @@
<template #header>
<el-row>
<el-col :span="4">
<div>当前角色:
<div>当前授权角色:
<el-tag>{{ props.roleName }}</el-tag>
</div>
</el-col>
@@ -49,7 +49,7 @@
</el-checkbox>
</div>
<div class="pccm-item">
<div class="pccm-item" v-if="item.columns&&item.columns.length>0">
<p>对这些数据有以下字段权限</p>
<ul class="columns-list">

View File

@@ -1,251 +1,189 @@
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud';
import {CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute} from '@fast-crud/fast-crud';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { columnPermission } from '../../../utils/columnPermission';
import { successMessage } from '../../../utils/message';
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;
output: any;
crudOptions: CrudOptions;
}
//此处为crudOptions配置
export const createCrudOptions = function ({
crudExpose,
rolePermission,
handleDrawerOpen,
}: {
crudExpose: CrudExpose;
rolePermission: any;
handleDrawerOpen: Function;
crudExpose,
rolePermission,
handleDrawerOpen,
}: {
crudExpose: CrudExpose;
rolePermission: any;
handleDrawerOpen: Function;
}): CreateCrudOptionsTypes {
const pageRequest = async (query: any) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
const pageRequest = async (query: any) => {
return await api.GetList(query);
};
const editRequest = async ({form, row}: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
return await api.AddObj(form);
};
//权限判定
//权限判定
// @ts-ignore
// @ts-ignore
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('role:Create')
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 320,
buttons: {
view: {
show: true,
},
edit: {
show: auth('role:Update'),
},
remove: {
show: auth('role:Delete'),
},
permission: {
type: 'primary',
text: '权限配置',
show: auth('role:Permission'),
tooltip: {
placement: 'top',
content: '权限配置',
},
click: (context: any): void => {
const { row } = context;
handleDrawerOpen(row);
},
},
},
},
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, //禁止在列设置中选择
},
},
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: 'custom',
show: columnPermission('name', 'is_query'),
},
// addForm: {
// show: columnPermission('name', 'is_create'),
// },
editForm: {
show: columnPermission('name', 'is_update'),
},
form: {
rules: [{ required: true, message: '角色名称必填' }],
component: {
placeholder: '请输入角色名称',
},
},
},
key: {
title: '权限标识',
type: 'text',
search: { show: false },
column: {
minWidth: 120,
sortable: 'custom',
show: columnPermission('key', 'is_query'),
columnSetDisabled: true,
},
addForm: {
show: columnPermission('key', 'is_create'),
},
editForm: {
show: columnPermission('key', 'is_update'),
},
form: {
rules: [{ required: true, message: '权限标识必填' }],
component: {
placeholder: '输入权限标识',
},
},
},
sort: {
title: '排序',
search: { show: false },
type: 'number',
column: {
minWidth: 90,
sortable: 'custom',
},
addForm: {
show: columnPermission('sort', 'is_create'),
},
editForm: {
show: columnPermission('sort', 'is_update'),
},
form: {
rules: [{ required: true, message: '排序必填' }],
value: 1,
},
},
status: {
title: '状态',
search: { show: true },
type: 'dict-radio',
column: {
width: 100,
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);
});
};
}),
},
show: columnPermission('status', 'is_query'),
},
addForm: {
show: columnPermission('status', 'is_create'),
},
editForm: {
show: columnPermission('status', 'is_update'),
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
update_datetime: {
title: '更新时间',
type: 'text',
search: { show: false },
column: {
minWidth: 170,
sortable: 'custom',
show: columnPermission('update_datetime', 'is_query'),
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
create_datetime: {
title: '创建时间',
type: 'text',
search: { show: false },
column: {
sortable: 'custom',
minWidth: 170,
show: columnPermission('create_datetime', 'is_query'),
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
// description: {
// title: '备注',
// type: 'textarea',
// search: {show: false},
// form: {
// component: {
// maxlength: 200,
// placeholder: '输入备注',
// },
// },
// },
},
},
};
// @ts-ignore
// @ts-ignore
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: true
},
actionbar: {
buttons: {
add: {
show: auth('role:Create')
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 320,
buttons: {
view: {
show: true,
},
edit: {
show: auth('role:Update'),
},
remove: {
show: auth('role:Delete'),
},
permission: {
type: 'primary',
text: '权限配置',
show: auth('role:Permission'),
tooltip: {
placement: 'top',
content: '权限配置',
},
click: (context: any): void => {
const {row} = context;
handleDrawerOpen(row);
},
},
},
},
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, //禁止在列设置中选择
},
},
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: 'custom',
},
form: {
rules: [{required: true, message: '角色名称必填'}],
component: {
placeholder: '请输入角色名称',
},
},
},
key: {
title: '权限标识',
type: 'text',
search: {show: false},
column: {
minWidth: 120,
sortable: 'custom',
columnSetDisabled: true,
},
form: {
rules: [{required: true, message: '权限标识必填'}],
component: {
placeholder: '输入权限标识',
},
},
valueBuilder(context) {
const {row, key} = context
return row[key]
}
},
sort: {
title: '排序',
search: {show: false},
type: 'number',
column: {
minWidth: 90,
sortable: 'custom',
},
form: {
rules: [{required: true, message: '排序必填'}],
value: 1,
},
},
status: {
title: '状态',
search: {show: true},
type: 'dict-radio',
column: {
width: 100,
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

@@ -14,13 +14,13 @@
<script lang="ts" setup name="role">
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 PermissionComNew from './components/PermissionComNew/index.vue';
import _ from "lodash-es";
import {columnPermission} from "/@/utils/columnPermission";
import {handleColumnPermission} from "/@/utils/columnPermission";
let drawerVisible = ref(false);
let roleId = ref(null);
let roleName = ref(null);
@@ -32,12 +32,6 @@ const crudRef = ref();
const crudBinding = ref();
const fetchColumnPermission = async () => {
const res = await GetPermission();
useColumnPermission().setPermissionData(res.data);
console.log(3333,res)
};
const handleDrawerOpen = (row: any) => {
roleId.value = row.id;
roleName.value = row.name;
@@ -49,51 +43,25 @@ 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 });
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
// 页面打开后获取列表数据
onMounted( async () => {
await handlecolumnPermission(crudOptions)
// //合并新的crudOptions
// const newOptions = _.merge(crudOptions, {
// columns: {
// text: {
// title: "追加字段",
// type: "text"
// }
// }
// });
const newOptions = await handleColumnPermission(GetPermission,crudOptions)
//重置crudBinding
// resetCrudOptions(newOptions);
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
//resetCrudOptions(newOptions);
crudExpose.doRefresh();
});

View File

@@ -1,370 +1,388 @@
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 { auth } from '/@/utils/authFunction';
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';
import {SystemConfigStore} from "/@/stores/systemConfig";
import {storeToRefs} from "pinia";
import {computed} from "vue";
import { Md5 } from 'ts-md5';
import {commonCrudConfig} from "/@/utils/commonCrud";
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 exportRequest = async (query: UserPageQuery) => {
return await api.exportData(query)
}
const exportRequest = async (query: UserPageQuery) => {
return await api.exportData(query)
}
const systemConfigStore = SystemConfigStore()
const {systemConfig} = storeToRefs(systemConfigStore)
const getSystemConfig = computed(() => {
console.log(systemConfig.value)
return systemConfig.value
})
return {
crudOptions: {
table: {
remove: {
confirmMessage: '是否删除该用户?',
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('user:Create')
},
export:{
text:"导出",//按钮文字
title:"导出",//鼠标停留显示的信息
click(){
return exportRequest(crudExpose!.getSearchFormData())
}
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: auth('user:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: auth('user:Delete'),
},
custom: {
text: '重设密码',
type: 'text',
show: auth('user:ResetPassword'),
tooltip: {
placement: 'top',
content: '重设密码',
},
//@ts-ignore
click: (ctx: any) => {
const { row } = ctx;
},
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
username: {
title: '账号',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '账号必填项',
},
],
component: {
placeholder: '请输入账号',
},
},
},
password: {
title: '密码',
type: 'input',
column: {
show: false,
},
editForm: {
show: false,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '密码必填项',
},
],
component: {
span: 12,
showPassword: true,
placeholder: '请输入密码',
},
// value: vm.systemConfig('base.default_password'),
},
/* valueResolve(row, key) {
if (row.password) {
row.password = vm.$md5(row.password)
return {
crudOptions: {
table: {
remove: {
confirmMessage: '是否删除该用户?',
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
form: {
initialForm: {
password: computed(() => {
return systemConfig.value['base.default_password']
}),
}
},
actionbar: {
buttons: {
add: {
show: auth('user:Create')
},
export: {
text: "导出",//按钮文字
title: "导出",//鼠标停留显示的信息
click() {
return exportRequest(crudExpose!.getSearchFormData())
}
} */
},
name: {
title: '姓名',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '姓名必填项',
},
],
component: {
span: 12,
placeholder: '请输入姓名',
},
},
},
dept: {
title: '部门',
search: {
disabled: true,
},
type: 'dict-tree',
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name',
getData: async ({ url }: { url: string }) => {
return request({
url: url,
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 150, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
filterable: true,
placeholder: '请选择',
props: {
props: {
value: 'id',
label: 'name',
},
},
},
},
},
role: {
title: '角色',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
url: '/api/system/role/',
value: 'id',
label: 'name',
isTree: true,
getData: async ({ url }: { url: string }) => {
return request({
url: url,
params: {
page: 1,
limit: 10,
},
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
multiple: true,
filterable: true,
placeholder: '请选择角色',
},
},
},
mobile: {
title: '手机号码',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 120, //最小列宽
},
form: {
rules: [
{
max: 20,
message: '请输入正确的手机号码',
trigger: 'blur',
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码',
},
],
component: {
placeholder: '请输入手机号码',
},
},
},
email: {
title: '邮箱',
column: {
width: 260,
},
form: {
rules: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change'],
},
],
component: {
placeholder: '请输入邮箱',
},
},
},
gender: {
title: '性别',
type: 'dict-select',
dict: dict({
data: dictionary('gender'),
}),
form: {
value: 1,
component: {
span: 12,
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
user_type: {
title: '用户类型',
search: {
show: true,
},
type: 'dict-select',
dict: dict({
data: dictionary('user_type'),
}),
column: {
minWidth: 100, //最小列宽
},
form: {
show: false,
value: 0,
component: {
span: 12,
},
},
},
is_active: {
title: '锁定',
search: {
show: true,
},
type: 'dict-radio',
column: {
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'),
}),
},
avatar: {
title: '头像',
type: 'avatar-cropper',
form: {
show: false,
},
},
},
},
};
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: auth('user:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: auth('user:Delete'),
},
custom: {
text: '重设密码',
type: 'text',
show: auth('user:ResetPassword'),
tooltip: {
placement: 'top',
content: '重设密码',
},
//@ts-ignore
click: (ctx: any) => {
const {row} = ctx;
},
},
},
},
columns: {
_index: {
title: '序号',
form: {show: false},
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
username: {
title: '账号',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '账号必填项',
},
],
component: {
placeholder: '请输入账号',
},
},
},
password: {
title: '密码',
type: 'password',
column: {
show: false,
},
editForm: {
show: false,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '密码必填项',
},
],
component: {
span: 12,
showPassword: true,
placeholder: '请输入密码',
},
},
valueResolve({form}) {
if (form.password) {
form.password = Md5.hashStr(form.password)
}
}
},
name: {
title: '姓名',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '姓名必填项',
},
],
component: {
span: 12,
placeholder: '请输入姓名',
},
},
},
dept: {
title: '部门',
search: {
disabled: true,
},
type: 'dict-tree',
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name'
}),
column: {
minWidth: 150, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
filterable: true,
placeholder: '请选择',
props: {
checkStrictly:true,
props: {
value: 'id',
label: 'name',
},
},
},
},
},
role: {
title: '角色',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
url: '/api/system/role/',
value: 'id',
label: 'name',
}),
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
multiple: true,
filterable: true,
placeholder: '请选择角色',
},
},
},
mobile: {
title: '手机号码',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 120, //最小列宽
},
form: {
rules: [
{
max: 20,
message: '请输入正确的手机号码',
trigger: 'blur',
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码',
},
],
component: {
placeholder: '请输入手机号码',
},
},
},
email: {
title: '邮箱',
column: {
width: 260,
},
form: {
rules: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change'],
},
],
component: {
placeholder: '请输入邮箱',
},
},
},
gender: {
title: '性别',
type: 'dict-select',
dict: dict({
data: dictionary('gender'),
}),
form: {
value: 1,
component: {
span: 12,
},
},
component: {props: {color: 'auto'}}, // 自动染色
},
user_type: {
title: '用户类型',
search: {
show: true,
},
type: 'dict-select',
dict: dict({
data: dictionary('user_type'),
}),
column: {
minWidth: 100, //最小列宽
},
form: {
show: false,
value: 0,
component: {
span: 12,
},
},
},
is_active: {
title: '锁定',
search: {
show: true,
},
type: 'dict-radio',
column: {
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'),
}),
},
avatar: {
title: '头像',
type: 'avatar-cropper',
form: {
show: false,
},
column: {
minWidth: 400, //最小列宽
},
},
...commonCrudConfig({
dept_belong_id: {
form: true,
table: true
}
})
},
},
};
};

View File

@@ -1,59 +1,69 @@
<template>
<fs-page>
<el-row class="mx-2">
<el-col xs="24" :sm="8" :md="6" :lg="4" :xl="4" class="p-1">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">
部门列表
<el-tooltip effect="dark" :content="content" placement="right">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</p>
<el-input v-model="filterText" :placeholder="placeholder" />
<el-tree ref="treeRef" class="font-mono font-bold leading-6 text-7xl" :data="data" :props="treeProps"
:filter-node-method="filterNode" icon="ArrowRightBold" :indent="12" @node-click="onTreeNodeClick">
<template #default="{ node, data }">
<span class="text-center font-black font-normal">{{ node.label }}</span>
</template>
</el-tree>
</el-card>
</el-col>
<el-col xs="24" :sm="16" :md="18" :lg="20" :xl="20" class="p-1">
<el-card :body-style="{ height: '100%' }">
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-right>
<importExcel api="api/system/user/">导入 </importExcel>
</template>
</fs-crud>
</el-card>
</el-col>
</el-row>
<fs-page>
<el-row class="mx-2">
<el-col xs="24" :sm="8" :md="6" :lg="4" :xl="4" class="p-1">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">
部门列表
<el-tooltip effect="dark" :content="content" placement="right">
<el-icon>
<QuestionFilled/>
</el-icon>
</el-tooltip>
</p>
<el-input v-model="filterText" :placeholder="placeholder"/>
<el-tree ref="treeRef" class="font-mono font-bold leading-6 text-7xl" :data="data" :props="treeProps"
:filter-node-method="filterNode" icon="ArrowRightBold" :indent="38" highlight-current @node-click="onTreeNodeClick">
<template #default="{ node, data }">
<element-tree-line :node="node" :showLabelLine="false" :indent="32">
<span v-if="data.status" class="text-center font-black font-normal">
<SvgIcon name="iconfont icon-shouye" color="var(--el-color-primary)"/>&nbsp;{{ node.label }}
</span>
<span v-else color="var(--el-color-primary)"> <SvgIcon name="iconfont icon-shouye"/>&nbsp;{{
node.label
}} </span>
</element-tree-line>
</template>
</el-tree>
</el-card>
</el-col>
<el-col xs="24" :sm="16" :md="18" :lg="20" :xl="20" class="p-1">
<el-card :body-style="{ height: '100%' }">
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-right>
<importExcel api="api/system/user/">导入</importExcel>
</template>
</fs-crud>
</el-card>
</el-col>
</el-row>
</fs-page>
</fs-page>
</template>
<script lang="ts" setup name="user">
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import {useExpose, useCrud} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
import * as api from './api';
import { ElTree } from 'element-plus';
import { ref, onMounted, watch, toRaw } from 'vue';
import {ElTree} from 'element-plus';
import {ref, onMounted, watch, toRaw, h} from 'vue';
import XEUtils from 'xe-utils';
import {getElementLabelLine} from 'element-tree-line';
import importExcel from '/@/components/importExcel/index.vue'
const ElementTreeLine = getElementLabelLine(h);
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
code?: number;
data: [];
msg?: string;
}
// 引入组件
@@ -62,18 +72,18 @@ const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
icon: 'icon',
children: 'children',
label: 'name',
icon: 'icon',
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
let data = ref([]);
@@ -83,27 +93,27 @@ const content = `
`;
const getData = () => {
api.GetDept({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
api.GetDept({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
data.value = result;
});
};
//树形点击事件
const onTreeNodeClick = (node: any) => {
const { id } = node;
crudExpose.doSearch({ form: { dept: id } });
const {id} = node;
crudExpose.doSearch({form: {dept: id}});
};
// 页面打开后获取列表数据
onMounted(() => {
getData();
getData();
});
// crud组件的ref
@@ -111,32 +121,32 @@ const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
const {crudExpose} = useExpose({crudRef, crudBinding});
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
const {crudOptions} = createCrudOptions({crudExpose});
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
const {resetCrudOptions} = useCrud({crudExpose, crudOptions});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
crudExpose.doRefresh();
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
height: 100%;
.el-col {
height: 100%;
}
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
height: 100%;
}
.font-normal {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
}
</style>