Compare commits
292 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
967e4e76c2 | ||
|
|
f7b93c349e | ||
|
|
72f1d2eae7 | ||
|
|
592bc947a0 | ||
|
|
953fcc3437 | ||
|
|
13aa6dbb99 | ||
|
|
460ae171dd | ||
|
|
0d556bfb2b | ||
|
|
61ae6e8eb3 | ||
|
|
175b151f7f | ||
|
|
a50f73d466 | ||
|
|
eea9d320af | ||
|
|
15c4808bbb | ||
|
|
d2d6ba3460 | ||
|
|
4f0295acb1 | ||
|
|
255c405e59 | ||
|
|
96ad956efd | ||
|
|
b586b46016 | ||
|
|
875146e588 | ||
|
|
1ee709b9eb | ||
|
|
f89a5228cd | ||
|
|
795f621637 | ||
|
|
c5d7a70f46 | ||
|
|
8710b047b1 | ||
|
|
300b6c6bb8 | ||
|
|
296640cb2a | ||
|
|
24861fda42 | ||
|
|
0e3fac37e9 | ||
|
|
93d8d94049 | ||
|
|
9d4f007d48 | ||
|
|
4d17a7d9df | ||
|
|
c4c9a81ac8 | ||
|
|
d1223dddd3 | ||
|
|
3dd68e5d21 | ||
|
|
0668cb087f | ||
|
|
38494edea5 | ||
|
|
2f04f22904 | ||
|
|
0ccf2e3725 | ||
|
|
68cd42246d | ||
|
|
dc6ddda4ab | ||
|
|
08b56ee663 | ||
|
|
82d7193dcb | ||
|
|
4b609ed7e4 | ||
|
|
61066b8caa | ||
|
|
160c376f81 | ||
|
|
b96c220f1d | ||
|
|
42884685ff | ||
|
|
c126c704a4 | ||
|
|
662c314518 | ||
|
|
1715fcb4d8 | ||
|
|
58e611cb22 | ||
|
|
0b2fa1e92c | ||
|
|
125dfd4dd5 | ||
|
|
af7bfcd215 | ||
|
|
6917786864 | ||
|
|
cd60b3d9be | ||
|
|
cfae363f53 | ||
|
|
4f8686b35e | ||
|
|
1407a25b6a | ||
|
|
7c78c7e53f | ||
|
|
f1da0507dd | ||
|
|
df74c792f6 | ||
|
|
4c697ef9d4 | ||
|
|
ab1d1c87e2 | ||
|
|
b6b3b4a39a | ||
|
|
581e4cd92f | ||
|
|
fb76d4271b | ||
|
|
a304a49107 | ||
|
|
fc1ab98b2b | ||
|
|
4f176cf88a | ||
|
|
af5f3b7137 | ||
|
|
b03037b866 | ||
|
|
92f57f8608 | ||
|
|
5ab2aaa066 | ||
|
|
b5f50bdf30 | ||
|
|
256c6e4ab9 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@
|
||||
.idea
|
||||
|
||||
.history/
|
||||
.vscode/
|
||||
.vscode/
|
||||
web/package-lock.json
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](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)
|
||||
|
||||
[preview](https://demo.django-vue-admin.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)
|
||||
[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」**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](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.django-vue-admin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
|
||||
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
|
||||
|
||||
|
||||
|
||||
|
||||
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@@ -91,9 +91,10 @@ ENV/
|
||||
**/migrations/*.py
|
||||
!**/migrations/__init__.py
|
||||
*.pyc
|
||||
conf/
|
||||
conf/*
|
||||
!conf/env.example.py
|
||||
db.sqlite3
|
||||
media/
|
||||
__pypackages__/
|
||||
package-lock.json
|
||||
gunicorn.pid
|
||||
|
||||
@@ -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
|
||||
@@ -43,6 +44,9 @@ DEBUG = locals().get("DEBUG", True)
|
||||
ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"])
|
||||
|
||||
# Application definition
|
||||
CUSTOM_APPS = [
|
||||
"dvadmin.system",
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
@@ -54,10 +58,10 @@ INSTALLED_APPS = [
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"corsheaders", # 注册跨域app
|
||||
"dvadmin.system",
|
||||
"drf_yasg",
|
||||
"captcha",
|
||||
'channels',
|
||||
*locals().get("CUSTOM_APPS", []), # 所有项目里写的app需要在env.py文件里的CUSTOM_APPS中
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -165,9 +169,9 @@ CORS_ORIGIN_ALLOW_ALL = True
|
||||
# 允许cookie
|
||||
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作
|
||||
|
||||
# ================================================= #
|
||||
# ===================================================== #
|
||||
# ********************* channels配置 ******************* #
|
||||
# ================================================= #
|
||||
# ===================================================== #
|
||||
ASGI_APPLICATION = 'application.asgi.application'
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
@@ -187,69 +191,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"
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -261,6 +279,7 @@ LOGGING = {
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.JSONParser',
|
||||
'rest_framework.parsers.MultiPartParser',
|
||||
),
|
||||
"DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式配置
|
||||
"DATE_FORMAT": "%Y-%m-%d",
|
||||
@@ -288,11 +307,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),
|
||||
# 设置前缀
|
||||
@@ -328,11 +345,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 = (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -44,6 +44,6 @@ LOGIN_NO_CAPTCHA_AUTH = True
|
||||
# ================================================= #
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# daphne启动命令
|
||||
#daphne application.asgi:application -b 0.0.0.0 -p 8000
|
||||
CUSTOM_APPS = [
|
||||
"dvadmin.system",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,8 +6,11 @@ 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, \
|
||||
from dvadmin.system.models import (
|
||||
Role, Dept, Users, Menu, MenuButton,
|
||||
ApiWhiteList, Dictionary, SystemConfig,
|
||||
RoleMenuPermission, RoleMenuButtonPermission
|
||||
)
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
|
||||
|
||||
@@ -50,7 +53,6 @@ class MenuButtonInitSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
|
||||
class MenuInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
递归深度获取数信息(用于生成初始化json文件)
|
||||
@@ -139,8 +141,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 +156,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 +170,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 +188,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 +198,6 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ApiWhiteListInitSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化获取数信息(用于生成初始化json文件)
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
"is_link": false,
|
||||
"is_catalog": true,
|
||||
"web_path": "/system",
|
||||
"component": "layout/routerView/parent",
|
||||
"component_name": "menu",
|
||||
"component": "",
|
||||
"component_name": "",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "菜单管理",
|
||||
@@ -25,7 +24,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 19,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -57,6 +55,30 @@
|
||||
"value": "menu:Delete",
|
||||
"api": "/api/system/menu/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "查询所有",
|
||||
"value": "menu:SearchAll",
|
||||
"api": "/api/system/menu/get_all_menu/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "路由",
|
||||
"value": "menu:router",
|
||||
"api": "/api/system/menu/web_router/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "上移",
|
||||
"value": "menu:MoveUp",
|
||||
"api": "/api/system/menu/mode_up/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "下移",
|
||||
"value": "menu:MoveDown",
|
||||
"api": "/api/system/menu/mode_down/",
|
||||
"method": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -72,7 +94,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": false,
|
||||
"parent": 19,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -113,7 +134,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 19,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -145,6 +165,36 @@
|
||||
"value": "dept:Delete",
|
||||
"api": "/api/system/dept/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "查询所有",
|
||||
"value": "dept:SearchAll",
|
||||
"api": "/api/system/dept/all_dept/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "懒加载查询所有",
|
||||
"value": "dept:LazySearchAll",
|
||||
"api": "/api/system/dept/dept_lazy_tree/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "上移",
|
||||
"value": "dept:MoveUp",
|
||||
"api": "/api/system/dept/mode_up/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "下移",
|
||||
"value": "dept:MoveDown",
|
||||
"api": "/api/system/dept/mode_down/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "头信息",
|
||||
"value": "dept:HeaderInfo",
|
||||
"api": "/api/system/dept/dept_info/",
|
||||
"method": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -160,7 +210,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 19,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -201,6 +250,64 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "列管理",
|
||||
"icon": "iconfont icon-bolangneng",
|
||||
"sort": 5,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/columns",
|
||||
"component": "system/columns/index",
|
||||
"component_name": "columns",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "column:Search",
|
||||
"api": "/api/system/column/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "column:Retrieve",
|
||||
"api": "/api/system/column/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "column:Create",
|
||||
"api": "/api/system/column/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "column:Update",
|
||||
"api": "/api/system/column/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "column:Delete",
|
||||
"api": "/api/system/column/{id}/",
|
||||
"method": 3
|
||||
},
|
||||
{
|
||||
"name": "所有模型表",
|
||||
"value": "column:AllModel",
|
||||
"api": "/api/system/column/get_models/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "自动匹配所有字段",
|
||||
"value": "column:AutoMatch",
|
||||
"api": "/api/system/column/auto_match_fields/",
|
||||
"method": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "用户管理",
|
||||
"icon": "iconfont icon-icon-",
|
||||
@@ -213,7 +320,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 19,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -284,7 +390,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 19,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -331,7 +436,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 19,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -376,12 +480,11 @@
|
||||
"is_link": false,
|
||||
"is_catalog": true,
|
||||
"web_path": "/generalConfig",
|
||||
"component": "layout/routerView/parent",
|
||||
"component_name": "config",
|
||||
"component": "",
|
||||
"component_name": "",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "系统配置",
|
||||
@@ -395,7 +498,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 27,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -442,7 +544,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 27,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -489,7 +590,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 27,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -536,7 +636,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 27,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -575,12 +674,11 @@
|
||||
"is_link": false,
|
||||
"is_catalog": true,
|
||||
"web_path": "/log",
|
||||
"component": "layout/routerView/parent",
|
||||
"component_name": "log",
|
||||
"component": "",
|
||||
"component_name": "",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "登录日志",
|
||||
@@ -594,7 +692,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 32,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
@@ -623,7 +720,6 @@
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 32,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
|
||||
@@ -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白名单
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
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
|
||||
from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(0, "禁用"),
|
||||
(1, "启用"),
|
||||
)
|
||||
|
||||
class Role(CoreModel):
|
||||
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
|
||||
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"
|
||||
verbose_name = "角色表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
|
||||
|
||||
class CustomUserManager(UserManager):
|
||||
|
||||
def create_superuser(self, username, email=None, password=None, **extra_fields):
|
||||
user = super(CustomUserManager, self).create_superuser(username, email, password, **extra_fields)
|
||||
user.set_password(password)
|
||||
try:
|
||||
user.role.add(Role.objects.get(name="管理员"))
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
except ObjectDoesNotExist:
|
||||
user.delete()
|
||||
raise ValidationError("角色`管理员`不存在, 创建失败, 请先执行python manage.py init")
|
||||
|
||||
|
||||
class Users(CoreModel, AbstractUser):
|
||||
@@ -48,6 +71,7 @@ class Users(CoreModel, AbstractUser):
|
||||
blank=True,
|
||||
help_text="关联部门",
|
||||
)
|
||||
objects = CustomUserManager()
|
||||
|
||||
def set_password(self, raw_password):
|
||||
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
|
||||
@@ -76,24 +100,9 @@ class Post(CoreModel):
|
||||
ordering = ("sort",)
|
||||
|
||||
|
||||
class Role(CoreModel):
|
||||
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
|
||||
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"
|
||||
verbose_name = "角色表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("sort",)
|
||||
|
||||
|
||||
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="联系电话")
|
||||
@@ -171,6 +180,24 @@ class Menu(CoreModel):
|
||||
ordering = ("sort",)
|
||||
|
||||
|
||||
class Columns(CoreModel):
|
||||
role = models.ForeignKey(to='Role', on_delete=models.CASCADE, verbose_name='角色', db_constraint=False)
|
||||
app = models.CharField(max_length=64, verbose_name='应用名')
|
||||
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='字段显示名')
|
||||
is_query = models.BooleanField(default=1, verbose_name='是否可查询')
|
||||
is_create = models.BooleanField(default=1, verbose_name='是否可创建')
|
||||
is_update = models.BooleanField(default=1, verbose_name='是否可更新')
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_columns"
|
||||
verbose_name = "列权限表"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("id",)
|
||||
|
||||
|
||||
class MenuButton(CoreModel):
|
||||
menu = models.ForeignKey(
|
||||
to="Menu",
|
||||
@@ -221,7 +248,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):
|
||||
@@ -274,8 +301,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",
|
||||
@@ -339,12 +365,16 @@ class OperationLog(CoreModel):
|
||||
def media_file_name(instance, filename):
|
||||
h = instance.md5sum
|
||||
basename, ext = os.path.splitext(filename)
|
||||
return os.path.join("files", h[0:1], h[1:2], h + ext.lower())
|
||||
return os.path.join("files", h[:1], h[1:2], h + ext.lower())
|
||||
|
||||
|
||||
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):
|
||||
@@ -353,6 +383,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:
|
||||
|
||||
@@ -16,6 +16,7 @@ from dvadmin.system.views.role_menu import RoleMenuPermissionViewSet
|
||||
from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermissionViewSet
|
||||
from dvadmin.system.views.system_config import SystemConfigViewSet
|
||||
from dvadmin.system.views.user import UserViewSet
|
||||
from dvadmin.system.views.column import ColumnViewSet
|
||||
|
||||
system_url = routers.SimpleRouter()
|
||||
system_url.register(r'menu', MenuViewSet)
|
||||
@@ -29,11 +30,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', ColumnViewSet)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
98
backend/dvadmin/system/views/column.py
Normal file
98
backend/dvadmin/system/views/column.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Columns, Role
|
||||
from dvadmin.utils.models import get_custom_app_models
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse
|
||||
|
||||
|
||||
class ColumnSerializer(CustomModelSerializer):
|
||||
"""
|
||||
列权限序列化器
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Columns
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class ColumnViewSet(CustomModelViewSet):
|
||||
"""
|
||||
列权限视图集
|
||||
"""
|
||||
queryset = Columns.objects.all()
|
||||
serializer_class = ColumnSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
role_id = request.query_params.get('role')
|
||||
app_name = request.query_params.get('app')
|
||||
model_name = request.query_params.get('model')
|
||||
menu = request.query_params.get('menu')
|
||||
if not role_id or not model_name or not app_name or not menu:
|
||||
return SuccessResponse([])
|
||||
queryset = self.filter_queryset(self.get_queryset().filter(role_id=role_id, model=model_name, app=app_name,menu_id=menu))
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True, request=request)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
payload = request.data
|
||||
for model in get_custom_app_models(payload.get('app')):
|
||||
if payload.get('model') == model['model']:
|
||||
break
|
||||
else:
|
||||
return ErrorResponse(msg='模型表不存在')
|
||||
|
||||
if Columns.objects.filter(app=model['app'], model=model['model'], 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 app in get_custom_app_models():
|
||||
for model in app:
|
||||
res.append({
|
||||
'app': model['app'],
|
||||
'title': model['verbose'],
|
||||
'key': model['model']
|
||||
})
|
||||
return DetailResponse(res)
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def auto_match_fields(self, request):
|
||||
"""自动匹配已有的字段"""
|
||||
role_id = request.data.get('role')
|
||||
app_name = request.data.get('app')
|
||||
model_name = request.data.get('model')
|
||||
if not role_id or not model_name or not app_name:
|
||||
return DetailResponse([], msg='无操作')
|
||||
for model in get_custom_app_models(app_name):
|
||||
if model['model'] != model_name:
|
||||
continue
|
||||
for field in model['fields']:
|
||||
if Columns.objects.filter(
|
||||
role_id=role_id, app=app_name, model=model_name, field_name=field['name']
|
||||
).exists():
|
||||
continue
|
||||
data = {
|
||||
'role': role_id,
|
||||
'app': app_name,
|
||||
'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='匹配成功')
|
||||
@@ -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()
|
||||
@@ -123,33 +127,114 @@ class DeptViewSet(CustomModelViewSet):
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
|
||||
def dept_lazy_tree(self, request, *args, **kwargs):
|
||||
parent = self.request.query_params.get('parent')
|
||||
is_superuser = request.user.is_superuser
|
||||
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)
|
||||
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):
|
||||
params = self.request.query_params
|
||||
parent = params.get('parent', None)
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(status=1, parent=parent)
|
||||
if self.action =='list':
|
||||
params = self.request.query_params
|
||||
parent = params.get('parent', None)
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(parent=parent)
|
||||
else:
|
||||
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:
|
||||
queryset = self.queryset.filter(status=1, parent__isnull=True)
|
||||
return queryset
|
||||
return self.queryset
|
||||
|
||||
|
||||
class InitDictionaryViewSet(APIView):
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
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
|
||||
|
||||
@@ -9,15 +15,52 @@ class FileSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
return '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)
|
||||
|
||||
|
||||
@@ -33,4 +76,4 @@ class FileViewSet(CustomModelViewSet):
|
||||
queryset = FileList.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filter_fields = ['name', ]
|
||||
permission_classes = []
|
||||
permission_classes = []
|
||||
@@ -1,11 +1,9 @@
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from captcha.views import CaptchaStore, captcha_image
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg import openapi
|
||||
@@ -14,9 +12,7 @@ from rest_framework import serializers
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import Users
|
||||
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
|
||||
@@ -58,6 +54,7 @@ class LoginSerializer(TokenObtainPairSerializer):
|
||||
captcha = serializers.CharField(
|
||||
max_length=6, required=False, allow_null=True, allow_blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Users
|
||||
fields = "__all__"
|
||||
@@ -79,13 +76,18 @@ class LoginSerializer(TokenObtainPairSerializer):
|
||||
raise CustomValidationError("验证码过期")
|
||||
else:
|
||||
if self.image_code and (
|
||||
self.image_code.response == captcha
|
||||
or self.image_code.challenge == captcha
|
||||
self.image_code.response == captcha
|
||||
or self.image_code.challenge == captcha
|
||||
):
|
||||
self.image_code and self.image_code.delete()
|
||||
else:
|
||||
self.image_code and self.image_code.delete()
|
||||
raise CustomValidationError("图片验证码错误")
|
||||
|
||||
user = Users.objects.get(username=attrs['username'])
|
||||
if not user.is_active:
|
||||
raise CustomValidationError("账号被锁定")
|
||||
|
||||
data = super().validate(attrs)
|
||||
data["name"] = self.user.name
|
||||
data["userId"] = self.user.id
|
||||
@@ -107,6 +109,7 @@ class LoginSerializer(TokenObtainPairSerializer):
|
||||
save_login_log(request=request)
|
||||
return {"code": 2000, "msg": "请求成功", "data": data}
|
||||
|
||||
|
||||
class LoginView(TokenObtainPairView):
|
||||
"""
|
||||
登录接口
|
||||
|
||||
@@ -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,15 +49,18 @@ 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__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class WebRouterSerializer(CustomModelSerializer):
|
||||
"""
|
||||
前端菜单路由的简单序列化器
|
||||
@@ -63,11 +68,11 @@ class WebRouterSerializer(CustomModelSerializer):
|
||||
path = serializers.CharField(source="web_path")
|
||||
title = serializers.CharField(source="name")
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ('id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
|
||||
'component_name', 'cache', 'visible')
|
||||
fields = (
|
||||
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
|
||||
'component_name', 'cache', 'visible', 'status')
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
@@ -86,13 +91,50 @@ class MenuViewSet(CustomModelViewSet):
|
||||
update_serializer_class = MenuCreateSerializer
|
||||
search_fields = ['name', 'status']
|
||||
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
|
||||
# extra_filter_class = []
|
||||
|
||||
def list(self, request):
|
||||
"""懒加载"""
|
||||
request.query_params._mutable = True
|
||||
params = request.query_params
|
||||
parent = params.get('parent', None)
|
||||
page = params.get('page', None)
|
||||
limit = params.get('limit', None)
|
||||
if page:
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter()
|
||||
else:
|
||||
queryset = self.queryset.filter(parent__isnull=True)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
serializer = MenuSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[])
|
||||
def web_router(self, request):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
queryset = self.queryset.filter(status=1)
|
||||
is_admin = user.role.values_list('admin', flat=True)
|
||||
if user.is_superuser or True in is_admin:
|
||||
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')
|
||||
@@ -101,25 +143,32 @@ class MenuViewSet(CustomModelViewSet):
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
def list(self,request):
|
||||
"""懒加载"""
|
||||
request.query_params._mutable = True
|
||||
params = request.query_params
|
||||
parent = params.get('parent', None)
|
||||
page = params.get('page',None)
|
||||
limit = params.get('limit', None)
|
||||
if page:
|
||||
del params['page']
|
||||
if limit:
|
||||
del params['limit']
|
||||
if params:
|
||||
if parent:
|
||||
queryset = self.queryset.filter(status=1, parent=parent)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=1)
|
||||
else:
|
||||
queryset = self.queryset.filter(status=1, parent__isnull=True)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
serializer = MenuSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data)
|
||||
@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="下移成功")
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
def get_is_read(self, instance):
|
||||
user_id = self.request.user.id
|
||||
message_center_id = instance.id
|
||||
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id,users_id=user_id).first()
|
||||
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id, users_id=user_id).first()
|
||||
if queryset:
|
||||
return queryset.is_read
|
||||
return False
|
||||
@@ -95,21 +95,22 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
def websocket_push(user_id, message):
|
||||
"""
|
||||
主动推送消息
|
||||
"""
|
||||
username = "user_"+str(user_id)
|
||||
print(103,message)
|
||||
username = "user_" + str(user_id)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
username,
|
||||
{
|
||||
"type": "push.message",
|
||||
"json": message
|
||||
}
|
||||
username,
|
||||
{
|
||||
"type": "push.message",
|
||||
"json": message
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
消息中心-新增-序列化器
|
||||
@@ -122,10 +123,10 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
# 在保存之前,根据目标类型,把目标用户查询出来并保存
|
||||
users = initial_data.get('target_user', [])
|
||||
if target_type in [1]: # 按角色
|
||||
target_role = initial_data.get('target_role',[])
|
||||
target_role = initial_data.get('target_role', [])
|
||||
users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
|
||||
if target_type in [2]: # 按部门
|
||||
target_dept = initial_data.get('target_dept',[])
|
||||
target_dept = initial_data.get('target_dept', [])
|
||||
users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
|
||||
if target_type in [3]: # 系统通知
|
||||
users = Users.objects.values_list('id', flat=True)
|
||||
@@ -141,7 +142,7 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
for user in users:
|
||||
unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count()
|
||||
websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM',
|
||||
"content": '您有一条新消息~', "unread": unread_count})
|
||||
"content": '您有一条新消息~', "unread": unread_count})
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
@@ -184,7 +185,7 @@ class MessageCenterViewSet(CustomModelViewSet):
|
||||
# 主动推送消息
|
||||
unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count()
|
||||
websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT',
|
||||
"content": '您查看了一条消息~', "unread": unread_count})
|
||||
"content": '您查看了一条消息~', "unread": unread_count})
|
||||
return DetailResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
@@ -195,7 +196,6 @@ class MessageCenterViewSet(CustomModelViewSet):
|
||||
self_user_id = self.request.user.id
|
||||
# queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime')
|
||||
queryset = MessageCenter.objects.filter(target_user__id=self_user_id)
|
||||
print(queryset)
|
||||
# queryset = self.filter_queryset(queryset)
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
角色管理 创建/更新时的列化器
|
||||
@@ -61,7 +59,7 @@ class RoleCreateUpdateSerializer(CustomModelSerializer):
|
||||
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,13 @@
|
||||
@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, Columns
|
||||
from dvadmin.system.views.menu import MenuSerializer
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -20,27 +22,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 +42,74 @@ 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 RoleColumnsSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Columns
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class RoleMenuPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单和按钮权限
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
btns = serializers.SerializerMethodField()
|
||||
columns = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
return RoleMenuPermission.objects.filter(
|
||||
menu__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).exists()
|
||||
|
||||
def get_btns(self, instance):
|
||||
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
|
||||
serializer = RoleButtonPermissionSerializer(btn_list,many=True,request=self.request)
|
||||
return serializer.data
|
||||
|
||||
def get_columns(self, instance):
|
||||
params = self.request.query_params
|
||||
col_list = Columns.objects.filter(role__id=params.get('role'),menu__id=instance['id'])
|
||||
serializer = RoleColumnsSerializer(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 +126,71 @@ 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)
|
||||
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').all()
|
||||
serializer = RoleMenuPermissionSerializer(queryset,many=True,request=request)
|
||||
data = serializer.data
|
||||
return DetailResponse(data=data)
|
||||
|
||||
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
|
||||
def set_role_premission(self,request,pk):
|
||||
"""
|
||||
对角色的菜单和按钮及按钮范围授权:
|
||||
:param request:
|
||||
:param pk: role
|
||||
:return:
|
||||
"""
|
||||
body = request.data
|
||||
RoleMenuPermission.objects.filter(role=pk).delete()
|
||||
RoleMenuButtonPermission.objects.filter(role=pk).delete()
|
||||
for menu in body:
|
||||
if menu.get('isCheck'):
|
||||
menu_parent = Menu.objects.filter(id=menu.get('id')).values('parent').first()
|
||||
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu_parent.get('parent'))
|
||||
RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
|
||||
for btn in menu.get('btns'):
|
||||
if btn.get('isCheck'):
|
||||
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),data_range=btn.get('data_range'))
|
||||
instance.dept.set(btn.get('dept',[]))
|
||||
for col in menu.get('columns'):
|
||||
Columns.objects.filter(id=col.get('id')).update(is_query=col.get('is_query'),is_create=col.get('is_create'),is_update=col.get('is_update'))
|
||||
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:
|
||||
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 +228,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:
|
||||
@@ -201,44 +296,70 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
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)
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
if params:
|
||||
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'),
|
||||
name=F('dept__name'),
|
||||
parent=F('dept__parent')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
else:
|
||||
if not params:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
menu_button = params.get('menu_button')
|
||||
if menu_button is None:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values(
|
||||
dept_id=F('dept__id'),
|
||||
name=F('dept__name'),
|
||||
parent=F('dept__parent')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
|
||||
|
||||
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
|
||||
def menu_to_button(self,request):
|
||||
@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)
|
||||
if menu_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
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(
|
||||
menu_id = params.get('menu', None)
|
||||
if menu_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
is_superuser = request.user.is_superuser
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button'
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
else:
|
||||
if params:
|
||||
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_menu(self, request):
|
||||
"""
|
||||
获取角色对应的按钮权限
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
|
||||
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
@@ -61,10 +61,10 @@ class SystemConfigChinldernSerializer(CustomModelSerializer):
|
||||
"""
|
||||
系统配置子级-序列化器
|
||||
"""
|
||||
chinldern = serializers.SerializerMethodField()
|
||||
children = serializers.SerializerMethodField()
|
||||
form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True)
|
||||
|
||||
def get_chinldern(self, instance):
|
||||
def get_children(self, instance):
|
||||
queryset = SystemConfig.objects.filter(parent=instance)
|
||||
serializer = SystemConfigSerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -41,6 +42,7 @@ class UserSerializer(CustomModelSerializer):
|
||||
exclude = ["password"]
|
||||
extra_kwargs = {
|
||||
"post": {"required": False},
|
||||
"mobile": {"required": False},
|
||||
}
|
||||
|
||||
def get_dept_name_all(self, instance):
|
||||
@@ -60,9 +62,6 @@ class UserSerializer(CustomModelSerializer):
|
||||
return serializer.data
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class UserCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
用户新增-序列化器
|
||||
@@ -82,10 +81,10 @@ class UserCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
对密码进行验证
|
||||
"""
|
||||
password = self.initial_data.get("password")
|
||||
if password:
|
||||
return make_password(value)
|
||||
return value
|
||||
md5 = hashlib.md5()
|
||||
md5.update(value.encode('utf-8'))
|
||||
md5_password = md5.hexdigest()
|
||||
return make_password(md5_password)
|
||||
|
||||
def save(self, **kwargs):
|
||||
data = super().save(**kwargs)
|
||||
@@ -100,6 +99,7 @@ class UserCreateSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
extra_kwargs = {
|
||||
"post": {"required": False},
|
||||
"mobile": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
@@ -114,14 +114,15 @@ class UserUpdateSerializer(CustomModelSerializer):
|
||||
CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一")
|
||||
],
|
||||
)
|
||||
|
||||
# password = serializers.CharField(required=False, allow_blank=True)
|
||||
mobile = serializers.CharField(
|
||||
max_length=50,
|
||||
validators=[
|
||||
CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
|
||||
],
|
||||
allow_blank=True
|
||||
)
|
||||
# mobile = serializers.CharField(
|
||||
# max_length=50,
|
||||
# validators=[
|
||||
# CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
|
||||
# ],
|
||||
# allow_blank=True
|
||||
# )
|
||||
|
||||
def save(self, **kwargs):
|
||||
data = super().save(**kwargs)
|
||||
@@ -136,6 +137,7 @@ class UserUpdateSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
extra_kwargs = {
|
||||
"post": {"required": False, "read_only": True},
|
||||
"mobile": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +161,7 @@ class UserInfoUpdateSerializer(CustomModelSerializer):
|
||||
fields = ['email', 'mobile', 'avatar', 'name', 'gender']
|
||||
extra_kwargs = {
|
||||
"post": {"required": False, "read_only": True},
|
||||
"mobile": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
@@ -194,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()
|
||||
@@ -229,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": "用户账号",
|
||||
@@ -262,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"}},
|
||||
}
|
||||
@@ -293,6 +297,11 @@ class UserViewSet(CustomModelViewSet):
|
||||
'dept_id': dept.id,
|
||||
'dept_name': dept.name
|
||||
}
|
||||
else:
|
||||
result['dept_info'] = {
|
||||
'dept_id': None,
|
||||
'dept_name': "暂无部门"
|
||||
}
|
||||
role = getattr(user, 'role', None)
|
||||
if role:
|
||||
result['role_info'] = role.values('id', 'name', 'key')
|
||||
@@ -317,11 +326,10 @@ 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()
|
||||
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(new_pwd)
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
@@ -344,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")
|
||||
@@ -357,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):
|
||||
user.last_login = timezone.now()
|
||||
user.save()
|
||||
return 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,12 +38,16 @@ def CustomExceptionHandler(ex, context):
|
||||
# 调用默认的异常处理函数
|
||||
response = exception_handler(ex, context)
|
||||
if isinstance(ex, AuthenticationFailed):
|
||||
code = 401
|
||||
code_type = response.data.get('detail').code
|
||||
if code_type == 'no_active_account':
|
||||
code=400
|
||||
# 如果是身份验证错误
|
||||
if response and response.data.get('detail') == "Given token not valid for any token type":
|
||||
code = 401
|
||||
msg = ex.detail
|
||||
elif response and response.data.get('detail') == "Token is blacklisted":
|
||||
# token在黑名单
|
||||
return ErrorResponse(status=HTTP_401_UNAUTHORIZED)
|
||||
msg = ex.detail
|
||||
else:
|
||||
code = 401
|
||||
msg = ex.detail
|
||||
elif isinstance(ex,Http404):
|
||||
code = 400
|
||||
msg = "接口地址不正确"
|
||||
|
||||
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 rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Columns
|
||||
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 app in get_custom_app_models():
|
||||
for model in app:
|
||||
if model['object'] is self.serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded:
|
||||
break
|
||||
if finded is False:
|
||||
return []
|
||||
roles = request.user.role.values_list('id', flat=True)
|
||||
user = request.user
|
||||
if user.is_superuser==1:
|
||||
data = Columns.objects.filter(app=model['app'], model=model['model']).values('field_name', 'is_create', 'is_query', 'is_update')
|
||||
for item in data:
|
||||
item['is_create'] = True
|
||||
item['is_query'] = True
|
||||
item['is_update'] = True
|
||||
else:
|
||||
data= Columns.objects.filter(
|
||||
app=model['app'], model=model['model'],role__in=roles
|
||||
).values('field_name', 'is_create', 'is_query', 'is_update')
|
||||
return DetailResponse(data=data)
|
||||
@@ -75,7 +75,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,74 +86,80 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
如果不是超级管理员,则进入下一步权限判断
|
||||
"""
|
||||
if request.user.is_superuser == 0:
|
||||
# 0. 获取用户的部门id,没有部门则返回空
|
||||
user_dept_id = getattr(request.user, "dept_id", None)
|
||||
if not user_dept_id:
|
||||
return queryset.none()
|
||||
|
||||
# 1. 判断过滤的数据是否有创建人所在部门 "dept_belong_id" 字段
|
||||
if not getattr(queryset.model, "dept_belong_id", None):
|
||||
return queryset
|
||||
|
||||
# 2. 如果用户没有关联角色则返回本部门数据
|
||||
if not hasattr(request.user, "role"):
|
||||
return queryset.filter(dept_belong_id=user_dept_id)
|
||||
|
||||
# 3. 根据所有角色 获取所有权限范围
|
||||
# (0, "仅本人数据权限"),
|
||||
# (1, "本部门及以下数据权限"),
|
||||
# (2, "本部门数据权限"),
|
||||
# (3, "全部数据权限"),
|
||||
# (4, "自定数据权限")
|
||||
replace_str = re.compile('\d')
|
||||
re_api = replace_str.sub('{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')
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
if 3 == ele.get("data_range") or ele.get("role_admin") == True:
|
||||
return queryset
|
||||
dataScope_list.append(ele.get("data_range"))
|
||||
dataScope_list = list(set(dataScope_list))
|
||||
|
||||
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
|
||||
if 0 in dataScope_list:
|
||||
return queryset.filter(
|
||||
creator=request.user, dept_belong_id=user_dept_id
|
||||
)
|
||||
|
||||
# 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:
|
||||
dept_list.append(user_dept_id)
|
||||
dept_list.extend(
|
||||
get_dept(
|
||||
user_dept_id,
|
||||
)
|
||||
)
|
||||
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)))
|
||||
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:
|
||||
return queryset.none()
|
||||
|
||||
# 1. 判断过滤的数据是否有创建人所在部门 "dept_belong_id" 字段
|
||||
if not getattr(queryset.model, "dept_belong_id", None):
|
||||
return queryset
|
||||
|
||||
# 2. 如果用户没有关联角色则返回本部门数据
|
||||
if not hasattr(request.user, "role"):
|
||||
return queryset.filter(dept_belong_id=user_dept_id)
|
||||
|
||||
# 3. 根据所有角色 获取所有权限范围
|
||||
# (0, "仅本人数据权限"),
|
||||
# (1, "本部门及以下数据权限"),
|
||||
# (2, "本部门数据权限"),
|
||||
# (3, "全部数据权限"),
|
||||
# (4, "自定数据权限")
|
||||
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')
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
if ele.get("data_range") == 3 or ele.get("role_admin") == True:
|
||||
return queryset
|
||||
dataScope_list.append(ele.get("data_range"))
|
||||
dataScope_list = list(set(dataScope_list))
|
||||
|
||||
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
|
||||
if 0 in dataScope_list:
|
||||
return queryset.filter(
|
||||
creator=request.user, dept_belong_id=user_dept_id
|
||||
)
|
||||
|
||||
# 5. 自定数据权限 获取部门,根据部门过滤
|
||||
dept_list = []
|
||||
for ele in dataScope_list:
|
||||
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_list.extend(
|
||||
request.user.role.filter(status=1).values_list(
|
||||
"dept__id", flat=True
|
||||
)
|
||||
)
|
||||
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)))
|
||||
|
||||
|
||||
class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
lookup_prefixes = {
|
||||
@@ -164,12 +170,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
"~": "icontains",
|
||||
}
|
||||
|
||||
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 field_name.endswith(lookup):
|
||||
return field_name
|
||||
return LOOKUP_SEP.join([field_name, lookup])
|
||||
|
||||
def find_filter_lookups(self, orm_lookups, search_term_key):
|
||||
@@ -249,7 +257,10 @@ 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)
|
||||
|
||||
@@ -298,7 +309,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
for search_field in filterset.filters:
|
||||
if isinstance(filterset.filters[search_field], CharFilter):
|
||||
orm_lookups.append(
|
||||
self.construct_search(six.text_type(search_field))
|
||||
self.construct_search(six.text_type(search_field), filterset.filters[search_field].lookup_expr)
|
||||
)
|
||||
else:
|
||||
orm_lookups.append(search_field)
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import logging
|
||||
import os.path
|
||||
from logging import LogRecord
|
||||
|
||||
from django.core.servers.basehttp import WSGIRequestHandler
|
||||
from loguru import logger
|
||||
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# 1.🎖️先声明一个类继承logging.Handler(制作一件品如的衣服)
|
||||
from loguru._defaults import LOGURU_FORMAT
|
||||
|
||||
|
||||
class InterceptTimedRotatingFileHandler(RotatingFileHandler):
|
||||
"""
|
||||
自定义反射时间回滚日志记录器
|
||||
缺少命名空间
|
||||
"""
|
||||
|
||||
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"):
|
||||
super(InterceptTimedRotatingFileHandler, self).__init__(filename)
|
||||
filename = os.path.abspath(filename)
|
||||
when = when.lower()
|
||||
# 2.🎖️需要本地用不同的文件名做为不同日志的筛选器
|
||||
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="<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>",
|
||||
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 = "-"
|
||||
locals_self = frame.f_locals.get('self', None)
|
||||
msg = self.format(record)
|
||||
if locals_self and hasattr(locals_self, 'client_address'):
|
||||
ip, port = locals_self.client_address
|
||||
# - 127.0.0.1:56525 -
|
||||
msg = f"{ip}:{port} - {msg}"
|
||||
self.logger_ \
|
||||
.opt(depth=depth, exception=record.exc_info, colors=True) \
|
||||
.bind(ip=ip, port=port) \
|
||||
.log(level, msg)
|
||||
@@ -6,22 +6,20 @@
|
||||
@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 +38,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)
|
||||
|
||||
|
||||
@@ -86,8 +84,6 @@ class CoreModel(models.Model):
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
|
||||
|
||||
def get_all_models_objects(model_name=None):
|
||||
"""
|
||||
获取所有 models 对象
|
||||
@@ -111,4 +107,39 @@ def get_all_models_objects(model_name=None):
|
||||
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
|
||||
if model_name:
|
||||
return settings.ALL_MODELS_OBJECTS[model_name] or {}
|
||||
return settings.ALL_MODELS_OBJECTS 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)
|
||||
res = []
|
||||
for app in settings.CUSTOM_APPS:
|
||||
res.append(get_model_from_app(app))
|
||||
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,9 @@ 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)
|
||||
('data', data),
|
||||
('permission', self.request.permission_fields)
|
||||
]))
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
@Created on: 2021/6/1 001 22:57
|
||||
@Remark: 自定义视图集
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.db import transaction
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
@@ -18,6 +16,8 @@ from dvadmin.utils.filters import DataLevelPermissionsFilter
|
||||
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.permission import CustomPermission
|
||||
from dvadmin.utils.models import get_custom_app_models
|
||||
from dvadmin.system.models import Columns
|
||||
from django_restql.mixins import QueryArgumentsMixin
|
||||
|
||||
|
||||
@@ -63,12 +63,38 @@ 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_column_permission(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_column_permission(self, serializer_class):
|
||||
"""获取列权限"""
|
||||
finded = False
|
||||
for app in get_custom_app_models():
|
||||
for model in app:
|
||||
if model['object'] is serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded:
|
||||
break
|
||||
if finded is False:
|
||||
return []
|
||||
return Columns.objects.filter(
|
||||
app=model['app'], model=model['model']
|
||||
).values('field_name', 'is_create', 'is_query', 'is_update')
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data, request=request)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
7
|
||||
@@ -1,31 +1,30 @@
|
||||
Django==4.1.5
|
||||
Django==4.2.6
|
||||
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
|
||||
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==4.0.0
|
||||
channels-redis==4.0.0
|
||||
websockets==10.4
|
||||
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
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
6818
web/package-lock.json
generated
6818
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.0.10",
|
||||
"@fast-crud/fast-crud": "^1.9.0",
|
||||
"@fast-crud/fast-extends": "^1.9.0",
|
||||
"@fast-crud/ui-element": "^1.9.0",
|
||||
"@fast-crud/fast-crud": "^1.18.3",
|
||||
"@fast-crud/fast-extends": "^1.18.3",
|
||||
"@fast-crud/ui-element": "^1.18.3",
|
||||
"@fast-crud/ui-interface": "^1.18.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"axios": "^1.2.1",
|
||||
"countup.js": "^2.3.2",
|
||||
"cropperjs": "^1.5.13",
|
||||
@@ -23,7 +25,8 @@
|
||||
"echarts": "^5.4.1",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"element-plus": "^2.2.26",
|
||||
"element-plus": "^2.3.9",
|
||||
"element-tree-line": "^0.2.1",
|
||||
"font-awesome": "^4.7.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"js-table2excel": "^1.0.3",
|
||||
@@ -32,19 +35,23 @@
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"postcss": "^8.4.21",
|
||||
"print-js": "^1.6.0",
|
||||
"qrcodejs2-fixes": "^0.0.2",
|
||||
"qs": "^6.11.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.0",
|
||||
"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": {
|
||||
|
||||
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -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,12 @@ onBeforeMount(() => {
|
||||
setIntroduction.cssCdn();
|
||||
// 设置批量第三方 js
|
||||
setIntroduction.jsCdn();
|
||||
//websockt 模块
|
||||
websocket.init(wsReceive)
|
||||
//websockt 模块
|
||||
try {
|
||||
//websocket.init(wsReceive)
|
||||
} catch (e) {
|
||||
console.log('websocket错误');
|
||||
}
|
||||
});
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
@@ -95,26 +100,24 @@ 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()
|
||||
messageCenter.setUnread(unread);
|
||||
if (data.contentType === 'SYSTEM') {
|
||||
ElNotification({
|
||||
title: '系统消息',
|
||||
message: data.content,
|
||||
type: 'success',
|
||||
position: 'bottom-right',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
const data = JSON.parse(message.data);
|
||||
const { unread } = data;
|
||||
const messageCenter = messageCenterStore();
|
||||
messageCenter.setUnread(unread);
|
||||
if (data.contentType === 'SYSTEM') {
|
||||
ElNotification({
|
||||
title: '系统消息',
|
||||
message: data.content,
|
||||
type: 'success',
|
||||
position: 'bottom-right',
|
||||
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;
|
||||
}
|
||||
}
|
||||
3
web/src/assets/style/tailwind.css
Normal file
3
web/src/assets/style/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
196
web/src/components/avatarSelector/index.vue
Normal file
196
web/src/components/avatarSelector/index.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="user-info-head" @click="editCropper()">
|
||||
<el-avatar :size="100" :src="options.img" />
|
||||
<el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
|
||||
<el-row>
|
||||
<el-col class="flex justify-center">
|
||||
<vue-cropper
|
||||
ref="cropper"
|
||||
:img="options.img"
|
||||
:info="true"
|
||||
:autoCrop="options.autoCrop"
|
||||
:autoCropWidth="options.autoCropWidth"
|
||||
:autoCropHeight="options.autoCropHeight"
|
||||
:fixedBox="options.fixedBox"
|
||||
:outputType="options.outputType"
|
||||
@realTime="realTime"
|
||||
:centerBox="true"
|
||||
v-if="visible"
|
||||
class="cropper"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<br />
|
||||
<el-row class="flex justify-center">
|
||||
<el-col :lg="2" :md="2">
|
||||
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
|
||||
<el-button type="success">
|
||||
选择
|
||||
<el-icon class="el-icon--right"><Plus /></el-icon>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
|
||||
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
|
||||
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 2, offset: 2 }" :md="2">
|
||||
<el-button type="primary" @click="uploadImg()">更新头像</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import 'vue-cropper/dist/index.css';
|
||||
import { VueCropper } from 'vue-cropper';
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue';
|
||||
import { base64ToFile } from '/@/utils/tools';
|
||||
const userStore = useUserInfo();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const open = ref(false);
|
||||
const visible = ref(false);
|
||||
const title = ref('修改头像');
|
||||
const emit = defineEmits(['uploadImg']);
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const dialogVisiable = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(newVal) {
|
||||
emit('update:modelValue', newVal);
|
||||
},
|
||||
});
|
||||
|
||||
//图片裁剪数据
|
||||
const options = reactive({
|
||||
img: userStore.userInfos.avatar, // 裁剪图片的地址
|
||||
fileName: '',
|
||||
autoCrop: true, // 是否默认生成截图框
|
||||
autoCropWidth: 200, // 默认生成截图框宽度
|
||||
autoCropHeight: 200, // 默认生成截图框高度
|
||||
fixedBox: true, // 固定截图框大小 不允许改变
|
||||
outputType: 'png', // 默认生成截图为PNG格式
|
||||
});
|
||||
|
||||
/** 编辑头像 */
|
||||
function editCropper() {
|
||||
dialogVisiable.value = true;
|
||||
}
|
||||
/** 打开弹出层结束时的回调 */
|
||||
function modalOpened() {
|
||||
nextTick(() => {
|
||||
visible.value = true;
|
||||
});
|
||||
}
|
||||
/** 覆盖默认上传行为 */
|
||||
function requestUpload() {}
|
||||
/** 向左旋转 */
|
||||
function rotateLeft() {
|
||||
proxy.$refs.cropper.rotateLeft();
|
||||
}
|
||||
/** 向右旋转 */
|
||||
function rotateRight() {
|
||||
proxy.$refs.cropper.rotateRight();
|
||||
}
|
||||
/** 图片缩放 */
|
||||
function changeScale(num) {
|
||||
num = num || 1;
|
||||
proxy.$refs.cropper.changeScale(num);
|
||||
}
|
||||
/** 上传预处理 */
|
||||
function beforeUpload(file) {
|
||||
if (file.type.indexOf('image/') == -1) {
|
||||
proxy.$modal.msgError('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。');
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
options.img = reader.result;
|
||||
options.fileName = file.name;
|
||||
};
|
||||
}
|
||||
}
|
||||
/** 上传图片 */
|
||||
function uploadImg() {
|
||||
// 获取截图的 base64 数据
|
||||
proxy.$refs.cropper.getCropData((data) => {
|
||||
let img = new Image();
|
||||
img.src = data;
|
||||
img.onload = async () => {
|
||||
let _data = compress(img);
|
||||
const imgFile = base64ToFile(_data, options.fileName);
|
||||
emit('uploadImg', imgFile);
|
||||
};
|
||||
});
|
||||
}
|
||||
// 压缩图片
|
||||
function compress(img) {
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx = canvas.getContext('2d');
|
||||
// let initSize = img.src.length;
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
// 铺底色
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
// 进行压缩
|
||||
let ndata = canvas.toDataURL('image/jpeg', 0.8);
|
||||
return ndata;
|
||||
}
|
||||
|
||||
/** 关闭窗口 */
|
||||
function closeDialog() {
|
||||
options.visible = false;
|
||||
options.img = userStore.userInfos.avatar;
|
||||
}
|
||||
|
||||
const updateAvatar = (img) => {
|
||||
options.img = img;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
updateAvatar,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-info-head {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.user-info-head:hover:after {
|
||||
content: '修改头像';
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
color: #000000;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
cursor: pointer;
|
||||
line-height: 110px;
|
||||
}
|
||||
.cropper {
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
}
|
||||
</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>
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -9,8 +9,8 @@ export default {
|
||||
two4: 'Links',
|
||||
},
|
||||
account: {
|
||||
accountPlaceholder1: 'The user name admin or not is common',
|
||||
accountPlaceholder2: 'Password: 123456',
|
||||
accountPlaceholder1: 'Please enter your login account',
|
||||
accountPlaceholder2: 'Please enter your login password',
|
||||
accountPlaceholder3: 'Please enter the verification code',
|
||||
accountBtnText: 'Sign in',
|
||||
},
|
||||
|
||||
@@ -9,8 +9,8 @@ export default {
|
||||
two4: '友情链接',
|
||||
},
|
||||
account: {
|
||||
accountPlaceholder1: '用户名 admin 或不输均为 common',
|
||||
accountPlaceholder2: '密码:123456',
|
||||
accountPlaceholder1: '请输入登录账号',
|
||||
accountPlaceholder2: '请输入登录密码',
|
||||
accountPlaceholder3: '请输入验证码',
|
||||
accountBtnText: '登 录',
|
||||
},
|
||||
|
||||
@@ -9,8 +9,8 @@ export default {
|
||||
two4: '友情連結',
|
||||
},
|
||||
account: {
|
||||
accountPlaceholder1: '用戶名admin或不輸均為common',
|
||||
accountPlaceholder2: '密碼:123456',
|
||||
accountPlaceholder1: '請輸入登入賬號',
|
||||
accountPlaceholder2: '請輸入登入密碼',
|
||||
accountPlaceholder3: '請輸入驗證碼',
|
||||
accountBtnText: '登入',
|
||||
},
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<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" />
|
||||
<!-- <LayoutFooter v-if="isFooter" /> -->
|
||||
</el-scrollbar>
|
||||
<el-backtop :target="setBacktopClass" />
|
||||
</el-main>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="layout-navbars-breadcrumb-user-icon">
|
||||
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
||||
<template #reference>
|
||||
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread===0">
|
||||
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
|
||||
<el-icon :title="$t('message.user.title4')">
|
||||
<ele-Bell />
|
||||
</el-icon>
|
||||
@@ -59,8 +59,8 @@
|
||||
</div>
|
||||
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
|
||||
<span class="layout-navbars-breadcrumb-user-link">
|
||||
<img :src="userInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
{{ userInfos.userName === '' ? 'common' : userInfos.userName }}
|
||||
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
{{ userInfos.username === '' ? 'common' : userInfos.username }}
|
||||
<el-icon class="el-icon--right">
|
||||
<ele-ArrowDown />
|
||||
</el-icon>
|
||||
@@ -68,7 +68,7 @@
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -90,7 +90,7 @@ 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';
|
||||
// 引入组件
|
||||
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
|
||||
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
|
||||
@@ -208,8 +208,8 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
//消息中心的未读数量
|
||||
import {messageCenterStore} from "/@/stores/messageCenter";
|
||||
const messageCenter = messageCenterStore()
|
||||
import { messageCenterStore } from '/@/stores/messageCenter';
|
||||
const messageCenter = messageCenterStore();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,18 +3,21 @@
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition :name="setTransitionName" mode="out-in">
|
||||
<keep-alive :include="getKeepAliveNames" v-if="showView">
|
||||
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" v-show="!isIframePage" />
|
||||
<div>
|
||||
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" v-show="!isIframePage" />
|
||||
</div>
|
||||
</keep-alive>
|
||||
</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 +47,13 @@ const showView = ref(true)
|
||||
/**
|
||||
* 刷新页面
|
||||
*/
|
||||
const refreshView=function () {
|
||||
showView.value = false // 通过v-if移除router-view节点
|
||||
nextTick(() => {
|
||||
showView.value = true // DOM更新后再通过v-if添加router-view节点
|
||||
})
|
||||
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(() => {
|
||||
@@ -105,7 +108,7 @@ onMounted(() => {
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
mittBus.off('onTagsViewRefreshRouterView', () => {});
|
||||
mittBus.off('onTagsViewRefreshRouterView', () => { });
|
||||
});
|
||||
// 监听路由变化,防止 tagsView 多标签时,切换动画消失
|
||||
// https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/38/files
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {createApp} from 'vue';
|
||||
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 '/@/utils/directive';
|
||||
import { i18n } from '/@/i18n';
|
||||
import other from '/@/utils/other';
|
||||
import '/@/assets/style/tailwind.css'; // 先引入tailwind css, 以免element-plus冲突
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import '/@/theme/index.scss';
|
||||
@@ -15,7 +16,7 @@ import fastCrud from './settings.ts';
|
||||
import pinia from './stores';
|
||||
import permission from '/@/plugin/permission/index';
|
||||
// @ts-ignore
|
||||
import eIconPicker, { iconList,analyzingIconForIconfont } from 'e-icon-picker';
|
||||
import eIconPicker, { iconList, analyzingIconForIconfont } from 'e-icon-picker';
|
||||
import 'e-icon-picker/icon/default-icon/symbol.js'; //基本彩色图标库
|
||||
import 'e-icon-picker/index.css'; // 基本样式,包含基本图标
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
@@ -24,6 +25,14 @@ import fontAwesome470 from 'e-icon-picker/icon/fontawesome/font-awesome.v4.7.0.j
|
||||
import eIconList from 'e-icon-picker/icon/default-icon/eIconList.js';
|
||||
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
|
||||
iconList.addIcon(elementPlus); // 添加element plus的图标
|
||||
@@ -31,17 +40,21 @@ iconList.addIcon(fontAwesome470); // 添加fontAwesome 470版本的图标
|
||||
|
||||
let app = createApp(App);
|
||||
|
||||
scanAndInstallPlugins(app);
|
||||
|
||||
app.use(eIconPicker, {
|
||||
addIconList: eIconList, //全局添加图标
|
||||
removeIconList: [], //全局删除图标
|
||||
zIndex: 3100, //选择器弹层的最低层,全局配置
|
||||
addIconList: eIconList, //全局添加图标
|
||||
removeIconList: [], //全局删除图标
|
||||
zIndex: 3100, //选择器弹层的最低层,全局配置
|
||||
});
|
||||
|
||||
pinia.use(piniaPersist);
|
||||
directive(app);
|
||||
other.elSvg(app);
|
||||
|
||||
|
||||
app.use(VXETable)
|
||||
app.use(permission);
|
||||
app.use(pinia).use(router).use(ElementPlus, {i18n: i18n.global.t}).use(i18n).use(VueGridLayout).use(fastCrud).mount('#app');
|
||||
app.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
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useRoutesList } from '/@/stores/routesList';
|
||||
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
|
||||
import { useMenuApi } from '/@/api/menu/index';
|
||||
import { handleMenu } from '../utils/menu';
|
||||
import {BtnPermissionStore} from "/@/plugin/permission/store.permission";
|
||||
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
|
||||
|
||||
const menuApi = useMenuApi();
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
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 {staticRoutes} from '/@/router/route';
|
||||
import {initFrontEndControlRoutes} from '/@/router/frontEnd';
|
||||
import {initBackEndControlRoutes} from '/@/router/backEnd';
|
||||
|
||||
/**
|
||||
* 1、前端控制路由时:isRequestRoutes 为 false,需要写 roles,需要走 setFilterRoute 方法。
|
||||
@@ -22,8 +22,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 应用程序使用的路由实例
|
||||
@@ -31,8 +31,8 @@ const { isRequestRoutes } = themeConfig.value;
|
||||
* @link 参考:https://next.router.vuejs.org/zh/api/#createrouter
|
||||
*/
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: staticRoutes,
|
||||
history: createWebHashHistory(),
|
||||
routes: staticRoutes,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -41,13 +41,13 @@ export const router = createRouter({
|
||||
* @returns 返回处理后的一维路由菜单数组
|
||||
*/
|
||||
export function formatFlatteningRoutes(arr: any) {
|
||||
if (arr.length <= 0) return false;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].children) {
|
||||
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
if (arr.length <= 0) return false;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].children) {
|
||||
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,73 +58,80 @@ export function formatFlatteningRoutes(arr: any) {
|
||||
* @returns 返回将一维数组重新处理成 `定义动态路由(dynamicRoutes)` 的格式
|
||||
*/
|
||||
export function formatTwoStageRoutes(arr: any) {
|
||||
if (arr.length <= 0) return false;
|
||||
const newArr: 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: [] });
|
||||
} else {
|
||||
// 判断是否是动态路由(xx/:id/:name),用于 tagsView 等中使用
|
||||
// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
|
||||
if (v.path.indexOf('/:') > -1) {
|
||||
v.meta['isDynamic'] = true;
|
||||
v.meta['isDynamicPath'] = v.path;
|
||||
}
|
||||
newArr[0].children.push({ ...v });
|
||||
// 存 name 值,keep-alive 中 include 使用,实现路由的缓存
|
||||
// 路径:/@/layout/routerView/parent.vue
|
||||
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
|
||||
cacheList.push(v.name);
|
||||
const stores = useKeepALiveNames(pinia);
|
||||
stores.setCacheKeepAlive(cacheList);
|
||||
}
|
||||
}
|
||||
});
|
||||
return newArr;
|
||||
if (arr.length <= 0) return false;
|
||||
const newArr: 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: []
|
||||
});
|
||||
} else {
|
||||
// 判断是否是动态路由(xx/:id/:name),用于 tagsView 等中使用
|
||||
// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
|
||||
if (v.path.indexOf('/:') > -1) {
|
||||
v.meta['isDynamic'] = true;
|
||||
v.meta['isDynamicPath'] = v.path;
|
||||
}
|
||||
newArr[0].children.push({...v});
|
||||
// 存 name 值,keep-alive 中 include 使用,实现路由的缓存
|
||||
// 路径:/@/layout/routerView/parent.vue
|
||||
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive && v.component_name != "") {
|
||||
cacheList.push(v.name);
|
||||
const stores = useKeepALiveNames(pinia);
|
||||
stores.setCacheKeepAlive(cacheList);
|
||||
}
|
||||
}
|
||||
});
|
||||
return newArr;
|
||||
}
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.configure({ showSpinner: false });
|
||||
if (to.meta.title) NProgress.start();
|
||||
const token = Session.get('token');
|
||||
if (to.path === '/login' && !token) {
|
||||
next();
|
||||
NProgress.done();
|
||||
} else {
|
||||
if (!token) {
|
||||
next(`/login?redirect=${to.path}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`);
|
||||
Session.clear();
|
||||
NProgress.done();
|
||||
} else if (token && to.path === '/login') {
|
||||
next('/home');
|
||||
NProgress.done();
|
||||
} else {
|
||||
const storesRoutesList = useRoutesList(pinia);
|
||||
const { routesList } = storeToRefs(storesRoutesList);
|
||||
if (routesList.value.length === 0) {
|
||||
if (isRequestRoutes) {
|
||||
// 后端控制路由:路由数据初始化,防止刷新时丢失
|
||||
await initBackEndControlRoutes();
|
||||
// 动态添加路由:防止非首页刷新时跳转回首页的问题
|
||||
// 确保 addRoute() 时动态添加的路由已经被完全加载上去
|
||||
next({ ...to, replace: true });
|
||||
} else {
|
||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
|
||||
await initFrontEndControlRoutes();
|
||||
next({ ...to, replace: true });
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
NProgress.configure({showSpinner: false});
|
||||
if (to.meta.title) NProgress.start();
|
||||
const token = Session.get('token');
|
||||
if (to.path === '/login' && !token) {
|
||||
next();
|
||||
NProgress.done();
|
||||
} else {
|
||||
if (!token) {
|
||||
next(`/login?redirect=${to.path}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`);
|
||||
Session.clear();
|
||||
NProgress.done();
|
||||
} else if (token && to.path === '/login') {
|
||||
next('/home');
|
||||
NProgress.done();
|
||||
} else {
|
||||
const storesRoutesList = useRoutesList(pinia);
|
||||
const {routesList} = storeToRefs(storesRoutesList);
|
||||
if (routesList.value.length === 0) {
|
||||
if (isRequestRoutes) {
|
||||
// 后端控制路由:路由数据初始化,防止刷新时丢失
|
||||
await initBackEndControlRoutes();
|
||||
// 动态添加路由:防止非首页刷新时跳转回首页的问题
|
||||
// 确保 addRoute() 时动态添加的路由已经被完全加载上去
|
||||
next({...to, replace: true});
|
||||
} else {
|
||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
|
||||
await initFrontEndControlRoutes();
|
||||
next({...to, replace: true});
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 路由加载后
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
NProgress.done();
|
||||
});
|
||||
|
||||
// 导出路由
|
||||
|
||||
@@ -7,8 +7,9 @@ import { setLogger } from '@fast-crud/fast-crud';
|
||||
import ui from '@fast-crud/ui-element';
|
||||
import { request } from '/@/utils/service';
|
||||
//扩展包
|
||||
import {FsExtendsEditor} from "@fast-crud/fast-extends";
|
||||
import "@fast-crud/fast-extends/dist/style.css";
|
||||
import { FsExtendsEditor,FsExtendsUploader } from '@fast-crud/fast-extends';
|
||||
import '@fast-crud/fast-extends/dist/style.css';
|
||||
import { successMessage, successNotification } from '/@/utils/message';
|
||||
export default {
|
||||
async install(app: any, options: any) {
|
||||
// 先安装ui
|
||||
@@ -18,7 +19,10 @@ export default {
|
||||
//i18n, //i18n配置,可选,默认使用中文,具体用法请看demo里的 src/i18n/index.js 文件
|
||||
// 此处配置公共的dictRequest(字典请求)
|
||||
async dictRequest({ dict }: any) {
|
||||
return await request({ url: dict.url,params:dict.params || {} }); //根据dict的url,异步返回一个字典数组
|
||||
//根据dict的url,异步返回一个字典数组
|
||||
return await request({ url: dict.url, params: dict.params || {} }).then((res:any)=>{
|
||||
return res.data
|
||||
});
|
||||
},
|
||||
//公共crud配置
|
||||
commonOptions() {
|
||||
@@ -28,6 +32,9 @@ export default {
|
||||
//你项目后台接口大概率与fast-crud所需要的返回结构不一致,所以需要配置此项
|
||||
//请参考文档http://fast-crud.docmirror.cn/api/crud-options/request.html
|
||||
transformQuery: ({ page, form, sort }: any) => {
|
||||
if (sort.asc !== undefined) {
|
||||
form['ordering'] = `${sort.asc ? '' : '-'}${sort.prop}`;
|
||||
}
|
||||
//转换为你pageRequest所需要的请求参数结构
|
||||
return { page: page.currentPage, limit: page.pageSize, ...form };
|
||||
},
|
||||
@@ -37,6 +44,14 @@ export default {
|
||||
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total };
|
||||
},
|
||||
},
|
||||
form: {
|
||||
afterSubmit(ctx: any) {
|
||||
// 增加crud提示
|
||||
if (ctx.res.code == 2000) {
|
||||
successNotification(ctx.res.msg);
|
||||
}
|
||||
},
|
||||
},
|
||||
/* search: {
|
||||
layout: 'multi-line',
|
||||
collapse: true,
|
||||
@@ -55,9 +70,42 @@ export default {
|
||||
},
|
||||
});
|
||||
//富文本
|
||||
app.use(FsExtendsEditor,{
|
||||
wangEditor:{
|
||||
width:300,
|
||||
app.use(FsExtendsEditor, {
|
||||
wangEditor: {
|
||||
width: 300,
|
||||
},
|
||||
});
|
||||
// 文件上传
|
||||
app.use(FsExtendsUploader, {
|
||||
defaultType: "form",
|
||||
form: {
|
||||
action: `/api/system/file/`,
|
||||
name: "file",
|
||||
withCredentials: false,
|
||||
uploadRequest: async ({ action, file, onProgress }) => {
|
||||
// @ts-ignore
|
||||
const data = new FormData();
|
||||
data.append("file", file);
|
||||
return await request({
|
||||
url: action,
|
||||
method: "post",
|
||||
timeout: 60000,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
},
|
||||
data,
|
||||
onUploadProgress: (p) => {
|
||||
onProgress({ percent: Math.round((p.loaded / p.total) * 100) });
|
||||
}
|
||||
});
|
||||
},
|
||||
successHandle(ret) {
|
||||
// 上传完成后的结果处理, 此处应返回格式为{url:xxx,key:xxx}
|
||||
return {
|
||||
url: getBaseURL() + ret.data.url,
|
||||
key: ret.data.id
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
setLogger({ level: 'error' });
|
||||
|
||||
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;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -5,11 +5,17 @@
|
||||
|
||||
// 用户信息
|
||||
export interface UserInfosState {
|
||||
authBtnList: string[];
|
||||
photo: string;
|
||||
roles: string[];
|
||||
time: number;
|
||||
userName: string;
|
||||
avatar: string;
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
gender: string;
|
||||
dept_info: {
|
||||
dept_id: number;
|
||||
dept_name: string;
|
||||
};
|
||||
role_info: any[];
|
||||
}
|
||||
export interface UserInfosStates {
|
||||
userInfos: UserInfosState;
|
||||
@@ -91,4 +97,7 @@ export interface ThemeConfigStates {
|
||||
|
||||
export interface DictionaryStates {
|
||||
data: any;
|
||||
}
|
||||
}
|
||||
export interface ConfigStates {
|
||||
systemConfig: any;
|
||||
}
|
||||
|
||||
28
web/src/stores/systemConfig.ts
Normal file
28
web/src/stores/systemConfig.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ConfigStates } from './interface';
|
||||
import { request } from '../utils/service';
|
||||
export const urlPrefix = '/api/init/settings/';
|
||||
|
||||
/**
|
||||
* 系统配置数据
|
||||
* @methods getSystemConfig 获取系统配置数据
|
||||
*/
|
||||
export const SystemConfigStore = defineStore('SystemConfig', {
|
||||
state: (): ConfigStates => ({
|
||||
systemConfig: {},
|
||||
}),
|
||||
actions: {
|
||||
async getSystemConfigs() {
|
||||
request({
|
||||
url: urlPrefix,
|
||||
method: 'get',
|
||||
}).then((ret: { data: [] }) => {
|
||||
// 转换数据格式并保存到pinia
|
||||
this.systemConfig = JSON.parse(JSON.stringify(ret.data));
|
||||
});
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import Cookies from 'js-cookie';
|
||||
import { UserInfosStates } from './interface';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { request } from '../utils/service';
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
* @methods setUserInfos 设置用户信息
|
||||
@@ -11,32 +9,59 @@ import { request } from '../utils/service';
|
||||
export const useUserInfo = defineStore('userInfo', {
|
||||
state: (): UserInfosStates => ({
|
||||
userInfos: {
|
||||
userName: '',
|
||||
photo: '',
|
||||
time: 0,
|
||||
roles: [],
|
||||
authBtnList: [],
|
||||
avatar: '',
|
||||
username: '',
|
||||
name: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
gender: '',
|
||||
dept_info: {
|
||||
dept_id: 0,
|
||||
dept_name: '',
|
||||
},
|
||||
role_info: [
|
||||
{
|
||||
id: 0,
|
||||
name: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
async updateUserInfos() {
|
||||
let userInfos: any = await this.getApiUserInfo();
|
||||
this.userInfos.username = userInfos.data.name;
|
||||
this.userInfos.avatar = userInfos.data.avatar;
|
||||
this.userInfos.name = userInfos.data.name;
|
||||
this.userInfos.email = userInfos.data.email;
|
||||
this.userInfos.mobile = userInfos.data.mobile;
|
||||
this.userInfos.gender = userInfos.data.gender;
|
||||
this.userInfos.dept_info = userInfos.data.dept_info;
|
||||
this.userInfos.role_info = userInfos.data.role_info;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
},
|
||||
async setUserInfos() {
|
||||
// 存储用户信息到浏览器缓存
|
||||
if (Session.get('userInfo')) {
|
||||
this.userInfos = Session.get('userInfo');
|
||||
} else {
|
||||
let userInfos: any = await this.getApiUserInfo();
|
||||
this.userInfos.userName = userInfos.data.name;
|
||||
this.userInfos.photo = userInfos.data.avatar || 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500'
|
||||
this.userInfos.time = new Date().getTime()
|
||||
this.userInfos.roles = ['admin']
|
||||
Session.set('userInfo', this.userInfos)
|
||||
this.userInfos.username = userInfos.data.name;
|
||||
this.userInfos.avatar = userInfos.data.avatar;
|
||||
this.userInfos.name = userInfos.data.name;
|
||||
this.userInfos.email = userInfos.data.email;
|
||||
this.userInfos.mobile = userInfos.data.mobile;
|
||||
this.userInfos.gender = userInfos.data.gender;
|
||||
this.userInfos.dept_info = userInfos.data.dept_info;
|
||||
this.userInfos.role_info = userInfos.data.role_info;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
}
|
||||
},
|
||||
async getApiUserInfo() {
|
||||
return request({
|
||||
url: '/api/system/user/user_info/',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
2
web/src/types/pinia.d.ts
vendored
2
web/src/types/pinia.d.ts
vendored
@@ -9,7 +9,7 @@ declare interface UserInfosState<T = any> {
|
||||
photo: string;
|
||||
roles: string[];
|
||||
time: number;
|
||||
userName: string;
|
||||
username: string;
|
||||
[key: string]: T;
|
||||
};
|
||||
}
|
||||
|
||||
2
web/src/types/views.d.ts
vendored
2
web/src/types/views.d.ts
vendored
@@ -90,7 +90,7 @@ declare type TreeType = {
|
||||
|
||||
// user
|
||||
declare type RowUserType<T = any> = {
|
||||
userName: string;
|
||||
username: string;
|
||||
userNickname: string;
|
||||
roleSign: string;
|
||||
department: string[];
|
||||
|
||||
@@ -1,61 +1,67 @@
|
||||
import { pluginsAll } from '/@/views/plugins/index';
|
||||
|
||||
/**
|
||||
* @description 校验是否为租户模式。租户模式把域名替换成 域名 加端口
|
||||
*/
|
||||
export const getBaseURL = function () {
|
||||
var baseURL = import.meta.env.VITE_API_URL as any
|
||||
var param = baseURL.split('/')[3] || ''
|
||||
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
|
||||
// 1.把127.0.0.1 替换成和前端一样域名
|
||||
// 2.把 ip 地址替换成和前端一样域名
|
||||
// 3.把 /api 或其他类似的替换成和前端一样域名
|
||||
// document.domain
|
||||
var host = baseURL.split('/')[2]
|
||||
if (host) {
|
||||
var prot = baseURL.split(':')[2] || 80
|
||||
if (prot === 80 || prot === 443) {
|
||||
host = document.domain
|
||||
} else {
|
||||
host = document.domain + ':' + prot
|
||||
}
|
||||
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param
|
||||
} else {
|
||||
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
|
||||
}
|
||||
}
|
||||
if (!baseURL.endsWith('/')) {
|
||||
baseURL += '/'
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
var baseURL = import.meta.env.VITE_API_URL as any;
|
||||
var param = baseURL.split('/')[3] || '';
|
||||
// @ts-ignore
|
||||
if (pluginsAll && pluginsAll.indexOf('dvadmin3-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
|
||||
// 1.把127.0.0.1 替换成和前端一样域名
|
||||
// 2.把 ip 地址替换成和前端一样域名
|
||||
// 3.把 /api 或其他类似的替换成和前端一样域名
|
||||
// document.domain
|
||||
|
||||
var host = baseURL.split('/')[2];
|
||||
if (host) {
|
||||
var port = baseURL.split(':')[2] || 80;
|
||||
if (port === 80 || port === 443) {
|
||||
host = document.domain;
|
||||
} else {
|
||||
host = document.domain + ':' + port;
|
||||
}
|
||||
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param;
|
||||
} else {
|
||||
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL;
|
||||
}
|
||||
}
|
||||
if (!baseURL.endsWith('/')) {
|
||||
baseURL += '/';
|
||||
}
|
||||
return baseURL;
|
||||
};
|
||||
|
||||
export const getWsBaseURL = function () {
|
||||
let baseURL = import.meta.env.VITE_API_URL as any
|
||||
let param = baseURL.split('/')[3] || ''
|
||||
if (window.pluginsAll && window.pluginsAll.indexOf('dvadmin-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
|
||||
// 1.把127.0.0.1 替换成和前端一样域名
|
||||
// 2.把 ip 地址替换成和前端一样域名
|
||||
// 3.把 /api 或其他类似的替换成和前端一样域名
|
||||
// document.domain
|
||||
var host = baseURL.split('/')[2]
|
||||
if (host) {
|
||||
var prot = baseURL.split(':')[2] || 80
|
||||
if (prot === 80 || prot === 443) {
|
||||
host = document.domain
|
||||
} else {
|
||||
host = document.domain + ':' + prot
|
||||
}
|
||||
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param
|
||||
} else {
|
||||
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL
|
||||
}
|
||||
} else if (param !== '' || baseURL.startsWith('/')) {
|
||||
baseURL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.hostname + (location.port ? ':' : '') + location.port + baseURL
|
||||
}
|
||||
if (!baseURL.endsWith('/')) {
|
||||
baseURL += '/'
|
||||
}
|
||||
if (baseURL.startsWith('http')) { // https 也默认会被替换成 wss
|
||||
baseURL = baseURL.replace('http', 'ws')
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
let baseURL = import.meta.env.VITE_API_URL as any;
|
||||
let param = baseURL.split('/')[3] || '';
|
||||
// @ts-ignore
|
||||
if (pluginsAll && pluginsAll.indexOf('dvadmin3-tenants-web') !== -1 && (!param || baseURL.startsWith('/'))) {
|
||||
// 1.把127.0.0.1 替换成和前端一样域名
|
||||
// 2.把 ip 地址替换成和前端一样域名
|
||||
// 3.把 /api 或其他类似的替换成和前端一样域名
|
||||
// document.domain
|
||||
var host = baseURL.split('/')[2];
|
||||
if (host) {
|
||||
var port = baseURL.split(':')[2] || 80;
|
||||
if (port === 80 || port === 443) {
|
||||
host = document.domain;
|
||||
} else {
|
||||
host = document.domain + ':' + port;
|
||||
}
|
||||
baseURL = baseURL.split('/')[0] + '//' + baseURL.split('/')[1] + host + '/' + param;
|
||||
} else {
|
||||
baseURL = location.protocol + '//' + location.hostname + (location.port ? ':' : '') + location.port + baseURL;
|
||||
}
|
||||
} else if (param !== '' || baseURL.startsWith('/')) {
|
||||
baseURL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.hostname + (location.port ? ':' : '') + location.port + baseURL;
|
||||
}
|
||||
if (!baseURL.endsWith('/')) {
|
||||
baseURL += '/';
|
||||
}
|
||||
if (baseURL.startsWith('http')) {
|
||||
// https 也默认会被替换成 wss
|
||||
baseURL = baseURL.replace('http', 'ws');
|
||||
}
|
||||
return baseURL;
|
||||
};
|
||||
|
||||
9
web/src/utils/columnPermission.ts
Normal file
9
web/src/utils/columnPermission.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useColumnPermission } from '/@/stores/columnPermission';
|
||||
|
||||
type permissionType = 'is_create' | 'is_query' | 'is_update';
|
||||
|
||||
export const columnPermission = (key: string, type: permissionType): boolean => {
|
||||
const permissions = useColumnPermission().permission || [];
|
||||
|
||||
return !!permissions.some((i) => i.field_name === key && i[type]);
|
||||
};
|
||||
@@ -12,13 +12,13 @@ export const handleMenu = (menuData: Array<any>) => {
|
||||
title: item.title,
|
||||
isLink: item.is_link,
|
||||
isHide: !item.visible,
|
||||
isKeepAlive: true,
|
||||
isKeepAlive: item.cache,
|
||||
isAffix: false,
|
||||
isIframe: false,
|
||||
roles: ['admin'],
|
||||
icon: item.icon
|
||||
}
|
||||
|
||||
item.name = item.component_name
|
||||
return item
|
||||
}
|
||||
menuData.forEach((val) => {
|
||||
@@ -46,4 +46,4 @@ export const handleMenu = (menuData: Array<any>) => {
|
||||
...data
|
||||
]
|
||||
return menu
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ElMessage, MessageOptions } from 'element-plus';
|
||||
import { ElMessage, ElNotification, MessageOptions } from 'element-plus';
|
||||
|
||||
export function message(message: string, option?: MessageOptions) {
|
||||
ElMessage({ message, ...option });
|
||||
}
|
||||
export function successMessage(message: string, option?: MessageOptions) {
|
||||
ElMessage({ message, ...option, type: 'success' });
|
||||
ElMessage({ message, type: 'success' });
|
||||
}
|
||||
export function warningMessage(message: string, option?: MessageOptions) {
|
||||
ElMessage({ message, ...option, type: 'warning' });
|
||||
@@ -15,3 +15,19 @@ export function errorMessage(message: string, option?: MessageOptions) {
|
||||
export function infoMessage(message: string, option?: MessageOptions) {
|
||||
ElMessage({ message, ...option, type: 'info' });
|
||||
}
|
||||
|
||||
export function notification(message: string) {
|
||||
ElNotification({ message });
|
||||
}
|
||||
export function successNotification(message: string) {
|
||||
ElNotification({ message, type: 'success' });
|
||||
}
|
||||
export function warningNotification(message: string) {
|
||||
ElNotification({ message, type: 'warning' });
|
||||
}
|
||||
export function errorNotification(message: string) {
|
||||
ElNotification({ message, type: 'error' });
|
||||
}
|
||||
export function infoNotification(message: string) {
|
||||
ElNotification({ message, type: 'info' });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { get } from 'lodash-es';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { Action } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { Action } from 'element-plus';
|
||||
|
||||
// @ts-ignore
|
||||
import { errorLog, errorCreate } from './tools.ts';
|
||||
@@ -9,6 +9,7 @@ import { errorLog, errorCreate } from './tools.ts';
|
||||
// import { useUserStore } from "../store/modules/user";
|
||||
import { Local, Session } from '/@/utils/storage';
|
||||
import qs from 'qs';
|
||||
import { getBaseURL } from './baseUrl';
|
||||
/**
|
||||
* @description 创建请求实例
|
||||
*/
|
||||
@@ -17,16 +18,23 @@ function createService() {
|
||||
const service = axios.create({
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
},
|
||||
paramsSerializer: {
|
||||
serialize(params) {
|
||||
return qs.stringify(params, { indices: false,encoder: (val:string) => {
|
||||
if (typeof val === 'boolean') {
|
||||
return val ? 1 : 0;
|
||||
}
|
||||
return val;
|
||||
} });
|
||||
interface paramsObj {
|
||||
[key: string]: any;
|
||||
}
|
||||
let result: paramsObj = {};
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== '') {
|
||||
result[key] = value;
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
result[key] = value ? 'True' : 'False';
|
||||
}
|
||||
}
|
||||
return qs.stringify(result);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -62,21 +70,21 @@ function createService() {
|
||||
// 有 code 代表这是一个后端接口 可以进行进一步的判断
|
||||
switch (code) {
|
||||
case 400:
|
||||
Local.clear();
|
||||
Session.clear();
|
||||
// Local.clear();
|
||||
// Session.clear();
|
||||
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
|
||||
window.location.reload();
|
||||
// window.location.reload();
|
||||
break;
|
||||
case 401:
|
||||
Local.clear();
|
||||
Session.clear();
|
||||
dataAxios.msg = '登录授权过期,请重新登录';
|
||||
dataAxios.msg = '登录认证失败,请重新登录';
|
||||
ElMessageBox.alert(dataAxios.msg, '提示', {
|
||||
confirmButtonText: 'OK',
|
||||
callback: (action: Action) => {
|
||||
window.location.reload();
|
||||
},
|
||||
})
|
||||
});
|
||||
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
|
||||
break;
|
||||
case 2000:
|
||||
@@ -86,6 +94,9 @@ function createService() {
|
||||
return dataAxios;
|
||||
}
|
||||
return dataAxios;
|
||||
case 4000:
|
||||
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
|
||||
return dataAxios;
|
||||
default:
|
||||
// 不是正确的 code
|
||||
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
|
||||
@@ -100,7 +111,15 @@ function createService() {
|
||||
error.message = '请求错误';
|
||||
break;
|
||||
case 401:
|
||||
error.message = '未授权,请登录';
|
||||
Local.clear();
|
||||
Session.clear();
|
||||
error.message = '登录授权过期,请重新登录';
|
||||
ElMessageBox.alert(error.message, '提示', {
|
||||
confirmButtonText: 'OK',
|
||||
callback: (action: Action) => {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 403:
|
||||
error.message = '拒绝访问';
|
||||
@@ -154,7 +173,7 @@ function createRequestFunction(service: any) {
|
||||
'Content-Type': get(config, 'headers.Content-Type', 'application/json'),
|
||||
},
|
||||
timeout: 5000,
|
||||
baseURL: import.meta.env.VITE_API_URL as any,
|
||||
baseURL: getBaseURL(),
|
||||
data: {},
|
||||
};
|
||||
|
||||
@@ -175,3 +194,34 @@ export const request = createRequestFunction(service);
|
||||
// 用于模拟网络请求的实例和请求方法
|
||||
export const serviceForMock = createService();
|
||||
export const requestForMock = createRequestFunction(serviceForMock);
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param url
|
||||
* @param params
|
||||
* @param method
|
||||
* @param filename
|
||||
*/
|
||||
export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) {
|
||||
request({
|
||||
url: url,
|
||||
method: method,
|
||||
params: params,
|
||||
responseType: 'blob'
|
||||
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
|
||||
}).then((res: any) => {
|
||||
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
|
||||
const fileName = xlsxName || `${filename}.xlsx`
|
||||
if (res) {
|
||||
const blob = new Blob([res.data], { type: 'charset=utf-8' })
|
||||
const elink = document.createElement('a')
|
||||
elink.download = fileName
|
||||
elink.style.display = 'none'
|
||||
elink.href = URL.createObjectURL(blob)
|
||||
document.body.appendChild(elink)
|
||||
elink.click()
|
||||
URL.revokeObjectURL(elink.href) // 释放URL 对象0
|
||||
document.body.removeChild(elink)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
const cssCdnUrlList: Array<string> = [
|
||||
'//at.alicdn.com/t/font_2298093_y6u00apwst.css',
|
||||
'//at.alicdn.com/t/c/font_3882322_9ah7y8m9175.css', //dvadmin3项目用icon
|
||||
'//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css',
|
||||
];
|
||||
// 第三方 js url
|
||||
const jsCdnUrlList: Array<string> = [];
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
* @param {String} jsonString 需要解析的 json 字符串
|
||||
* @param {String} defaultValue 默认值
|
||||
*/
|
||||
import { uiContext } from "@fast-crud/fast-crud";
|
||||
import { uiContext } from '@fast-crud/fast-crud';
|
||||
|
||||
export function parse(jsonString = "{}", defaultValue = {}) {
|
||||
let result = defaultValue;
|
||||
try {
|
||||
result = JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return result;
|
||||
export function parse(jsonString = '{}', defaultValue = {}) {
|
||||
let result = defaultValue;
|
||||
try {
|
||||
result = JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,8 +21,8 @@ export function parse(jsonString = "{}", defaultValue = {}) {
|
||||
* @param {String} msg 状态信息
|
||||
* @param {Number} code 状态码
|
||||
*/
|
||||
export function response(data = {}, msg = "", code = 0) {
|
||||
return [200, { code, msg, data }];
|
||||
export function response(data = {}, msg = '', code = 0) {
|
||||
return [200, { code, msg, data }];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,8 +30,8 @@ export function response(data = {}, msg = "", code = 0) {
|
||||
* @param {Any} data 返回值
|
||||
* @param {String} msg 状态信息
|
||||
*/
|
||||
export function responseSuccess(data = {}, msg = "成功") {
|
||||
return response(data, msg);
|
||||
export function responseSuccess(data = {}, msg = '成功') {
|
||||
return response(data, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,30 +40,62 @@ export function responseSuccess(data = {}, msg = "成功") {
|
||||
* @param {String} msg 状态信息
|
||||
* @param {Number} code 状态码
|
||||
*/
|
||||
export function responseError(data = {}, msg = "请求失败", code = 500) {
|
||||
return response(data, msg, code);
|
||||
export function responseError(data = {}, msg = '请求失败', code = 500) {
|
||||
return response(data, msg, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 记录和显示错误
|
||||
* @param {Error} error 错误对象
|
||||
*/
|
||||
export function errorLog(error:any,notification=true) {
|
||||
// 打印到控制台
|
||||
console.error(error);
|
||||
// 显示提示
|
||||
if(notification){
|
||||
uiContext.get().notification.error({ message: error.message });
|
||||
}
|
||||
|
||||
export function errorLog(error: any, notification = true) {
|
||||
// 打印到控制台
|
||||
console.error(error);
|
||||
// 显示提示
|
||||
if (notification) {
|
||||
uiContext.get().notification.error({ message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建一个错误
|
||||
* @param {String} msg 错误信息
|
||||
*/
|
||||
export function errorCreate(msg:any,notification=true) {
|
||||
const error = new Error(msg);
|
||||
errorLog(error,notification);
|
||||
// throw error;
|
||||
export function errorCreate(msg: any, notification = true) {
|
||||
const error = new Error(msg);
|
||||
errorLog(error, notification);
|
||||
// throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description base64转file
|
||||
* @param {String} base64 base64字符串
|
||||
* @param {String} fileName 文件名
|
||||
*/
|
||||
export function base64ToFile(base64: any, fileName: string) {
|
||||
// 将base64按照 , 进行分割 将前缀 与后续内容分隔开
|
||||
let data = base64.split(',');
|
||||
// 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
|
||||
let type = data[0].match(/:(.*?);/)[1];
|
||||
// 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
|
||||
let suffix = type.split('/')[1];
|
||||
// 使用atob()对base64数据进行解码 结果是一个文件数据流 以字符串的格式输出
|
||||
const bstr = window.atob(data[1]);
|
||||
// 获取解码结果字符串的长度
|
||||
let n = bstr.length;
|
||||
// 根据解码结果字符串的长度创建一个等长的整形数字数组
|
||||
// 但在创建时 所有元素初始值都为 0
|
||||
const u8arr = new Uint8Array(n);
|
||||
// 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
|
||||
while (n--) {
|
||||
// charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
// 利用构造函数创建File文件对象
|
||||
// new File(bits, name, options)
|
||||
const file = new File([u8arr], `${fileName}.${suffix}`, {
|
||||
type: type,
|
||||
});
|
||||
// 将File文件对象返回给方法的调用者
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const websocket: socket = {
|
||||
}
|
||||
const token = Session.get('token')
|
||||
if(!token){
|
||||
message.warning('websocket认证失败')
|
||||
// message.warning('websocket认证失败')
|
||||
return null
|
||||
}
|
||||
const wsUrl = `${getWsBaseURL()}ws/${token}/`
|
||||
|
||||
9
web/src/views/interface/index.ts
Normal file
9
web/src/views/interface/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
*
|
||||
* 后端API接口响应数据
|
||||
*/
|
||||
interface APIResponseData {
|
||||
code?: number;
|
||||
data: [];
|
||||
msg?: string;
|
||||
}
|
||||
16
web/src/views/plugins/index.ts
Normal file
16
web/src/views/plugins/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineAsyncComponent, AsyncComponentLoader } from 'vue';
|
||||
export let pluginsAll: any = [];
|
||||
// 扫描插件目录并注册插件
|
||||
export const scanAndInstallPlugins = (app: any) => {
|
||||
const components = import.meta.glob('./**/*.vue');
|
||||
const pluginNames = new Set();
|
||||
// 遍历对象并注册异步组件
|
||||
for (const [key, value] of Object.entries(components)) {
|
||||
const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.'));
|
||||
app.component(name, defineAsyncComponent(value as AsyncComponentLoader));
|
||||
const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
|
||||
pluginNames.add(pluginsName);
|
||||
}
|
||||
pluginsAll = Array.from(pluginNames);
|
||||
console.log('已发现插件:', pluginsAll);
|
||||
};
|
||||
@@ -1,41 +1,41 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
|
||||
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
|
||||
|
||||
export const apiPrefix = '/api/system/area/';
|
||||
export function GetList(query: PageQuery) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
export function GetList(query: UserPageQuery) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
export function GetObj(id: InfoReq) {
|
||||
return request({
|
||||
url: apiPrefix + id,
|
||||
method: 'get',
|
||||
});
|
||||
return request({
|
||||
url: apiPrefix + id,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function AddObj(obj: AddReq) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'post',
|
||||
data: obj,
|
||||
});
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'post',
|
||||
data: obj,
|
||||
});
|
||||
}
|
||||
|
||||
export function UpdateObj(obj: EditReq) {
|
||||
return request({
|
||||
url: apiPrefix + obj.id + '/',
|
||||
method: 'put',
|
||||
data: obj,
|
||||
});
|
||||
return request({
|
||||
url: apiPrefix + obj.id + '/',
|
||||
method: 'put',
|
||||
data: obj,
|
||||
});
|
||||
}
|
||||
|
||||
export function DelObj(id: DelReq) {
|
||||
return request({
|
||||
url: apiPrefix + id + '/',
|
||||
method: 'delete',
|
||||
data: { id },
|
||||
});
|
||||
return request({
|
||||
url: apiPrefix + id + '/',
|
||||
method: 'delete',
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import * as api from './api';
|
||||
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
|
||||
import { request } from '/@/utils/service';
|
||||
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
interface CreateCrudOptionsTypes {
|
||||
crudOptions: CrudOptions;
|
||||
}
|
||||
import { successMessage } from '/@/utils/message';
|
||||
|
||||
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
|
||||
const pageRequest = async (query: PageQuery) => {
|
||||
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
@@ -26,8 +23,8 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
* @param row
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
const loadContentMethod = (tree: any, treeNode: any, resolve: any) => {
|
||||
api.GetList({ pcode: tree.code }).then((res: any) => {
|
||||
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
|
||||
pageRequest({ pcode: tree.code }).then((res: APIResponseData) => {
|
||||
resolve(res.data);
|
||||
});
|
||||
};
|
||||
@@ -40,6 +37,24 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
iconRight: 'Edit',
|
||||
type: 'text',
|
||||
},
|
||||
remove: {
|
||||
iconRight: 'Delete',
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
show: false,
|
||||
},
|
||||
@@ -90,8 +105,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
show: true,
|
||||
},
|
||||
treeNode: true,
|
||||
width: 160,
|
||||
type: 'input',
|
||||
column:{
|
||||
minWidth: 120,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
@@ -108,6 +125,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
show: true,
|
||||
},
|
||||
type: 'input',
|
||||
column:{
|
||||
minWidth: 90,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
@@ -124,6 +144,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
disabled: true,
|
||||
},
|
||||
type: 'input',
|
||||
column:{
|
||||
minWidth: 120,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
@@ -140,6 +163,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
disabled: true,
|
||||
},
|
||||
type: 'input',
|
||||
column:{
|
||||
minWidth: 100,
|
||||
},
|
||||
form: {
|
||||
disabled: false,
|
||||
rules: [
|
||||
@@ -153,6 +179,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
},
|
||||
initials: {
|
||||
title: '首字母',
|
||||
column:{
|
||||
minWidth: 100,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
@@ -169,14 +198,26 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
width: 90,
|
||||
type: 'dict-radio',
|
||||
column: {
|
||||
minWidth:90,
|
||||
component: {
|
||||
name: 'fs-dict-switch',
|
||||
activeText: '',
|
||||
inactiveText: '',
|
||||
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
|
||||
onChange: compute((context) => {
|
||||
return () => {
|
||||
api.UpdateObj(context.row).then((res: APIResponseData) => {
|
||||
successMessage(res.msg as string);
|
||||
});
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
dict: dict({
|
||||
data: dictionary('button_status_bool'),
|
||||
}),
|
||||
form: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,20 +4,12 @@
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup name="areas">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useExpose, useCrud } from '@fast-crud/fast-crud';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
// crud组件的ref
|
||||
const crudRef = ref();
|
||||
// crud 配置的ref
|
||||
const crudBinding = ref();
|
||||
// 暴露的方法
|
||||
const { crudExpose } = useExpose({ crudRef, crudBinding });
|
||||
// 你的crud配置
|
||||
const { crudOptions } = createCrudOptions({ crudExpose });
|
||||
// 初始化crud配置
|
||||
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
|
||||
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
|
||||
25
web/src/views/system/columns/api.ts
Normal file
25
web/src/views/system/columns/api.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import { PageQuery } from './types'
|
||||
|
||||
export function getRoleList(query: PageQuery) {
|
||||
return request({
|
||||
url: '/api/system/role/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
export function getMenuList(query: PageQuery) {
|
||||
return request({
|
||||
url: '/api/system/menu/',
|
||||
method: 'get',
|
||||
params: {is_catalog:0,...query},
|
||||
});
|
||||
}
|
||||
|
||||
export function getModelList() {
|
||||
return request({
|
||||
url: '/api/system/column/get_models/',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
115
web/src/views/system/columns/components/ColumnsFormCom/index.vue
Normal file
115
web/src/views/system/columns/components/ColumnsFormCom/index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="columns-form-com">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
<el-form-item label="字段名" prop="field_name">
|
||||
<el-input v-model="formData.field_name" placeholder="请输入字段名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="列名" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入列名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="创建显示">
|
||||
<el-switch v-model="formData.is_create" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="编辑显示">
|
||||
<el-switch v-model="formData.is_update" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="查询显示">
|
||||
<el-switch v-model="formData.is_query" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="btnLoading"> 确定 </el-button>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { addColumnsData, updateColumnsData } from '../ColumnsTableCom/api';
|
||||
import { successNotification } from '/@/utils/message';
|
||||
import { CurrentInfoType, ColumnsFormDataType } from '../../types';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
currentInfo: {
|
||||
type: Object as () => CurrentInfoType,
|
||||
required: true,
|
||||
default: () => {},
|
||||
},
|
||||
initFormData: {
|
||||
type: Object as () => Partial<ColumnsFormDataType>,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['drawerClose']);
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const formRules = reactive({
|
||||
field_name: [{ required: true, message: '请输入字段名!', trigger: 'blur' }],
|
||||
title: [{ required: true, message: '请输入列名!', trigger: 'blur' }],
|
||||
});
|
||||
|
||||
let formData = reactive<ColumnsFormDataType>({
|
||||
field_name: '',
|
||||
title: '',
|
||||
is_create: true,
|
||||
is_update: true,
|
||||
is_query: true,
|
||||
});
|
||||
let btnLoading = ref(false);
|
||||
|
||||
const setMenuFormData = () => {
|
||||
if (props.initFormData?.id) {
|
||||
formData.id = props.initFormData?.id || '';
|
||||
formData.field_name = props.initFormData.field_name || '';
|
||||
formData.title = props.initFormData.title || '';
|
||||
formData.is_create = !!props.initFormData.is_create;
|
||||
formData.is_update = !!props.initFormData.is_update;
|
||||
formData.is_query = !!props.initFormData.is_query;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
try {
|
||||
btnLoading.value = true;
|
||||
let res;
|
||||
if (formData.id) {
|
||||
res = await updateColumnsData({ ...formData, ...props.currentInfo });
|
||||
} else {
|
||||
res = await addColumnsData({ ...formData, ...props.currentInfo });
|
||||
}
|
||||
if (res?.code === 2000) {
|
||||
successNotification(res.msg as string);
|
||||
handleClose('submit');
|
||||
}
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = (type: string = '') => {
|
||||
emit('drawerClose', type);
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setMenuFormData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.columns-form-com {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import { CurrentInfoType, AddColumnsDataType } from '../../types'
|
||||
|
||||
export function getColumnsData(query: any) {
|
||||
return request({
|
||||
url: '/api/system/column/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
export function automatchColumnsData(data: CurrentInfoType) {
|
||||
return request({
|
||||
url: '/api/system/column/auto_match_fields/',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function addColumnsData(data: AddColumnsDataType) {
|
||||
return request({
|
||||
url: '/api/system/column/',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteColumnsData(id: number) {
|
||||
return request({
|
||||
url: `/api/system/column/${id}/`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
export function updateColumnsData(data: AddColumnsDataType) {
|
||||
return request({
|
||||
url: `/api/system/column/${data.id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="columns-table-com">
|
||||
<p class="ctc-title">字段权限</p>
|
||||
|
||||
<div class="ctc-head">
|
||||
<el-button type="primary" @click="handleUpdateColumn('create')">新增</el-button>
|
||||
<el-button type="primary" @click="handleAutomatch">自动匹配</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="state.data" border v-loading="state.loading" class="ctc-table">
|
||||
<el-table-column prop="field_name" label="字段名" />
|
||||
<el-table-column prop="title" label="列名" />
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" @click="handleUpdateColumn('update', scope.row)">编辑</el-button>
|
||||
<el-button type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="ctc-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="searchParams.page"
|
||||
v-model:page-size="searchParams.limit"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
:total="state.total"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="drawerVisible" title="字段权限" direction="rtl" size="500px" :close-on-click-modal="false" :before-close="handleDrawerClose">
|
||||
<ColumnsFormCom v-if="drawerVisible" :currentInfo="props.currentInfo" :initFormData="drawerFormData" @drawerClose="handleDrawerClose" />
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import ColumnsFormCom from '../ColumnsFormCom/index.vue';
|
||||
import { getColumnsData, automatchColumnsData, deleteColumnsData, updateColumnsData } from './api';
|
||||
import { successNotification, warningNotification } from '/@/utils/message';
|
||||
import { APIResponseData, CurrentInfoType, ColumnsFormDataType, AddColumnsDataType } from '../../types';
|
||||
|
||||
const props = defineProps({
|
||||
currentInfo: {
|
||||
type: Object as () => CurrentInfoType,
|
||||
required: true,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
let searchParams = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
});
|
||||
let state = reactive({
|
||||
loading: false,
|
||||
data: [],
|
||||
total: 0,
|
||||
});
|
||||
let drawerVisible = ref(false);
|
||||
let drawerFormData = ref<Partial<ColumnsFormDataType>>({});
|
||||
|
||||
const fetchData = async (query: CurrentInfoType = props.currentInfo) => {
|
||||
try {
|
||||
state.loading = true;
|
||||
const res = await getColumnsData({ ...searchParams, ...query });
|
||||
if (res?.code === 2000) {
|
||||
state.data = res.data;
|
||||
state.total = res.total;
|
||||
}
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 自动匹配列
|
||||
*/
|
||||
const handleAutomatch = async () => {
|
||||
if (props.currentInfo?.role && props.currentInfo?.model && props.currentInfo?.app) {
|
||||
const res = await automatchColumnsData(props.currentInfo);
|
||||
if (res?.code === 2000) {
|
||||
successNotification('匹配成功');
|
||||
fetchData();
|
||||
}
|
||||
return;
|
||||
}
|
||||
warningNotification('请选择角色和模型表!');
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增 or 编辑
|
||||
*/
|
||||
const handleUpdateColumn = (type: string, record?: ColumnsFormDataType) => {
|
||||
if (props.currentInfo?.role && props.currentInfo?.model && props.currentInfo?.app) {
|
||||
if (type === 'update' && record) {
|
||||
drawerFormData.value = record;
|
||||
}
|
||||
drawerVisible.value = true;
|
||||
return;
|
||||
}
|
||||
warningNotification('请选择角色和模型表!');
|
||||
};
|
||||
const handleDrawerClose = (type?: string) => {
|
||||
if (type === 'submit') {
|
||||
fetchData();
|
||||
}
|
||||
drawerVisible.value = false;
|
||||
drawerFormData.value = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除 deleteColumnsData
|
||||
*/
|
||||
const handleDelete = ({ id }: { id: number }) => {
|
||||
ElMessageBox.confirm('确定删除该字段吗?', '提示', {
|
||||
type: 'error',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteColumnsData(id);
|
||||
if (res?.code === 2000) {
|
||||
successNotification('删除成功');
|
||||
fetchData();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const handleChange = (record: AddColumnsDataType) => {
|
||||
updateColumnsData(record).then((res: APIResponseData) => {
|
||||
successNotification(res.msg || '更新成功');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
const handleSizeChange = (limit: number) => {
|
||||
searchParams.limit = limit;
|
||||
fetchData();
|
||||
};
|
||||
const handleCurrentChange = (page: number) => {
|
||||
searchParams.page = page;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
defineExpose({ fetchData });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.columns-table-com {
|
||||
height: 100%;
|
||||
.ctc-title {
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
.ctc-head {
|
||||
height: 35px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.ctc-table {
|
||||
width: 100%;
|
||||
height: calc(100% - 135px);
|
||||
margin: 10px 0;
|
||||
}
|
||||
.ctc-pagination {
|
||||
height: 35px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
web/src/views/system/columns/components/ItemCom/index.vue
Normal file
140
web/src/views/system/columns/components/ItemCom/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="item-com">
|
||||
<p class="item-com-title">{{ props.title }}</p>
|
||||
<ul class="item-com-list" :style="{ height: showPagination ? 'calc(100% - 75px)' : 'calc(100% - 45px)' }">
|
||||
<li
|
||||
v-for="item in state.data"
|
||||
:key="item[props.value]"
|
||||
@click="handleClick(item)"
|
||||
:class="state.current === item[props.value] ? 'item-com-item active' : 'item-com-item'"
|
||||
>
|
||||
{{ item[props.label] }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="showPagination" class="item-com-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
small
|
||||
hide-on-single-page
|
||||
v-model:current-page="state.page"
|
||||
v-model:page-size="state.limit"
|
||||
layout="prev, pager, next"
|
||||
:pager-count="5"
|
||||
:total="state.total"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import { RoleInfoStateType } from './types';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'role',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '标题',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
showPagination: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['fetchData', 'itemClick']);
|
||||
|
||||
const state = reactive<RoleInfoStateType>({
|
||||
current: '',
|
||||
page: 1,
|
||||
limit: 20,
|
||||
data: [],
|
||||
total: 10,
|
||||
});
|
||||
|
||||
const fetchData = () => {
|
||||
emit(
|
||||
'fetchData',
|
||||
{
|
||||
page: state.page,
|
||||
limit: state.limit,
|
||||
},
|
||||
(res: { code: number; data: any[]; total: number }) => {
|
||||
if (res?.code === 2000) {
|
||||
state.data = res.data;
|
||||
state.total = res?.total || 10;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleClick = (record: any) => {
|
||||
state.current = record[props.value];
|
||||
emit('itemClick', props.type, record);
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
state.page = page;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-com {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.item-com-title {
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
.item-com-list {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
.item-com-item {
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 500ms;
|
||||
}
|
||||
.active {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
transition: all 500ms;
|
||||
}
|
||||
.item-com-item:hover {
|
||||
color: var(--el-color-primary);
|
||||
transition: all 500ms;
|
||||
}
|
||||
}
|
||||
.item-com-pagination {
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
web/src/views/system/columns/components/ItemCom/types.ts
Normal file
29
web/src/views/system/columns/components/ItemCom/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface PageQuery {
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface RoleItemType {
|
||||
id: number | string;
|
||||
modifier_name: string;
|
||||
creator_name: string;
|
||||
create_datetime: string;
|
||||
update_datetime: string;
|
||||
description: string;
|
||||
modifier: string;
|
||||
dept_belong_id: number | string | null,
|
||||
name: string;
|
||||
key: string;
|
||||
sort: number;
|
||||
status: boolean;
|
||||
admin: boolean;
|
||||
creator: string;
|
||||
}
|
||||
|
||||
export interface RoleInfoStateType {
|
||||
current: string;
|
||||
page: number;
|
||||
limit: number;
|
||||
data: any[],
|
||||
total: number;
|
||||
}
|
||||
124
web/src/views/system/columns/index.vue
Normal file
124
web/src/views/system/columns/index.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<fs-page class="columns">
|
||||
<el-row class="columns-el-row" :gutter="10">
|
||||
<el-col :span="6">
|
||||
<div class="columns-box columns-left">
|
||||
<ItemCom title="角色" type="role" showPagination @fetchData="fetchRoleData" @itemClick="handleClick" />
|
||||
</div>
|
||||
</el-col>
|
||||
<!-- <el-col :span="4">-->
|
||||
<!-- <div class="columns-box columns-left">-->
|
||||
<!-- <ItemCom title="菜单" type="menu" showPagination @fetchData="fetchMenuData" @itemClick="handleClick" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </el-col>-->
|
||||
<el-col :span="8">
|
||||
<div class="columns-box columns-center">
|
||||
<ItemCom title="模型表" type="model" label="showText" value="key" @fetchData="fetchModelData" @itemClick="handleClick" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<div class="columns-box columns-right">
|
||||
<ColumnsTableCom ref="columnsTableRef" :currentInfo="currentInfo" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import ItemCom from './components/ItemCom/index.vue';
|
||||
import ColumnsTableCom from './components/ColumnsTableCom/index.vue';
|
||||
import { getRoleList, getModelList,getMenuList } from './api';
|
||||
import { PageQuery, CurrentInfoType, ModelItemType } from './types';
|
||||
|
||||
const columnsTableRef = ref<InstanceType<typeof ColumnsTableCom> | null>(null);
|
||||
let currentInfo = reactive<CurrentInfoType>({
|
||||
role: '',
|
||||
model: '',
|
||||
app: '',
|
||||
menu:''
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取角色
|
||||
* @param query
|
||||
* @param callback
|
||||
*/
|
||||
const fetchRoleData = async (query: PageQuery, callback: Function) => {
|
||||
const res = await getRoleList(query);
|
||||
callback(res);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
* @param query
|
||||
* @param callback
|
||||
*/
|
||||
const fetchMenuData= async (query: PageQuery, callback: Function) => {
|
||||
const res = await getMenuList(query);
|
||||
callback(res);
|
||||
};
|
||||
|
||||
const fetchModelData = async (query: PageQuery, callback: Function) => {
|
||||
const res = await getModelList();
|
||||
res.data.forEach((item: ModelItemType) => {
|
||||
item.showText = `${item.app}-${item.title}(${item.key})`;
|
||||
});
|
||||
callback(res);
|
||||
};
|
||||
|
||||
const fetchTableData = () => {
|
||||
if (currentInfo.role && currentInfo.model && currentInfo.app) {
|
||||
columnsTableRef.value?.fetchData(currentInfo);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (type: string, record: any) => {
|
||||
if (type === 'role') {
|
||||
currentInfo.role = record.id;
|
||||
}
|
||||
|
||||
if(type === 'menu'){
|
||||
currentInfo.menu = record.id;
|
||||
}
|
||||
|
||||
if (type === 'model') {
|
||||
currentInfo.model = record.key;
|
||||
currentInfo.app = record.app;
|
||||
}
|
||||
fetchTableData();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.columns {
|
||||
.columns-el-row {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.el-col {
|
||||
height: 100%;
|
||||
padding: 10px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.columns-box {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.columns-left {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
.columns-center {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.columns-right {
|
||||
position: relative;
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
web/src/views/system/columns/types.ts
Normal file
43
web/src/views/system/columns/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface PageQuery {
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface APIResponseData {
|
||||
code?: number;
|
||||
data: any;
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
export interface CurrentInfoType {
|
||||
role: string;
|
||||
model: string;
|
||||
app: string;
|
||||
|
||||
menu: string;
|
||||
}
|
||||
|
||||
export interface ModelItemType {
|
||||
app: string;
|
||||
key: string;
|
||||
title: string;
|
||||
showText?: string;
|
||||
}
|
||||
|
||||
export interface AddColumnsDataType extends CurrentInfoType {
|
||||
id?: number | string;
|
||||
field_name: string;
|
||||
title: string;
|
||||
is_query: boolean;
|
||||
is_create: boolean;
|
||||
is_update: boolean;
|
||||
}
|
||||
|
||||
export interface ColumnsFormDataType {
|
||||
id?: number | string;
|
||||
field_name: string;
|
||||
title: string;
|
||||
is_create: boolean;
|
||||
is_update: boolean;
|
||||
is_query: boolean;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
|
||||
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
|
||||
import XEUtils from 'xe-utils';
|
||||
|
||||
export const apiPrefix = '/api/system/system_config/';
|
||||
export function GetList(query: PageQuery) {
|
||||
export function GetList(query: UserPageQuery) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'get',
|
||||
@@ -52,10 +52,10 @@ export function GetAssociationTable() {
|
||||
});
|
||||
}
|
||||
|
||||
export function saveContent (data:any) {
|
||||
export function saveContent(data: any) {
|
||||
return request({
|
||||
url: apiPrefix + 'save_content/',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<el-input-number v-model="form.sort" :min="0" :max="99"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSubmit">立即创建</el-button>
|
||||
<el-button type="primary" @click="onSubmit(formRef)">立即创建</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
<script setup lang="ts">
|
||||
import * as api from '../api';
|
||||
import associationTable from './components/associationTable.vue';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import {ref, reactive, onMounted, inject} from 'vue';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { successMessage } from '/@/utils/message';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
@@ -118,12 +118,17 @@ const getParent = () => {
|
||||
parentOptions.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
const refreshView:any = inject('refreshView')
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
api.AddObj(form).then((res: any) => {
|
||||
if (res.code == 2000) successMessage('新增成功');
|
||||
if (res.code == 2000) {
|
||||
successMessage('新增成功');
|
||||
refreshView()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('error submit!', fields);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user