Compare commits
372 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82a16545b4 | ||
|
|
39638e2e6a | ||
|
|
d3e5f258e5 | ||
|
|
77a27cba14 | ||
|
|
26bbf67da8 | ||
|
|
d4f754976e | ||
|
|
85805e0d4b | ||
|
|
8a646e5ef7 | ||
|
|
71eec9cfbd | ||
|
|
9bd79b9dc6 | ||
|
|
e210b7dd17 | ||
|
|
4d00661163 | ||
|
|
6798fca362 | ||
|
|
6f5bbb045d | ||
|
|
bbf3018b20 | ||
|
|
5510a18280 | ||
|
|
97737c3ef1 | ||
|
|
0b554f3669 | ||
|
|
63453450ad | ||
|
|
fea6ebd98e | ||
|
|
d4e43e28e8 | ||
|
|
9973688675 | ||
|
|
98a5bbd60a | ||
|
|
ac7e8a7764 | ||
|
|
26ed59d1ff | ||
|
|
369157fa4f | ||
|
|
8961733025 | ||
|
|
577b88332f | ||
|
|
411f065bfc | ||
|
|
8d37d929b2 | ||
|
|
c0d6cd54c3 | ||
|
|
ae8366f097 | ||
|
|
4baf5da0ff | ||
|
|
c00f5f2beb | ||
|
|
5143afdd85 | ||
|
|
b4153d1848 | ||
|
|
d2c0c54080 | ||
|
|
7f184b2a9a | ||
|
|
87f9784445 | ||
|
|
e900cc2280 | ||
|
|
4a26257a61 | ||
|
|
06476c182e | ||
|
|
6146e6edb6 | ||
|
|
85f5ff1935 | ||
|
|
86e33e44e1 | ||
|
|
217f3c8e14 | ||
|
|
d0feaa23e5 | ||
|
|
fa67a27908 | ||
|
|
56145dfe61 | ||
|
|
3def205e6e | ||
|
|
873400bcce | ||
|
|
0c1cf1218b | ||
|
|
d8e5ad9057 | ||
|
|
3639cad58c | ||
|
|
c99eb1a9bc | ||
|
|
7ae15022c0 | ||
|
|
68b3d479ca | ||
|
|
2826cafa75 | ||
|
|
ba4a580bb0 | ||
|
|
f322d38ab8 | ||
|
|
7d315a7d28 | ||
|
|
916071814a | ||
|
|
acf113e5e6 | ||
|
|
083eef9df1 | ||
|
|
e8c64d6d11 | ||
|
|
d1abc58b40 | ||
|
|
0c8ce0ce27 | ||
|
|
23d320434e | ||
|
|
14907e140d | ||
|
|
1132e04080 | ||
|
|
39fee0875d | ||
|
|
5898ae85de | ||
|
|
172a867bad | ||
|
|
5a308695fd | ||
|
|
0c5c3c4ecd | ||
|
|
9a8241023b | ||
|
|
27be35616b | ||
|
|
e6f0a5e0ea | ||
|
|
cbebd8305c | ||
|
|
66b68523f5 | ||
|
|
95b14e5a42 | ||
|
|
583b172a37 | ||
|
|
2227564c6f | ||
|
|
5e3ecfc0a6 | ||
|
|
d467915f97 | ||
|
|
197714e845 | ||
|
|
bc15ed15ca | ||
|
|
349986f14f | ||
|
|
fb4bba9000 | ||
|
|
e0d46871c4 | ||
|
|
7691481006 | ||
|
|
120c737de7 | ||
|
|
5729aeb521 | ||
|
|
205cfcca2e | ||
|
|
0fdc108ebf | ||
|
|
064cbee8a2 | ||
|
|
a2d4d6e07e | ||
|
|
d721405ee4 | ||
|
|
0650c95745 | ||
|
|
f291885d57 | ||
|
|
4b77fb903c | ||
|
|
fca82c093a | ||
|
|
49300fb17e | ||
|
|
c763333024 | ||
|
|
42e6c7b600 | ||
|
|
6172dea399 | ||
|
|
2baba8e36f | ||
|
|
0131d31808 | ||
|
|
dd2dcd4ad1 | ||
|
|
fb8b0a5ac6 | ||
|
|
6045312f7e | ||
|
|
4641d9c774 | ||
|
|
c04b33ed31 | ||
|
|
0286ca003f | ||
|
|
54e4d23cf7 | ||
|
|
645f43c887 | ||
|
|
4ac8ed7627 | ||
|
|
94ad6b1bae | ||
|
|
96748da99d | ||
|
|
b74a196c94 | ||
|
|
dd98b98775 | ||
|
|
8e107d9944 | ||
|
|
7159253c6b | ||
|
|
075d457dc7 | ||
|
|
564e9a112b | ||
|
|
3b84773a7d | ||
|
|
996d644d9b | ||
|
|
97f7bfd3f0 | ||
|
|
d1fdbd7e51 | ||
|
|
802ee7f50e | ||
|
|
ddf9fcd8e8 | ||
|
|
6f15c2f669 | ||
|
|
084d1c0c3e | ||
|
|
6ded198206 | ||
|
|
e549d33a48 | ||
|
|
cbba5a60f4 | ||
|
|
773b56a5e6 | ||
|
|
fb29929f55 | ||
|
|
0772a605bf | ||
|
|
496c453615 | ||
|
|
58b59c2ed0 | ||
|
|
94815f4b20 | ||
|
|
ad6ec5ca58 | ||
|
|
4bcaf8d45f | ||
|
|
be5db33a55 | ||
|
|
cfc5f8597a | ||
|
|
209e2ecced | ||
|
|
4b948fe5c3 | ||
|
|
ea335922b2 | ||
|
|
d290836f4f | ||
|
|
9a426dbd72 | ||
|
|
a0b6e57a3c | ||
|
|
5a89cdf889 | ||
|
|
b6b970f7a2 | ||
|
|
2722631276 | ||
|
|
478aea32f1 | ||
|
|
cf55897552 | ||
|
|
8020da03e2 | ||
|
|
c646563d26 | ||
|
|
43a4572a71 | ||
|
|
1582353279 | ||
|
|
1df2101658 | ||
|
|
0679198d0f | ||
|
|
e3c478a263 | ||
|
|
07cf81c9fe | ||
|
|
1b98fbc982 | ||
|
|
85040c808f | ||
|
|
00b2cac7a5 | ||
|
|
7c27f95353 | ||
|
|
d59df6db7c | ||
|
|
34c55cfd7e | ||
|
|
191e57dcd9 | ||
|
|
9be50863d9 | ||
|
|
9c218a856c | ||
|
|
b4ebb14843 | ||
|
|
17d80b0b00 | ||
|
|
01394c6904 | ||
|
|
59b1b54da1 | ||
|
|
8084ca7b0b | ||
|
|
427b6f8478 | ||
|
|
41a9888441 | ||
|
|
5139d37007 | ||
|
|
44bceb6e22 | ||
|
|
c2f8c43905 | ||
|
|
9f479e476d | ||
|
|
f5a7b157f9 | ||
|
|
7110d10432 | ||
|
|
05f4e2970d | ||
|
|
5353a27187 | ||
|
|
f4ff5d87af | ||
|
|
ceac44813f | ||
|
|
a490ced370 | ||
|
|
20188261c5 | ||
|
|
15ef01aed9 | ||
|
|
1dea6ed29c | ||
|
|
2bad36c982 | ||
|
|
941dbbc783 | ||
|
|
14881707e2 | ||
|
|
d60c795ea2 | ||
|
|
9cf5d13f1c | ||
|
|
e1618359d7 | ||
|
|
abe7972ab4 | ||
|
|
0709e72ae1 | ||
|
|
2e676d64bb | ||
|
|
98e974b55e | ||
|
|
e7f78fbae0 | ||
|
|
111edb3f4a | ||
|
|
86f539a193 | ||
|
|
3b9dc7c171 | ||
|
|
8175c620ed | ||
|
|
de5884d28d | ||
|
|
e63a7cab6f | ||
|
|
cdfdf0a483 | ||
|
|
515f720a59 | ||
|
|
5524446dbc | ||
|
|
9c32c7f60b | ||
|
|
6eed2814f8 | ||
|
|
f03e61ce63 | ||
|
|
5450102275 | ||
|
|
689ce4b43b | ||
|
|
65af240757 | ||
|
|
f61025c6db | ||
|
|
516f0ac675 | ||
|
|
b1b5ae5fcd | ||
|
|
9a847d078f | ||
|
|
dcac5c759a | ||
|
|
13999142aa | ||
|
|
cd6318c6fd | ||
|
|
3c58bcf467 | ||
|
|
b9bdf878de | ||
|
|
db989671c0 | ||
|
|
f67869d428 | ||
|
|
78c693718b | ||
|
|
653483e5de | ||
|
|
89ba768932 | ||
|
|
e4326b6d4e | ||
|
|
97020ba4e8 | ||
|
|
945db3344c | ||
|
|
19df23ce5f | ||
|
|
97c76b6afe | ||
|
|
6597826fdd | ||
|
|
ee419e282e | ||
|
|
3d12d7a5c5 | ||
|
|
b2aa22ce84 | ||
|
|
06d7d718e1 | ||
|
|
6067dfac2f | ||
|
|
637e45ef83 | ||
|
|
202f67460e | ||
|
|
7da23ab6a6 | ||
|
|
be51124bb6 | ||
|
|
7e7637f10b | ||
|
|
dbb94fb9ff | ||
|
|
fea61e35ef | ||
|
|
8ecfc1143b | ||
|
|
7c72cc74df | ||
|
|
12f5125207 | ||
|
|
770c5f73c0 | ||
|
|
98b81a789d | ||
|
|
c13a5f0dda | ||
|
|
a524137e18 | ||
|
|
2c9d8766e3 | ||
|
|
8a0f18eab1 | ||
|
|
d8511eeb7b | ||
|
|
52a91f1703 | ||
|
|
14395621c0 | ||
|
|
c4e2451ac8 | ||
|
|
936928e854 | ||
|
|
619ebeacf1 | ||
|
|
b95679a986 | ||
|
|
8e930c6a10 | ||
|
|
d2811a1f87 | ||
|
|
7ac5c86ed3 | ||
|
|
eea5cdadfe | ||
|
|
4e5f9ce946 | ||
|
|
e790882ab6 | ||
|
|
0f86f5c6fc | ||
|
|
4018113161 | ||
|
|
252b6a8328 | ||
|
|
c26abef364 | ||
|
|
a40dc9afb1 | ||
|
|
9114c6bf81 | ||
|
|
7b0a6bc2be | ||
|
|
e4ca462153 | ||
|
|
840323ff07 | ||
|
|
ed0dca7346 | ||
|
|
2b63966ee2 | ||
|
|
55a2f73420 | ||
|
|
7ccbcebe77 | ||
|
|
8f57a240db | ||
|
|
1ccfd619f3 | ||
|
|
dcdc7bbe3b | ||
|
|
90557ea039 | ||
|
|
a462e15643 | ||
|
|
6a24683769 | ||
|
|
9fa1775b77 | ||
|
|
16605779ed | ||
|
|
186658858d | ||
|
|
996cb1447d | ||
|
|
e8b84ad71f | ||
|
|
e7065ab89e | ||
|
|
e962ca6559 | ||
|
|
4bdc256311 | ||
|
|
f7b94e496c | ||
|
|
8c0a04ce61 | ||
|
|
31f20a766f | ||
|
|
65a34a683c | ||
|
|
66effde1d9 | ||
|
|
f4f958d2e9 | ||
|
|
d10127eafc | ||
|
|
60035767ad | ||
|
|
adc522c180 | ||
|
|
17b67e43c9 | ||
|
|
3cccfaa7f4 | ||
|
|
ce4d410e53 | ||
|
|
7b0341b527 | ||
|
|
31f7175186 | ||
|
|
2c22ecbac5 | ||
|
|
ad59bc68aa | ||
|
|
8d7d775164 | ||
|
|
9c2dea9db5 | ||
|
|
8c31bda3da | ||
|
|
7f7d88fe5b | ||
|
|
19e18a7b6f | ||
|
|
cd6cd775e7 | ||
|
|
111ca9554c | ||
|
|
c9fddbd4a7 | ||
|
|
d3b057f75a | ||
|
|
096f5919af | ||
|
|
25cc2c83c1 | ||
|
|
054bcf3eea | ||
|
|
9c24e61123 | ||
|
|
cc36ff3d8f | ||
|
|
93e74dbaa2 | ||
|
|
5ab3ad4ed9 | ||
|
|
9468abd4a2 | ||
|
|
1080af6413 | ||
|
|
c4c9a81ac8 | ||
|
|
d1223dddd3 | ||
|
|
3dd68e5d21 | ||
|
|
0668cb087f | ||
|
|
38494edea5 | ||
|
|
2f04f22904 | ||
|
|
0ccf2e3725 | ||
|
|
68cd42246d | ||
|
|
dc6ddda4ab | ||
|
|
08b56ee663 | ||
|
|
82d7193dcb | ||
|
|
4b609ed7e4 | ||
|
|
61066b8caa | ||
|
|
160c376f81 | ||
|
|
b96c220f1d | ||
|
|
42884685ff | ||
|
|
58e611cb22 | ||
|
|
0b2fa1e92c | ||
|
|
125dfd4dd5 | ||
|
|
af7bfcd215 | ||
|
|
6917786864 | ||
|
|
cd60b3d9be | ||
|
|
cfae363f53 | ||
|
|
4f8686b35e | ||
|
|
1407a25b6a | ||
|
|
7c78c7e53f | ||
|
|
f1da0507dd | ||
|
|
df74c792f6 | ||
|
|
4c697ef9d4 | ||
|
|
ab1d1c87e2 | ||
|
|
b6b3b4a39a | ||
|
|
581e4cd92f | ||
|
|
fb76d4271b | ||
|
|
4f176cf88a | ||
|
|
af5f3b7137 | ||
|
|
b03037b866 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
|
||||
.history/
|
||||
.vscode/
|
||||
web/package-lock.json
|
||||
10
CHANGELOG.md
Normal file
10
CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Django-Vue3-Admin 更新日志
|
||||
|
||||
## 正式发布v3.0.0版本
|
||||
### 1.新增:列权限管理与授权;
|
||||
### 2.新增:代码新版本发布后,进行升级提醒;
|
||||
### 3.优化:角色管理中按钮权限的操作;
|
||||
### 4.优化:websocket 连接状态显示;
|
||||
### 5.优化:初始化获取系统配置与字典配置,进行动态渲染登录页面;
|
||||
### 6.修复:登录页面中系统配置不生效问题;
|
||||
### 7.其他优化
|
||||
241
README.md
241
README.md
@@ -1,196 +1,175 @@
|
||||
# Django-Vue3-Admin
|
||||
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](https://gitee.com/liqianglog/django-vue-admin)
|
||||
[](https://gitee.com/huge-dream/django-vue3-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](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)
|
||||
|
||||
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
|
||||
* demo account:superadmin
|
||||
|
||||
* demo password:admin123456
|
||||
|
||||
👩👦👦docs:[https://django-vue-admin.com](https://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 授权。
|
||||
## 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:
|
||||
|
||||
👩👧👦演示地址:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com)
|
||||
gitee(Main push):[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
- 账号:superadmin
|
||||
github:[https://github.com/huge-dream/django-vue3-admin](https://github.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
- 密码:admin123456
|
||||
## core function
|
||||
|
||||
👩👦👦文档地址:[https://django-vue-admin.com](https://django-vue-admin.com)
|
||||
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
|
||||
|
||||
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩👦👦
|
||||
## before start project you need:
|
||||
|
||||
- 插件市场:[戳我](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)
|
||||
|
||||
- 二维码
|
||||
|
||||
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
|
||||
|
||||
## 源码地址
|
||||
|
||||
gitee地址(主推):[https://gitee.com/liqianglog/django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)👩👦👦
|
||||
|
||||
github地址:[https://github.com/liqianglog/django-vue-admin](https://github.com/liqianglog/django-vue-admin)👩👦👦
|
||||
|
||||
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 👨⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
|
||||
2. 🧑⚕️部门管理:配置系统组织机构(公司、部门、角色)。
|
||||
3. 👩⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
|
||||
4. 🧑🎓权限权限:授权角色的权限范围。
|
||||
5. 👨🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
6. 👬接口白名单:配置不需要进行权限校验的接口。
|
||||
7. 🧑🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
8. 🧑🔧地区管理:对省市县区域进行管理。
|
||||
9. 📁附件管理:对平台上所有文件、图片等进行统一管理。
|
||||
10. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
11. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。
|
||||
|
||||
## 插件市场 🔌
|
||||
|
||||
- Celery异步任务:[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
|
||||
- 升级中心后端:[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
|
||||
- 升级中心前端:[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
|
||||
|
||||
## 准备工作
|
||||
~~~
|
||||
Python >= 3.8.0 (推荐3.8+版本)
|
||||
nodejs >= 14.0 (推荐最新)
|
||||
Mysql >= 5.7.0 (可选,默认数据库sqlite3,推荐8.0版本)
|
||||
Redis(可选,最新版)
|
||||
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
|
||||
# 克隆项目
|
||||
git clone https://gitee.com/liqianglog/django-vue-admin.git
|
||||
# clone code
|
||||
git clone https://gitee.com/huge-dream/django-vue3-admin.git
|
||||
|
||||
# 进入项目目录
|
||||
# enter code dir
|
||||
cd web
|
||||
|
||||
# 安装依赖
|
||||
npm install --registry=https://registry.npm.taobao.org
|
||||
# install dependence
|
||||
npm install yarn
|
||||
yarn install --registry=https://registry.npm.taobao.org
|
||||
|
||||
# 启动服务
|
||||
npm run dev
|
||||
# 浏览器访问 http://localhost:8080
|
||||
# .env.development 文件中可配置启动端口等参数
|
||||
# 构建生产环境
|
||||
# npm run build
|
||||
# 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. 安装依赖环境
|
||||
1. enter code dir cd backend
|
||||
2. copy ./conf/env.example.py to ./conf dir,rename 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. 执行迁移命令:
|
||||
5. Execute the migration command:
|
||||
python3 manage.py makemigrations
|
||||
python3 manage.py migrate
|
||||
6. 初始化数据
|
||||
6. Initialization data
|
||||
python3 manage.py init
|
||||
7. 初始化省市县数据:
|
||||
7. Initialize provincial, municipal and county data:
|
||||
python3 manage.py init_area
|
||||
8. 启动项目
|
||||
8. start backend
|
||||
python3 manage.py runserver 0.0.0.0:8000
|
||||
或使用 daphne :
|
||||
daphne -b 0.0.0.0 -p 8000 application.asgi:application
|
||||
or uvicorn :
|
||||
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 8
|
||||
~~~
|
||||
|
||||
### 访问项目
|
||||
### visit backend swagger
|
||||
|
||||
- 访问地址:[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
|
||||
# 初始化后端数据(第一次执行即可)
|
||||
docker exec -ti dvadmin-django bash
|
||||
# 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
|
||||
|
||||
前端地址:http://127.0.0.1:8080
|
||||
后端地址:http://127.0.0.1:8080/api
|
||||
# 在服务器上请把127.0.0.1 换成自己公网ip
|
||||
账号:superadmin 密码:admin123456
|
||||
frontend url:http://127.0.0.1:8080
|
||||
backend url:http://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✅
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
214
README.zh.md
Normal file
214
README.zh.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Django-Vue3-Admin
|
||||
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](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
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
## 演示图✅
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@@ -91,10 +91,11 @@ ENV/
|
||||
**/migrations/*.py
|
||||
!**/migrations/__init__.py
|
||||
*.pyc
|
||||
conf/
|
||||
conf/*
|
||||
!conf/env.example.py
|
||||
db.sqlite3
|
||||
media/
|
||||
__pypackages__/
|
||||
package-lock.json
|
||||
gunicorn.pid
|
||||
!plugins/__init__.py
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,5 @@ from django.urls import path
|
||||
from application.websocketConfig import MegCenter
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws/<str:service_uid>/', MegCenter.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者
|
||||
path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者
|
||||
]
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
@@ -42,7 +43,8 @@ sys.path.insert(0, os.path.join(PLUGINS_PATH))
|
||||
DEBUG = locals().get("DEBUG", True)
|
||||
ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"])
|
||||
|
||||
# Application definition
|
||||
# 列权限需要排除的App应用
|
||||
COLUMN_EXCLUDE_APPS = ['channels', 'captcha'] + locals().get("COLUMN_EXCLUDE_APPS", [])
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
@@ -54,13 +56,14 @@ INSTALLED_APPS = [
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"corsheaders", # 注册跨域app
|
||||
"dvadmin.system",
|
||||
"drf_yasg",
|
||||
"captcha",
|
||||
'channels',
|
||||
"channels",
|
||||
"dvadmin.system",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"dvadmin.utils.middleware.HealthCheckMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
@@ -152,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')
|
||||
@@ -165,9 +173,9 @@ CORS_ORIGIN_ALLOW_ALL = True
|
||||
# 允许cookie
|
||||
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作
|
||||
|
||||
# ================================================= #
|
||||
# ===================================================== #
|
||||
# ********************* channels配置 ******************* #
|
||||
# ================================================= #
|
||||
# ===================================================== #
|
||||
ASGI_APPLICATION = 'application.asgi.application'
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
@@ -187,69 +195,83 @@ CHANNEL_LAYERS = {
|
||||
# ================================================= #
|
||||
# ********************* 日志配置 ******************* #
|
||||
# ================================================= #
|
||||
|
||||
# log 配置部分BEGIN #
|
||||
# # log 配置部分BEGIN #
|
||||
SERVER_LOGS_FILE = os.path.join(BASE_DIR, "logs", "server.log")
|
||||
ERROR_LOGS_FILE = os.path.join(BASE_DIR, "logs", "error.log")
|
||||
LOGS_FILE = os.path.join(BASE_DIR, "logs")
|
||||
if not os.path.exists(os.path.join(BASE_DIR, "logs")):
|
||||
os.makedirs(os.path.join(BASE_DIR, "logs"))
|
||||
|
||||
# 格式:[2020-04-22 23:33:01][micoservice.apps.ready():16] [INFO] 这是一条日志:
|
||||
# 格式:[日期][模块.函数名称():行号] [级别] 信息
|
||||
STANDARD_LOG_FORMAT = (
|
||||
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
|
||||
)
|
||||
CONSOLE_LOG_FORMAT = (
|
||||
"[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s"
|
||||
)
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"servers": {
|
||||
"format": "%(message)s",
|
||||
"standard": {"format": STANDARD_LOG_FORMAT},
|
||||
"console": {
|
||||
"format": CONSOLE_LOG_FORMAT,
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"format": CONSOLE_LOG_FORMAT,
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"servers": {
|
||||
"logging_levels": ["info", "error", "warning"],
|
||||
"class": "dvadmin.utils.log.InterceptTimedRotatingFileHandler", # 这个路径看你本地放在哪里(下面的log文件)
|
||||
"filename": os.path.join(LOGS_FILE, "server.log"),
|
||||
"when": "D",
|
||||
"interval": 1,
|
||||
"file": {
|
||||
"level": "INFO",
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": SERVER_LOGS_FILE,
|
||||
"maxBytes": 1024 * 1024 * 100, # 100 MB
|
||||
"backupCount": 1,
|
||||
"formatter": "servers",
|
||||
"backupCount": 5, # 最多备份5个
|
||||
"formatter": "standard",
|
||||
"encoding": "utf-8",
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"level": "ERROR",
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": ERROR_LOGS_FILE,
|
||||
"maxBytes": 1024 * 1024 * 100, # 100 MB
|
||||
"backupCount": 3, # 最多备份3个
|
||||
"formatter": "standard",
|
||||
"encoding": "utf-8",
|
||||
},
|
||||
"console": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "console",
|
||||
},
|
||||
|
||||
},
|
||||
"loggers": {
|
||||
# default日志
|
||||
'django': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "INFO"
|
||||
"": {
|
||||
"handlers": ["console", "error", "file"],
|
||||
"level": "INFO",
|
||||
},
|
||||
'': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "ERROR"
|
||||
},
|
||||
'celery': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "INFO"
|
||||
"django": {
|
||||
"handlers": ["console", "error", "file"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
'django.db.backends': {
|
||||
'handlers': ['servers'],
|
||||
'handlers': ["console", "error", "file"],
|
||||
'propagate': False,
|
||||
'level': "INFO"
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['servers'],
|
||||
'propagate': False,
|
||||
'level': "DEBUG"
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"level": "INFO",
|
||||
"handlers": ["servers"],
|
||||
"handlers": ["console", "error", "file"],
|
||||
},
|
||||
"uvicorn.access": {
|
||||
"handlers": ["servers"],
|
||||
"level": "INFO",
|
||||
"propagate": False
|
||||
"handlers": ["console", "error", "file"],
|
||||
"level": "INFO"
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -289,11 +311,9 @@ AUTHENTICATION_BACKENDS = ["dvadmin.utils.backends.CustomBackend"]
|
||||
# ================================================= #
|
||||
# ****************** simplejwt配置 ***************** #
|
||||
# ================================================= #
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
# token有效时长
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=120),
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=1440),
|
||||
# token刷新后的有效时间
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||
# 设置前缀
|
||||
@@ -329,11 +349,11 @@ SWAGGER_SETTINGS = {
|
||||
# ================================================= #
|
||||
# **************** 验证码配置 ******************* #
|
||||
# ================================================= #
|
||||
CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小
|
||||
CAPTCHA_IMAGE_SIZE = (160, 46) # 设置 captcha 图片大小
|
||||
CAPTCHA_LENGTH = 4 # 字符个数
|
||||
CAPTCHA_TIMEOUT = 1 # 超时(minutes)
|
||||
CAPTCHA_OUTPUT_FORMAT = "%(image)s %(text_field)s %(hidden_field)s "
|
||||
CAPTCHA_FONT_SIZE = 40 # 字体大小
|
||||
CAPTCHA_FONT_SIZE = 36 # 字体大小
|
||||
CAPTCHA_FOREGROUND_COLOR = "#64DAAA" # 前景色
|
||||
CAPTCHA_BACKGROUND_COLOR = "#F5F7F4" # 背景色
|
||||
CAPTCHA_NOISE_FUNCTIONS = (
|
||||
@@ -388,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 *
|
||||
# ...
|
||||
# ********** 一键导入插件配置结束 **********
|
||||
|
||||
@@ -30,6 +30,7 @@ from dvadmin.system.views.login import (
|
||||
CaptchaView,
|
||||
ApiLogin,
|
||||
LogoutView,
|
||||
LoginTokenView
|
||||
)
|
||||
from dvadmin.system.views.system_config import InitSettingsViewSet
|
||||
from dvadmin.utils.swagger import CustomOpenAPISchemaGenerator
|
||||
@@ -81,6 +82,9 @@ urlpatterns = (
|
||||
path("api/init/dictionary/", InitDictionaryViewSet.as_view()),
|
||||
path("api/init/settings/", InitSettingsViewSet.as_view()),
|
||||
path("apiLogin/", ApiLogin.as_view()),
|
||||
|
||||
# 仅用于开发,上线需关闭
|
||||
path("api/token/", LoginTokenView.as_view()),
|
||||
]
|
||||
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL)
|
||||
|
||||
@@ -73,7 +73,7 @@ class DvadminWebSocket(AsyncJsonWebsocketConsumer):
|
||||
unread_count = await _get_message_unread(self.user_id)
|
||||
if unread_count == 0:
|
||||
# 发送连接成功
|
||||
await self.send_json(set_message('system', 'SYSTEM', '连接成功'))
|
||||
await self.send_json(set_message('system', 'SYSTEM', '您已上线'))
|
||||
else:
|
||||
await self.send_json(
|
||||
set_message('system', 'SYSTEM', "请查看您的未读消息~",
|
||||
@@ -122,7 +122,8 @@ class MessageCreateSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
def websocket_push(user_id,message):
|
||||
|
||||
def websocket_push(user_id, message):
|
||||
username = "user_" + str(user_id)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
@@ -133,8 +134,9 @@ def websocket_push(user_id,message):
|
||||
}
|
||||
)
|
||||
|
||||
def create_message_push(title: str, content: str, target_type: int=0, target_user: list=[], target_dept=None, target_role=None,
|
||||
message: dict = {'contentType': 'INFO', 'content': '测试~'}, request= Request):
|
||||
|
||||
def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None,
|
||||
target_role=None, message: dict = None, request=Request):
|
||||
if message is None:
|
||||
message = {"contentType": "INFO", "content": None}
|
||||
if target_role is None:
|
||||
@@ -145,11 +147,11 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use
|
||||
"title": title,
|
||||
"content": content,
|
||||
"target_type": target_type,
|
||||
"target_user":target_user,
|
||||
"target_dept":target_dept,
|
||||
"target_role":target_role
|
||||
"target_user": target_user,
|
||||
"target_dept": target_dept,
|
||||
"target_role": target_role
|
||||
}
|
||||
message_center_instance = MessageCreateSerializer(data=data,request=request)
|
||||
message_center_instance = MessageCreateSerializer(data=data, request=request)
|
||||
message_center_instance.is_valid(raise_exception=True)
|
||||
message_center_instance.save()
|
||||
users = target_user or []
|
||||
@@ -176,6 +178,6 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use
|
||||
username,
|
||||
{
|
||||
"type": "push.message",
|
||||
"json": {**message,'unread':unread_count}
|
||||
"json": {**message, 'unread': unread_count}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
# ================================================= #
|
||||
# ****************** 功能 启停 ******************* #
|
||||
# ================================================= #
|
||||
@@ -44,6 +44,5 @@ LOGIN_NO_CAPTCHA_AUTH = True
|
||||
# ================================================= #
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# daphne启动命令
|
||||
#daphne application.asgi:application -b 0.0.0.0 -p 8000
|
||||
# 列权限中排除App应用
|
||||
COLUMN_EXCLUDE_APPS = []
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
exclude = ["venv"] # 需要排除的文件目录
|
||||
for root, dirs, files in os.walk('.'):
|
||||
dirs[:] = [d for d in set(dirs) - set(exclude)]
|
||||
dirs[:] = list(set(dirs) - set(exclude))
|
||||
if 'migrations' in dirs:
|
||||
dir = dirs[dirs.index('migrations')]
|
||||
for root_j, dirs_j, files_j in os.walk(os.path.join(root, dir)):
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# python manage.py makemigrations
|
||||
# python manage.py migrate
|
||||
# python manage.py init -y
|
||||
gunicorn -c gunicorn_conf.py application.asgi:application
|
||||
uvicorn application.asgi:application --port 8000 --host 0.0.0.0 --workers 4
|
||||
|
||||
@@ -5,9 +5,13 @@ 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, ApiWhiteList, Dictionary, SystemConfig, \
|
||||
RoleMenuPermission, RoleMenuButtonPermission
|
||||
from dvadmin.system.models import (
|
||||
Role, Dept, Users, Menu, MenuButton,
|
||||
ApiWhiteList, Dictionary, SystemConfig,
|
||||
RoleMenuPermission, RoleMenuButtonPermission, MenuField
|
||||
)
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
|
||||
|
||||
@@ -50,6 +54,16 @@ class MenuButtonInitSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class MenuFieldInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化列权限-序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = MenuField
|
||||
fields = ['id', 'menu', 'field_name', 'title', 'model']
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class MenuInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
@@ -58,6 +72,7 @@ class MenuInitSerializer(CustomModelSerializer):
|
||||
name = serializers.CharField(required=False)
|
||||
children = serializers.SerializerMethodField()
|
||||
menu_button = serializers.SerializerMethodField()
|
||||
menu_field = serializers.SerializerMethodField()
|
||||
|
||||
def get_children(self, obj: Menu):
|
||||
data = []
|
||||
@@ -74,10 +89,18 @@ class MenuInitSerializer(CustomModelSerializer):
|
||||
data = list(instance.values('name', 'value', 'api', 'method'))
|
||||
return data
|
||||
|
||||
def get_menu_field(self, obj: Menu):
|
||||
data = []
|
||||
instance = obj.menufield_set.order_by('field_name')
|
||||
if instance:
|
||||
data = list(instance.values('field_name', 'title', 'model'))
|
||||
return data
|
||||
|
||||
def save(self, **kwargs):
|
||||
instance = super().save(**kwargs)
|
||||
children = self.initial_data.get('children')
|
||||
menu_button = self.initial_data.get('menu_button')
|
||||
menu_field = self.initial_data.get('menu_field')
|
||||
# 菜单表
|
||||
if children:
|
||||
for menu_data in children:
|
||||
@@ -106,12 +129,25 @@ class MenuInitSerializer(CustomModelSerializer):
|
||||
serializer = MenuButtonInitSerializer(instance_obj, data=menu_button_data, request=self.request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
# 列权限
|
||||
if menu_field:
|
||||
for field_data in menu_field:
|
||||
field_data['menu'] = instance.id
|
||||
filter_data = {
|
||||
'menu': field_data['menu'],
|
||||
'field_name': field_data['field_name'],
|
||||
'model': field_data['model']
|
||||
}
|
||||
instance_obj = MenuField.objects.filter(**filter_data).first()
|
||||
serializer = MenuFieldInitSerializer(instance_obj, data=field_data, request=self.request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
|
||||
'cache', 'visible', 'parent', 'children', 'menu_button', 'creator', 'dept_belong_id']
|
||||
'cache', 'visible', 'parent', 'children', 'menu_button', 'menu_field', 'creator', 'dept_belong_id']
|
||||
extra_kwargs = {
|
||||
'creator': {'write_only': True},
|
||||
'dept_belong_id': {'write_only': True}
|
||||
@@ -126,7 +162,7 @@ class RoleInitSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['name', 'key', 'sort', 'status', 'admin',
|
||||
fields = ['name', 'key', 'sort', 'status',
|
||||
'creator', 'dept_belong_id']
|
||||
read_only_fields = ["id"]
|
||||
extra_kwargs = {
|
||||
@@ -139,8 +175,8 @@ class RoleMenuInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化角色菜单(用于生成初始化json文件)
|
||||
"""
|
||||
role_key = serializers.CharField(max_length=100,required=True)
|
||||
menu_component_name = serializers.CharField(max_length=100,required=True)
|
||||
role_key = serializers.CharField(max_length=100, required=True)
|
||||
menu_component_name = serializers.CharField(max_length=100, required=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
init_data = self.initial_data
|
||||
@@ -154,7 +190,7 @@ class RoleMenuInitSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuPermission
|
||||
fields = ['role_key','menu_component_name','creator', 'dept_belong_id']
|
||||
fields = ['role_key', 'menu_component_name', 'creator', 'dept_belong_id']
|
||||
read_only_fields = ["id"]
|
||||
extra_kwargs = {
|
||||
'role': {'required': False},
|
||||
@@ -168,8 +204,8 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化角色菜单按钮(用于生成初始化json文件)
|
||||
"""
|
||||
role_key = serializers.CharField(max_length=100,required=True)
|
||||
menu_button_value = serializers.CharField(max_length=100,required=True)
|
||||
role_key = serializers.CharField(max_length=100, required=True)
|
||||
menu_button_value = serializers.CharField(max_length=100, required=True)
|
||||
data_range = serializers.CharField(max_length=100, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -186,7 +222,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
fields = ['role_key','menu_button_value','data_range','dept','creator', 'dept_belong_id']
|
||||
fields = ['role_key', 'menu_button_value', 'data_range', 'dept', 'creator', 'dept_belong_id']
|
||||
read_only_fields = ["id"]
|
||||
extra_kwargs = {
|
||||
'role': {'required': False},
|
||||
@@ -196,7 +232,6 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ApiWhiteListInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化获取数信息(用于生成初始化json文件)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@
|
||||
"key": "admin",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"admin": true,
|
||||
"remark": null
|
||||
},
|
||||
{
|
||||
@@ -12,7 +11,6 @@
|
||||
"key": "public",
|
||||
"sort": 2,
|
||||
"status": true,
|
||||
"admin": true,
|
||||
"remark": null
|
||||
}
|
||||
]
|
||||
|
||||
@@ -65,6 +65,20 @@
|
||||
"placeholder": null,
|
||||
"setting": null,
|
||||
"children": [
|
||||
{
|
||||
"parent": 1,
|
||||
"title": "网站标题",
|
||||
"key": "site_title",
|
||||
"value": "Dvadmin",
|
||||
"sort": 1,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
"form_item_type": 0,
|
||||
"rule": [],
|
||||
"placeholder": "请输入网站标题",
|
||||
"setting": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"parent": 1,
|
||||
"title": "网站名称",
|
||||
@@ -116,7 +130,7 @@
|
||||
"parent": 1,
|
||||
"title": "版权信息",
|
||||
"key": "copyright",
|
||||
"value": "2021-2022 django-vue-admin.com 版权所有",
|
||||
"value": "2021-2024 django-vue-admin.com 版权所有",
|
||||
"sort": 4,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
@@ -168,7 +182,7 @@
|
||||
"parent": 1,
|
||||
"title": "隐私链接",
|
||||
"key": "privacy_url",
|
||||
"value": "#",
|
||||
"value": "/api/system/clause/privacy.html",
|
||||
"sort": 7,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
@@ -182,7 +196,7 @@
|
||||
"parent": 1,
|
||||
"title": "条款链接",
|
||||
"key": "clause_url",
|
||||
"value": "#",
|
||||
"value": "/api/system/clause/terms_service.html",
|
||||
"sort": 8,
|
||||
"status": true,
|
||||
"data_options": null,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"gender": 1,
|
||||
"user_type": 0,
|
||||
"role": [],
|
||||
"dept_key": "dvadmin",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"is_staff": true,
|
||||
|
||||
@@ -8,9 +8,11 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings")
|
||||
django.setup()
|
||||
|
||||
from dvadmin.utils.core_initialize import CoreInitialize
|
||||
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
|
||||
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
|
||||
RoleMenuInitSerializer, RoleMenuButtonInitSerializer
|
||||
from dvadmin.system.fixtures.initSerializer import (
|
||||
UsersInitSerializer, DeptInitSerializer, RoleInitSerializer,
|
||||
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer,
|
||||
SystemConfigInitSerializer, RoleMenuInitSerializer, RoleMenuButtonInitSerializer
|
||||
)
|
||||
|
||||
|
||||
class Initialize(CoreInitialize):
|
||||
@@ -51,7 +53,6 @@ class Initialize(CoreInitialize):
|
||||
"""
|
||||
self.init_base(RoleMenuButtonInitSerializer, unique_fields=['role', 'menu_button'])
|
||||
|
||||
|
||||
def init_api_white_list(self):
|
||||
"""
|
||||
初始API白名单
|
||||
|
||||
@@ -5,12 +5,7 @@ from django.contrib.auth.models import AbstractUser, UserManager
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from application import dispatch
|
||||
from dvadmin.utils.models import CoreModel, table_prefix
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(0, "禁用"),
|
||||
(1, "启用"),
|
||||
)
|
||||
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
|
||||
|
||||
|
||||
class Role(CoreModel):
|
||||
@@ -18,7 +13,6 @@ class Role(CoreModel):
|
||||
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
|
||||
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
|
||||
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
|
||||
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_role"
|
||||
@@ -107,8 +101,7 @@ class Post(CoreModel):
|
||||
|
||||
class Dept(CoreModel):
|
||||
name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
|
||||
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符",
|
||||
help_text="关联字符")
|
||||
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符", help_text="关联字符")
|
||||
sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序")
|
||||
owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人")
|
||||
phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话")
|
||||
@@ -169,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="组件地址")
|
||||
@@ -178,13 +172,59 @@ 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 = "菜单表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
|
||||
class MenuField(CoreModel):
|
||||
model = models.CharField(max_length=64, verbose_name='表名')
|
||||
menu = models.ForeignKey(to='Menu', on_delete=models.CASCADE, verbose_name='菜单', db_constraint=False)
|
||||
field_name = models.CharField(max_length=64, verbose_name='模型表字段名')
|
||||
title = models.CharField(max_length=64, verbose_name='字段显示名')
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_menu_field"
|
||||
verbose_name = "菜单字段表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("id",)
|
||||
|
||||
class FieldPermission(CoreModel):
|
||||
role = models.ForeignKey(to='Role', on_delete=models.CASCADE, verbose_name='角色', db_constraint=False)
|
||||
field = models.ForeignKey(to='MenuField', on_delete=models.CASCADE,related_name='menu_field', verbose_name='字段', db_constraint=False)
|
||||
is_query = models.BooleanField(default=1, verbose_name='是否可查询')
|
||||
is_create = models.BooleanField(default=1, verbose_name='是否可创建')
|
||||
is_update = models.BooleanField(default=1, verbose_name='是否可更新')
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_field_permission"
|
||||
verbose_name = "字段权限表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("id",)
|
||||
|
||||
|
||||
class MenuButton(CoreModel):
|
||||
menu = models.ForeignKey(
|
||||
@@ -236,7 +276,7 @@ class RoleMenuPermission(CoreModel):
|
||||
db_table = table_prefix + "role_menu_permission"
|
||||
verbose_name = "角色菜单权限表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("-create_datetime",)
|
||||
# ordering = ("-create_datetime",)
|
||||
|
||||
|
||||
class RoleMenuButtonPermission(CoreModel):
|
||||
@@ -289,8 +329,7 @@ class Dictionary(CoreModel):
|
||||
(7, "images"),
|
||||
)
|
||||
label = models.CharField(max_length=100, blank=True, null=True, verbose_name="字典名称", help_text="字典名称")
|
||||
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号",
|
||||
help_text="字典编号/实际值")
|
||||
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号", help_text="字典编号/实际值")
|
||||
parent = models.ForeignKey(
|
||||
to="self",
|
||||
related_name="sublist",
|
||||
@@ -359,7 +398,11 @@ def media_file_name(instance, filename):
|
||||
|
||||
class FileList(CoreModel):
|
||||
name = models.CharField(max_length=200, null=True, blank=True, verbose_name="名称", help_text="名称")
|
||||
url = models.FileField(upload_to=media_file_name)
|
||||
url = models.FileField(upload_to=media_file_name, null=True, blank=True,)
|
||||
file_url = models.CharField(max_length=255, blank=True, verbose_name="文件地址", help_text="文件地址")
|
||||
engine = models.CharField(max_length=100, default='local', blank=True, verbose_name="引擎", help_text="引擎")
|
||||
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
|
||||
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
|
||||
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -368,6 +411,11 @@ class FileList(CoreModel):
|
||||
for chunk in self.url.chunks():
|
||||
md5.update(chunk)
|
||||
self.md5sum = md5.hexdigest()
|
||||
if not self.size:
|
||||
self.size = self.url.size
|
||||
if not self.file_url:
|
||||
url = media_file_name(self, self.name)
|
||||
self.file_url = f'media/{url}'
|
||||
super(FileList, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -16,6 +17,7 @@ from dvadmin.system.views.role_menu import RoleMenuPermissionViewSet
|
||||
from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermissionViewSet
|
||||
from dvadmin.system.views.system_config import SystemConfigViewSet
|
||||
from dvadmin.system.views.user import UserViewSet
|
||||
from dvadmin.system.views.menu_field import MenuFieldViewSet
|
||||
|
||||
system_url = routers.SimpleRouter()
|
||||
system_url.register(r'menu', MenuViewSet)
|
||||
@@ -29,11 +31,10 @@ system_url.register(r'area', AreaViewSet)
|
||||
system_url.register(r'file', FileViewSet)
|
||||
system_url.register(r'api_white_list', ApiWhiteListViewSet)
|
||||
system_url.register(r'system_config', SystemConfigViewSet)
|
||||
system_url.register(r'message_center',MessageCenterViewSet)
|
||||
system_url.register(r'message_center', MessageCenterViewSet)
|
||||
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
|
||||
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
|
||||
|
||||
|
||||
system_url.register(r'column', MenuFieldViewSet)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -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
|
||||
|
||||
@@ -60,12 +60,9 @@ class AreaViewSet(CustomModelViewSet):
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params:
|
||||
if pcode:
|
||||
if params and pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True, pcode__isnull=True)
|
||||
return queryset
|
||||
|
||||
|
||||
23
backend/dvadmin/system/views/clause.py
Normal file
23
backend/dvadmin/system/views/clause.py
Normal 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')
|
||||
@@ -9,9 +9,8 @@ from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Dept
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.permission import AnonymousUserPermission
|
||||
from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -25,6 +24,11 @@ class DeptSerializer(CustomModelSerializer):
|
||||
has_children = serializers.SerializerMethodField()
|
||||
hasChild = serializers.SerializerMethodField()
|
||||
|
||||
dept_user_count = serializers.SerializerMethodField()
|
||||
|
||||
def get_dept_user_count(self, obj: Dept):
|
||||
return Users.objects.filter(dept=obj).count()
|
||||
|
||||
def get_hasChild(self, instance):
|
||||
hasChild = Dept.objects.filter(parent=instance.id)
|
||||
if hasChild:
|
||||
@@ -56,18 +60,18 @@ class DeptImportSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DeptCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
部门管理 创建/更新时的列化器
|
||||
"""
|
||||
|
||||
def create(self, validated_data):
|
||||
value = validated_data.get('parent',None)
|
||||
value = validated_data.get('parent', None)
|
||||
if value is None:
|
||||
validated_data['parent'] = self.request.user.dept
|
||||
dept_obj = Dept.objects.filter(parent=self.request.user.dept).order_by('-sort').first()
|
||||
last_sort = dept_obj.sort if dept_obj else 0
|
||||
validated_data['sort'] = last_sort + 1
|
||||
instance = super().create(validated_data)
|
||||
instance.dept_belong_id = instance.id
|
||||
instance.save()
|
||||
@@ -111,13 +115,10 @@ class DeptViewSet(CustomModelViewSet):
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params:
|
||||
if parent:
|
||||
if params and parent:
|
||||
queryset = self.queryset.filter(status=True, parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=True)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=True, parent__isnull=True)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
serializer = DeptSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
@@ -130,27 +131,107 @@ class DeptViewSet(CustomModelViewSet):
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
data_range = request.user.role.values_list('data_range', flat=True)
|
||||
role_ids = request.user.role.values_list('id', flat=True)
|
||||
data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
|
||||
user_dept_id = request.user.dept.id
|
||||
dept_list = [user_dept_id]
|
||||
data_range_list = list(set(data_range))
|
||||
for item in data_range_list:
|
||||
if item in [0,2]:
|
||||
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)
|
||||
dept_list = Dept.objects.values_list('id', flat=True)
|
||||
elif item == 4:
|
||||
dept_list = request.user.role.values_list('dept',flat=True)
|
||||
dept_list = request.user.role.values_list('dept', flat=True)
|
||||
else:
|
||||
dept_list = []
|
||||
queryset = Dept.objects.filter(id__in=dept_list).values('id', 'name', 'parent')
|
||||
return DetailResponse(data=queryset, msg="获取成功")
|
||||
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated],extra_filter_class=[])
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
|
||||
def all_dept(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')
|
||||
return DetailResponse(data=data, msg="获取成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def move_up(self, request):
|
||||
"""部门上移"""
|
||||
dept_id = request.data.get('dept_id')
|
||||
try:
|
||||
dept = Dept.objects.get(id=dept_id)
|
||||
except Dept.DoesNotExist:
|
||||
return ErrorResponse(msg="部门不存在")
|
||||
previous_menu = Dept.objects.filter(sort__lt=dept.sort, parent=dept.parent).order_by('-sort').first()
|
||||
if previous_menu:
|
||||
previous_menu.sort, dept.sort = dept.sort, previous_menu.sort
|
||||
previous_menu.save()
|
||||
dept.save()
|
||||
return SuccessResponse(data=[], msg="上移成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def move_down(self, request):
|
||||
"""部门下移"""
|
||||
dept_id = request.data['dept_id']
|
||||
try:
|
||||
dept = Dept.objects.get(id=dept_id)
|
||||
except Dept.DoesNotExist:
|
||||
return ErrorResponse(msg="部门不存在")
|
||||
next_menu = Dept.objects.filter(sort__gt=dept.sort, parent=dept.parent).order_by('sort').first()
|
||||
if next_menu:
|
||||
next_menu.sort, dept.sort = dept.sort, next_menu.sort
|
||||
next_menu.save()
|
||||
dept.save()
|
||||
return SuccessResponse(data=[], msg="下移成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def dept_info(self, request):
|
||||
"""部门信息"""
|
||||
def inner(did, li):
|
||||
sub = Dept.objects.filter(parent_id=did)
|
||||
if not sub.exists():
|
||||
return li
|
||||
for i in sub:
|
||||
li.append(i.pk)
|
||||
inner(i, li)
|
||||
return li
|
||||
dept_id = request.query_params.get('dept_id')
|
||||
show_all = request.query_params.get('show_all')
|
||||
if dept_id is None:
|
||||
return ErrorResponse(msg="部门不存在")
|
||||
if not show_all:
|
||||
show_all = 0
|
||||
if int(show_all): # 递归当前部门下的所有部门,查询用户
|
||||
all_did = [dept_id]
|
||||
inner(dept_id, all_did)
|
||||
users = Users.objects.filter(dept_id__in=all_did)
|
||||
else:
|
||||
if dept_id != '':
|
||||
users = Users.objects.filter(dept_id=dept_id)
|
||||
else:
|
||||
users = Users.objects.none()
|
||||
dept_obj = Dept.objects.get(id=dept_id) if dept_id != '' else None
|
||||
sub_dept = Dept.objects.filter(parent_id=dept_obj.pk) if dept_id != '' else []
|
||||
data = {
|
||||
'dept_name': dept_obj and dept_obj.name,
|
||||
'dept_user': users.count(),
|
||||
'owner': dept_obj and dept_obj.owner,
|
||||
'description': dept_obj and dept_obj.description,
|
||||
'gender': {
|
||||
'male': users.filter(gender=1).count(),
|
||||
'female': users.filter(gender=2).count(),
|
||||
'unknown': users.filter(gender=0).count(),
|
||||
},
|
||||
'sub_dept_map': []
|
||||
}
|
||||
for dept in sub_dept:
|
||||
all_did = [dept.pk]
|
||||
inner(dept.pk, all_did)
|
||||
sub_data = {
|
||||
'name': dept.name,
|
||||
'count': Users.objects.filter(dept_id__in=all_did).count()
|
||||
}
|
||||
data['sub_dept_map'].append(sub_data)
|
||||
return SuccessResponse(data)
|
||||
|
||||
@@ -34,6 +34,19 @@ class DictionaryCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
字典管理 创建/更新时的列化器
|
||||
"""
|
||||
value = serializers.CharField(max_length=100)
|
||||
|
||||
def validate_value(self, value):
|
||||
"""
|
||||
在父级的字典编号验证重复性
|
||||
"""
|
||||
initial_data = self.initial_data
|
||||
parent = initial_data.get('parent',None)
|
||||
if parent is None:
|
||||
unique = Dictionary.objects.filter(value=value).exists()
|
||||
if unique:
|
||||
raise serializers.ValidationError("字典编号不能重复")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = Dictionary
|
||||
@@ -51,20 +64,24 @@ class DictionaryViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = Dictionary.objects.all()
|
||||
serializer_class = DictionarySerializer
|
||||
create_serializer_class = DictionaryCreateUpdateSerializer
|
||||
extra_filter_class = []
|
||||
search_fields = ['label']
|
||||
|
||||
def get_queryset(self):
|
||||
if self.action =='list':
|
||||
params = self.request.query_params
|
||||
parent = params.get('parent', None)
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(status=1, parent=parent)
|
||||
queryset = self.queryset.filter(parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=1, parent__isnull=True)
|
||||
queryset = self.queryset.filter(parent__isnull=True)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=1, parent__isnull=True)
|
||||
queryset = self.queryset.filter(parent__isnull=True)
|
||||
return queryset
|
||||
else:
|
||||
return self.queryset
|
||||
|
||||
|
||||
class InitDictionaryViewSet(APIView):
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import FileList
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -8,15 +15,52 @@ class FileSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
return f'media/{str(instance.url)}'
|
||||
# return 'media/' + str(instance.url)
|
||||
return instance.file_url or (f'media/{str(instance.url)}')
|
||||
|
||||
class Meta:
|
||||
model = FileList
|
||||
fields = "__all__"
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = str(self.initial_data.get('file'))
|
||||
validated_data['url'] = self.initial_data.get('file')
|
||||
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local'
|
||||
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
|
||||
file = self.initial_data.get('file')
|
||||
file_size = file.size
|
||||
validated_data['name'] = str(file)
|
||||
validated_data['size'] = file_size
|
||||
md5 = hashlib.md5()
|
||||
for chunk in file.chunks():
|
||||
md5.update(chunk)
|
||||
validated_data['md5sum'] = md5.hexdigest()
|
||||
validated_data['engine'] = file_engine
|
||||
validated_data['mime_type'] = file.content_type
|
||||
if file_backup:
|
||||
validated_data['url'] = file
|
||||
if file_engine == 'oss':
|
||||
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload
|
||||
file_path = ali_oss_upload(file)
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
raise ValueError("上传失败")
|
||||
elif file_engine == 'cos':
|
||||
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload
|
||||
file_path = tencent_cos_upload(file)
|
||||
if file_path:
|
||||
validated_data['file_url'] = file_path
|
||||
else:
|
||||
raise ValueError("上传失败")
|
||||
else:
|
||||
validated_data['url'] = file
|
||||
# 审计字段
|
||||
try:
|
||||
request_user = self.request.user
|
||||
validated_data['dept_belong_id'] = request_user.dept.id
|
||||
validated_data['creator'] = request_user.id
|
||||
validated_data['modifier'] = request_user.id
|
||||
except:
|
||||
pass
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from dvadmin.system.models import Menu, MenuButton, RoleMenuPermission
|
||||
from dvadmin.utils.json_response import SuccessResponse
|
||||
from dvadmin.system.models import Menu, RoleMenuPermission
|
||||
from dvadmin.system.views.menu_button import MenuButtonSerializer
|
||||
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -23,7 +24,8 @@ class MenuSerializer(CustomModelSerializer):
|
||||
hasChild = serializers.SerializerMethodField()
|
||||
|
||||
def get_menuPermission(self, instance):
|
||||
queryset = instance.menuPermission.order_by('-name').values_list('name', flat=True)
|
||||
queryset = instance.menuPermission.order_by('-name').values('id', 'name', 'value')
|
||||
# MenuButtonSerializer(instance.menuPermission.all(), many=True)
|
||||
if queryset:
|
||||
return queryset
|
||||
else:
|
||||
@@ -47,6 +49,12 @@ class MenuCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
name = serializers.CharField(required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
menu_obj = Menu.objects.filter(parent_id=validated_data.get('parent', None)).order_by('-sort').first()
|
||||
last_sort = menu_obj.sort if menu_obj else 0
|
||||
validated_data['sort'] = last_sort + 1
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = "__all__"
|
||||
@@ -63,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"]
|
||||
|
||||
|
||||
@@ -84,45 +92,6 @@ class MenuViewSet(CustomModelViewSet):
|
||||
search_fields = ['name', 'status']
|
||||
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
|
||||
|
||||
# extra_filter_class = []
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def web_router(self, request):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
queryset = self.queryset.filter(status=1)
|
||||
if not user.is_superuser:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def get_all_menu(self, request):
|
||||
"""用于菜单管理获取所有的菜单"""
|
||||
user = request.user
|
||||
queryset = self.queryset.all()
|
||||
if not user.is_superuser:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[])
|
||||
def drag_menu(self, request):
|
||||
"""前端拖拽菜单之后重写parent"""
|
||||
menu_id = request.data['menu_id']
|
||||
parent_id = request.data['parent_id']
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
menu.parent_id = parent_id
|
||||
menu.save()
|
||||
|
||||
return SuccessResponse(data=[], msg="拖拽菜单成功")
|
||||
|
||||
def list(self, request):
|
||||
"""懒加载"""
|
||||
request.query_params._mutable = True
|
||||
@@ -145,3 +114,60 @@ class MenuViewSet(CustomModelViewSet):
|
||||
serializer = MenuSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def web_router(self, request):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
if user.is_superuser:
|
||||
queryset = self.queryset.filter(status=1)
|
||||
else:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def get_all_menu(self, request):
|
||||
"""用于菜单管理获取所有的菜单"""
|
||||
user = request.user
|
||||
queryset = self.queryset.all()
|
||||
if not user.is_superuser:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[])
|
||||
def move_up(self, request):
|
||||
"""菜单上移"""
|
||||
menu_id = request.data.get('menu_id')
|
||||
try:
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
except Menu.DoesNotExist:
|
||||
return ErrorResponse(msg="菜单不存在")
|
||||
previous_menu = Menu.objects.filter(sort__lt=menu.sort, parent=menu.parent).order_by('-sort').first()
|
||||
if previous_menu:
|
||||
previous_menu.sort, menu.sort = menu.sort, previous_menu.sort
|
||||
previous_menu.save()
|
||||
menu.save()
|
||||
return SuccessResponse(data=[], msg="上移成功")
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[])
|
||||
def move_down(self, request):
|
||||
"""菜单下移"""
|
||||
menu_id = request.data['menu_id']
|
||||
try:
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
except Menu.DoesNotExist:
|
||||
return ErrorResponse(msg="菜单不存在")
|
||||
next_menu = Menu.objects.filter(sort__gt=menu.sort, parent=menu.parent).order_by('sort').first()
|
||||
if next_menu:
|
||||
next_menu.sort, menu.sort = menu.sort, next_menu.sort
|
||||
next_menu.save()
|
||||
menu.save()
|
||||
return SuccessResponse(data=[], msg="下移成功")
|
||||
|
||||
@@ -11,7 +11,7 @@ from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -23,7 +23,7 @@ class MenuButtonSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = MenuButton
|
||||
fields = ['id', 'name', 'value', 'api', 'method']
|
||||
fields = ['id', 'name', 'value', 'api', 'method','menu']
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
@@ -49,12 +49,24 @@ class MenuButtonViewSet(CustomModelViewSet):
|
||||
retrieve:单例
|
||||
destroy:删除
|
||||
"""
|
||||
queryset = MenuButton.objects.all()
|
||||
queryset = MenuButton.objects.order_by('create_datetime')
|
||||
serializer_class = MenuButtonSerializer
|
||||
create_serializer_class = MenuButtonCreateUpdateSerializer
|
||||
update_serializer_class = MenuButtonCreateUpdateSerializer
|
||||
extra_filter_class = []
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
重写list方法
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
queryset = self.filter_queryset(self.get_queryset()).order_by('name')
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(serializer.data,msg="获取成功")
|
||||
|
||||
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
|
||||
def menu_button_all_permission(self,request):
|
||||
"""
|
||||
@@ -63,8 +75,7 @@ class MenuButtonViewSet(CustomModelViewSet):
|
||||
:return:
|
||||
"""
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
if is_superuser:
|
||||
queryset = MenuButton.objects.values_list('value',flat=True)
|
||||
else:
|
||||
role_id = request.user.role.values_list('id', flat=True)
|
||||
|
||||
89
backend/dvadmin/system/views/menu_field.py
Normal file
89
backend/dvadmin/system/views/menu_field.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import apps
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Role, MenuField
|
||||
from dvadmin.utils.models import get_custom_app_models
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse
|
||||
|
||||
|
||||
class MenuFieldSerializer(CustomModelSerializer):
|
||||
"""
|
||||
列权限序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = MenuField
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class MenuFieldViewSet(CustomModelViewSet):
|
||||
"""
|
||||
列权限视图集
|
||||
"""
|
||||
queryset = MenuField.objects.order_by('-model')
|
||||
serializer_class = MenuFieldSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
menu = request.query_params.get('menu')
|
||||
if not menu:
|
||||
return SuccessResponse([])
|
||||
queryset = self.filter_queryset(self.get_queryset().filter(menu=menu))
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
payload = request.data
|
||||
for model in apps.get_models():
|
||||
if payload.get('model') == model.__name__:
|
||||
break
|
||||
else:
|
||||
return ErrorResponse(msg='模型表不存在')
|
||||
|
||||
if MenuField.objects.filter(menu=payload.get('menu'),model=model.__name__, field_name=payload.get('field_name')).exists():
|
||||
return ErrorResponse(msg='‘%s’ 字段权限已有,不可重复创建' % payload.get('title'))
|
||||
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def get_models(self, request):
|
||||
"""获取所有项目app下的model"""
|
||||
res = []
|
||||
for model in get_custom_app_models():
|
||||
res.append({
|
||||
'app': model['app'],
|
||||
'title': model['verbose'],
|
||||
'key': model['model']
|
||||
})
|
||||
return DetailResponse(res)
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def auto_match_fields(self, request):
|
||||
"""自动匹配已有的字段"""
|
||||
menu_id = request.data.get('menu')
|
||||
model_name = request.data.get('model')
|
||||
if not menu_id or not model_name:
|
||||
return ErrorResponse( msg='参数错误')
|
||||
for model in get_custom_app_models():
|
||||
if model['model'] != model_name:
|
||||
continue
|
||||
for field in model['fields']:
|
||||
if MenuField.objects.filter(
|
||||
menu_id=menu_id, model=model_name, field_name=field['name']
|
||||
).exists():
|
||||
continue
|
||||
data = {
|
||||
'menu': menu_id,
|
||||
'model': model_name,
|
||||
'field_name': field['name'],
|
||||
'title': str(field['title']),
|
||||
}
|
||||
serializer = self.get_serializer(data=data, request=request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return SuccessResponse(msg='匹配成功')
|
||||
@@ -15,6 +15,7 @@ from dvadmin.system.views.dept import DeptSerializer
|
||||
from dvadmin.system.views.menu import MenuSerializer
|
||||
from dvadmin.system.views.menu_button import MenuButtonSerializer
|
||||
from dvadmin.utils.crud_mixin import FastCrudMixin
|
||||
from dvadmin.utils.field_permission import FieldPermissionMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, DetailResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.validator import CustomUniqueValidator
|
||||
@@ -32,9 +33,6 @@ class RoleSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class RoleCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色管理 创建/更新时的列化器
|
||||
@@ -49,19 +47,19 @@ 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
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MenuPermissonSerializer(CustomModelSerializer):
|
||||
class MenuPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单的按钮权限
|
||||
"""
|
||||
@@ -72,9 +70,9 @@ class MenuPermissonSerializer(CustomModelSerializer):
|
||||
if is_superuser:
|
||||
queryset = MenuButton.objects.filter(menu__id=instance.id)
|
||||
else:
|
||||
menu_permission_id_list = self.request.user.role.values_list('permission',flat=True)
|
||||
queryset = MenuButton.objects.filter(id__in=menu_permission_id_list,menu__id=instance.id)
|
||||
serializer = MenuButtonSerializer(queryset,many=True, read_only=True)
|
||||
menu_permission_id_list = self.request.user.role.values_list('permission', flat=True)
|
||||
queryset = MenuButton.objects.filter(id__in=menu_permission_id_list, menu__id=instance.id)
|
||||
serializer = MenuButtonSerializer(queryset, many=True, read_only=True)
|
||||
return serializer.data
|
||||
|
||||
class Meta:
|
||||
@@ -82,7 +80,29 @@ class MenuPermissonSerializer(CustomModelSerializer):
|
||||
fields = ['id', 'parent', 'name', 'menuPermission']
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet,FastCrudMixin):
|
||||
class MenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单和按钮权限
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
is_superuser = self.request.user.is_superuser
|
||||
if is_superuser:
|
||||
return True
|
||||
else:
|
||||
return MenuButton.objects.filter(
|
||||
menu__id=instance.id,
|
||||
role__id__in=self.request.user.role.values_list('id', flat=True),
|
||||
).exists()
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
|
||||
"""
|
||||
角色管理接口
|
||||
list:查询
|
||||
|
||||
@@ -72,7 +72,14 @@ class RoleMenuPermissionViewSet(CustomModelViewSet):
|
||||
menu_list = body.get('menu',None)
|
||||
if menu_list is None:
|
||||
return ErrorResponse(msg="未获取到菜单参数")
|
||||
data = [{"role":role_id,"menu":item} for item in menu_list]
|
||||
obj_list = RoleMenuPermission.objects.filter(role__id=role_id).values_list('menu__id',flat=True)
|
||||
old_set = set(obj_list)
|
||||
new_set = set(menu_list)
|
||||
#need_update = old_set.intersection(new_set) # 需要更新的
|
||||
need_del = old_set.difference(new_set) # 需要删除的
|
||||
need_add = new_set.difference(old_set) # 需要新增的
|
||||
RoleMenuPermission.objects.filter(role__id=role_id,menu__in=list(need_del)).delete()
|
||||
data = [{"role": role_id, "menu": item} for item in list(need_add)]
|
||||
serializer = RoleMenuPermissionSerializer(data=data,many=True,request=request)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save()
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
@Created on: 2021/6/3 003 0:30
|
||||
@Remark: 菜单按钮管理
|
||||
"""
|
||||
from django.db.models import F
|
||||
from django.db.models import F, Subquery, OuterRef, Exists
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept
|
||||
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \
|
||||
MenuField
|
||||
from dvadmin.system.views.menu import MenuSerializer
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -20,27 +23,19 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单按钮-序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class RoleMenuButtonPermissionInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化菜单按钮-序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化菜单按钮-序列化器
|
||||
"""
|
||||
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
|
||||
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RoleMenuButtonPermission
|
||||
@@ -48,6 +43,107 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class RoleButtonPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色按钮权限
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
data_range = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
return RoleMenuButtonPermission.objects.filter(
|
||||
menu_button__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).exists()
|
||||
|
||||
def get_data_range(self, instance):
|
||||
params = self.request.query_params
|
||||
obj = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).first()
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.data_range
|
||||
|
||||
|
||||
class Meta:
|
||||
model = MenuButton
|
||||
fields = ['id','name','value','isCheck','data_range']
|
||||
|
||||
class RoleFieldPermissionSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FieldPermission
|
||||
fields = "__all__"
|
||||
|
||||
class RoleMenuFieldSerializer(CustomModelSerializer):
|
||||
is_query = serializers.SerializerMethodField()
|
||||
is_create = serializers.SerializerMethodField()
|
||||
is_update = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_query(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
if queryset:
|
||||
return queryset.is_query
|
||||
return False
|
||||
|
||||
def get_is_create(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
if queryset:
|
||||
return queryset.is_create
|
||||
return False
|
||||
|
||||
def get_is_update(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
if queryset:
|
||||
return queryset.is_update
|
||||
return False
|
||||
class Meta:
|
||||
model = MenuField
|
||||
fields = ['id','field_name','title','is_query','is_create','is_update']
|
||||
|
||||
|
||||
class RoleMenuPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单和按钮权限
|
||||
"""
|
||||
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(
|
||||
menu__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).exists()
|
||||
|
||||
def get_btns(self, instance):
|
||||
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
|
||||
serializer = RoleButtonPermissionSerializer(btn_list,many=True,request=self.request)
|
||||
return serializer.data
|
||||
|
||||
def get_columns(self, instance):
|
||||
col_list = MenuField.objects.filter(menu=instance['id'])
|
||||
serializer = RoleMenuFieldSerializer(col_list,many=True,request=self.request)
|
||||
return serializer.data
|
||||
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['id','name','isCheck','btns','columns']
|
||||
|
||||
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
"""
|
||||
菜单按钮接口
|
||||
@@ -64,38 +160,106 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
extra_filter_class = []
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_get_menu(self, request):
|
||||
"""根据当前用户的角色返回角色拥有的菜单"""
|
||||
def get_role_premission(self, request):
|
||||
"""
|
||||
角色授权获取:
|
||||
:param request: role
|
||||
:return: menu,btns,columns
|
||||
"""
|
||||
params = request.query_params
|
||||
role = params.get('role',None)
|
||||
if role is None:
|
||||
return ErrorResponse(msg="未获取到角色信息")
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = Menu.objects.filter(status=1).values('id','name','parent','is_catalog')
|
||||
# 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)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values(id=F('menu__id'),name=F('menu__name'),parent=F('menu__parent'),is_catalog=F('menu__is_catalog'))
|
||||
return DetailResponse(data=queryset)
|
||||
role_id = request.user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id',flat=True)
|
||||
queryset = Menu.objects.filter(status=1, is_catalog=False,id__in=menu_list).values('name', 'id')
|
||||
for item in queryset:
|
||||
parent_list = Menu.get_all_parent(item['id'])
|
||||
names = [d["name"] for d in parent_list]
|
||||
completeName = "/".join(names)
|
||||
isCheck = RoleMenuPermission.objects.filter(
|
||||
menu__id=item['id'],
|
||||
role__id=role,
|
||||
).exists()
|
||||
mbCheck = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button=OuterRef("pk"),
|
||||
role__id=role,
|
||||
)
|
||||
btns = MenuButton.objects.filter(
|
||||
menu__id=item['id'],
|
||||
).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',
|
||||
data_range=F('menu_button_permission__data_range'))
|
||||
dicts = {
|
||||
'name': completeName,
|
||||
'id': item['id'],
|
||||
'isCheck': isCheck,
|
||||
'btns': btns,
|
||||
|
||||
}
|
||||
data.append(dicts)
|
||||
return DetailResponse(data=data)
|
||||
|
||||
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
|
||||
def set_role_premission(self,request,pk):
|
||||
"""
|
||||
对角色的菜单和按钮及按钮范围授权:
|
||||
:param request:
|
||||
:param pk: role
|
||||
:return:
|
||||
"""
|
||||
body = request.data
|
||||
RoleMenuPermission.objects.filter(role=pk).delete()
|
||||
RoleMenuButtonPermission.objects.filter(role=pk).delete()
|
||||
for menu in body:
|
||||
if menu.get('isCheck'):
|
||||
menu_parent = Menu.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
|
||||
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),data_range=data_range)
|
||||
instance.dept.set(btn.get('dept',[]))
|
||||
for col in menu.get('columns'):
|
||||
FieldPermission.objects.update_or_create(role_id=pk,field_id=col.get('id'),is_query=col.get('is_query'),is_create=col.get('is_create'),is_update=col.get('is_update'))
|
||||
return DetailResponse(msg="授权成功")
|
||||
|
||||
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_menu_get_button(self,request):
|
||||
def role_menu_get_button(self, request):
|
||||
"""
|
||||
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
if params:
|
||||
menu_id = params.get('menu',None)
|
||||
if menu_id:
|
||||
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)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role_in=role_list,menu_button__menu=menu_id).values(
|
||||
id=F('menu_button__id'),
|
||||
name=F('menu_button__name')
|
||||
)
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_list, menu_button__menu=menu_id
|
||||
).values(btn_id=F('menu_button__id'), name=F('menu_button__name'))
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="参数错误")
|
||||
|
||||
@@ -133,12 +297,12 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
return DetailResponse(data=data)
|
||||
else:
|
||||
data = []
|
||||
role_id = request.user.role.id
|
||||
params = request.query_params
|
||||
if params:
|
||||
menu_button_id = params.get('menu_button', None)
|
||||
if menu_button_id:
|
||||
role_queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button=menu_button_id).values_list('data_range',flat=True)
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
if params := request.query_params:
|
||||
if menu_button_id := params.get('menu_button', None):
|
||||
role_queryset = RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_list, menu_button__id=menu_button_id
|
||||
).values_list('data_range', flat=True)
|
||||
data_range_list = list(set(role_queryset))
|
||||
for item in data_range_list:
|
||||
if item == 0:
|
||||
@@ -199,46 +363,70 @@ 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:
|
||||
queryset = Dept.objects.values('id','name','parent')
|
||||
return DetailResponse(data=queryset)
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
if params:
|
||||
if not params:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
menu_button = params.get('menu_button')
|
||||
if menu_button is None:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role_in=role_list,menu_button=None).values(
|
||||
id=F('dept__id'),
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values(
|
||||
dept_id=F('dept__id'),
|
||||
name=F('dept__name'),
|
||||
parent=F('dept__parent')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
else:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
|
||||
|
||||
|
||||
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
|
||||
def menu_to_button(self,request):
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def menu_to_button(self, request):
|
||||
"""
|
||||
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
if params:
|
||||
menu_id = params.get('menu',None)
|
||||
menu_id = params.get('menu', None)
|
||||
if menu_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
else:
|
||||
if params:
|
||||
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button__menu=menu_id).values(
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button'
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_menu(self, request):
|
||||
"""
|
||||
获取角色对应的按钮权限
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
|
||||
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import hashlib
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
from django_restql.fields import DynamicSerializerMethodField
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action, permission_classes
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.db import connection
|
||||
from django.db.models import Q
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import Users, Role, Dept
|
||||
from dvadmin.system.views.role import RoleSerializer
|
||||
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.json_response import ErrorResponse, DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.validator import CustomUniqueValidator
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -23,7 +24,7 @@ def recursion(instance, parent, result):
|
||||
res.append(data)
|
||||
if new_instance:
|
||||
array = recursion(new_instance, parent, result)
|
||||
res += (array)
|
||||
res += array
|
||||
return res
|
||||
|
||||
|
||||
@@ -196,10 +197,12 @@ class ExportUserProfileSerializer(CustomModelSerializer):
|
||||
|
||||
|
||||
class UserProfileImportSerializer(CustomModelSerializer):
|
||||
password = serializers.CharField(read_only=True, required=False)
|
||||
|
||||
def save(self, **kwargs):
|
||||
data = super().save(**kwargs)
|
||||
password = hashlib.new(
|
||||
"md5", str(self.initial_data.get("password", "")).encode(encoding="UTF-8")
|
||||
"md5", str(self.initial_data.get("password", "admin123456")).encode(encoding="UTF-8")
|
||||
).hexdigest()
|
||||
data.set_password(password)
|
||||
data.save()
|
||||
@@ -231,7 +234,7 @@ class UserViewSet(CustomModelViewSet):
|
||||
create_serializer_class = UserCreateSerializer
|
||||
update_serializer_class = UserUpdateSerializer
|
||||
filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"]
|
||||
search_fields = ["username", "name", "gender", "dept__name", "role__name"]
|
||||
search_fields = ["username", "name", "dept__name", "role__name"]
|
||||
# 导出
|
||||
export_field_label = {
|
||||
"username": "用户账号",
|
||||
@@ -264,7 +267,6 @@ class UserViewSet(CustomModelViewSet):
|
||||
"data": {"启用": True, "禁用": False},
|
||||
}
|
||||
},
|
||||
"password": "登录密码",
|
||||
"dept": {"title": "部门", "choices": {"queryset": Dept.objects.filter(status=True), "values_name": "name"}},
|
||||
"role": {"title": "角色", "choices": {"queryset": Role.objects.filter(status=True), "values_name": "name"}},
|
||||
}
|
||||
@@ -313,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
|
||||
@@ -324,12 +326,11 @@ class UserViewSet(CustomModelViewSet):
|
||||
return ErrorResponse(msg="参数不能为空")
|
||||
if new_pwd != new_pwd2:
|
||||
return ErrorResponse(msg="两次密码不匹配")
|
||||
check_password = request.user.check_password(old_pwd)
|
||||
if not check_password:
|
||||
check_password = request.user.check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
if check_password:
|
||||
new_pwd = hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()
|
||||
request.user.password = make_password(new_pwd)
|
||||
verify_password = check_password(old_pwd, self.request.user.password)
|
||||
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(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
else:
|
||||
@@ -351,6 +352,8 @@ class UserViewSet(CustomModelViewSet):
|
||||
"""
|
||||
密码重置
|
||||
"""
|
||||
if not self.request.user.is_superuser:
|
||||
return ErrorResponse(msg="只允许超级管理员对其进行密码重置")
|
||||
instance = Users.objects.filter(id=pk).first()
|
||||
data = request.data
|
||||
new_pwd = data.get("newPassword")
|
||||
@@ -364,3 +367,43 @@ class UserViewSet(CustomModelViewSet):
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
else:
|
||||
return ErrorResponse(msg="未获取到用户")
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
dept_id = request.query_params.get('dept')
|
||||
show_all = request.query_params.get('show_all')
|
||||
if not dept_id:
|
||||
dept_id = ''
|
||||
if not show_all:
|
||||
show_all = 0
|
||||
if int(show_all):
|
||||
all_did = [dept_id]
|
||||
def inner(did):
|
||||
sub = Dept.objects.filter(parent_id=did)
|
||||
if not sub.exists():
|
||||
return
|
||||
for i in sub:
|
||||
all_did.append(i.pk)
|
||||
inner(i)
|
||||
if dept_id != '':
|
||||
inner(dept_id)
|
||||
searchs = [
|
||||
Q(**{f+'__icontains':i})
|
||||
for f in self.search_fields
|
||||
] if (i:=request.query_params.get('search')) else []
|
||||
q_obj = []
|
||||
if searchs:
|
||||
q = searchs[0]
|
||||
for i in searchs[1:]:
|
||||
q |= i
|
||||
q_obj.append(Q(q))
|
||||
queryset = Users.objects.filter(*q_obj, dept_id__in=all_did)
|
||||
else:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
else:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True, request=request)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
@@ -3,8 +3,11 @@ import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.utils import timezone
|
||||
|
||||
from dvadmin.utils.validator import CustomValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
UserModel = get_user_model()
|
||||
|
||||
@@ -24,10 +27,13 @@ class CustomBackend(ModelBackend):
|
||||
except UserModel.DoesNotExist:
|
||||
UserModel().set_password(password)
|
||||
else:
|
||||
check_password = user.check_password(password)
|
||||
if not check_password:
|
||||
check_password = user.check_password(hashlib.md5(password.encode(encoding='UTF-8')).hexdigest())
|
||||
if check_password and self.user_can_authenticate(user):
|
||||
verify_password = check_password(password, user.password)
|
||||
if not verify_password:
|
||||
password = hashlib.md5(password.encode(encoding='UTF-8')).hexdigest()
|
||||
verify_password = check_password(password, user.password)
|
||||
if verify_password:
|
||||
if self.user_can_authenticate(user):
|
||||
user.last_login = timezone.now()
|
||||
user.save()
|
||||
return user
|
||||
raise CustomValidationError("当前用户已被禁用,请联系管理员!")
|
||||
|
||||
38
backend/dvadmin/utils/field_permission.py
Normal file
38
backend/dvadmin/utils/field_permission.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.db.models import F
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import FieldPermission, MenuField
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.models import get_custom_app_models
|
||||
|
||||
|
||||
class FieldPermissionMixin:
|
||||
@action(methods=['get'], detail=False,permission_classes=[IsAuthenticated])
|
||||
def field_permission(self, request):
|
||||
"""
|
||||
获取字段权限
|
||||
"""
|
||||
finded = False
|
||||
for model in get_custom_app_models():
|
||||
if model['object'] is self.serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded:
|
||||
break
|
||||
if finded is False:
|
||||
return []
|
||||
user = request.user
|
||||
if user.is_superuser==1:
|
||||
data = MenuField.objects.filter( model=model['model']).values('field_name')
|
||||
for item in data:
|
||||
item['is_create'] = True
|
||||
item['is_query'] = True
|
||||
item['is_update'] = True
|
||||
else:
|
||||
roles = request.user.role.values_list('id', flat=True)
|
||||
data= FieldPermission.objects.filter(
|
||||
field__model=model['model'],role__in=roles
|
||||
).values( 'is_create', 'is_query', 'is_update',field_name=F('field__field_name'))
|
||||
return DetailResponse(data=data)
|
||||
@@ -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):
|
||||
"""
|
||||
@@ -75,7 +108,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
if item.get("permission__api")
|
||||
]
|
||||
for item in api_white_list:
|
||||
new_api = api + ":" + str(method)
|
||||
new_api = f"{api}:{method}"
|
||||
matchObj = re.match(item, new_api, re.M | re.I)
|
||||
if matchObj is None:
|
||||
continue
|
||||
@@ -86,6 +119,12 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
如果不是超级管理员,则进入下一步权限判断
|
||||
"""
|
||||
if request.user.is_superuser == 0:
|
||||
return self._extracted_from_filter_queryset_33(request, queryset, api, method)
|
||||
else:
|
||||
return queryset
|
||||
|
||||
# TODO Rename this here and in `filter_queryset`
|
||||
def _extracted_from_filter_queryset_33(self, request, queryset, api, method):
|
||||
# 0. 获取用户的部门id,没有部门则返回空
|
||||
user_dept_id = getattr(request.user, "dept_id", None)
|
||||
if not user_dept_id:
|
||||
@@ -105,21 +144,22 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
# (2, "本部门数据权限"),
|
||||
# (3, "全部数据权限"),
|
||||
# (4, "自定数据权限")
|
||||
replace_str = re.compile('\d')
|
||||
re_api = replace_str.sub('{id}', api)
|
||||
re_api = api
|
||||
_pk = request.parser_context["kwargs"].get('pk')
|
||||
if _pk: # 判断是否是单例查询
|
||||
re_api = re.sub(_pk,'{id}', api)
|
||||
role_id_list = request.user.role.values_list('id', flat=True)
|
||||
role_permission_list=RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
menu_button__api=re_api,
|
||||
menu_button__method=method).values(
|
||||
'data_range',
|
||||
role_admin=F('role__admin')
|
||||
'data_range'
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
if 3 == ele.get("data_range") or ele.get("role_admin") == True:
|
||||
if ele.get("data_range") == 3:
|
||||
return queryset
|
||||
dataScope_list.append(ele.get("data_range"))
|
||||
dataScope_list = list(set(dataScope_list))
|
||||
@@ -133,26 +173,28 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
# 5. 自定数据权限 获取部门,根据部门过滤
|
||||
dept_list = []
|
||||
for ele in dataScope_list:
|
||||
if ele == 4:
|
||||
dept_list.extend(
|
||||
request.user.role.filter(status=1).values_list(
|
||||
"dept__id", flat=True
|
||||
)
|
||||
)
|
||||
elif ele == 2:
|
||||
dept_list.append(user_dept_id)
|
||||
elif ele == 1:
|
||||
if ele == 1:
|
||||
dept_list.append(user_dept_id)
|
||||
dept_list.extend(
|
||||
get_dept(
|
||||
user_dept_id,
|
||||
)
|
||||
)
|
||||
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(
|
||||
dept_ids
|
||||
)
|
||||
if queryset.model._meta.model_name == 'dept':
|
||||
return queryset.filter(id__in=list(set(dept_list)))
|
||||
return queryset.filter(dept_belong_id__in=list(set(dept_list)))
|
||||
else:
|
||||
return queryset
|
||||
|
||||
|
||||
class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
@@ -163,19 +205,24 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
"$": "iregex",
|
||||
"~": "icontains",
|
||||
}
|
||||
filter_fields = "__all__"
|
||||
|
||||
def construct_search(self, field_name):
|
||||
def construct_search(self, field_name, lookup_expr=None):
|
||||
lookup = self.lookup_prefixes.get(field_name[0])
|
||||
if lookup:
|
||||
field_name = field_name[1:]
|
||||
else:
|
||||
lookup = "icontains"
|
||||
lookup = lookup_expr
|
||||
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
|
||||
@@ -191,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
|
||||
@@ -222,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):
|
||||
"""
|
||||
@@ -249,7 +345,13 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
# warn if the field doesn't exist.
|
||||
if field is None:
|
||||
undefined.append(field_name)
|
||||
|
||||
# 更新默认字符串搜索为模糊搜索
|
||||
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)
|
||||
|
||||
@@ -259,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
|
||||
@@ -294,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))
|
||||
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()],
|
||||
)
|
||||
else:
|
||||
orm_lookups.append(search_field)
|
||||
)
|
||||
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
|
||||
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))
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from django.db import connection
|
||||
from loguru import logger
|
||||
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# 1.🎖️先声明一个类继承logging.Handler(制作一件品如的衣服)
|
||||
|
||||
from application.dispatch import is_tenants_mode
|
||||
|
||||
|
||||
class InterceptTimedRotatingFileHandler(RotatingFileHandler, logging.Filter):
|
||||
"""
|
||||
自定义反射时间回滚日志记录器
|
||||
缺少命名空间
|
||||
"""
|
||||
|
||||
def __init__(self, filename, when='d', interval=1, backupCount=5, encoding="utf-8", delay=False, utc=False,
|
||||
maxBytes=1024 * 1024 * 100, atTime=None, logging_levels="all", format=None):
|
||||
super(InterceptTimedRotatingFileHandler, self).__init__(filename)
|
||||
filename = os.path.abspath(filename)
|
||||
# 定义默认格式
|
||||
if not format:
|
||||
format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <green>{extra[ip]}:{extra[port]}</green> | <level>{level: <8}</level>| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
|
||||
when = when.lower()
|
||||
# 2.🎖️需要本地用不同的文件名做为不同日志的筛选器
|
||||
logger.configure(
|
||||
handlers=[
|
||||
dict(sink=sys.stderr, format=format),
|
||||
],
|
||||
)
|
||||
self.logger_ = logger.bind(sime=filename, ip="-", port="-", username="张三")
|
||||
self.filename = filename
|
||||
key_map = {
|
||||
'h': 'hour',
|
||||
'w': 'week',
|
||||
's': 'second',
|
||||
'm': 'minute',
|
||||
'd': 'day',
|
||||
}
|
||||
# 根据输入文件格式及时间回滚设立文件名称
|
||||
rotation = f"{maxBytes / 1024 / 1024}MB"
|
||||
retention = "%d %ss" % (backupCount, key_map[when])
|
||||
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
|
||||
if when == "s":
|
||||
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
|
||||
elif when == "m":
|
||||
time_format = "{time:%Y-%m-%d_%H-%M}"
|
||||
elif when == "h":
|
||||
time_format = "{time:%Y-%m-%d_%H}"
|
||||
elif when == "d":
|
||||
time_format = "{time:%Y-%m-%d}"
|
||||
elif when == "w":
|
||||
time_format = "{time:%Y-%m-%d}"
|
||||
level_keys = ["info"]
|
||||
# 3.🎖️构建一个筛选器
|
||||
levels = {
|
||||
"debug": lambda x: "DEBUG" == x['level'].name.upper() and x['extra'].get('sime') == filename,
|
||||
"error": lambda x: "ERROR" == x['level'].name.upper() and x['extra'].get('sime') == filename,
|
||||
"info": lambda x: "INFO" == x['level'].name.upper() and x['extra'].get('sime') == filename,
|
||||
"warning": lambda x: "WARNING" == x['level'].name.upper() and x['extra'].get('sime') == filename
|
||||
}
|
||||
# 4. 🎖️根据输出构建筛选器
|
||||
if isinstance(logging_levels, str):
|
||||
if logging_levels.lower() == "all":
|
||||
level_keys = levels.keys()
|
||||
elif logging_levels.lower() in levels:
|
||||
level_keys = [logging_levels]
|
||||
elif isinstance(logging_levels, (list, tuple)):
|
||||
level_keys = logging_levels
|
||||
for k, f in {_: levels[_] for _ in level_keys}.items():
|
||||
|
||||
# 5.🎖️为防止重复添加sink,而重复写入日志,需要判断是否已经装载了对应sink,防止其使用秘技:反复横跳。
|
||||
filename_fmt = filename.replace(".log", "_%s_%s.log" % (time_format, k))
|
||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||
file_key = {_._name: han_id for han_id, _ in self.logger_._core.handlers.items()}
|
||||
filename_fmt_key = "'{}'".format(filename_fmt)
|
||||
if filename_fmt_key in file_key:
|
||||
continue
|
||||
# self.logger_.remove(file_key[filename_fmt_key])
|
||||
self.logger_.add(
|
||||
filename_fmt,
|
||||
format=format,
|
||||
retention=retention,
|
||||
encoding=encoding,
|
||||
level=self.level,
|
||||
rotation=rotation,
|
||||
compression="zip", # 日志归档自行压缩文件
|
||||
delay=delay,
|
||||
enqueue=True,
|
||||
backtrace=True,
|
||||
filter=f
|
||||
)
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
level = self.logger_.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
frame, depth = logging.currentframe(), 2
|
||||
# 6.🎖️把当前帧的栈深度回到发生异常的堆栈深度,不然就是当前帧发生异常而无法回溯
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
# 设置自定义属性
|
||||
port = "-"
|
||||
ip = "-"
|
||||
details = frame.f_locals.get('details', None)
|
||||
msg = self.format(record)
|
||||
bind = {}
|
||||
if details and details.get('client'):
|
||||
ip, port = details.get('client').split(':')
|
||||
if is_tenants_mode():
|
||||
bind["schema_name"] = connection.tenant.schema_name
|
||||
bind["domain_url"] = getattr(connection.tenant, 'domain_url', None)
|
||||
bind["ip"] = ip
|
||||
bind["port"] = port
|
||||
self.logger_ \
|
||||
.opt(depth=depth, exception=record.exc_info, colors=True) \
|
||||
.bind(**bind) \
|
||||
.log(level, msg)
|
||||
@@ -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")
|
||||
|
||||
@@ -6,22 +6,21 @@
|
||||
@Created on: 2021/5/31 031 22:08
|
||||
@Remark: 公共基础model类
|
||||
"""
|
||||
import uuid
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.conf import settings
|
||||
|
||||
from application import settings
|
||||
|
||||
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
|
||||
|
||||
|
||||
class SoftDeleteQuerySet(QuerySet):
|
||||
class SoftDeleteQuerySet(models.QuerySet):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
class SoftDeleteManager(models.Manager):
|
||||
"""支持软删除"""
|
||||
|
||||
@@ -40,7 +39,7 @@ class SoftDeleteManager(models.Manager):
|
||||
return SoftDeleteQuerySet(self.model, using=self._db).exclude(is_deleted=False)
|
||||
return SoftDeleteQuerySet(self.model).exclude(is_deleted=True)
|
||||
|
||||
def get_by_natural_key(self,name):
|
||||
def get_by_natural_key(self, name):
|
||||
return SoftDeleteQuerySet(self.model).get(username=name)
|
||||
|
||||
|
||||
@@ -73,10 +72,13 @@ class CoreModel(models.Model):
|
||||
id = models.BigAutoField(primary_key=True, help_text="Id", verbose_name="Id")
|
||||
description = models.CharField(max_length=255, verbose_name="描述", null=True, blank=True, help_text="描述")
|
||||
creator = models.ForeignKey(to=settings.AUTH_USER_MODEL, related_query_name='creator_query', null=True,
|
||||
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL, db_constraint=False)
|
||||
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL,
|
||||
db_constraint=False)
|
||||
modifier = models.CharField(max_length=255, null=True, blank=True, help_text="修改人", verbose_name="修改人")
|
||||
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True, verbose_name="数据归属部门")
|
||||
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间")
|
||||
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True,
|
||||
verbose_name="数据归属部门")
|
||||
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间",
|
||||
verbose_name="修改时间")
|
||||
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
|
||||
verbose_name="创建时间")
|
||||
|
||||
@@ -86,8 +88,6 @@ class CoreModel(models.Model):
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
|
||||
|
||||
def get_all_models_objects(model_name=None):
|
||||
"""
|
||||
获取所有 models 对象
|
||||
@@ -112,3 +112,51 @@ def get_all_models_objects(model_name=None):
|
||||
if model_name:
|
||||
return settings.ALL_MODELS_OBJECTS[model_name] or {}
|
||||
return settings.ALL_MODELS_OBJECTS or {}
|
||||
|
||||
|
||||
def get_model_from_app(app_name):
|
||||
"""获取模型里的字段"""
|
||||
model_module = import_module(app_name + '.models')
|
||||
filter_model = [
|
||||
getattr(model_module, item) for item in dir(model_module)
|
||||
if item != 'CoreModel' and issubclass(getattr(model_module, item).__class__, models.base.ModelBase)
|
||||
]
|
||||
model_list = []
|
||||
for model in filter_model:
|
||||
if model.__name__ == 'AbstractUser':
|
||||
continue
|
||||
fields = [
|
||||
{'title': field.verbose_name, 'name': field.name, 'object': field}
|
||||
for field in model._meta.fields
|
||||
]
|
||||
model_list.append({
|
||||
'app': app_name,
|
||||
'verbose': model._meta.verbose_name,
|
||||
'model': model.__name__,
|
||||
'object': model,
|
||||
'fields': fields
|
||||
})
|
||||
return model_list
|
||||
|
||||
|
||||
def get_custom_app_models(app_name=None):
|
||||
"""
|
||||
获取所有项目下的app里的models
|
||||
"""
|
||||
if app_name:
|
||||
return get_model_from_app(app_name)
|
||||
all_apps = apps.get_app_configs()
|
||||
res = []
|
||||
for app in all_apps:
|
||||
if app.name.startswith('django'):
|
||||
continue
|
||||
if app.name in settings.COLUMN_EXCLUDE_APPS:
|
||||
continue
|
||||
try:
|
||||
all_models = get_model_from_app(app.name)
|
||||
if all_models:
|
||||
for model in all_models:
|
||||
res.append(model)
|
||||
except Exception as e:
|
||||
pass
|
||||
return res
|
||||
|
||||
@@ -40,13 +40,11 @@ class CustomPagination(PageNumberPagination):
|
||||
try:
|
||||
self.page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
|
||||
# msg = self.invalid_page_message.format(
|
||||
# page_number=page_number, message=str(exc)
|
||||
# )
|
||||
# raise NotFound(msg)
|
||||
empty = False
|
||||
pass
|
||||
|
||||
if paginator.num_pages > 1 and self.template is not None:
|
||||
# The browsable API should display pagination controls.
|
||||
@@ -58,15 +56,15 @@ class CustomPagination(PageNumberPagination):
|
||||
self.page = []
|
||||
|
||||
return list(self.page)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
code = 2000
|
||||
msg = 'success'
|
||||
page =int(self.get_page_number(self.request, paginator)) or 1
|
||||
total=self.page.paginator.count if self.page else 0
|
||||
limit= int(self.get_page_size(self.request)) or 10
|
||||
is_next= self.page.has_next()
|
||||
is_previous= self.page.has_previous()
|
||||
data=data
|
||||
page = int(self.get_page_number(self.request, paginator)) or 1
|
||||
total = self.page.paginator.count if self.page else 0
|
||||
limit = int(self.get_page_size(self.request)) or 10
|
||||
is_next = self.page.has_next() if self.page else False
|
||||
is_previous = self.page.has_previous() if self.page else False
|
||||
|
||||
if not data:
|
||||
code = 2000
|
||||
@@ -78,8 +76,8 @@ class CustomPagination(PageNumberPagination):
|
||||
('msg', msg),
|
||||
('page', page),
|
||||
('limit', limit),
|
||||
('total',total),
|
||||
('is_next',is_next),
|
||||
('total', total),
|
||||
('is_next', is_next),
|
||||
('is_previous', is_previous),
|
||||
('data', data)
|
||||
]))
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -6,18 +6,20 @@
|
||||
@Created on: 2021/6/1 001 22:57
|
||||
@Remark: 自定义视图集
|
||||
"""
|
||||
import uuid
|
||||
|
||||
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, 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 = {}
|
||||
@@ -63,12 +65,34 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
serializer_class = self.get_serializer_class()
|
||||
kwargs.setdefault('context', self.get_serializer_context())
|
||||
# 全部以可见字段为准
|
||||
can_see = self.get_menu_field(serializer_class)
|
||||
# 排除掉序列化器级的字段
|
||||
# sub_set = set(serializer_class._declared_fields.keys()) - set(can_see)
|
||||
# for field in sub_set:
|
||||
# serializer_class._declared_fields.pop(field)
|
||||
# if not self.request.user.is_superuser:
|
||||
# serializer_class.Meta.fields = can_see
|
||||
# 在分页器中使用
|
||||
self.request.permission_fields = can_see
|
||||
if isinstance(self.request.data, list):
|
||||
with transaction.atomic():
|
||||
return serializer_class(many=True, *args, **kwargs)
|
||||
else:
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
def get_menu_field(self, serializer_class):
|
||||
"""获取字段权限"""
|
||||
finded = False
|
||||
for model in get_custom_app_models():
|
||||
if model['object'] is serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded is False:
|
||||
return []
|
||||
return MenuField.objects.filter(model=model['model']
|
||||
).values('field_name', 'title')
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data, request=request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
0
backend/logs/__init__.py
Normal file
0
backend/logs/__init__.py
Normal file
@@ -1,31 +1,31 @@
|
||||
Django==4.1.5
|
||||
Django==4.2.7
|
||||
django-comment-migrate==0.1.7
|
||||
django-cors-headers==3.13.0
|
||||
django-filter==21.1
|
||||
django-cors-headers==4.3.0
|
||||
django-filter==23.3
|
||||
django-ranged-response==0.2.0
|
||||
djangorestframework==3.14.0
|
||||
django-restql==0.15.3
|
||||
django-simple-captcha==0.5.17
|
||||
django-timezone-field==5.0
|
||||
djangorestframework-simplejwt==5.2.2
|
||||
drf-yasg==1.21.4
|
||||
mysqlclient==2.1.1
|
||||
pypinyin==0.48.0
|
||||
ua-parser==0.16.1
|
||||
pyparsing==3.0.9
|
||||
openpyxl==3.0.10
|
||||
requests==2.28.2
|
||||
loguru==0.6.0
|
||||
typing-extensions==4.4.0
|
||||
smmap==5.0.0
|
||||
tzlocal==4.1
|
||||
channels==4.0.0
|
||||
channels-redis==4.0.0
|
||||
websockets==10.4
|
||||
django-simple-captcha==0.5.20
|
||||
django-timezone-field==6.0.1
|
||||
djangorestframework-simplejwt==5.3.0
|
||||
drf-yasg==1.21.7
|
||||
mysqlclient==2.2.0
|
||||
pypinyin==0.49.0
|
||||
ua-parser==0.18.0
|
||||
pyparsing==3.1.1
|
||||
openpyxl==3.1.2
|
||||
requests==2.31.0
|
||||
typing-extensions==4.8.0
|
||||
tzlocal==5.1
|
||||
channels==3.0.5
|
||||
channels-redis==4.1.0
|
||||
websockets==11.0.3
|
||||
user-agents==2.2.0
|
||||
six==1.16.0
|
||||
whitenoise==6.3.0
|
||||
psycopg2==2.9.5
|
||||
uvicorn==0.20.0
|
||||
gunicorn==20.1.0
|
||||
gevent==22.10.2
|
||||
whitenoise==6.6.0
|
||||
psycopg2==2.9.9
|
||||
uvicorn==0.23.2
|
||||
gunicorn==21.2.0
|
||||
gevent==23.9.1
|
||||
Pillow==10.1.0
|
||||
dvadmin-celery==1.0.5
|
||||
|
||||
34
backend/static/clause/privacy.css
Normal file
34
backend/static/clause/privacy.css
Normal 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;}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
233
backend/templates/privacy.html
Normal file
233
backend/templates/privacy.html
Normal 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>
|
||||
96
backend/templates/terms_service.html
Normal file
96
backend/templates/terms_service.html
Normal 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>
|
||||
@@ -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:
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
~~~sh
|
||||
# 编译打包到本地
|
||||
docker build -f ./docker_env/web/DockerfileBuild -t registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:latest .
|
||||
docker build -f ./docker_env/web/DockerfileBuild -t registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine .
|
||||
# 上传到阿里云仓库
|
||||
docker push registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:latest
|
||||
docker push registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -28,6 +28,6 @@ server {
|
||||
proxy_send_timeout 600s;
|
||||
real_ip_header X-Forwarded-For;
|
||||
rewrite ^/api/(.*)$ /$1 break; #重写
|
||||
proxy_pass http://178.10.0.12:8000/; # 设置代理服务器的协议和地址
|
||||
proxy_pass http://177.10.0.12:8000/; # 设置代理服务器的协议和地址
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:16.19-alpine
|
||||
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine
|
||||
WORKDIR /web/
|
||||
COPY web/. .
|
||||
RUN yarn install
|
||||
RUN yarn install --registry=https://registry.npm.taobao.org
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.19-alpine
|
||||
WORKDIR /
|
||||
COPY ./web/package.json .
|
||||
RUN npm install -g yarn
|
||||
RUN yarn install --registry=https://registry.npm.taobao.org
|
||||
|
||||
2
web/.env
2
web/.env
@@ -1,6 +1,6 @@
|
||||
# port 端口号
|
||||
VITE_PORT = 8080
|
||||
|
||||
VITE_API_URL = 'http://dvadmin3api.django.icu:8001'
|
||||
# open 运行 npm run dev 时自动打开浏览器
|
||||
VITE_OPEN = false
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
ENV = 'development'
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = 'http://127.0.0.1:8000/'
|
||||
VITE_API_URL = 'http://127.0.0.1:8000'
|
||||
|
||||
# 是否启用按钮权限
|
||||
VITE_PM_ENABLED = true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
ENV = 'production'
|
||||
|
||||
# 线上环境接口地址
|
||||
VITE_API_URL = ''
|
||||
VITE_API_URL = '/api' # docker-compose部署不需要修改,nginx容器自动代理了这个地址
|
||||
|
||||
# 是否启用按钮权限
|
||||
VITE_PM_ENABLED = true
|
||||
|
||||
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
yarn.lock
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Django-Vue3-Admin
|
||||
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](https://gitee.com/liqianglog/django-vue-admin)
|
||||
[](https://gitee.com/huge-dream/django-vue3-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](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)
|
||||
|
||||
@@ -23,7 +23,7 @@ Because of love, so embrace the future
|
||||
|
||||
## Online experience
|
||||
|
||||
👩👧👦👩👧👦 demo address:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com)
|
||||
👩👧👦👩👧👦 demo address:[https://demo.dvadmin.com](https://demo.dvadmin.com)
|
||||
|
||||
* demo account:superadmin
|
||||
|
||||
@@ -39,9 +39,9 @@ Because of love, so embrace the future
|
||||
|
||||
## source code url:
|
||||
|
||||
gitee(Main push):[https://gitee.com/liqianglog/django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)👩👦👦
|
||||
gitee(Main push):[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
github:[https://github.com/liqianglog/django-vue-admin](https://github.com/liqianglog/django-vue-admin)👩👦👦
|
||||
github:no data
|
||||
|
||||
## core function
|
||||
|
||||
@@ -55,7 +55,7 @@ github:[https://github.com/liqianglog/django-vue-admin](https://github.com/liq
|
||||
8. 🧑🔧 Regional management: to manage provinces, cities, counties and regions.
|
||||
9. 📁 Attachment management: Unified management of all files and pictures on the platform.
|
||||
10. 🗓 ️operation logs: log and query the system normal operation; Log and query system exception information.
|
||||
11.🔌 [plugins market] (<https://bbs.django-vue-admin.com/plugMarket.html>) : based on the Django framework - Vue - Admin application and plug-in development.
|
||||
11.🔌 [plugins market] (<https://bbs.django-vue-admin.com/plugMarket.html>) : based on the Django framework - Vue - Admin application and plug-in development.
|
||||
|
||||
## plugins market 🔌
|
||||
|
||||
@@ -76,7 +76,7 @@ Redis(Optional, the latest edition)
|
||||
|
||||
```bash
|
||||
# clone code
|
||||
git clone https://gitee.com/liqianglog/django-vue-admin.git
|
||||
git clone https://gitee.com/huge-dream/django-vue3-admin.git
|
||||
|
||||
# enter code dir
|
||||
cd web
|
||||
202
web/README.md
202
web/README.md
@@ -1,8 +1,32 @@
|
||||
<div align="center">django-vue3-admin:web </div>
|
||||
# Django-Vue3-Admin
|
||||
|
||||
#### 🌈 介绍
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://python.org/) [](https://docs.djangoproject.com/zh-hans/3.2/) [](https://nodejs.org/zh-cn/) [](https://gitee.com/liqianglog/django-vue-admin)
|
||||
|
||||
django-vue3-admin,基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!
|
||||
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
|
||||
|
||||
|
||||
|
||||
💡 **「关于」**
|
||||
|
||||
我们是一群热爱代码的青年,在这个炙热的时代下,我们希望静下心来通过Code带来一点我们的色彩和颜色。
|
||||
|
||||
因为热爱,所以拥抱未来!
|
||||
|
||||
|
||||
## 平台简介
|
||||
|
||||
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
|
||||
django-vue3-admin 基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!
|
||||
|
||||
|
||||
|
||||
|
||||
* 🧑🤝🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/))
|
||||
* 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。
|
||||
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
|
||||
* 👬支持加载动态权限菜单,多方式轻松权限控制。
|
||||
* 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。
|
||||
* 💡 特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。
|
||||
|
||||
#### 🏭 环境支持
|
||||
|
||||
@@ -12,21 +36,173 @@ django-vue3-admin,基于 vue3 + CompositionAPI + typescript + vite + element p
|
||||
|
||||
> 由于 Vue3 不再支持 IE11,故而 ElementPlus 也不支持 IE11 及之前版本。
|
||||
|
||||
#### ⚡ 使用说明
|
||||
|
||||
建议使用 yarn,yarn 是一个类似于npm的包管理器 <a href="http://nodejs.cn/" target="_blank">node 版本 > 16</a>
|
||||
|
||||
## 在线体验
|
||||
|
||||
👩👧👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
|
||||
|
||||
- 账号:superadmin
|
||||
|
||||
- 密码:admin123456
|
||||
|
||||
👩👦👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
|
||||
|
||||
|
||||
|
||||
## 交流
|
||||
|
||||
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩👦👦
|
||||
|
||||
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩👦👦
|
||||
|
||||
- django-vue-admin交流01群(已满):812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi)
|
||||
- django-vue-admin交流02群(已满):687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi)
|
||||
- django-vue-admin交流03群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
|
||||
|
||||
- 二维码
|
||||
|
||||
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
|
||||
|
||||
## 源码地址
|
||||
|
||||
gitee地址(主推):[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩👦👦
|
||||
|
||||
github地址:暂无
|
||||
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 👨⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
|
||||
2. 🧑⚕️部门管理:配置系统组织机构(公司、部门、角色)。
|
||||
3. 👩⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
|
||||
4. 🧑🎓按钮权限权限:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
|
||||
5. 🧑🎓字段权限权限:授权页面的字段显示权限。
|
||||
5. 👨🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
6. 👬接口白名单:配置不需要进行权限校验的接口。
|
||||
7. 🧑🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
8. 🧑🔧地区管理:对省市县区域进行管理。
|
||||
9. 📁附件管理:对平台上所有文件、图片等进行统一管理。
|
||||
10. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
11. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。
|
||||
|
||||
## 插件市场 🔌
|
||||
|
||||
- Celery异步任务:[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
|
||||
- 升级中心后端:[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
|
||||
- 升级中心前端:[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
|
||||
|
||||
## 准备工作
|
||||
~~~
|
||||
Python >= 3.8.0 (推荐3.8+版本)
|
||||
nodejs >= 14.0 (推荐最新)
|
||||
Mysql >= 5.7.0 (可选,默认数据库sqlite3,推荐8.0版本)
|
||||
Redis(可选,最新版)
|
||||
~~~
|
||||
|
||||
## 前端♝
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://gitee.com/huge-dream/django-vue3-admin.git
|
||||
|
||||
# 进入项目
|
||||
cd django-vue-admin/web
|
||||
# 进入项目目录
|
||||
cd web
|
||||
|
||||
# 安装依赖
|
||||
yarn install
|
||||
npm install --registry=https://registry.npm.taobao.org
|
||||
|
||||
# 运行项目
|
||||
yarn dev
|
||||
|
||||
# 打包发布
|
||||
yarn build
|
||||
# 启动服务
|
||||
npm run dev
|
||||
# 浏览器访问 http://localhost:8080
|
||||
# .env.development 文件中可配置启动端口等参数
|
||||
# 构建生产环境
|
||||
# npm run build
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 后端💈
|
||||
|
||||
~~~bash
|
||||
1. 进入项目目录 cd backend
|
||||
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
|
||||
3. 在 env.py 中配置数据库信息
|
||||
mysql数据库版本建议:8.0
|
||||
mysql数据库字符集:utf8mb4
|
||||
4. 安装依赖环境
|
||||
pip3 install -r requirements.txt
|
||||
5. 执行迁移命令:
|
||||
python3 manage.py makemigrations
|
||||
python3 manage.py migrate
|
||||
6. 初始化数据
|
||||
python3 manage.py init
|
||||
7. 初始化省市县数据:
|
||||
python3 manage.py init_area
|
||||
8. 启动项目
|
||||
python3 manage.py runserver 0.0.0.0:8000
|
||||
或使用 daphne :
|
||||
daphne -b 0.0.0.0 -p 8000 application.asgi:application
|
||||
~~~
|
||||
|
||||
### 访问项目
|
||||
|
||||
- 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
|
||||
- 账号:`superadmin` 密码:`admin123456`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### docker-compose 运行
|
||||
|
||||
~~~shell
|
||||
# 先安装docker-compose (自行百度安装),执行此命令等待安装,如有使用celery插件请打开docker-compose.yml中celery 部分注释
|
||||
docker-compose up -d
|
||||
# 初始化后端数据(第一次执行即可)
|
||||
docker exec -ti dvadmin-django bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py init_area
|
||||
python manage.py init
|
||||
exit
|
||||
|
||||
前端地址:http://127.0.0.1:8080
|
||||
后端地址:http://127.0.0.1:8080/api
|
||||
# 在服务器上请把127.0.0.1 换成自己公网ip
|
||||
账号:superadmin 密码:admin123456
|
||||
|
||||
# docker-compose 停止
|
||||
docker-compose down
|
||||
# docker-compose 重启
|
||||
docker-compose restart
|
||||
# docker-compose 启动时重新进行 build
|
||||
docker-compose up -d --build
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
## 演示图✅
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,23 +10,22 @@
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="django-vue3-admin,基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!"
|
||||
content="django-vue-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>django-vue3-admin</title>
|
||||
<title>django-vue-admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript">
|
||||
// let _hmt = _hmt || [];
|
||||
(function () {
|
||||
let hm = document.createElement('script');
|
||||
hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4';
|
||||
let s = document.getElementsByTagName('script')[0];
|
||||
<script>
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?9ba8fc809b5584167a2fb9b31bb3970c";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "django-vue3-admin",
|
||||
"version": "1.0.0",
|
||||
"description": "django-vue3-admin,基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!",
|
||||
"version": "3.0.0",
|
||||
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite --force",
|
||||
@@ -10,10 +10,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.0.10",
|
||||
"@fast-crud/fast-crud": "^1.11.10",
|
||||
"@fast-crud/fast-extends": "^1.11.10",
|
||||
"@fast-crud/ui-element": "^1.11.10",
|
||||
"@fast-crud/ui-interface": "^1.11.9",
|
||||
"@fast-crud/fast-crud": "^1.19.2",
|
||||
"@fast-crud/fast-extends": "^1.19.2",
|
||||
"@fast-crud/ui-element": "^1.19.2",
|
||||
"@fast-crud/ui-interface": "^1.19.2",
|
||||
"@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,8 @@
|
||||
"echarts": "^5.4.1",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"element-plus": "^2.2.26",
|
||||
"element-plus": "^2.5.5",
|
||||
"element-tree-line": "^0.2.1",
|
||||
"font-awesome": "^4.7.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"js-table2excel": "^1.0.3",
|
||||
@@ -43,13 +45,14 @@
|
||||
"splitpanes": "^3.1.5",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"ts-md5": "^1.3.1",
|
||||
"upgrade": "^1.1.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-cropper": "^1.0.8",
|
||||
"vue-grid-layout": "^3.0.0-beta1",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vxe-table": "^4.3.10",
|
||||
"vxe-table": "^4.4.1",
|
||||
"xe-utils": "^3.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,7 +63,7 @@
|
||||
"@typescript-eslint/parser": "^5.46.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/compiler-sfc": "^3.2.45",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"prettier": "^2.8.1",
|
||||
"sass": "^1.56.2",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
|
||||
<!-- v-show="themeConfig.lockScreenTime > 1" -->
|
||||
<router-view v-show="themeConfig.lockScreenTime > 1" />
|
||||
<LockScreen v-if="themeConfig.isLockScreen" />
|
||||
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
|
||||
@@ -9,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="app">
|
||||
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch,onBeforeUnmount } from 'vue';
|
||||
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -33,8 +34,8 @@ const route = useRoute();
|
||||
const stores = useTagsViewRoutes();
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
import websocket from "/@/utils/websocket";
|
||||
import {ElNotification} from "element-plus";
|
||||
import websocket from '/@/utils/websocket';
|
||||
import { ElNotification } from 'element-plus';
|
||||
// 获取版本号
|
||||
const getVersion = computed(() => {
|
||||
let isVersion = false;
|
||||
@@ -58,8 +59,6 @@ onBeforeMount(() => {
|
||||
setIntroduction.cssCdn();
|
||||
// 设置批量第三方 js
|
||||
setIntroduction.jsCdn();
|
||||
//websockt 模块
|
||||
websocket.init(wsReceive)
|
||||
});
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
@@ -88,6 +87,14 @@ watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
other.useTitle();
|
||||
if (!websocket.websocket) {
|
||||
//websockt 模块
|
||||
try {
|
||||
websocket.init(wsReceive)
|
||||
} catch (e) {
|
||||
console.log('websocket错误');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
@@ -95,11 +102,11 @@ watch(
|
||||
);
|
||||
|
||||
// websocket相关代码
|
||||
import {messageCenterStore} from "/@/stores/messageCenter";
|
||||
import { messageCenterStore } from '/@/stores/messageCenter';
|
||||
const wsReceive = (message: any) => {
|
||||
const data = JSON.parse(message.data)
|
||||
const { unread } = data
|
||||
const messageCenter = messageCenterStore()
|
||||
const data = JSON.parse(message.data);
|
||||
const { unread } = data;
|
||||
const messageCenter = messageCenterStore();
|
||||
messageCenter.setUnread(unread);
|
||||
if (data.contentType === 'SYSTEM') {
|
||||
ElNotification({
|
||||
@@ -107,14 +114,12 @@ const wsReceive = (message: any) => {
|
||||
message: data.content,
|
||||
type: 'success',
|
||||
position: 'bottom-right',
|
||||
duration: 5000
|
||||
})
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
// 关闭连接
|
||||
websocket.close()
|
||||
})
|
||||
|
||||
websocket.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
BIN
web/src/assets/img/headerImage.png
Normal file
BIN
web/src/assets/img/headerImage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
web/src/assets/img/menu-tree-head-icon.png
Normal file
BIN
web/src/assets/img/menu-tree-head-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 698 B |
BIN
web/src/assets/img/menu-tree-hidden-icon.png
Normal file
BIN
web/src/assets/img/menu-tree-hidden-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 663 B |
BIN
web/src/assets/img/menu-tree-show-icon.png
Normal file
BIN
web/src/assets/img/menu-tree-show-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 737 B |
9
web/src/assets/style/reset.scss
Normal file
9
web/src/assets/style/reset.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.fs-crud-container {
|
||||
.el-table thead {
|
||||
color: #606266;
|
||||
}
|
||||
.el-input__inner::placeholder {
|
||||
color: #dcdfe6;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
29
web/src/components/dept-format/index.vue
Normal file
29
web/src/components/dept-format/index.vue
Normal 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>
|
||||
141
web/src/components/dvaSelect/index.vue
Normal file
141
web/src/components/dvaSelect/index.vue
Normal 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>
|
||||
146
web/src/components/importExcel/index.vue
Normal file
146
web/src/components/importExcel/index.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div style="display: inline-block">
|
||||
<el-button size="default" type="success" @click="handleImport()">
|
||||
<slot>导入</slot>
|
||||
</el-button>
|
||||
<el-dialog :title="props.upload.title" v-model="uploadShow" width="400px" append-to-body>
|
||||
<div v-loading="loading">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
accept=".xlsx, .xls"
|
||||
:headers="props.upload.headers"
|
||||
:action="props.upload.url"
|
||||
:disabled="isUploading"
|
||||
:on-progress="handleFileUploadProgress"
|
||||
:on-success="handleFileSuccess"
|
||||
:auto-upload="false"
|
||||
drag
|
||||
>
|
||||
<i class="el-icon-upload"/>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或
|
||||
<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip" style="color:red">提示:仅允许导入“xls”或“xlsx”格式文件!</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div>
|
||||
<el-button type="warning" style="font-size:14px;margin-top: 20px" @click="importTemplate">下载导入模板</el-button>
|
||||
<el-button type="warning" style="font-size:14px;margin-top: 20px" @click="updateTemplate">批量更新模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" :disabled="loading" @click="submitFileForm">确 定</el-button>
|
||||
<el-button :disabled="loading" @click="uploadShow = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="importExcel">
|
||||
import { request, downloadFile } from '/@/utils/service';
|
||||
import {inject,ref} from "vue";
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import type { Action } from 'element-plus'
|
||||
const refreshView = inject('refreshView')
|
||||
|
||||
let props = defineProps({
|
||||
upload: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
// 是否显示弹出层
|
||||
open: true,
|
||||
// 弹出层标题
|
||||
title: '',
|
||||
// 是否禁用上传
|
||||
isUploading: false,
|
||||
// 是否更新已经存在的用户数据
|
||||
updateSupport: 0,
|
||||
// 设置上传的请求头部
|
||||
headers: { Authorization: 'JWT ' + Session.get('token') },
|
||||
// 上传的地址
|
||||
url: getBaseURL() + 'api/system/file/'
|
||||
}
|
||||
}
|
||||
},
|
||||
api: { // 导入接口地址
|
||||
type: String,
|
||||
default () {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let loading = ref(false)
|
||||
const uploadRef = ref()
|
||||
const uploadShow = ref(false)
|
||||
const isUploading = ref(false)
|
||||
/** 导入按钮操作 */
|
||||
const handleImport = function () {
|
||||
uploadShow.value = true
|
||||
}
|
||||
|
||||
/** 下载模板操作 */
|
||||
const importTemplate=function () {
|
||||
downloadFile({
|
||||
url: props.api + 'import_data/',
|
||||
params: {},
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
/***
|
||||
* 批量更新模板
|
||||
*/
|
||||
const updateTemplate=function () {
|
||||
downloadFile({
|
||||
url: props.api + 'update_template/',
|
||||
params: {},
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
// 文件上传中处理
|
||||
const handleFileUploadProgress=function (event:any, file:any, fileList:any) {
|
||||
isUploading.value = true
|
||||
}
|
||||
// 文件上传成功处理
|
||||
const handleFileSuccess=function (response:any, file:any, fileList:any) {
|
||||
isUploading.value = false
|
||||
loading.value = true
|
||||
uploadRef.value.clearFiles()
|
||||
// 是否更新已经存在的用户数据
|
||||
return request({
|
||||
url: props.api + 'import_data/',
|
||||
method: 'post',
|
||||
data: {
|
||||
url: response.data.url
|
||||
}
|
||||
}).then((response:any) => {
|
||||
loading.value = false
|
||||
ElMessageBox.alert('导入成功', '导入完成', {
|
||||
confirmButtonText: 'OK',
|
||||
callback: (action: Action) => {
|
||||
refreshView()
|
||||
},
|
||||
})
|
||||
}).catch(()=>{
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
}
|
||||
// 提交上传文件
|
||||
const submitFileForm=function () {
|
||||
uploadRef.value.submit()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
23
web/src/directive/sizeDirective.ts
Normal file
23
web/src/directive/sizeDirective.ts
Normal 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); // 停止监听
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
*/
|
||||
|
||||
// element plus 自带国际化
|
||||
import enLocale from 'element-plus/lib/locale/lang/en';
|
||||
import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn';
|
||||
import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw';
|
||||
import enLocale from 'element-plus/es/locale/lang/en';
|
||||
import zhcnLocale from 'element-plus/es/locale/lang/zh-cn';
|
||||
import zhtwLocale from 'element-plus/es/locale/lang/zh-tw';
|
||||
|
||||
// 定义变量内容
|
||||
const messages = {};
|
||||
|
||||
@@ -120,4 +120,12 @@ export default {
|
||||
copyTextSuccess: 'Copy succeeded!',
|
||||
copyTextError: 'Copy failed!',
|
||||
},
|
||||
upgrade: {
|
||||
title: 'New version upgrade',
|
||||
msg: 'It\'s a new version. Update it now!Don\'t worry, update quickly oh!',
|
||||
desc: 'Tip: The update restores the default configuration',
|
||||
btnOne: 'Cruel refusal',
|
||||
btnTwo: 'Update now',
|
||||
btnTwoLoading: 'updating',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,6 +40,8 @@ export default {
|
||||
title4: '消息',
|
||||
title5: '开全屏',
|
||||
title6: '关全屏',
|
||||
retry: '重试上线',
|
||||
onlinePrompt: '当前离线状态,是否重试上线?',
|
||||
dropdownLarge: '大型',
|
||||
dropdownDefault: '默认',
|
||||
dropdownSmall: '小型',
|
||||
@@ -75,7 +77,7 @@ export default {
|
||||
},
|
||||
noAccess: {
|
||||
accessTitle: '您未被授权,没有操作权限~',
|
||||
accessMsg: '联系方式:加QQ群探讨 665452019',
|
||||
accessMsg: '请联系管理员',
|
||||
accessBtn: '重新授权',
|
||||
},
|
||||
layout: {
|
||||
@@ -89,10 +91,12 @@ export default {
|
||||
twoIsTopBarColorGradual: '顶栏背景渐变',
|
||||
twoMenuBar: '菜单背景',
|
||||
twoMenuBarColor: '菜单默认字体颜色',
|
||||
twoMenuBarActiveColor: '菜单高亮背景色',
|
||||
twoIsMenuBarColorGradual: '菜单背景渐变',
|
||||
twoColumnsMenuBar: '分栏菜单背景',
|
||||
twoColumnsMenuBarColor: '分栏菜单默认字体颜色',
|
||||
twoIsColumnsMenuBarColorGradual: '分栏菜单背景渐变',
|
||||
twoIsColumnsMenuHoverPreload: '分栏菜单滑鼠悬停预加载',
|
||||
threeTitle: '界面设置',
|
||||
threeIsCollapse: '菜单水平折叠',
|
||||
threeIsUniqueOpened: '菜单手风琴',
|
||||
@@ -131,4 +135,12 @@ export default {
|
||||
copyTextSuccess: '复制成功!',
|
||||
copyTextError: '复制失败!',
|
||||
},
|
||||
upgrade: {
|
||||
title: '新版本升级',
|
||||
msg: '新版本来啦,马上更新尝鲜吧!不用担心,更新很快的哦!',
|
||||
desc: '提示:更新会还原默认配寘',
|
||||
btnOne: '残忍拒绝',
|
||||
btnTwo: '马上更新',
|
||||
btnTwoLoading: '更新中',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ export default {
|
||||
},
|
||||
noAccess: {
|
||||
accessTitle: '您未被授權,沒有操作許可權~',
|
||||
accessMsg: '聯繫方式:加QQ群探討665452019',
|
||||
accessMsg: '請聯系管理員',
|
||||
accessBtn: '重新授權',
|
||||
},
|
||||
layout: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 定义内容
|
||||
export default {
|
||||
label: {
|
||||
one1: '用户名登录',
|
||||
one1: '账号密码登录',
|
||||
two2: '手机号登录',
|
||||
},
|
||||
link: {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<el-main class="layout-main" :style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
|
||||
<el-scrollbar
|
||||
ref="layoutMainScrollbarRef"
|
||||
class="layout-main-scroll layout-backtop-header-fixed"
|
||||
wrap-class="layout-main-scroll"
|
||||
view-class="layout-main-scroll"
|
||||
>
|
||||
<el-main class="layout-main"
|
||||
:style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
|
||||
<el-scrollbar ref="layoutMainScrollbarRef" class="layout-main-scroll layout-backtop-header-fixed"
|
||||
wrap-class="layout-main-scroll" view-class="layout-main-scroll">
|
||||
<LayoutParentView />
|
||||
<LayoutFooter v-if="isFooter" />
|
||||
</el-scrollbar>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="layout-footer pb15">
|
||||
<div class="layout-footer pb5 pt2">
|
||||
<div class="layout-footer-warp">
|
||||
<div>❤️ Powered by Django-Vue3-Admin ❤️</div>
|
||||
<div class="mt5">Copyright DVAdmin团队</div>
|
||||
<div>❤️ Powered by Django-Vue3-Admin Copyright © DVAdmin团队 ❤️</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -57,10 +57,34 @@
|
||||
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
|
||||
></i>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="!isSocketOpen">
|
||||
<el-popconfirm
|
||||
width="250"
|
||||
ref="onlinePopoverRef"
|
||||
:confirm-button-text="$t('message.user.retry')"
|
||||
:icon="InfoFilled"
|
||||
trigger="hover"
|
||||
icon-color="#626AEF"
|
||||
:title="$t('message.user.onlinePrompt')"
|
||||
@confirm="onlineConfirmEvent"
|
||||
>
|
||||
<template #reference>
|
||||
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
|
||||
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</span>
|
||||
</div>
|
||||
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
|
||||
<span class="layout-navbars-breadcrumb-user-link">
|
||||
<img :src="userInfos.avatar" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
{{ userInfos.userName === '' ? 'common' : userInfos.userName }}
|
||||
<span v-if="isSocketOpen">
|
||||
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
|
||||
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
</el-badge>
|
||||
</span>
|
||||
{{ userInfos.username === '' ? 'common' : userInfos.username }}
|
||||
<el-icon class="el-icon--right">
|
||||
<ele-ArrowDown />
|
||||
</el-icon>
|
||||
@@ -79,7 +103,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutBreadcrumbUser">
|
||||
import { defineAsyncComponent, ref, computed, reactive, onMounted } from 'vue';
|
||||
import { defineAsyncComponent, ref, computed, reactive, onMounted, unref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import screenfull from 'screenfull';
|
||||
@@ -90,7 +114,9 @@ import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
import other from '/@/utils/other';
|
||||
import mittBus from '/@/utils/mitt';
|
||||
import { Session, Local } from '/@/utils/storage';
|
||||
|
||||
import headerImage from '/@/assets/img/headerImage.png';
|
||||
import websocket from '/@/utils/websocket';
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
// 引入组件
|
||||
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
|
||||
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
|
||||
@@ -118,6 +144,21 @@ const layoutUserFlexNum = computed(() => {
|
||||
else num = '';
|
||||
return num;
|
||||
});
|
||||
|
||||
// 定义变量内容
|
||||
const { isSocketOpen } = storeToRefs(useUserInfo());
|
||||
|
||||
// websocket状态
|
||||
const onlinePopoverRef = ref()
|
||||
const onlineConfirmEvent = () => {
|
||||
if (!isSocketOpen.value) {
|
||||
websocket.is_reonnect = true
|
||||
websocket.reconnect_current = 1
|
||||
websocket.reconnect()
|
||||
}
|
||||
// 手动隐藏弹出
|
||||
unref(onlinePopoverRef).popperRef?.delayHide?.()
|
||||
}
|
||||
// 全屏点击时
|
||||
const onScreenfullClick = () => {
|
||||
if (!screenfull.isEnabled) {
|
||||
@@ -256,5 +297,29 @@ const messageCenter = messageCenterStore();
|
||||
:deep(.el-badge__content.is-fixed) {
|
||||
top: 12px;
|
||||
}
|
||||
.online-status{
|
||||
cursor: pointer;
|
||||
:deep(.el-badge__content.is-fixed) {
|
||||
top: 30px;
|
||||
font-size: 14px;
|
||||
left: 5px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
padding: 0;
|
||||
background-color: #18bc9c;
|
||||
}
|
||||
}
|
||||
.online-down{
|
||||
cursor: pointer;
|
||||
:deep(.el-badge__content.is-fixed) {
|
||||
top: 30px;
|
||||
font-size: 14px;
|
||||
left: 5px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
padding: 0;
|
||||
background-color: #979b9c;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
</transition>
|
||||
</router-view>
|
||||
<transition :name="setTransitionName" mode="out-in">
|
||||
<Iframes class="w100" v-show="isIframePage" :refreshKey="state.iframeRefreshKey" :name="setTransitionName" :list="state.iframeList" />
|
||||
<Iframes class="w100" v-show="isIframePage" :refreshKey="state.iframeRefreshKey" :name="setTransitionName"
|
||||
:list="state.iframeList" />
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutParentView">
|
||||
import { defineAsyncComponent, computed, reactive, onBeforeMount, onUnmounted, nextTick, watch, onMounted,ref,provide } from 'vue';
|
||||
import { defineAsyncComponent, computed, reactive, onBeforeMount, onUnmounted, nextTick, watch, onMounted, ref, provide } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useKeepALiveNames } from '/@/stores/keepAliveNames';
|
||||
@@ -44,13 +45,13 @@ const showView = ref(true)
|
||||
/**
|
||||
* 刷新页面
|
||||
*/
|
||||
const refreshView=function () {
|
||||
const refreshView = function () {
|
||||
showView.value = false // 通过v-if移除router-view节点
|
||||
nextTick(() => {
|
||||
showView.value = true // DOM更新后再通过v-if添加router-view节点
|
||||
})
|
||||
}
|
||||
provide('refreshView',refreshView)
|
||||
provide('refreshView', refreshView)
|
||||
|
||||
// 设置主界面切换动画
|
||||
const setTransitionName = computed(() => {
|
||||
@@ -58,6 +59,7 @@ const setTransitionName = computed(() => {
|
||||
});
|
||||
// 获取组件缓存列表(name值)
|
||||
const getKeepAliveNames = computed(() => {
|
||||
console.log(cachedViews.value)
|
||||
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
|
||||
});
|
||||
// 设置 iframe 显示/隐藏
|
||||
@@ -105,7 +107,7 @@ onMounted(() => {
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
mittBus.off('onTagsViewRefreshRouterView', () => {});
|
||||
mittBus.off('onTagsViewRefreshRouterView', () => { });
|
||||
});
|
||||
// 监听路由变化,防止 tagsView 多标签时,切换动画消失
|
||||
// https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/38/files
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
<div class="upgrade-content">
|
||||
{{ getThemeConfig.globalTitle }} {{ $t('message.upgrade.msg') }}
|
||||
<div class="mt5">
|
||||
<el-link type="primary" class="font12" href="https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md" target="_black">
|
||||
<el-link type="primary" class="font12" href="https://gitee.com/huge-dream/django-vue3-admin/blob/master/CHANGELOG.md" target="_black">
|
||||
CHANGELOG.md
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="upgrade-content-desc mt5">{{ $t('message.upgrade.desc') }}</div>
|
||||
</div>
|
||||
<div class="upgrade-btn">
|
||||
<el-button round size="default" type="info" text @click="onCancel">{{ $t('message.upgrade.btnOne') }}</el-button>
|
||||
<el-button type="primary" round size="default" @click="onUpgrade" :loading="state.isLoading">{{ state.btnTxt }}</el-button>
|
||||
<el-button round size="default" type="info" text @click="onCancel" >{{ $t('message.upgrade.btnOne') }}</el-button>
|
||||
<el-button type="primary" round size="default" @click="onUpgrade" :loading="state.isLoading" >{{ state.btnTxt }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -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,20 +67,24 @@ const onUpgrade = () => {
|
||||
Local.clear();
|
||||
window.location.reload();
|
||||
Local.set('version', state.version);
|
||||
Session.set('isUpgrade', false)
|
||||
}, 2000);
|
||||
};
|
||||
// 延迟显示,防止刷新时界面显示太快
|
||||
const delayShow = () => {
|
||||
const isUpgrade = Session.get('isUpgrade')===false?Session.get('isUpgrade'):true
|
||||
if(isUpgrade){
|
||||
setTimeout(() => {
|
||||
state.isUpgrade = true;
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
// delayShow();
|
||||
// setTimeout(() => {
|
||||
// state.btnTxt = t('message.upgrade.btnTwo');
|
||||
// }, 200);
|
||||
delayShow();
|
||||
setTimeout(() => {
|
||||
state.btnTxt = t('message.upgrade.btnTwo');
|
||||
}, 200);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import { directive } from '/@/utils/directive';
|
||||
import { i18n } from '/@/i18n/index';
|
||||
import { directive } from '/@/directive/index';
|
||||
import { i18n } from '/@/i18n';
|
||||
import other from '/@/utils/other';
|
||||
import '/@/assets/style/tailwind.css'; // 先引入tailwind css, 以免element-plus冲突
|
||||
import ElementPlus from 'element-plus';
|
||||
@@ -14,7 +14,7 @@ import piniaPersist from 'pinia-plugin-persist';
|
||||
// @ts-ignore
|
||||
import fastCrud from './settings.ts';
|
||||
import pinia from './stores';
|
||||
import permission from '/@/plugin/permission/index';
|
||||
import {RegisterPermission} from '/@/plugin/permission/index';
|
||||
// @ts-ignore
|
||||
import eIconPicker, { iconList, analyzingIconForIconfont } from 'e-icon-picker';
|
||||
import 'e-icon-picker/icon/default-icon/symbol.js'; //基本彩色图标库
|
||||
@@ -27,6 +27,11 @@ import iconfont from '/@/assets/iconfont/iconfont.json'; //引入json文件
|
||||
import '/@/assets/iconfont/iconfont.css'; //引入css
|
||||
// 自动注册插件
|
||||
import { scanAndInstallPlugins } from '/@/views/plugins/index';
|
||||
import VXETable from 'vxe-table'
|
||||
import 'vxe-table/lib/style.css'
|
||||
|
||||
import '/@/assets/style/reset.scss';
|
||||
import 'element-tree-line/dist/style.css'
|
||||
|
||||
let forIconfont = analyzingIconForIconfont(iconfont); //解析class
|
||||
iconList.addIcon(forIconfont.list); // 添加iconfont dvadmin3的icon
|
||||
@@ -47,7 +52,8 @@ pinia.use(piniaPersist);
|
||||
directive(app);
|
||||
other.elSvg(app);
|
||||
|
||||
app.use(permission);
|
||||
|
||||
app.use(VXETable)
|
||||
app.use(pinia).use(router).use(ElementPlus, { i18n: i18n.global.t }).use(i18n).use(VueGridLayout).use(fastCrud).mount('#app');
|
||||
|
||||
app.config.globalProperties.mittBus = mitt();
|
||||
|
||||
@@ -9,7 +9,7 @@ export default {
|
||||
return XEUtils.includeArrays(BtnPermission, value)
|
||||
}else if(typeof value === 'string'){
|
||||
const index = XEUtils.arrayIndexOf(BtnPermission, value)
|
||||
return index>0?true:false
|
||||
return index>-1?true:false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import permissionDirective from './directive.permission'
|
||||
import permissionFunc from './func.permission'
|
||||
const install = function (app:any) {
|
||||
export const RegisterPermission = function (app:any) {
|
||||
app.directive('permission', permissionDirective)
|
||||
app.provide('$hasPermissions',permissionFunc.hasPermissions)
|
||||
}
|
||||
|
||||
export default {
|
||||
install
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import {createRouter, createWebHashHistory} from 'vue-router';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import pinia from '/@/stores/index';
|
||||
import { storeToRefs } from 'pinia';
|
||||
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 { initFrontEndControlRoutes } from '/@/router/frontEnd';
|
||||
import { initBackEndControlRoutes } from '/@/router/backEnd';
|
||||
import {storeToRefs} from 'pinia';
|
||||
import {useKeepALiveNames} from '/@/stores/keepAliveNames';
|
||||
import {useRoutesList} from '/@/stores/routesList';
|
||||
import {useThemeConfig} from '/@/stores/themeConfig';
|
||||
import {Session} from '/@/utils/storage';
|
||||
import {dynamicRoutes, notFoundAndNoPower, staticRoutes} from '/@/router/route';
|
||||
import {initFrontEndControlRoutes} from '/@/router/frontEnd';
|
||||
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 方法。
|
||||
@@ -22,8 +25,8 @@ import { initBackEndControlRoutes } from '/@/router/backEnd';
|
||||
|
||||
// 读取 `/src/stores/themeConfig.ts` 是否开启后端控制路由配置
|
||||
const storesThemeConfig = useThemeConfig(pinia);
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
const { isRequestRoutes } = themeConfig.value;
|
||||
const {themeConfig} = storeToRefs(storesThemeConfig);
|
||||
const {isRequestRoutes} = themeConfig.value;
|
||||
|
||||
/**
|
||||
* 创建一个可以被 Vue 应用程序使用的路由实例
|
||||
@@ -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,7 +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
|
||||
@@ -71,10 +80,10 @@ export function formatTwoStageRoutes(arr: any) {
|
||||
v.meta['isDynamic'] = true;
|
||||
v.meta['isDynamicPath'] = v.path;
|
||||
}
|
||||
newArr[0].children.push({ ...v });
|
||||
newArr[0].children.push({...v});
|
||||
// 存 name 值,keep-alive 中 include 使用,实现路由的缓存
|
||||
// 路径:/@/layout/routerView/parent.vue
|
||||
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
|
||||
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive && v.component_name != "") {
|
||||
cacheList.push(v.name);
|
||||
const stores = useKeepALiveNames(pinia);
|
||||
stores.setCacheKeepAlive(cacheList);
|
||||
@@ -86,7 +95,7 @@ export function formatTwoStageRoutes(arr: any) {
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.configure({ showSpinner: false });
|
||||
NProgress.configure({showSpinner: false});
|
||||
if (to.meta.title) NProgress.start();
|
||||
const token = Session.get('token');
|
||||
if (to.path === '/login' && !token) {
|
||||
@@ -101,19 +110,21 @@ router.beforeEach(async (to, from, next) => {
|
||||
next('/home');
|
||||
NProgress.done();
|
||||
} else {
|
||||
|
||||
const storesRoutesList = useRoutesList(pinia);
|
||||
const { routesList } = storeToRefs(storesRoutesList);
|
||||
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();
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// 引入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 } from '@fast-crud/fast-extends';
|
||||
import {FsExtendsEditor, FsExtendsUploader,FsCropperUploader} from '@fast-crud/fast-extends';
|
||||
import '@fast-crud/fast-extends/dist/style.css';
|
||||
import {successNotification} from '/@/utils/message';
|
||||
import XEUtils from "xe-utils";
|
||||
export default {
|
||||
async install(app: any, options: any) {
|
||||
// 先安装ui
|
||||
@@ -17,8 +21,15 @@ export default {
|
||||
app.use(FastCrud, {
|
||||
//i18n, //i18n配置,可选,默认使用中文,具体用法请看demo里的 src/i18n/index.js 文件
|
||||
// 此处配置公共的dictRequest(字典请求)
|
||||
async dictRequest({ dict }: any) {
|
||||
return await request({ url: dict.url, params: dict.params || {} }); //根据dict的url,异步返回一个字典数组
|
||||
async dictRequest({dict}: any) {
|
||||
const {isTree} = dict
|
||||
//根据dict的url,异步返回一个字典数组
|
||||
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() {
|
||||
@@ -27,17 +38,25 @@ export default {
|
||||
//接口请求配置
|
||||
//你项目后台接口大概率与fast-crud所需要的返回结构不一致,所以需要配置此项
|
||||
//请参考文档http://fast-crud.docmirror.cn/api/crud-options/request.html
|
||||
transformQuery: ({ page, form, sort }: any) => {
|
||||
transformQuery: ({page, form, sort}: any) => {
|
||||
if (sort.asc !== undefined) {
|
||||
form['ordering'] = `${sort.asc ? '' : '-'}${sort.prop}`;
|
||||
}
|
||||
//转换为你pageRequest所需要的请求参数结构
|
||||
return { page: page.currentPage, limit: page.pageSize, ...form };
|
||||
return {page: page.currentPage, limit: page.pageSize, ...form};
|
||||
},
|
||||
transformRes: ({ res }: any) => {
|
||||
transformRes: ({res}: any) => {
|
||||
//将pageRequest的返回数据,转换为fast-crud所需要的格式
|
||||
//return {records,currentPage,pageSize,total};
|
||||
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.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: {
|
||||
@@ -63,11 +82,77 @@ export default {
|
||||
width: 300,
|
||||
},
|
||||
});
|
||||
setLogger({ level: 'error' });
|
||||
// 文件上传
|
||||
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'};
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
26
web/src/stores/btnPermission.ts
Normal file
26
web/src/stores/btnPermission.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {DictionaryStates} from "/@/stores/interface";
|
||||
import {request} from "/@/utils/service";
|
||||
|
||||
export const BtnPermissionStore = defineStore('BtnPermission', {
|
||||
state: (): DictionaryStates => ({
|
||||
data: []
|
||||
}),
|
||||
actions: {
|
||||
async getBtnPermissionStore() {
|
||||
request({
|
||||
url: '/api/system/menu_button/menu_button_all_permission/',
|
||||
method: 'get',
|
||||
}).then((ret: {
|
||||
data: []
|
||||
}) => {
|
||||
// 转换数据格式并保存到pinia
|
||||
let dataList = ret.data
|
||||
this.data=dataList
|
||||
})
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
20
web/src/stores/columnPermission.ts
Normal file
20
web/src/stores/columnPermission.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface DataItemType {
|
||||
field_name: string;
|
||||
is_create: boolean;
|
||||
is_query: boolean;
|
||||
is_update: boolean;
|
||||
}
|
||||
|
||||
export const useColumnPermission = defineStore('columnPermission', {
|
||||
state: () => ({
|
||||
permission: [] as DataItemType[],
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setPermissionData(data: DataItemType[]) {
|
||||
this.permission = data;
|
||||
},
|
||||
},
|
||||
});
|
||||
169
web/src/stores/frontendMenu.ts
Normal file
169
web/src/stores/frontendMenu.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2,11 +2,12 @@
|
||||
* 定义接口来定义对象的类型
|
||||
* `stores` 全部类型定义在这里
|
||||
*/
|
||||
import {useFrontendMenuStore} from "/@/stores/frontendMenu";
|
||||
|
||||
// 用户信息
|
||||
export interface UserInfosState {
|
||||
avatar: string;
|
||||
userName: string;
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
@@ -19,6 +20,7 @@ export interface UserInfosState {
|
||||
}
|
||||
export interface UserInfosStates {
|
||||
userInfos: UserInfosState;
|
||||
isSocketOpen: boolean
|
||||
}
|
||||
|
||||
// 路由缓存列表
|
||||
@@ -101,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>;
|
||||
}
|
||||
|
||||
30
web/src/stores/modules/dept.ts
Normal file
30
web/src/stores/modules/dept.ts
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user