diff --git a/README.zh.md b/README.zh.md index a95a364..e3e1048 100644 --- a/README.zh.md +++ b/README.zh.md @@ -54,16 +54,21 @@ ## 交流 - 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦 - - 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦 - - django-vue-admin交流01群(已满):812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi) - django-vue-admin交流02群(已满):687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi) -- django-vue-admin交流03群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) +- django-vue-admin交流03群(已满):442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) +- django-vue-admin交流04群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) -- 二维码 - + +## 给框架点赞 + +
+ + +
+ ## 源码地址 @@ -88,7 +93,19 @@ github地址:[https://github.com/huge-dream/django-vue3-admin](https://github. 13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。 ## 插件市场 🔌 -更新中... +1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html) + +2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html) + +3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html) + +4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html) + +5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git) + +6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线) + + ## 仓库分支说明 💈 主分支:master(稳定版本) @@ -210,5 +227,19 @@ docker-compose up -d --build ![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png) +## 审批流插件 + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/97fbbf29673edfd66a1edd49237791bb.png) + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/c43aa51278cbc478287c718d22397479.png) + + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/9732a5cca9c1166d1a65c35e313ab90d.png) + + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/3ca9dd0801ce76d21435abcc8a3d505a.png) + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/a87a8d2329ef66880af5b0f16c5ff823.png) + diff --git a/backend/.gitignore b/backend/.gitignore index 047099f..6c50cc9 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -98,5 +98,4 @@ media/ __pypackages__/ package-lock.json gunicorn.pid -plugins/* !plugins/__init__.py diff --git a/backend/application/asgi.py b/backend/application/asgi.py index 14aacec..37e9f35 100644 --- a/backend/application/asgi.py +++ b/backend/application/asgi.py @@ -8,9 +8,7 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ """ import os -from channels.auth import AuthMiddlewareStack -from channels.security.websocket import AllowedHostsOriginValidator -from channels.routing import ProtocolTypeRouter, URLRouter +from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') @@ -18,15 +16,6 @@ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" http_application = get_asgi_application() -from application.routing import websocket_urlpatterns - application = ProtocolTypeRouter({ "http": http_application, - 'websocket': AllowedHostsOriginValidator( - AuthMiddlewareStack( - URLRouter( - websocket_urlpatterns # 指明路由文件是devops/routing.py - ) - ) - ), }) diff --git a/backend/application/celery.py b/backend/application/celery.py index 10bce56..c719c46 100644 --- a/backend/application/celery.py +++ b/backend/application/celery.py @@ -1,6 +1,8 @@ import functools import os +from celery.signals import task_postrun + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') from django.conf import settings @@ -38,3 +40,12 @@ def retry_base_task_error(): return wrapper return wraps + + +@task_postrun.connect +def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras): + periodic_task_name = kwargs.get('periodic_task_name') + if periodic_task_name: + from django_celery_results.models import TaskResult + # 更新 TaskResult 表中的 periodic_task_name 字段 + TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name) diff --git a/backend/application/dispatch.py b/backend/application/dispatch.py index 3bd364c..101b3ab 100644 --- a/backend/application/dispatch.py +++ b/backend/application/dispatch.py @@ -2,6 +2,10 @@ # -*- coding: utf-8 -*- from django.conf import settings from django.db import connection +from django.core.cache import cache +from dvadmin.utils.validator import CustomValidationError + +dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis def is_tenants_mode(): @@ -68,6 +72,9 @@ def init_dictionary(): :return: """ try: + if dispatch_db_type == 'redis': + cache.set(f"init_dictionary", _get_all_dictionary()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -88,7 +95,9 @@ def init_system_config(): :return: """ try: - + if dispatch_db_type == 'redis': + cache.set(f"init_system_config", _get_all_system_config()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -107,6 +116,9 @@ def refresh_dictionary(): 刷新字典配置 :return: """ + if dispatch_db_type == 'redis': + cache.set(f"init_dictionary", _get_all_dictionary()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -122,6 +134,9 @@ def refresh_system_config(): 刷新系统配置 :return: """ + if dispatch_db_type == 'redis': + cache.set(f"init_system_config", _get_all_system_config()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + init_dictionary_data = cache.get(f"init_dictionary") + if not init_dictionary_data: + refresh_dictionary() + return cache.get(f"init_dictionary") or {} if not settings.DICTIONARY_CONFIG: refresh_dictionary() if is_tenants_mode(): @@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + dictionary_config = cache.get(f"init_dictionary") + if not dictionary_config: + refresh_dictionary() + dictionary_config = cache.get(f"init_dictionary") + return dictionary_config.get(key) dictionary_config = get_dictionary_config(schema_name) return dictionary_config.get(key) @@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ - children = get_dictionary_values(key, schema_name) or [] - for ele in children: + res = get_dictionary_values(key, schema_name) or [] + for ele in res.get('children'): if ele.get("value") == str(name): return ele.get("label") return "" @@ -187,6 +213,11 @@ def get_system_config(schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + init_dictionary_data = cache.get(f"init_system_config") + if not init_dictionary_data: + refresh_system_config() + return cache.get(f"init_system_config") or {} if not settings.SYSTEM_CONFIG: refresh_system_config() if is_tenants_mode(): @@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None): :param schema_name: 对应系统配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + system_config = cache.get(f"init_system_config") + if not system_config: + refresh_system_config() + system_config = cache.get(f"init_system_config") + return system_config.get(key) system_config = get_system_config(schema_name) return system_config.get(key) +def get_system_config_values_to_dict(key, schema_name=None): + """ + 获取系统配置数据并转换为字典 **仅限于数组类型系统配置 + :param key: 对应系统配置的key值(字典编号) + :param schema_name: 对应系统配置的租户schema_name值 + :return: + """ + values_dict = {} + config_values = get_system_config_values(key, schema_name) + if not isinstance(config_values, list): + raise CustomValidationError("该方式仅限于数组类型系统配置") + for ele in get_system_config_values(key, schema_name): + values_dict[ele.get('key')] = ele.get('value') + return values_dict + + def get_system_config_label(key, name, schema_name=None): """ 获取获取系统配置label值 diff --git a/backend/application/routing.py b/backend/application/routing.py deleted file mode 100644 index d4df9f8..0000000 --- a/backend/application/routing.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -from django.urls import path -from application.websocketConfig import MegCenter - -websocket_urlpatterns = [ - path('ws//', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者 -] diff --git a/backend/application/settings.py b/backend/application/settings.py index 1d0adf7..71ed7d5 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -399,8 +399,12 @@ DICTIONARY_CONFIG = {} # ================================================= # # 租户共享app TENANT_SHARED_APPS = [] +# 普通租户独有app +TENANT_EXCLUSIVE_APPS = [] # 插件 urlpatterns PLUGINS_URL_PATTERNS = [] +# 所有模式有的 +SHARED_APPS = [] # ********** 一键导入插件配置开始 ********** # 例如: # from dvadmin_upgrade_center.settings import * # 升级中心 diff --git a/backend/application/sse_views.py b/backend/application/sse_views.py new file mode 100644 index 0000000..f1cbe01 --- /dev/null +++ b/backend/application/sse_views.py @@ -0,0 +1,33 @@ +# views.py +import time + +import jwt +from django.http import StreamingHttpResponse + +from application import settings +from dvadmin.system.models import MessageCenterTargetUser +from django.core.cache import cache + + +def event_stream(user_id): + last_sent_time = 0 + + while True: + # 从 Redis 中获取最后数据库变更时间 + last_db_change_time = cache.get('last_db_change_time', 0) + # 只有当数据库发生变化时才检查总数 + if last_db_change_time and last_db_change_time > last_sent_time: + count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count() + yield f"data: {count}\n\n" + last_sent_time = time.time() + + time.sleep(1) + + +def sse_view(request): + token = request.GET.get('token') + decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256']) + user_id = decoded.get('user_id') + response = StreamingHttpResponse(event_stream(user_id), content_type='text/event-stream') + response['Cache-Control'] = 'no-cache' + return response diff --git a/backend/application/urls.py b/backend/application/urls.py index cb5a899..d1902fc 100644 --- a/backend/application/urls.py +++ b/backend/application/urls.py @@ -24,6 +24,7 @@ from rest_framework_simplejwt.views import ( from application import dispatch from application import settings +from application.sse_views import sse_view from dvadmin.system.views.dictionary import InitDictionaryViewSet from dvadmin.system.views.login import ( LoginView, @@ -40,6 +41,7 @@ dispatch.init_system_config() dispatch.init_dictionary() # =========== 初始化系统配置 ================= +permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ] schema_view = get_schema_view( openapi.Info( title="Snippets API", @@ -50,7 +52,7 @@ schema_view = get_schema_view( license=openapi.License(name="BSD License"), ), public=True, - permission_classes=(permissions.AllowAny,), + permission_classes=permission_classes, generator_class=CustomOpenAPISchemaGenerator, ) # 前端页面映射 @@ -115,6 +117,8 @@ urlpatterns = ( # 前端页面映射 path('web/', web_view, name='web_view'), path('web/', serve_web_files, name='serve_web_files'), + # sse + path('sse/', sse_view, name='sse'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_URL) diff --git a/backend/application/websocketConfig.py b/backend/application/websocketConfig.py deleted file mode 100644 index ab2cd64..0000000 --- a/backend/application/websocketConfig.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -import urllib - -from asgiref.sync import sync_to_async, async_to_sync -from channels.db import database_sync_to_async -from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer -import json - -from channels.layers import get_channel_layer -from jwt import InvalidSignatureError -from rest_framework.request import Request - -from application import settings -from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser -from dvadmin.system.views.message_center import MessageCenterTargetUserSerializer -from dvadmin.utils.serializers import CustomModelSerializer - -send_dict = {} - - -# 发送消息结构体 -def set_message(sender, msg_type, msg, unread=0): - text = { - 'sender': sender, - 'contentType': msg_type, - 'content': msg, - 'unread': unread - } - return text - - -# 异步获取消息中心的目标用户 -@database_sync_to_async -def _get_message_center_instance(message_id): - from dvadmin.system.models import MessageCenter - _MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user', flat=True) - if _MessageCenter: - return _MessageCenter - else: - return [] - - -@database_sync_to_async -def _get_message_unread(user_id): - """获取用户的未读消息数量""" - from dvadmin.system.models import MessageCenterTargetUser - count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count() - return count or 0 - - -def request_data(scope): - query_string = scope.get('query_string', b'').decode('utf-8') - qs = urllib.parse.parse_qs(query_string) - return qs - - -class DvadminWebSocket(AsyncJsonWebsocketConsumer): - async def connect(self): - try: - import jwt - self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"] - decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"]) - if decoded_result: - self.user_id = decoded_result.get('user_id') - self.chat_group_name = "user_" + str(self.user_id) - # 收到连接时候处理, - await self.channel_layer.group_add( - self.chat_group_name, - self.channel_name - ) - await self.accept() - # 主动推送消息 - unread_count = await _get_message_unread(self.user_id) - if unread_count == 0: - # 发送连接成功 - await self.send_json(set_message('system', 'SYSTEM', '您已上线')) - else: - await self.send_json( - set_message('system', 'SYSTEM', "请查看您的未读消息~", - unread=unread_count)) - except InvalidSignatureError: - await self.disconnect(None) - - async def disconnect(self, close_code): - # Leave room group - await self.channel_layer.group_discard(self.chat_group_name, self.channel_name) - print("连接关闭") - try: - await self.close(close_code) - except Exception: - pass - - -class MegCenter(DvadminWebSocket): - """ - 消息中心 - """ - - async def receive(self, text_data): - # 接受客户端的信息,你处理的函数 - text_data_json = json.loads(text_data) - message_id = text_data_json.get('message_id', None) - user_list = await _get_message_center_instance(message_id) - for send_user in user_list: - await self.channel_layer.group_send( - "user_" + str(send_user), - {'type': 'push.message', 'json': text_data_json} - ) - - async def push_message(self, event): - """消息发送""" - message = event['json'] - await self.send(text_data=json.dumps(message)) - - -class MessageCreateSerializer(CustomModelSerializer): - """ - 消息中心-新增-序列化器 - """ - class Meta: - model = MessageCenter - fields = "__all__" - read_only_fields = ["id"] - - -def websocket_push(user_id, message): - username = "user_" + str(user_id) - channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - username, - { - "type": "push.message", - "json": message - } - ) - - -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: - target_role = [] - if target_dept is None: - target_dept = [] - data = { - "title": title, - "content": content, - "target_type": target_type, - "target_user": target_user, - "target_dept": target_dept, - "target_role": target_role - } - message_center_instance = MessageCreateSerializer(data=data, request=request) - message_center_instance.is_valid(raise_exception=True) - message_center_instance.save() - users = target_user or [] - if target_type in [1]: # 按角色 - users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True) - if target_type in [2]: # 按部门 - 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) - targetuser_data = [] - for user in users: - targetuser_data.append({ - "messagecenter": message_center_instance.instance.id, - "users": user - }) - targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=request) - targetuser_instance.is_valid(raise_exception=True) - targetuser_instance.save() - for user in users: - username = "user_" + str(user) - unread_count = async_to_sync(_get_message_unread)(user) - channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - username, - { - "type": "push.message", - "json": {**message, 'unread': unread_count} - } - ) diff --git a/backend/del_migrations.py b/backend/del_migrations.py index d649e05..775d1dc 100644 --- a/backend/del_migrations.py +++ b/backend/del_migrations.py @@ -2,7 +2,7 @@ import os -exclude = ["venv"] # 需要排除的文件目录 +exclude = ["venv", ".venv"] # 需要排除的文件目录 for root, dirs, files in os.walk('.'): dirs[:] = list(set(dirs) - set(exclude)) if 'migrations' in dirs: diff --git a/backend/dvadmin/system/apps.py b/backend/dvadmin/system/apps.py index 191aade..8302f72 100644 --- a/backend/dvadmin/system/apps.py +++ b/backend/dvadmin/system/apps.py @@ -4,3 +4,7 @@ from django.apps import AppConfig class SystemConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'dvadmin.system' + + def ready(self): + # 注册信号 + import dvadmin.system.signals # 确保路径正确 diff --git a/backend/dvadmin/system/fixtures/initSerializer.py b/backend/dvadmin/system/fixtures/initSerializer.py index 9ed094f..f983aea 100644 --- a/backend/dvadmin/system/fixtures/initSerializer.py +++ b/backend/dvadmin/system/fixtures/initSerializer.py @@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer): """ 初始化获取数信息(用于生成初始化json文件) """ + role_key = serializers.SerializerMethodField() + dept_key = serializers.SerializerMethodField() + + def get_dept_key(self, obj): + if obj.dept: + return obj.dept.key + else: + return None + + def get_role_key(self, obj): + if obj.role.all(): + return [role.key for role in obj.role.all()] + else: + return [] def save(self, **kwargs): instance = super().save(**kwargs) @@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer): model = Users fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id', - 'password', 'last_login', 'is_superuser'] + 'password', 'last_login', 'is_superuser', 'role_key' ,'dept_key'] read_only_fields = ['id'] extra_kwargs = { 'creator': {'write_only': True}, @@ -175,15 +189,21 @@ class RoleMenuInitSerializer(CustomModelSerializer): """ 初始化角色菜单(用于生成初始化json文件) """ - role__key = serializers.CharField(max_length=100, required=True) - menu__web_path = serializers.CharField(max_length=100, required=True) - menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True) + role__key = serializers.CharField(source='role.key') + menu__web_path = serializers.CharField(source='menu.web_path') + menu__component_name = serializers.CharField(source='menu.component_name', allow_blank=True) + + def update(self, instance, validated_data): + init_data = self.initial_data + role_id = Role.objects.filter(key=init_data['role__key']).first() + menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first() + validated_data['role'] = role_id + validated_data['menu'] = menu_id + return super().update(instance, validated_data) + def create(self, validated_data): init_data = self.initial_data - validated_data.pop('menu__web_path') - validated_data.pop('menu__component_name') - validated_data.pop('role__key') role_id = Role.objects.filter(key=init_data['role__key']).first() menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first() validated_data['role'] = role_id @@ -192,7 +212,7 @@ class RoleMenuInitSerializer(CustomModelSerializer): class Meta: model = RoleMenuPermission - fields = ['role__key', 'menu__web_path', 'menu__component_name', 'creator', 'dept_belong_id'] + fields = ['role__key', 'menu__web_path', 'menu__component_name','creator', 'dept_belong_id'] read_only_fields = ["id"] extra_kwargs = { 'role': {'required': False}, @@ -206,14 +226,22 @@ 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(source='role.key') + menu_button__value = serializers.CharField(source='menu_button.value') data_range = serializers.CharField(max_length=100, required=False) + def update(self, instance, validated_data): + init_data = self.initial_data + role_id = Role.objects.filter(key=init_data['role__key']).first() + menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first() + validated_data['role'] = role_id + validated_data['menu_button'] = menu_button_id + instance = super().create(validated_data) + instance.dept.set([]) + return super().update(instance, validated_data) + def create(self, validated_data): init_data = self.initial_data - validated_data.pop('menu_button__value') - validated_data.pop('role__key') role_id = Role.objects.filter(key=init_data['role__key']).first() menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first() validated_data['role'] = role_id @@ -223,7 +251,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer): return instance def save(self, **kwargs): - if self.instance and self.initial_data.get('reset'): + if not self.instance or self.initial_data.get('reset'): return super().save(**kwargs) return self.instance diff --git a/backend/dvadmin/system/fixtures/init_dictionary.json b/backend/dvadmin/system/fixtures/init_dictionary.json index f750c40..c4bd186 100644 --- a/backend/dvadmin/system/fixtures/init_dictionary.json +++ b/backend/dvadmin/system/fixtures/init_dictionary.json @@ -546,5 +546,50 @@ "children": [] } ] + }, + { + "label": "文件存储引擎", + "value": "file_engine", + "type": 0, + "color": null, + "is_value": false, + "status": true, + "sort": 9, + "remark": null, + "children": [ + { + "label": "本地", + "value": "local", + "type": 0, + "color": "primary", + "is_value": true, + "status": true, + "sort": 1, + "remark": null, + "children": [] + }, + { + "label": "阿里云oss", + "value": "oss", + "type": 0, + "color": "success", + "is_value": true, + "status": true, + "sort": 2, + "remark": null, + "children": [] + }, + { + "label": "腾讯cos", + "value": "cos", + "type": 0, + "color": "warning", + "is_value": true, + "status": true, + "sort": 3, + "remark": null, + "children": [] + } + ] } ] \ No newline at end of file diff --git a/backend/dvadmin/system/fixtures/init_systemconfig.json b/backend/dvadmin/system/fixtures/init_systemconfig.json index 98c95cd..cc692f2 100644 --- a/backend/dvadmin/system/fixtures/init_systemconfig.json +++ b/backend/dvadmin/system/fixtures/init_systemconfig.json @@ -235,5 +235,252 @@ "children": [] } ] - } + }, + { + "title": "文件存储配置", + "key": "file_storage", + "value": null, + "sort": 0, + "status": true, + "data_options": null, + "form_item_type": 0, + "rule": null, + "placeholder": null, + "setting": null, + "children": [ + { + "title": "存储引擎", + "key": "file_engine", + "value": "local", + "sort": 1, + "status": true, + "data_options": null, + "form_item_type": 4, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请选择存储引擎", + "setting": "file_engine", + "children": [] + }, + { + "title": "文件是否备份", + "key": "file_backup", + "value": false, + "sort": 2, + "status": true, + "data_options": null, + "form_item_type": 9, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "启用云存储时,文件是否备份到本地", + "setting": null, + "children": [] + }, + { + "title": "阿里云-AccessKey", + "key": "aliyun_access_key", + "value": null, + "sort": 3, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入AccessKey", + "setting": null, + "children": [] + }, + { + "title": "阿里云-Secret", + "key": "aliyun_access_secret", + "value": null, + "sort": 4, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Secret", + "setting": null, + "children": [] + }, + { + "title": "阿里云-Endpoint", + "key": "aliyun_endpoint", + "value": null, + "sort": 5, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Endpoint", + "setting": null, + "children": [] + }, + { + "title": "阿里云-上传路径", + "key": "aliyun_path", + "value": "/media/", + "sort": 5, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入上传路径", + "setting": null, + "children": [] + }, + { + "title": "阿里云-Bucket", + "key": "aliyun_bucket", + "value": null, + "sort": 7, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Bucket", + "setting": null, + "children": [] + },{ + "title": "阿里云-cdn地址", + "key": "aliyun_cdn_url", + "value": null, + "sort": 7, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入cdn地址", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-SecretId", + "key": "tencent_secret_id", + "value": null, + "sort": 8, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入SecretId", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-SecretKey", + "key": "tencent_secret_key", + "value": null, + "sort": 9, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入SecretKey", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-Region", + "key": "tencent_region", + "value": null, + "sort": 10, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Region", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-Bucket", + "key": "tencent_bucket", + "value": null, + "sort": 11, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Bucket", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-上传路径", + "key": "tencent_path", + "value": "/media/", + "sort": 12, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入上传路径", + "setting": null, + "children": [] + } + ] + } ] \ No newline at end of file diff --git a/backend/dvadmin/system/management/commands/generate_init_json.py b/backend/dvadmin/system/management/commands/generate_init_json.py index b074344..e0de3e3 100644 --- a/backend/dvadmin/system/management/commands/generate_init_json.py +++ b/backend/dvadmin/system/management/commands/generate_init_json.py @@ -10,7 +10,7 @@ django.setup() from django.core.management.base import BaseCommand from application.settings import BASE_DIR -from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig +from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig, RoleMenuButtonPermission, RoleMenuPermission from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \ MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \ RoleMenuInitSerializer, RoleMenuButtonInitSerializer @@ -29,7 +29,7 @@ class Command(BaseCommand): def serializer_data(self, serializer, query_set: QuerySet): serializer = serializer(query_set, many=True) data = json.loads(json.dumps(serializer.data, ensure_ascii=False)) - with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w') as f: + with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w',encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) return @@ -57,6 +57,12 @@ class Command(BaseCommand): def generate_system_config(self): self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True)) + def generate_role_menu(self): + self.serializer_data(RoleMenuInitSerializer, RoleMenuPermission.objects.all()) + + def generate_role_menu_button(self): + self.serializer_data(RoleMenuButtonInitSerializer, RoleMenuButtonPermission.objects.all()) + def handle(self, *args, **options): generate_name = options.get('generate_name') generate_name_dict = { @@ -67,6 +73,8 @@ class Command(BaseCommand): "api_white_list": self.generate_api_white_list, "dictionary": self.generate_dictionary, "system_config": self.generate_system_config, + "role_menu": self.generate_role_menu, + "role_menu_button": self.generate_role_menu_button, } if not generate_name: for ele in generate_name_dict.keys(): diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index d9a4274..b6fe279 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -73,10 +73,17 @@ class Users(CoreModel, AbstractUser): help_text="关联部门", ) login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数") + pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数") objects = CustomUserManager() def set_password(self, raw_password): - super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) + if raw_password: + super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) + + def save(self, *args, **kwargs): + if self.name == "": + self.name = self.username + super().save(*args, **kwargs) class Meta: db_table = table_prefix + "system_users" @@ -121,6 +128,27 @@ class Dept(CoreModel): help_text="上级部门", ) + @classmethod + def _recursion(cls, instance, parent, result): + new_instance = getattr(instance, parent, None) + res = [] + data = getattr(instance, result, None) + if data: + res.append(data) + if new_instance: + array = cls._recursion(new_instance, parent, result) + res += array + return res + + @classmethod + def get_region_name(cls, obj): + """ + 获取某个用户的递归所有部门名称 + """ + dept_name_all = cls._recursion(obj, "parent", "name") + dept_name_all.reverse() + return "/".join(dept_name_all) + @classmethod def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None): """ @@ -407,6 +435,18 @@ class FileList(CoreModel): 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") + UPLOAD_METHOD_CHOIDES = ( + (0, '默认上传'), + (1, '文件选择器上传'), + ) + upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式') + FILE_TYPE_CHOIDES = ( + (0, '图片'), + (1, '视频'), + (2, '音频'), + (3, '其他'), + ) + file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型') def save(self, *args, **kwargs): if not self.md5sum: # file is new diff --git a/backend/dvadmin/system/signals.py b/backend/dvadmin/system/signals.py index 9728228..d00770c 100644 --- a/backend/dvadmin/system/signals.py +++ b/backend/dvadmin/system/signals.py @@ -1,4 +1,10 @@ -from django.dispatch import Signal +import time + +from django.db.models.signals import post_save, post_delete +from django.dispatch import Signal, receiver +from django.core.cache import cache +from dvadmin.system.models import MessageCenterTargetUser + # 初始化信号 pre_init_complete = Signal() detail_init_complete = Signal() @@ -10,3 +16,12 @@ post_tenants_init_complete = Signal() post_tenants_all_init_complete = Signal() # 租户创建完成信号 tenants_create_complete = Signal() + +# 全局变量用于标记最后修改时间 +last_db_change_time = time.time() + + +@receiver(post_save, sender=MessageCenterTargetUser) +@receiver(post_delete, sender=MessageCenterTargetUser) +def update_last_change_time(sender, **kwargs): + cache.set('last_db_change_time', time.time(), timeout=None) # 设置永不超时的键值对 diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py index c9c12e9..0ee2cb8 100644 --- a/backend/dvadmin/system/urls.py +++ b/backend/dvadmin/system/urls.py @@ -49,7 +49,7 @@ urlpatterns = [ path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), # path('login_log//', LoginLogViewSet.as_view({'get': 'retrieve'})), - path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), + # path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), path('clause/privacy.html', PrivacyView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()), ] diff --git a/backend/dvadmin/system/views/download_center.py b/backend/dvadmin/system/views/download_center.py index 2587d74..4e6b061 100644 --- a/backend/dvadmin/system/views/download_center.py +++ b/backend/dvadmin/system/views/download_center.py @@ -41,6 +41,14 @@ class DownloadCenterViewSet(CustomModelViewSet): serializer_class = DownloadCenterSerializer filter_class = DownloadCenterFilterSet permission_classes = [] + extra_filter_class = [] def get_queryset(self): + # 判断是否是 Swagger 文档生成阶段,防止报错 + if getattr(self, 'swagger_fake_view', False): + return self.queryset.model.objects.none() + + # 正常请求下的逻辑 + if self.request.user.is_superuser: + return super().get_queryset() return super().get_queryset().filter(creator=self.request.user) diff --git a/backend/dvadmin/system/views/file_list.py b/backend/dvadmin/system/views/file_list.py index c595699..a155ea1 100644 --- a/backend/dvadmin/system/views/file_list.py +++ b/backend/dvadmin/system/views/file_list.py @@ -1,12 +1,15 @@ import hashlib import mimetypes +import django_filters +from django.conf import settings +from django.db import connection 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.json_response import DetailResponse, SuccessResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet @@ -15,16 +18,25 @@ class FileSerializer(CustomModelSerializer): url = serializers.SerializerMethodField(read_only=True) def get_url(self, instance): - base_url = f"{self.request.scheme}://{self.request.get_host()}/" - return base_url + (instance.file_url or (f'media/{str(instance.url)}')) + if self.request.query_params.get('prefix'): + if settings.ENVIRONMENT in ['local']: + prefix = 'http://127.0.0.1:8000' + elif settings.ENVIRONMENT in ['test']: + prefix = 'http://{host}/api'.format(host=self.request.get_host()) + else: + prefix = 'https://{host}/api'.format(host=self.request.get_host()) + if instance.file_url: + return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}" + return (f'{prefix}/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): - file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local' - file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup") + file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local' + file_backup = dispatch.get_system_config_values("file_storage.file_backup") file = self.initial_data.get('file') file_size = file.size validated_data['name'] = str(file) @@ -35,18 +47,20 @@ class FileSerializer(CustomModelSerializer): validated_data['md5sum'] = md5.hexdigest() validated_data['engine'] = file_engine validated_data['mime_type'] = file.content_type + ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None) + validated_data['file_type'] = 3 if ft is None else ft 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) + from dvadmin.utils.aliyunoss import ali_oss_upload + file_path = ali_oss_upload(file, file_name=validated_data['name']) 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) + from dvadmin.utils.tencentcos import tencent_cos_upload + file_path = tencent_cos_upload(file, file_name=validated_data['name']) if file_path: validated_data['file_url'] = file_path else: @@ -64,6 +78,22 @@ class FileSerializer(CustomModelSerializer): return super().create(validated_data) +class FileAllSerializer(CustomModelSerializer): + + class Meta: + model = FileList + fields = ['id', 'name'] + + +class FileFilter(django_filters.FilterSet): + name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名") + mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型") + + class Meta: + model = FileList + fields = ['name', 'mime_type', 'upload_method', 'file_type'] + + class FileViewSet(CustomModelViewSet): """ 文件管理接口 @@ -75,5 +105,22 @@ class FileViewSet(CustomModelViewSet): """ queryset = FileList.objects.all() serializer_class = FileSerializer - filter_fields = ['name', ] - permission_classes = [] \ No newline at end of file + filter_class = FileFilter + permission_classes = [] + + @action(methods=['GET'], detail=False) + def get_all(self, request): + data1 = self.get_serializer(self.get_queryset(), many=True).data + data2 = [] + if dispatch.is_tenants_mode(): + from django_tenants.utils import schema_context + with schema_context('public'): + data2 = self.get_serializer(FileList.objects.all(), many=True).data + return DetailResponse(data=data2+data1) + + def list(self, request, *args, **kwargs): + if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode(): + from django_tenants.utils import schema_context + with schema_context('public'): + return super().list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index 1996906..3b1209d 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -4,12 +4,15 @@ 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 check_password, make_password from django.db.models import Q from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import serializers +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.views import TokenObtainPairView @@ -97,16 +100,17 @@ class LoginSerializer(TokenObtainPairSerializer): # 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误 attrs['username'] = user.username data = super().validate(attrs) + data["username"] = self.user.username data["name"] = self.user.name data["userId"] = self.user.id data["avatar"] = self.user.avatar data['user_type'] = self.user.user_type + data['pwd_change_count'] = self.user.pwd_change_count dept = getattr(self.user, 'dept', None) if dept: data['dept_info'] = { 'dept_id': dept.id, 'dept_name': dept.name, - } role = getattr(self.user, 'role', None) if role: diff --git a/backend/dvadmin/system/views/menu.py b/backend/dvadmin/system/views/menu.py index c0c6b65..33ed979 100644 --- a/backend/dvadmin/system/views/menu.py +++ b/backend/dvadmin/system/views/menu.py @@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet): """用于前端获取当前角色的路由""" user = request.user if user.is_superuser: - queryset = self.queryset.filter(status=1).order_by("id") + queryset = self.queryset.filter(status=1).order_by("sort") 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).order_by("id") + queryset = Menu.objects.filter(id__in=menu_list).order_by("sort") serializer = WebRouterSerializer(queryset, many=True, request=request) data = serializer.data return SuccessResponse(data=data, total=len(data), msg="获取成功") diff --git a/backend/dvadmin/system/views/message_center.py b/backend/dvadmin/system/views/message_center.py index db91b75..26faa3f 100644 --- a/backend/dvadmin/system/views/message_center.py +++ b/backend/dvadmin/system/views/message_center.py @@ -36,7 +36,7 @@ class MessageCenterSerializer(CustomModelSerializer): return serializer.data def get_user_info(self, instance, parsed_query): - if instance.target_type in (1,2,3): + if instance.target_type in (1, 2, 3): return [] users = instance.target_user.all() # You can do what ever you want in here @@ -108,7 +108,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer): return serializer.data def get_user_info(self, instance, parsed_query): - if instance.target_type in (1,2,3): + if instance.target_type in (1, 2, 3): return [] users = instance.target_user.all() # You can do what ever you want in here @@ -139,21 +139,6 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer): read_only_fields = ["id"] -def websocket_push(user_id, message): - """ - 主动推送消息 - """ - username = "user_" + str(user_id) - channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - username, - { - "type": "push.message", - "json": message - } - ) - - class MessageCenterCreateSerializer(CustomModelSerializer): """ 消息中心-新增-序列化器 @@ -182,10 +167,6 @@ class MessageCenterCreateSerializer(CustomModelSerializer): targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request) targetuser_instance.is_valid(raise_exception=True) targetuser_instance.save() - 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}) return data class Meta: @@ -225,10 +206,6 @@ class MessageCenterViewSet(CustomModelViewSet): queryset.save() instance = self.get_object() serializer = self.get_serializer(instance) - # 主动推送消息 - 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}) return DetailResponse(data=serializer.data, msg="获取成功") @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) diff --git a/backend/dvadmin/system/views/role.py b/backend/dvadmin/system/views/role.py index a5b5a6f..f396860 100644 --- a/backend/dvadmin/system/views/role.py +++ b/backend/dvadmin/system/views/role.py @@ -10,22 +10,29 @@ from rest_framework import serializers from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated -from dvadmin.system.models import Role, Menu, MenuButton, Dept +from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users 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.json_response import SuccessResponse, DetailResponse, ErrorResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.viewset import CustomModelViewSet +from dvadmin.utils.permission import CustomPermission class RoleSerializer(CustomModelSerializer): """ 角色-序列化器 """ + users = serializers.SerializerMethodField() + + @staticmethod + def get_users(instance): + users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name') + return users class Meta: model = Role @@ -101,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer): fields = '__all__' - class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): """ 角色管理接口 @@ -116,3 +122,82 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): create_serializer_class = RoleCreateUpdateSerializer update_serializer_class = RoleCreateUpdateSerializer search_fields = ['name', 'key'] + + @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) + def set_role_users(self, request, pk): + """ + 设置 角色-用户 + :param request: + :return: + """ + data = request.data + direction = data.get('direction') + movedKeys = data.get('movedKeys') + role = Role.objects.get(pk=pk) + if direction == "left": + # left : 移除用户权限 + role.users_set.remove(*movedKeys) + else: + # right : 添加用户权限 + role.users_set.add(*movedKeys) + serializer = RoleSerializer(role) + return DetailResponse(data=serializer.data, msg="更新成功") + + @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission]) + def get_role_users(self, request): + """ + 获取角色已授权、未授权的用户 + 已授权的用户:1 + 未授权的用户:0 + """ + role_id = request.query_params.get('role_id', None) + + if not role_id: + return ErrorResponse(msg="请选择角色") + + if request.query_params.get('authorized', 0) == "1": + queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True) + else: + queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True) + + if name := request.query_params.get('name', None): + queryset = queryset.filter(name__icontains=name) + + if dept := request.query_params.get('dept', None): + queryset = queryset.filter(dept=dept) + + page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name')) + if page is not None: + return self.get_paginated_response(page) + + return SuccessResponse(data=page) + + @action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission]) + def remove_role_user(self, request, pk): + """ + 角色-删除用户 + """ + user_id = request.data.get('user_id', None) + + if not user_id: + return ErrorResponse(msg="请选择用户") + + role = self.get_object() + role.users_set.remove(*user_id) + + return SuccessResponse(msg="删除成功") + + @action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission]) + def add_role_users(self, request, pk): + """ + 角色-添加用户 + """ + users_id = request.data.get('users_id', None) + + if not users_id: + return ErrorResponse(msg="请选择用户") + + role = self.get_object() + role.users_set.add(*users_id) + + return DetailResponse(msg="添加成功") diff --git a/backend/dvadmin/system/views/role_menu_button_permission.py b/backend/dvadmin/system/views/role_menu_button_permission.py index 3ee596d..c041887 100644 --- a/backend/dvadmin/system/views/role_menu_button_permission.py +++ b/backend/dvadmin/system/views/role_menu_button_permission.py @@ -6,24 +6,20 @@ @Created on: 2021/6/3 003 0:30 @Remark: 菜单按钮管理 """ -from django.db.models import F, Subquery, OuterRef, Exists, BooleanField, Q, Case, Value, When -from django.db.models.functions import Coalesce from rest_framework import serializers from rest_framework.decorators import action -from rest_framework.fields import ListField from rest_framework.permissions import IsAuthenticated -from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \ - MenuField -from dvadmin.system.views.menu import MenuSerializer -from dvadmin.utils.json_response import DetailResponse, ErrorResponse +from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \ + MenuField, FieldPermission +from dvadmin.utils.json_response import DetailResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet class RoleMenuButtonPermissionSerializer(CustomModelSerializer): """ - 菜单按钮-序列化器 + 角色-菜单-按钮-权限 查询序列化 """ class Meta: @@ -34,7 +30,7 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer): 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) @@ -45,63 +41,99 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer): read_only_fields = ["id"] -class RoleButtonPermissionSerializer(CustomModelSerializer): +class RoleMenuSerializer(CustomModelSerializer): """ - 角色按钮权限 + 角色-菜单 序列化 """ isCheck = serializers.SerializerMethodField() - data_range = serializers.SerializerMethodField() def get_isCheck(self, instance): params = self.request.query_params + data = self.request.data + return RoleMenuPermission.objects.filter( + menu_id=instance.id, + role_id=params.get('roleId', data.get('roleId')), + ).exists() + + class Meta: + model = Menu + fields = ["id", "name", "parent", "is_catalog", "isCheck"] + + +class RoleMenuButtonSerializer(CustomModelSerializer): + """ + 角色-菜单-按钮 序列化 + """ + isCheck = serializers.SerializerMethodField() + data_range = serializers.SerializerMethodField() + role_menu_btn_perm_id = serializers.SerializerMethodField() + dept = serializers.SerializerMethodField() + + def get_isCheck(self, instance): + params = self.request.query_params + data = self.request.data return RoleMenuButtonPermission.objects.filter( - menu_button__id=instance['id'], - role__id=params.get('role'), + menu_button_id=instance.id, + role_id=params.get('roleId', data.get('roleId')), ).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() + obj = self.get_role_menu_btn_prem(instance) if obj is None: return None return obj.data_range + def get_role_menu_btn_perm_id(self, instance): + obj = self.get_role_menu_btn_prem(instance) + if obj is None: + return None + return obj.id + + def get_dept(self, instance): + obj = self.get_role_menu_btn_prem(instance) + if obj is None: + return None + return obj.dept.all().values_list('id', flat=True) + + def get_role_menu_btn_prem(self, instance): + params = self.request.query_params + data = self.request.data + obj = RoleMenuButtonPermission.objects.filter( + menu_button_id=instance.id, + role_id=params.get('roleId', data.get('roleId')), + ).first() + return obj + class Meta: model = MenuButton - fields = ['id', 'name', 'value', 'isCheck', 'data_range'] - - -class RoleFieldPermissionSerializer(CustomModelSerializer): - class Meta: - model = FieldPermission - fields = "__all__" + fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept'] class RoleMenuFieldSerializer(CustomModelSerializer): + """ + 角色-菜单-字段 序列化 + """ is_query = serializers.SerializerMethodField() is_create = serializers.SerializerMethodField() is_update = serializers.SerializerMethodField() def get_is_query(self, instance): params = self.request.query_params - queryset = instance.menu_field.filter(role=params.get('role')).first() + queryset = instance.menu_field.filter(role=params.get('roleId')).first() if queryset: return queryset.is_query return False def get_is_create(self, instance): params = self.request.query_params - queryset = instance.menu_field.filter(role=params.get('role')).first() + queryset = instance.menu_field.filter(role=params.get('roleId')).first() if queryset: return queryset.is_create return False def get_is_update(self, instance): params = self.request.query_params - queryset = instance.menu_field.filter(role=params.get('role')).first() + queryset = instance.menu_field.filter(role=params.get('roleId')).first() if queryset: return queryset.is_update return False @@ -111,54 +143,6 @@ class RoleMenuFieldSerializer(CustomModelSerializer): fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update'] -class RoleMenuSerializer(CustomModelSerializer): - menus = serializers.SerializerMethodField() - - def get_menus(self, instance): - menu_list = Menu.objects.filter(parent=instance['id']).values('id', 'name') - serializer = RoleMenuPermissionSerializer(menu_list, many=True, request=self.request) - return serializer.data - - class Meta: - model = Menu - fields = ['id', 'name', 'menus'] - - -class RoleMenuPermissionSerializer(CustomModelSerializer): - """ - 菜单和按钮权限 - """ - # name = serializers.SerializerMethodField() - isCheck = serializers.SerializerMethodField() - btns = serializers.SerializerMethodField() - columns = serializers.SerializerMethodField() - - # def get_name(self, instance): - # parent_list = Menu.get_all_parent(instance['id']) - # names = [d["name"] for d in parent_list] - # return "/".join(names) - def get_isCheck(self, instance): - params = self.request.query_params - return RoleMenuPermission.objects.filter( - menu__id=instance['id'], - role__id=params.get('role'), - ).exists() - - def get_btns(self, instance): - btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value') - serializer = RoleButtonPermissionSerializer(btn_list, many=True, request=self.request) - return serializer.data - - def get_columns(self, instance): - col_list = MenuField.objects.filter(menu=instance['id']) - serializer = RoleMenuFieldSerializer(col_list, many=True, request=self.request) - return serializer.data - - class Meta: - model = Menu - fields = ['id', 'name', 'isCheck', 'btns', 'columns'] - - class RoleMenuButtonPermissionViewSet(CustomModelViewSet): """ 菜单按钮接口 @@ -174,202 +158,111 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer extra_filter_class = [] - # @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - # def get_role_premission(self, request): - # """ - # 角色授权获取: - # :param request: role - # :return: menu,btns,columns - # """ - # params = request.query_params - # is_superuser = request.user.is_superuser - # if is_superuser: - # queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all() - # else: - # role_id = request.user.role.values_list('id', flat=True) - # menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('menu__id', flat=True) - # queryset = Menu.objects.filter(status=1, is_catalog=True, id__in=menu_list).values('name', 'id').all() - # serializer = RoleMenuSerializer(queryset, many=True, request=request) - # data = serializer.data - # return DetailResponse(data=data) + @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) + def get_role_menu(self, request): + """ + 获取 角色-菜单 + :param request: + :return: + """ + menu_queryset = Menu.objects.all() + serializer = RoleMenuSerializer(menu_queryset, many=True, request=request) + return DetailResponse(data=serializer.data) + + @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) + def set_role_menu(self, request): + """ + 设置 角色-菜单 + :param request: + :return: + """ + data = request.data + roleId = data.get('roleId') + menuId = data.get('menuId') + isCheck = data.get('isCheck') + if isCheck: + # 添加权限:创建关联记录 + instance = RoleMenuPermission.objects.create(role_id=roleId, menu_id=menuId) + else: + # 删除权限:移除关联记录 + RoleMenuPermission.objects.filter(role_id=roleId, menu_id=menuId).delete() + menu_instance = Menu.objects.get(id=menuId) + serializer = RoleMenuSerializer(menu_instance, request=request) + return DetailResponse(data=serializer.data, msg="更新成功") @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - def get_role_permission(self, request): + def get_role_menu_btn_field(self, request): + """ + 获取 角色-菜单-按钮-列字段 + :param request: + :return: + """ params = request.query_params - # 需要授权的角色信息 - current_role = params.get('role', None) - # 当前登录用户的角色 - role_list = request.user.role.values_list('id', flat=True) - if current_role is None: - return ErrorResponse(msg='参数错误') - is_superuser = request.user.is_superuser - if is_superuser: - menu_queryset = Menu.objects.prefetch_related('menuPermission').prefetch_related( - 'menufield_set') - else: - role_id_list = request.user.role.values_list('id', flat=True) - menu_list = RoleMenuPermission.objects.filter(role__in=role_id_list).values_list('menu__id', flat=True) - - # 当前角色已授权的菜单 - menu_queryset = Menu.objects.filter(id__in=menu_list).prefetch_related('menuPermission').prefetch_related( - 'menufield_set') - result = [] - for menu_item in menu_queryset: - isCheck = RoleMenuPermission.objects.filter( - menu_id=menu_item.id, - role_id=current_role - ).exists() - dicts = { - 'name': menu_item.name, - 'id': menu_item.id, - 'parent': menu_item.parent.id if menu_item.parent else None, - 'isCheck': isCheck, - 'btns': [], - 'columns': [] - } - for mb_item in menu_item.menuPermission.all(): - rolemenubuttonpermission_queryset = RoleMenuButtonPermission.objects.filter( - menu_button_id=mb_item.id, - role_id=current_role - ).first() - dicts['btns'].append( - { - 'id': mb_item.id, - 'name': mb_item.name, - 'value': mb_item.value, - 'data_range': rolemenubuttonpermission_queryset.data_range - if rolemenubuttonpermission_queryset - else None, - 'isCheck': bool(rolemenubuttonpermission_queryset), - 'dept': rolemenubuttonpermission_queryset.dept.all().values_list('id', flat=True) - if rolemenubuttonpermission_queryset - else [], - } - ) - for column_item in menu_item.menufield_set.all(): - # 需要授权角色已拥有的列权限 - fieldpermission_queryset = column_item.menu_field.filter(role_id=current_role).first() - is_query = fieldpermission_queryset.is_query if fieldpermission_queryset else False - is_create = fieldpermission_queryset.is_create if fieldpermission_queryset else False - is_update = fieldpermission_queryset.is_update if fieldpermission_queryset else False - # 当前登录用户角色可分配的列权限 - fieldpermission_queryset_disabled = column_item.menu_field.filter(role_id__in=role_list).first() - disabled_query = fieldpermission_queryset_disabled.is_query if fieldpermission_queryset_disabled else True - disabled_create = fieldpermission_queryset_disabled.is_create if fieldpermission_queryset_disabled else True - disabled_update = fieldpermission_queryset_disabled.is_update if fieldpermission_queryset_disabled else True - - dicts['columns'].append({ - 'id': column_item.id, - 'field_name': column_item.field_name, - 'title': column_item.title, - 'is_query': is_query, - 'is_create': is_create, - 'is_update': is_update, - 'disabled_query': False if is_superuser else not disabled_query, - 'disabled_create': False if is_superuser else not disabled_create, - 'disabled_update': False if is_superuser else not disabled_update, - }) - result.append(dicts) - return DetailResponse(data=result) + menuId = params.get('menuId', None) + menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId) + menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request) + menu_field_queryset = MenuField.objects.filter(menu_id=menuId) + menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request) + return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data}) @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) - def set_role_premission(self, request, pk): + def set_role_menu_field(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 item in body: - if item.get('isCheck'): - RoleMenuPermission.objects.create(role_id=pk, menu_id=item["id"]) - for btn in item.get('btns'): - if btn.get('isCheck'): - data_range = btn.get('data_range', 0) or 0 - instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'), - data_range=data_range) - instance.dept.set(btn.get('dept', [])) - for col in item.get('columns'): - FieldPermission.objects.update_or_create(role_id=pk, field_id=col.get('id'), - defaults={ - 'is_query': col.get('is_query'), - 'is_create': col.get('is_create'), - 'is_update': col.get('is_update') - }) - return DetailResponse(msg="授权成功") + data = request.data + for col in data: + FieldPermission.objects.update_or_create( + role_id=pk, field_id=col.get('id'), + defaults={ + 'is_create': col.get('is_create'), + 'is_update': col.get('is_update'), + 'is_query': col.get('is_query'), + }) - @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - def role_menu_get_button(self, request): - """ - 当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用 - :param request: - :return: - """ - if params := request.query_params: - if menu_id := params.get('menu', None): - is_superuser = request.user.is_superuser - if is_superuser: - queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name') - else: - role_list = request.user.role.values_list('id', flat=True) - queryset = RoleMenuButtonPermission.objects.filter( - role__in=role_list, menu_button__menu=menu_id - ).values(btn_id=F('menu_button__id'), name=F('menu_button__name')) - return DetailResponse(data=queryset) - return ErrorResponse(msg="参数错误") + return DetailResponse(data=[], msg="更新成功") - @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - def data_scope(self, request): + @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) + def set_role_menu_btn(self, request): """ - 获取数据权限范围:角色授权页面使用 - :param request: - :return: + 设置 角色-菜单-按钮 """ - is_superuser = request.user.is_superuser - if is_superuser: - data = [ - {"value": 0, "label": '仅本人数据权限'}, - {"value": 1, "label": '本部门及以下数据权限'}, - {"value": 2, "label": '本部门数据权限'}, - {"value": 3, "label": '全部数据权限'}, - {"value": 4, "label": '自定义数据权限'} - ] - return DetailResponse(data=data) + data = request.data + isCheck = data.get('isCheck', None) + roleId = data.get('roleId', None) + btnId = data.get('btnId', None) + data_range = data.get('data_range', None) or 0 # 默认仅本人权限 + dept = data.get('dept', None) or [] # 默认空部门 + + if isCheck: + # 添加权限:创建关联记录 + instance = RoleMenuButtonPermission.objects.create(role_id=roleId, + menu_button_id=btnId, + data_range=data_range) + # 自定义部门权限 + if data_range == 4 and dept: + instance.dept.set(dept) else: - params = request.query_params - data = [{"value": 0, "label": '仅本人数据权限'}] - role_list = request.user.role.values_list('id', flat=True) - # 权限页面进入初始化获取所有的数据权限范围 - role_queryset = RoleMenuButtonPermission.objects.filter( - role__in=role_list - ).values_list('data_range', flat=True) - # 通过按钮小齿轮获取指定按钮的权限 - 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) + # 删除权限:移除关联记录 + RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() + menu_btn_instance = MenuButton.objects.get(id=btnId) + serializer = RoleMenuButtonSerializer(menu_btn_instance, request=request) + return DetailResponse(data=serializer.data, msg="更新成功") - data_range_list = list(set(role_queryset)) - for item in data_range_list: - if item == 0: - data = data - elif item == 1: - data.extend([ - {"value": 1, "label": '本部门及以下数据权限'}, - {"value": 2, "label": '本部门数据权限'} - ]) - elif item == 2: - data.extend([{"value": 2, "label": '本部门数据权限'}]) - elif item == 3: - data.extend([{"value": 3, "label": '全部数据权限'}]) - elif item == 4: - data.extend([{"value": 4, "label": '自定义数据权限'}]) - else: - data = [] - return DetailResponse(data=data) + @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) + def set_role_menu_btn_data_range(self, request): + """ + 设置 角色-菜单-按钮-权限 + """ + data = request.data + instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id')) + instance.data_range = data.get('data_range') + instance.dept.add(*data.get('dept')) + if not data.get('dept'): + instance.dept.clear() + instance.save() + serializer = RoleMenuButtonPermissionSerializer(instance, request=request) + return DetailResponse(data=serializer.data, msg="更新成功") @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) def role_to_dept_all(self, request): @@ -395,55 +288,3 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled data.append(dept) return DetailResponse(data=data) - - @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) - def menu_to_button(self, request): - """ - 根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用 - :param request: - :return: - """ - params = request.query_params - menu_id = params.get('menu', None) - if menu_id is None: - return ErrorResponse(msg="未获取到参数") - is_superuser = request.user.is_superuser - if is_superuser: - queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values( - 'id', - 'data_range', - 'menu_button', - 'menu_button__name', - 'menu_button__value' - ) - return DetailResponse(data=queryset) - else: - if params: - - role_id = params.get('role', None) - if role_id is None: - return ErrorResponse(msg="未获取到参数") - queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values( - '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) diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index 68de06b..c31540c 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -286,6 +286,7 @@ class UserViewSet(CustomModelViewSet): "dept": user.dept_id, "is_superuser": user.is_superuser, "role": user.role.values_list('id', flat=True), + "pwd_change_count":user.pwd_change_count } if hasattr(connection, 'tenant'): result['tenant_id'] = connection.tenant and connection.tenant.id @@ -319,7 +320,6 @@ class UserViewSet(CustomModelViewSet): """密码修改""" data = request.data old_pwd = data.get("oldPassword") - print(old_pwd) new_pwd = data.get("newPassword") new_pwd2 = data.get("newPassword2") if old_pwd is None or new_pwd is None or new_pwd2 is None: @@ -335,12 +335,28 @@ class UserViewSet(CustomModelViewSet): old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest() verify_password = check_password(str(old_pwd_md5), request.user.password) if verify_password: + # request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) + request.user.pwd_change_count += 1 request.user.save() return DetailResponse(data=None, msg="修改成功") else: return ErrorResponse(msg="旧密码不正确") + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated]) + def login_change_password(self, request, *args, **kwargs): + """初次登录进行密码修改""" + data = request.data + new_pwd = data.get("password") + new_pwd2 = data.get("password_regain") + if new_pwd != new_pwd2: + return ErrorResponse(msg="两次密码不匹配") + else: + request.user.password = make_password(new_pwd) + request.user.pwd_change_count += 1 + request.user.save() + return DetailResponse(data=None, msg="修改成功") + @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) def reset_to_default_password(self, request,pk): """恢复默认密码""" diff --git a/backend/dvadmin/utils/aliyunoss.py b/backend/dvadmin/utils/aliyunoss.py new file mode 100644 index 0000000..b4e2894 --- /dev/null +++ b/backend/dvadmin/utils/aliyunoss.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import oss2 +from rest_framework.exceptions import ValidationError + +from application import dispatch + + +# 进度条 +# 当无法确定待上传的数据长度时,total_bytes的值为None。 +def percentage(consumed_bytes, total_bytes): + if total_bytes: + rate = int(100 * (float(consumed_bytes) / float(total_bytes))) + print('\r{0}% '.format(rate), end='') + + +def ali_oss_upload(file, file_name): + """ + 阿里云OSS上传 + """ + try: + file.seek(0) + file_read = file.read() + except Exception as e: + file_read = file + if not file: + raise ValidationError('请上传文件') + # 转存到oss + path_prefix = dispatch.get_system_config_values("file_storage.aliyun_path") + if not path_prefix.endswith('/'): + path_prefix = path_prefix + '/' + if path_prefix.startswith('/'): + path_prefix = path_prefix[1:] + base_fil_name = f'{path_prefix}{file_name}' + # 获取OSS配置 + # 获取的AccessKey + access_key_id = dispatch.get_system_config_values("file_storage.aliyun_access_key") + access_key_secret = dispatch.get_system_config_values("file_storage.aliyun_access_secret") + auth = oss2.Auth(access_key_id, access_key_secret) + # 这个是需要用特定的地址,不同地域的服务器地址不同,不要弄错了 + # 参考官网给的地址配置https://www.alibabacloud.com/help/zh/object-storage-service/latest/regions-and-endpoints#concept-zt4-cvy-5db + endpoint = dispatch.get_system_config_values("file_storage.aliyun_endpoint") + bucket_name = dispatch.get_system_config_values("file_storage.aliyun_bucket") + if bucket_name.endswith(endpoint): + bucket_name = bucket_name.replace(f'.{endpoint}', '') + # 你的项目名称,类似于不同的项目上传的图片前缀url不同 + bucket = oss2.Bucket(auth, endpoint, bucket_name) # 项目名称 + # 生成外网访问的文件路径 + aliyun_cdn_url = dispatch.get_system_config_values("file_storage.aliyun_cdn_url") + if aliyun_cdn_url: + if aliyun_cdn_url.endswith('/'): + aliyun_cdn_url = aliyun_cdn_url[1:] + file_path = f"{aliyun_cdn_url}/{base_fil_name}" + else: + file_path = f"https://{bucket_name}.{endpoint}/{base_fil_name}" + # 这个是阿里提供的SDK方法 + res = bucket.put_object(base_fil_name, file_read, progress_callback=percentage) + # 如果上传状态是200 代表成功 返回文件外网访问路径 + if res.status == 200: + return file_path + else: + return None diff --git a/backend/dvadmin/utils/field_permission.py b/backend/dvadmin/utils/field_permission.py index 7c259f6..8702684 100644 --- a/backend/dvadmin/utils/field_permission.py +++ b/backend/dvadmin/utils/field_permission.py @@ -1,71 +1,43 @@ # -*- coding: utf-8 -*- - -from itertools import groupby - from django.db.models import F from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from dvadmin.system.models import FieldPermission, MenuField from dvadmin.utils.json_response import DetailResponse -from dvadmin.utils.models import get_custom_app_models + + +def merge_permission(data): + """ + 合并权限 + """ + result = {} + for item in data: + field_name = item.pop('field_name') + if field_name not in result: + result[field_name] = item + else: + for key, value in item.items(): + result[field_name][key] = result[field_name][key] or value + return result class FieldPermissionMixin: - @action(methods=['get'], detail=False,permission_classes=[IsAuthenticated]) + @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) def field_permission(self, request): """ 获取字段权限 """ - finded = False - for model in get_custom_app_models(): - if model['object'] is self.serializer_class.Meta.model: - finded = True - break - if finded: - break - if finded is False: - return [] + model = self.serializer_class.Meta.model.__name__ user = request.user - if user.is_superuser==1: - data = MenuField.objects.filter( model=model['model']).values('field_name') - for item in data: - item['is_create'] = True - item['is_query'] = True - item['is_update'] = True + # 创建一个默认字典来存储最终的结果 + if user.is_superuser == 1: + data = MenuField.objects.filter(model=model).values('field_name') + result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data} else: roles = request.user.role.values_list('id', flat=True) - data= FieldPermission.objects.filter( - field__model=model['model'],role__in=roles - ).values( 'is_create', 'is_query', 'is_update',field_name=F('field__field_name')) - - """ - 合并权限 - - 这段代码首先根据 field_name 对列表进行排序, - 然后使用 groupby 按 field_name 进行分组。 - 对于每个组,它创建一个新的字典 merged, - 并遍历组中的每个字典,将布尔值字段使用逻辑或(or)操作符进行合并(如果 merged 中还没有该字段,则默认为 False), - 其他字段(如 field_name)则直接取组的关键字(即 key) - """ - - # 使用field_name对列表进行分组, # groupby 需要先对列表进行排序,因为它只能对连续相同的元素进行分组。 - grouped = groupby(sorted(list(data), key=lambda x: x['field_name']), key=lambda x: x['field_name']) - - data = [] - - # 遍历分组,合并权限 - for key, group in grouped: - - # 初始化一个空字典来存储合并后的结果 - merged = {} - for item in group: - # 合并权限, True值优先 - merged['is_create'] = merged.get('is_create', False) or item['is_create'] - merged['is_query'] = merged.get('is_query', False) or item['is_query'] - merged['is_update'] = merged.get('is_update', False) or item['is_update'] - merged['field_name'] = key - - data.append(merged) - - return DetailResponse(data=data) \ No newline at end of file + data = FieldPermission.objects.filter( + field__model=model, role__in=roles + ).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name')) + result = merge_permission(data) + return DetailResponse(data=result) diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py index d09a8a1..f61fc62 100644 --- a/backend/dvadmin/utils/filters.py +++ b/backend/dvadmin/utils/filters.py @@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend from django_filters.utils import get_model_field from rest_framework.filters import BaseFilterBackend from django_filters.conf import settings -from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission +from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton from dvadmin.utils.models import CoreModel class CoreModelFilterBankend(BaseFilterBackend): @@ -33,7 +33,7 @@ class CoreModelFilterBankend(BaseFilterBackend): create_datetime_after = request.query_params.get('create_datetime_after', None) create_datetime_before = request.query_params.get('create_datetime_before', None) update_datetime_after = request.query_params.get('update_datetime_after', None) - update_datetime_before = request.query_params.get('update_datetime_after', None) + update_datetime_before = request.query_params.get('update_datetime_before', None) if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]): create_filter = Q() if create_datetime_after and create_datetime_before: @@ -149,13 +149,16 @@ class DataLevelPermissionsFilter(BaseFilterBackend): if _pk: # 判断是否是单例查询 re_api = re.sub(_pk,'{id}', api) role_id_list = request.user.role.values_list('id', flat=True) - role_permission_list=RoleMenuButtonPermission.objects.filter( - role__in=role_id_list, - role__status=1, - menu_button__api=re_api, - menu_button__method=method).values( - 'data_range' - ) + # 修复权限获取bug + menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True) + role_permission_list = [] + if menu_button_ids: + role_permission_list=RoleMenuButtonPermission.objects.filter( + role__in=role_id_list, + role__status=1, + menu_button_id__in=menu_button_ids).values( + 'data_range' + ) dataScope_list = [] # 权限范围列表 for ele in role_permission_list: # 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据 @@ -340,7 +343,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): from timezone_field import TimeZoneField # 不进行 过滤的model 类 - if isinstance(field, (models.JSONField, TimeZoneField)): + if isinstance(field, (models.JSONField, TimeZoneField, models.FileField)): continue # warn if the field doesn't exist. if field is None: diff --git a/backend/dvadmin/utils/import_export.py b/backend/dvadmin/utils/import_export.py index 2bd6e1e..a3bccd9 100644 --- a/backend/dvadmin/utils/import_export.py +++ b/backend/dvadmin/utils/import_export.py @@ -86,4 +86,5 @@ def import_to_data(file_url, field_data, m2m_fields=None): else: array[key] = cell_value tables.append(array) - return tables + data = [i for i in tables if len(i) != 0] + return data diff --git a/backend/dvadmin/utils/import_export_mixin.py b/backend/dvadmin/utils/import_export_mixin.py index 74e85fd..e188ff5 100644 --- a/backend/dvadmin/utils/import_export_mixin.py +++ b/backend/dvadmin/utils/import_export_mixin.py @@ -305,11 +305,10 @@ class ExportSerializerMixin: assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__ data = self.export_serializer_class(queryset, many=True, request=request).data try: - from dvadmin3_celery import settings async_export_data.delay( data, str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"), - DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务').pk, + DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务', dept_belong_id=request.user.dept_id).pk, self.export_field_label ) return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载") diff --git a/backend/dvadmin/utils/models.py b/backend/dvadmin/utils/models.py index b387ea4..1283351 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -81,6 +81,26 @@ class SoftDeleteModel(models.Model): super().delete(using=using, *args, **kwargs) +class CoreModelManager(models.Manager): + def get_queryset(self): + is_deleted = getattr(self.model, 'is_soft_delete', False) + flow_work_status = getattr(self.model, 'flow_work_status', False) + queryset = super().get_queryset() + if flow_work_status: + queryset = queryset.filter(flow_work_status=1) + if is_deleted: + queryset = queryset.filter(is_deleted=False) + return queryset + def create(self,request: Request=None, **kwargs): + data = {**kwargs} + if request: + request_user = request.user + data["creator"] = request_user + data["modifier"] = request_user.id + data["dept_belong_id"] = request_user.dept_id + # 调用父类的create方法执行实际的创建操作 + return super().create(**data) + class CoreModel(models.Model): """ 核心标准抽象模型模型,可直接继承使用 @@ -98,7 +118,8 @@ class CoreModel(models.Model): verbose_name="修改时间") create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", verbose_name="创建时间") - + objects = CoreModelManager() + all_objects = models.Manager() class Meta: abstract = True verbose_name = '核心模型' diff --git a/backend/dvadmin/utils/tencentcos.py b/backend/dvadmin/utils/tencentcos.py new file mode 100644 index 0000000..a515124 --- /dev/null +++ b/backend/dvadmin/utils/tencentcos.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from rest_framework.exceptions import ValidationError + +from application import dispatch +from qcloud_cos import CosConfig +from qcloud_cos import CosS3Client + + +# 进度条 +# 当无法确定待上传的数据长度时,total_bytes的值为None。 +def percentage(consumed_bytes, total_bytes): + if total_bytes: + rate = int(100 * (float(consumed_bytes) / float(total_bytes))) + print('\r{0}% '.format(rate), end='') + +def tencent_cos_upload(file, file_name): + try: + file.seek(0) + file_read = file.read() + except Exception as e: + file_read = file + if not file: + raise ValidationError('请上传文件') + # 生成文件名 + path_prefix = dispatch.get_system_config_values("file_storage.tencent_path") + if not path_prefix.endswith('/'): + path_prefix = path_prefix + '/' + if path_prefix.startswith('/'): + path_prefix = path_prefix[1:] + base_fil_name = f'{path_prefix}{file_name}' + # 获取cos配置 + # 1. 设置用户属性, 包括 secret_id, secret_key, region等。Appid 已在 CosConfig 中移除,请在参数 Bucket 中带上 Appid。Bucket 由 BucketName-Appid 组成 + secret_id = dispatch.get_system_config_values("file_storage.tencent_secret_id") # 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140 + secret_key = dispatch.get_system_config_values("file_storage.tencent_secret_key") # 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140 + region = dispatch.get_system_config_values("file_storage.tencent_region") # 替换为用户的 region,已创建桶归属的 region 可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket # COS 支持的所有 region 列表参见https://cloud.tencent.com/document/product/436/6224 + bucket = dispatch.get_system_config_values("file_storage.tencent_bucket") # 要访问的桶名称 + config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key) + client = CosS3Client(config) + # 访问地址 + base_file_url = f'https://{bucket}.cos.{region}.myqcloud.com' + # 生成外网访问的文件路径 + if base_file_url.endswith('/'): + file_path = base_file_url + base_fil_name + else: + file_path = f'{base_file_url}/{base_fil_name}' + # 这个是阿里提供的SDK方法 bucket是调用的4.1中配置的变量名 + try: + response = client.put_object( + Bucket=bucket, + Body=file_read, + Key=base_fil_name, + EnableMD5=False + ) + return file_path + except: + return None diff --git a/backend/dvadmin/utils/viewset.py b/backend/dvadmin/utils/viewset.py index b85007a..42948b1 100644 --- a/backend/dvadmin/utils/viewset.py +++ b/backend/dvadmin/utils/viewset.py @@ -6,6 +6,8 @@ @Created on: 2021/6/1 001 22:57 @Remark: 自定义视图集 """ +import copy + from django.db import transaction from django_filters import DateTimeFromToRangeFilter from django_filters.rest_framework import FilterSet @@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi kwargs.setdefault('context', self.get_serializer_context()) # 全部以可见字段为准 can_see = self.get_menu_field(serializer_class) - # 排除掉序列化器级的字段 - # sub_set = set(serializer_class._declared_fields.keys()) - set(can_see) - # for field in sub_set: - # serializer_class._declared_fields.pop(field) + # 排除掉序列化器级的字段(排除字段权限中未授权的字段) # if not self.request.user.is_superuser: - # serializer_class.Meta.fields = can_see + # exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see) + # for field in exclude_set: + # serializer_class._declared_fields.pop(field) + # meta = copy.deepcopy(serializer_class.Meta) + # meta.fields = list(can_see) + # serializer_class.Meta = meta # 在分页器中使用 self.request.permission_fields = can_see if isinstance(self.request.data, list): @@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi def get_menu_field(self, serializer_class): """获取字段权限""" - finded = False - for model in get_custom_app_models(): - if model['object'] is serializer_class.Meta.model: - finded = True - break - if finded is False: + + if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()): return [] - return MenuField.objects.filter(model=model['model'] - ).values('field_name', 'title') + + # 匿名用户没有角色 + ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__) + if hasattr(self.request.user, 'role'): + roles = self.request.user.role.values_list('id', flat=True) + ret = ret.filter(is_query=True, role__in=roles) + + return ret.values_list('field__field_name', flat=True) def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data, request=request) @@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi instance.delete() return DetailResponse(data=[], msg="删除成功") - keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING) - + keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING)) @swagger_auto_schema(request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['keys'], diff --git a/backend/requirements.txt b/backend/requirements.txt index 3395719..f443f6f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,25 +7,27 @@ djangorestframework==3.15.2 django-restql==0.15.4 django-simple-captcha==0.6.0 django-timezone-field==7.0 -djangorestframework-simplejwt==5.3.1 +djangorestframework_simplejwt==5.4.0 drf-yasg==1.21.7 mysqlclient==2.2.0 pypinyin==0.51.0 ua-parser==0.18.0 pyparsing==3.1.2 openpyxl==3.1.5 -requests==2.32.3 +requests==2.32.4 typing-extensions==4.12.2 tzlocal==5.2 channels==4.1.0 channels-redis==4.2.0 -websockets==11.0.3 user-agents==2.2.0 six==1.16.0 whitenoise==6.7.0 psycopg2==2.9.9 uvicorn==0.30.3 -gunicorn==22.0.0 +gunicorn==23.0.0 gevent==24.2.1 Pillow==10.4.0 -pyinstaller==6.9.0 \ No newline at end of file +pyinstaller==6.9.0 +dvadmin3-celery==3.1.6 +oss2==2.19.1 +cos-python-sdk-v5==1.9.37 \ No newline at end of file diff --git a/crud-gen.sh b/crud-gen.sh new file mode 100644 index 0000000..51fe5f3 --- /dev/null +++ b/crud-gen.sh @@ -0,0 +1,87 @@ +if ! [ -f ".env" ];then + echo ".env file not found" + exit 1 +fi + +if [ -z "$3" ]; then + echo "Use: $0 " + exit 1 +fi + + +DIR=./web/src/views/$1/$2 + + +# 设置数据库连接信息 +HOST="177.10.0.13" +USER="root" +PASSWORD=$(cat .env | grep MYSQL_PASSWORD | sed 's/^.*MYSQL_PASSWORD=//g') +DATABASE="django-vue3-admin" +TABLE=$3 +TARGET_FILE="./web/src/views/$1/$2/crud.tsx" + + +# 表是否存在 +TABLE_EXISTS=$(mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "SHOW TABLES LIKE '$TABLE';" -N | grep "$TABLE" | wc -l) + +if [ "$TABLE_EXISTS" -eq 0 ]; then + echo "Table $TABLE does not exist in database $DATABASE." + exit 1 +fi + +mkdir -p $DIR +cp -r ./web/src/views/template/* $DIR +sed -i "s/VIEWSETNAME/$2/g" $DIR/* + +sed -n -e :a -e '1,5!{P;N;D;};N;ba' -i $TARGET_FILE + +# 查询表结构 +QUERY="SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$DATABASE' AND TABLE_NAME = '$TABLE' ORDER BY ORDINAL_POSITION;" + +# 使用 MySQL 查询获取字段信息,并生成 fast-crud 配置 +mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "$QUERY" -N | while read COLUMN_NAME DATA_TYPE COLUMN_COMMENT IS_NULLABLE; do + # 映射 MySQL 数据类型到 fast-crud 类型 + case "$DATA_TYPE" in + "int"|"bigint"|"smallint"|"mediumint"|"tinyint"|"decimal"|"float"|"double") + TYPE="number" + ;; + "date"|"datetime"|"timestamp") + TYPE="date" + ;; + *) + TYPE="text" + ;; + esac + + echo " $COLUMN_NAME: { + title: '$COLUMN_NAME', + type: '$TYPE', + search: { show: true }, + column: { + minWidth: 120, + sortable: 'custom', + }, + form: {" >> $TARGET_FILE + + if [ "$IS_NULLABLE" = "NO" ]; then + echo " helper: { + render() { + return
$COLUMN_NAME 是必填的
; + } + }, + rules: [{ + required: true, message: '$COLUMN_NAME 是必填的' + }]," >> $TARGET_FILE + fi + + echo " component: { + placeholder: '请输入 $COLUMN_NAME', + }, + }, + }," >> $TARGET_FILE +done + +echo " }, + }, + }; +}" >> $TARGET_FILE diff --git a/docker_env/web/Dockerfile b/docker_env/web/Dockerfile index dc0cdb8..73fc4e7 100644 --- a/docker_env/web/Dockerfile +++ b/docker_env/web/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine +FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:18.20-alpine WORKDIR /web/ COPY web/. . RUN yarn install --registry=https://registry.npmmirror.com diff --git a/init.sh b/init.sh index 8c377ed..ab828cb 100644 --- a/init.sh +++ b/init.sh @@ -1,5 +1,6 @@ #!/bin/bash ENV_FILE=".env" +HOST="177.10.0.13" # 检查 .env 文件是否存在 if [ -f "$ENV_FILE" ]; then echo "$ENV_FILE 文件已存在。" @@ -15,17 +16,60 @@ else echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。" awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }' - sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.13'|g" ./backend/conf/env.py + sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '$HOST'|g" ./backend/conf/env.py sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py echo "初始化密码创建成功" fi +echo "正在启动容器..." docker-compose up -d -docker exec dvadmin3-django python manage.py makemigrations -docker exec dvadmin3-django python manage.py migrate -docker exec dvadmin3-django python manage.py init -echo "欢迎使用dvadmin3项目" -echo "登录地址:http://ip:8080" -echo "如访问不到,请检查防火墙配置" + +if [ $? -ne 0 ]; then + echo "docker-compose up -d 执行失败!" + exit 1 +fi + +MYSQL_PORT=3306 +REDIS_PORT=6379 + +check_mysql() { + if nc -z "$HOST" "$MYSQL_PORT" >/dev/null 2>&1; then + echo "MySQL 服务正在运行在 $HOST:$MYSQL_PORT" + return 0 + else + return 1 + fi +} + +check_redis() { + if nc -z "$HOST" "$REDIS_PORT" >/dev/null 2>&1; then + echo "Redis 服务正在运行在 $HOST:$REDIS_PORT" + return 0 + else + return 1 + fi +} + +i=1 +while [ $i -le 8 ]; do + if check_mysql || check_redis; then + echo "正在迁移数据..." + docker exec dvadmin3-django python3 manage.py makemigrations + docker exec dvadmin3-django python3 manage.py migrate + echo "正在初始化数据..." + docker exec dvadmin3-django python3 manage.py init + echo "欢迎使用dvadmin3项目" + echo "登录地址:http://ip:8080" + echo "如访问不到,请检查防火墙配置" + exit 0 + else + echo "第 $i 次尝试:MySQL 或 REDIS服务未运行,等待 2 秒后重试..." + sleep 2 + fi + i=$((i+1)) +done + +echo "尝试 5 次后,MySQL 或 REDIS服务仍未运行" +exit 1 diff --git a/web/.env b/web/.env index 0d828a7..a77f485 100644 --- a/web/.env +++ b/web/.env @@ -1,6 +1,6 @@ # port 端口号 VITE_PORT = 8080 -VITE_API_URL = 'http://dvadmin3api.django.icu:8001' +VITE_API_URL = 'http://127.0.0.1:8000' # open 运行 npm run dev 时自动打开浏览器 VITE_OPEN = false diff --git a/web/README.md b/web/README.md index e70cc71..86d36b1 100644 --- a/web/README.md +++ b/web/README.md @@ -49,6 +49,10 @@ 👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2) +## 给框架点赞 + + + ## 交流 diff --git a/web/flowH5.config.ts b/web/flowH5.config.ts new file mode 100644 index 0000000..505f760 --- /dev/null +++ b/web/flowH5.config.ts @@ -0,0 +1,77 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import path, {resolve} from 'path'; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import vueSetupExtend from "vite-plugin-vue-setup-extend"; +import { terser } from 'rollup-plugin-terser'; +import postcss from 'rollup-plugin-postcss'; +import pxtorem from 'postcss-pxtorem'; +const pathResolve = (dir: string) => { + return resolve(__dirname, '.', dir); +}; +export default defineConfig({ + build: { + // outDir: '../backend/static/previewer', + lib: { + entry: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'), // 库的入口文件 + name: 'previewer', // 库的全局变量名称 + fileName: (format) => `index.${format}.js`, // 输出文件名格式 + }, + rollupOptions: { + input:{ + previewer: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'), + }, + external: ['vue','xe-utils'], // 指定外部依赖 + output:{ + // dir: '../backend/static/previewer', // 输出目录 + entryFileNames: 'index.[format].js', // 入口文件名格式 + format: 'commonjs', + globals: { + vue: 'Vue' + }, + chunkFileNames: `[name].[hash].js` + }, + plugins: [ + terser({ + compress: { + drop_console: false, // 确保不移除 console.log + }, + }), + postcss({ + plugins: [ + pxtorem({ + rootValue: 37.5, + unitPrecision: 5, + propList: ['*'], + selectorBlackList: [], + replace: true, + mediaQuery: false, + minPixelValue: 0, + exclude: /node_modules/i, + }), + ], + }), + ], + }, + }, + plugins: [ + vue(), + vueJsx(), + vueSetupExtend(), + ], + resolve: { + alias: { + '/@': path.resolve(__dirname, 'src'), // '@' 别名指向 'src' 目录 + '@views': pathResolve('./src/views'), + '/src':path.resolve(__dirname, 'src') + }, + }, + css:{ + postcss:{ + + } + }, + define: { + 'process.env': {} + } +}); \ No newline at end of file diff --git a/web/package.json b/web/package.json index 7022283..73981a4 100644 --- a/web/package.json +++ b/web/package.json @@ -1,10 +1,11 @@ { "name": "django-vue3-admin", - "version": "3.0.4", + "version": "3.2.0", "description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus", "license": "MIT", "scripts": { "dev": "vite --force", + "build:dev": "vite build --mode development", "build": "vite build", "build:local": "vite build --mode local_prod", "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" @@ -15,6 +16,7 @@ "@fast-crud/fast-extends": "^1.21.2", "@fast-crud/ui-element": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2", + "@great-dream/dvadmin3-celery-web": "^3.1.3", "@iconify/vue": "^4.1.2", "@types/lodash": "^4.17.7", "@vitejs/plugin-vue-jsx": "^4.0.1", @@ -24,6 +26,7 @@ "axios": "^1.7.4", "countup.js": "^2.8.0", "cropperjs": "^1.6.2", + "date-holidays": "^3.24.1", "e-icon-picker": "2.1.1", "echarts": "^5.5.1", "echarts-gl": "^2.0.9", @@ -34,7 +37,9 @@ "js-cookie": "^3.0.5", "js-table2excel": "^1.1.2", "jsplumb": "^2.15.6", + "less": "^4.3.0", "lodash-es": "^4.17.21", + "lunar-javascript": "^1.7.1", "mitt": "^3.0.1", "nprogress": "^0.2.0", "pinia": "^2.0.28", @@ -49,9 +54,12 @@ "tailwindcss": "^3.2.7", "ts-md5": "^1.3.1", "upgrade": "^1.1.0", + "vant": "^4.9.19", + "vant4-kit": "^1.0.3", "vue": "^3.4.38", "vue-clipboard3": "^2.0.0", "vue-cropper": "^1.0.8", + "vue-draggable-plus": "^0.6.0", "vue-grid-layout": "^3.0.0-beta1", "vue-i18n": "^9.14.0", "vue-router": "^4.4.3", diff --git a/web/src/App.vue b/web/src/App.vue index 56f585e..449b965 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -11,7 +11,7 @@ diff --git a/web/src/assets/iconfont/iconfont-01/iconfont.css b/web/src/assets/iconfont/iconfont-01/iconfont.css new file mode 100644 index 0000000..6ac83a6 --- /dev/null +++ b/web/src/assets/iconfont/iconfont-01/iconfont.css @@ -0,0 +1,55 @@ +@font-face { + font-family: "iconfont"; /* Project id 3882322 */ + src: url('iconfont.woff2?t=1676037377315') format('woff2'), + url('iconfont.woff?t=1676037377315') format('woff'), + url('iconfont.ttf?t=1676037377315') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-xiaoxizhongxin:before { + content: "\e665"; +} + +.icon-xitongshezhi:before { + content: "\e7ba"; +} + +.icon-caozuorizhi:before { + content: "\e611"; +} + +.icon-guanlidenglurizhi:before { + content: "\ea45"; +} + +.icon-rizhi:before { + content: "\e60c"; +} + +.icon-system:before { + content: "\e684"; +} + +.icon-Area:before { + content: "\eaa2"; +} + +.icon-file:before { + content: "\e671"; +} + +.icon-dict:before { + content: "\e626"; +} + +.icon-configure:before { + content: "\e733"; +} + diff --git a/web/src/assets/iconfont/iconfont-01/iconfont.ttf b/web/src/assets/iconfont/iconfont-01/iconfont.ttf new file mode 100644 index 0000000..1220a01 Binary files /dev/null and b/web/src/assets/iconfont/iconfont-01/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont-01/iconfont.woff b/web/src/assets/iconfont/iconfont-01/iconfont.woff new file mode 100644 index 0000000..4ce6f7c Binary files /dev/null and b/web/src/assets/iconfont/iconfont-01/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont-01/iconfont.woff2 b/web/src/assets/iconfont/iconfont-01/iconfont.woff2 new file mode 100644 index 0000000..c7a73e7 Binary files /dev/null and b/web/src/assets/iconfont/iconfont-01/iconfont.woff2 differ diff --git a/web/src/assets/iconfont/iconfont-02/iconfont.css b/web/src/assets/iconfont/iconfont-02/iconfont.css new file mode 100644 index 0000000..a7393cc --- /dev/null +++ b/web/src/assets/iconfont/iconfont-02/iconfont.css @@ -0,0 +1,427 @@ +@font-face { + font-family: "iconfont"; /* Project id 2298093 */ + src: url('iconfont.woff2?t=1627014681704') format('woff2'), + url('iconfont.woff?t=1627014681704') format('woff'), + url('iconfont.ttf?t=1627014681704') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-diannao101:before { + content: "\e670"; +} + +.icon-diannao:before { + content: "\e618"; +} + +.icon-diannao1:before { + content: "\e622"; +} + +.icon-diannao-shuju:before { + content: "\e63e"; +} + +.icon-shoujidiannao:before { + content: "\e62e"; +} + +.icon-diannaobangong:before { + content: "\e647"; +} + +.icon-LoggedinPC:before { + content: "\e604"; +} + +.icon-barcode-qr:before { + content: "\e61e"; +} + +.icon-zhongduancanshuchaxun:before { + content: "\e638"; +} + +.icon-shouye_dongtaihui:before { + content: "\e606"; +} + +.icon-putong:before { + content: "\e603"; +} + +.icon-dongtai:before { + content: "\e659"; +} + +.icon-wenducanshu-05:before { + content: "\e634"; +} + +.icon-zhongduancanshu:before { + content: "\e63b"; +} + +.icon-tongzhi1:before { + content: "\e63a"; +} + +.icon-tongzhi2:before { + content: "\e649"; +} + +.icon-tongzhi3:before { + content: "\e648"; +} + +.icon-tongzhi4:before { + content: "\e60c"; +} + +.icon-dianhua:before { + content: "\e615"; +} + +.icon-xianshimima:before { + content: "\e63c"; +} + +.icon-yincangmima:before { + content: "\e63d"; +} + +.icon-shuxing:before { + content: "\e67a"; +} + +.icon-juxingkaobei:before { + content: "\e7a5"; +} + +.icon-shuxingtu:before { + content: "\e685"; +} + +.icon-bolangneng:before { + content: "\e745"; +} + +.icon-bolangnengshiyanchang:before { + content: "\e746"; +} + +.icon--chaifenhang:before { + content: "\e6d1"; +} + +.icon--chaifenlie:before { + content: "\e6d0"; +} + +.icon-tupianyulan:before { + content: "\e67e"; +} + +.icon-15tupianyulan:before { + content: "\e624"; +} + +.icon-728bianjiqi_zitidaxiao:before { + content: "\e660"; +} + +.icon-ziti:before { + content: "\e7b1"; +} + +.icon-font-size:before { + content: "\eaef"; +} + +.icon-tuodong:before { + content: "\e6a8"; +} + +.icon-zhongyingwen1:before { + content: "\e7a3"; +} + +.icon-fuhao-yingwen:before { + content: "\e714"; +} + +.icon-fuhao-zhongwen:before { + content: "\e712"; +} + +.icon-diqiu:before { + content: "\e689"; +} + +.icon-xingqiu:before { + content: "\e65c"; +} + +.icon-diqiu1:before { + content: "\e631"; +} + +.icon-huanjingxingqiu:before { + content: "\e617"; +} + +.icon-zidingyibuju:before { + content: "\e637"; +} + +.icon-dayin:before { + content: "\e612"; +} + +.icon-step:before { + content: "\e601"; +} + +.icon-30xuanzhongyuanxingfill:before { + content: "\e677"; +} + +.icon-shibai:before { + content: "\e60b"; +} + +.icon-7_round_solid:before { + content: "\e64d"; +} + +.icon-6_round_solid:before { + content: "\e64e"; +} + +.icon-9_round_solid:before { + content: "\e64f"; +} + +.icon-1_round_solid:before { + content: "\e650"; +} + +.icon-5_round_solid:before { + content: "\e651"; +} + +.icon-2_round_solid:before { + content: "\e654"; +} + +.icon-0_round_solid:before { + content: "\e655"; +} + +.icon-3_round_solid:before { + content: "\e656"; +} + +.icon-4_round_solid:before { + content: "\e657"; +} + +.icon-8_round_solid:before { + content: "\e658"; +} + +.icon-radio-off-full:before { + content: "\ea6b"; +} + +.icon-tongzhi:before { + content: "\e600"; +} + +.icon-ditu:before { + content: "\e8bc"; +} + +.icon-ico:before { + content: "\e646"; +} + +.icon-chazhaobiaodanliebiao:before { + content: "\e76a"; +} + +.icon-biaodan:before { + content: "\e61d"; +} + +.icon-siweidaotu:before { + content: "\e614"; +} + +.icon-jiliandongxuanzeqi:before { + content: "\e616"; +} + +.icon-caijian:before { + content: "\e611"; +} + +.icon-fuwenben:before { + content: "\e7e4"; +} + +.icon-fuwenbenkuang:before { + content: "\e66f"; +} + +.icon-shangchuan:before { + content: "\e663"; +} + +.icon-xuanzeqi:before { + content: "\e635"; +} + +.icon-fangkuang:before { + content: "\e642"; +} + +.icon-gouxuan-weixuanzhong-xianxingfangkuang:before { + content: "\e77b"; +} + +.icon-shidu:before { + content: "\e60a"; +} + +.icon-yangan:before { + content: "\e67d"; +} + +.icon-wendu:before { + content: "\e686"; +} + +.icon-zaosheng:before { + content: "\e61c"; +} + +.icon-jinridaiban:before { + content: "\e60f"; +} + +.icon-AIshiyanshi:before { + content: "\e609"; +} + +.icon-shenqingkaiban:before { + content: "\e639"; +} + +.icon-zhongyingwenqiehuan:before { + content: "\e611"; +} + +.icon-zhongyingwen:before { + content: "\e605"; +} + +.icon-zhongyingzhuanhuan:before { + content: "\e6a2"; +} + +.icon-zhongyingwenyuyan:before { + content: "\e609"; +} + +.icon-shuju:before { + content: "\e613"; +} + +.icon-ico_shuju:before { + content: "\e6ff"; +} + +.icon-shuju1:before { + content: "\e60e"; +} + +.icon-fuzhiyemian:before { + content: "\e772"; +} + +.icon-caozuo-wailian:before { + content: "\e711"; +} + +.icon-icon-:before { + content: "\e620"; +} + +.icon-gerenzhongxin:before { + content: "\e60d"; +} + +.icon-caidan:before { + content: "\e652"; +} + +.icon-xitongshezhi:before { + content: "\e69b"; +} + +.icon-neiqianshujuchucun:before { + content: "\e62f"; +} + +.icon-shouye:before { + content: "\e653"; +} + +.icon-quanxian:before { + content: "\e610"; +} + +.icon-zujian:before { + content: "\e85e"; +} + +.icon-crew_feature:before { + content: "\e602"; +} + +.icon-gongju:before { + content: "\e62d"; +} + +.icon-skin:before { + content: "\e636"; +} + +.icon-shixinyuan:before { + content: "\e669"; +} + +.icon-webicon318:before { + content: "\e6a9"; +} + +.icon-dian:before { + content: "\e608"; +} + +.icon-fullscreen:before { + content: "\e623"; +} + +.icon-tuichuquanping:before { + content: "\e641"; +} + diff --git a/web/src/assets/iconfont/iconfont-02/iconfont.ttf b/web/src/assets/iconfont/iconfont-02/iconfont.ttf new file mode 100644 index 0000000..2d3a61d Binary files /dev/null and b/web/src/assets/iconfont/iconfont-02/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont-02/iconfont.woff b/web/src/assets/iconfont/iconfont-02/iconfont.woff new file mode 100644 index 0000000..c3575c3 Binary files /dev/null and b/web/src/assets/iconfont/iconfont-02/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont-02/iconfont.woff2 b/web/src/assets/iconfont/iconfont-02/iconfont.woff2 new file mode 100644 index 0000000..9eaf90e Binary files /dev/null and b/web/src/assets/iconfont/iconfont-02/iconfont.woff2 differ diff --git a/web/src/assets/login-bg.png b/web/src/assets/login-bg.png new file mode 100644 index 0000000..9fd3aa4 Binary files /dev/null and b/web/src/assets/login-bg.png differ diff --git a/web/src/components/avatarSelector/index.vue b/web/src/components/avatarSelector/index.vue index f979cb0..2464554 100644 --- a/web/src/components/avatarSelector/index.vue +++ b/web/src/components/avatarSelector/index.vue @@ -1,6 +1,6 @@ + + diff --git a/web/src/views/system/login/component/oauth2.vue b/web/src/views/system/login/component/oauth2.vue new file mode 100644 index 0000000..1b0c0c6 --- /dev/null +++ b/web/src/views/system/login/component/oauth2.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/web/src/views/system/login/index.vue b/web/src/views/system/login/index.vue index dbaf061..897ddc4 100644 --- a/web/src/views/system/login/index.vue +++ b/web/src/views/system/login/index.vue @@ -5,51 +5,54 @@ - - -
- +
+
- - - - diff --git a/web/src/views/system/role/components/PermissionComNew/types.ts b/web/src/views/system/role/components/PermissionComNew/types.ts deleted file mode 100644 index 5afc5c0..0000000 --- a/web/src/views/system/role/components/PermissionComNew/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface DataPermissionRangeType { - label: string; - value: number; -} - -export interface CustomDataPermissionDeptType { - id: number; - name: string; - patent: number; - children: CustomDataPermissionDeptType[]; -} - -export interface CustomDataPermissionMenuType { - id: number; - name: string; - is_catalog: boolean; - menuPermission: { id: number; name: string; value: string }[] | null; - columns: { id: number; name: string; title: string }[] | null; - children: CustomDataPermissionMenuType[]; -} - -export interface MenuDataType { - id: string; - name: string; - isCheck: boolean; - btns: { id: number; name: string; value: string; isCheck: boolean; data_range: number; dept: object }[]; - columns: { [key: string]: boolean | string; }[]; - children: MenuDataType[]; -} diff --git a/web/src/views/system/role/components/RoleDrawer.vue b/web/src/views/system/role/components/RoleDrawer.vue new file mode 100644 index 0000000..23b3a60 --- /dev/null +++ b/web/src/views/system/role/components/RoleDrawer.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/web/src/views/system/role/components/RoleMenuBtn.vue b/web/src/views/system/role/components/RoleMenuBtn.vue new file mode 100644 index 0000000..5f897a5 --- /dev/null +++ b/web/src/views/system/role/components/RoleMenuBtn.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/web/src/views/system/role/components/RoleMenuField.vue b/web/src/views/system/role/components/RoleMenuField.vue new file mode 100644 index 0000000..e367677 --- /dev/null +++ b/web/src/views/system/role/components/RoleMenuField.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/web/src/views/system/role/components/RoleMenuTree.vue b/web/src/views/system/role/components/RoleMenuTree.vue new file mode 100644 index 0000000..90c24bd --- /dev/null +++ b/web/src/views/system/role/components/RoleMenuTree.vue @@ -0,0 +1,82 @@ + + + diff --git a/web/src/views/system/role/components/RoleUsers.vue b/web/src/views/system/role/components/RoleUsers.vue new file mode 100644 index 0000000..92d3bf7 --- /dev/null +++ b/web/src/views/system/role/components/RoleUsers.vue @@ -0,0 +1,35 @@ + + + diff --git a/web/src/views/system/role/components/addUsers/api.ts b/web/src/views/system/role/components/addUsers/api.ts new file mode 100644 index 0000000..8b5096a --- /dev/null +++ b/web/src/views/system/role/components/addUsers/api.ts @@ -0,0 +1,30 @@ +import { request } from '/@/utils/service'; +import { UserPageQuery} from '@fast-crud/fast-crud'; + +/** + * 当前角色查询未授权的用户 + * @param role_id 角色id + * @param query 查询条件 需要有角色id + * @returns + */ +export function getRoleUsersUnauthorized(query: UserPageQuery) { + query["authorized"] = 0; // 未授权的用户 + return request({ + url: '/api/system/role/get_role_users/', + method: 'get', + params: query, + }); +} +/** + * 当前用户角色添加用户 + * @param role_id 角色id + * @param users_id 用户id数组 + * @returns + */ +export function addRoleUsers(role_id: number, users_id: Array) { + return request({ + url: `/api/system/role/${role_id}/add_role_users/`, + method: 'post', + data: {users_id: users_id}, + }); +} \ No newline at end of file diff --git a/web/src/views/system/role/components/addUsers/crud.tsx b/web/src/views/system/role/components/addUsers/crud.tsx new file mode 100644 index 0000000..f634d94 --- /dev/null +++ b/web/src/views/system/role/components/addUsers/crud.tsx @@ -0,0 +1,184 @@ +import {getRoleUsersUnauthorized} from './api'; +import { + compute, + dict, + UserPageQuery, + AddReq, + DelReq, + EditReq, + CrudOptions, + CreateCrudOptionsProps, + CreateCrudOptionsRet +} from '@fast-crud/fast-crud'; + +import { ref , nextTick} from 'vue'; +import XEUtils from 'xe-utils'; + +export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery) => { + return await getRoleUsersUnauthorized(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + return undefined; + }; + const delRequest = async ({ row }: DelReq) => { + return undefined; + }; + const addRequest = async ({ form }: AddReq) => { + return undefined; + }; + + // 记录选中的行 + const selectedRows = ref([]); + + const onSelectionChange = (changed: any) => { + const tableData = crudExpose.getTableData(); + const unChanged = tableData.filter((row: any) => !changed.includes(row)); + // 添加已选择的行 + XEUtils.arrayEach(changed, (item: any) => { + const ids = XEUtils.pluck(selectedRows.value, 'id'); + if (!ids.includes(item.id)) { + selectedRows.value = XEUtils.union(selectedRows.value, [item]); + } + }); + // 剔除未选择的行 + XEUtils.arrayEach(unChanged, (unItem: any) => { + selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id); + }); + }; + const toggleRowSelection = () => { + // 多选后,回显默认勾选 + const tableRef = crudExpose.getBaseTableRef(); + const tableData = crudExpose.getTableData(); + const selected = XEUtils.filter(tableData, (item: any) => { + const ids = XEUtils.pluck(selectedRows.value, 'id'); + return ids.includes(item.id); + }); + + nextTick(() => { + XEUtils.arrayEach(selected, (item) => { + tableRef.toggleRowSelection(item, true); + }); + }); + }; + + return { + selectedRows, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + actionbar: { + show: false, + buttons: { + add: { + show: false, + }, + }, + }, + rowHandle: { + show: false, + //固定右侧 + fixed: 'left', + width: 150, + buttons: { + view: { + show: false, + }, + edit: { + show: false, + }, + remove: { + show: false, + }, + }, + }, + table: { + rowKey: "id", + onSelectionChange, + onRefreshed: () => toggleRowSelection(), + }, + columns: { + $checked: { + title: "选择", + form: { show: false}, + column: { + show: true, + type: "selection", + align: "center", + width: "55px", + columnSetDisabled: true, //禁止在列设置中选择 + } + }, + _index: { + title: '序号', + form: { show: false }, + column: { + //type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = crudExpose!.crudBinding.value.pagination; + // @ts-ignore + return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1; + }, + }, + }, + name: { + title: '用户名', + search: { + show: true, + component: { + props: { + clearable: true, + }, + }, + }, + type: 'text', + form: { + show: false, + }, + }, + dept: { + title: '部门', + show: true, + type: 'dict-tree', + column: { + name: 'text', + formatter({value,row,index}){ + return row.dept__name + } + }, + search: { + show: true, + disabled: true, + col:{span: 6}, + component: { + multiple: false, + props: { + checkStrictly: true, + clearable: true, + filterable: true, + }, + }, + }, + form: { + show: false + }, + dict: dict({ + isTree: true, + url: '/api/system/dept/all_dept/', + value: 'id', + label: 'name' + }), + }, + }, + }, + }; +}; diff --git a/web/src/views/system/role/components/addUsers/index.vue b/web/src/views/system/role/components/addUsers/index.vue new file mode 100644 index 0000000..0d46d22 --- /dev/null +++ b/web/src/views/system/role/components/addUsers/index.vue @@ -0,0 +1,91 @@ + + + diff --git a/web/src/views/system/role/components/api.ts b/web/src/views/system/role/components/api.ts new file mode 100644 index 0000000..7d98b6a --- /dev/null +++ b/web/src/views/system/role/components/api.ts @@ -0,0 +1,121 @@ +import { request } from '/@/utils/service'; +import XEUtils from 'xe-utils'; +/** + * 获取 角色-菜单 + * @param query + */ +export function getRoleMenu(query: object) { + return request({ + url: '/api/system/role_menu_button_permission/get_role_menu/', + method: 'get', + params: query, + }).then((res: any) => { + return XEUtils.toArrayTree(res.data, { key: 'id', parentKey: 'parent', children: 'children', strict: false }); + }); +} +/** + * 设置 角色-菜单 + * @param data + * @returns + */ +export function setRoleMenu(data: object) { + return request({ + url: '/api/system/role_menu_button_permission/set_role_menu/', + method: 'put', + data, + }); +} +/** + * 获取 角色-菜单-按钮-列字段 + * @param query + */ +export function getRoleMenuBtnField(query: object) { + return request({ + url: '/api/system/role_menu_button_permission/get_role_menu_btn_field/', + method: 'get', + params: query, + }); +} + +/** + * 设置 角色-菜单-按钮 + * @param data + */ +export function setRoleMenuBtn(data: object) { + return request({ + url: '/api/system/role_menu_button_permission/set_role_menu_btn/', + method: 'put', + data, + }); +} + +/** + * 设置 角色-菜单-列字段 + * @param data + */ +export function setRoleMenuField(roleId: string | number | undefined, data: object) { + return request({ + url: `/api/system/role_menu_button_permission/${roleId}/set_role_menu_field/`, + method: 'put', + data, + }); +} + +/** + * 设置 角色-菜单-按钮-数据权限 + * @param query + * @returns + */ +export function setRoleMenuBtnDataRange(data: object) { + return request({ + url: '/api/system/role_menu_button_permission/set_role_menu_btn_data_range/', + method: 'put', + data, + }); +} + +/** + * 获取当前用户角色下所能授权的部门 + * @param query + * @returns + */ +export function getRoleToDeptAll(query: object) { + return request({ + url: '/api/system/role_menu_button_permission/role_to_dept_all/', + method: 'get', + params: query, + }); +} + +/** + * 获取所有用户 + * @param query + * @returns + */ +export function getAllUsers() { + return request({ + url: '/api/system/user/', + method: 'get', + params: { limit: 999 }, + }).then((res: any) => { + return XEUtils.map(res.data, (item: any) => { + return { + id: item.id, + name: item.name, + }; + }); + }); +} + +/** + * 设置角色-用户 + * @param query + * @returns + */ +export function setRoleUsers(roleId: string | number | undefined, data: object) { + return request({ + url: `/api/system/role/${roleId}/set_role_users/`, + method: 'put', + data, + }); +} diff --git a/web/src/views/system/role/components/searchUsers/api.ts b/web/src/views/system/role/components/searchUsers/api.ts new file mode 100644 index 0000000..cd37641 --- /dev/null +++ b/web/src/views/system/role/components/searchUsers/api.ts @@ -0,0 +1,44 @@ +import { request } from '/@/utils/service'; +import { UserPageQuery} from '@fast-crud/fast-crud'; + +/** + * 当前角色查询授权的用户 + * @param query 查询条件 需要有角色id + * @returns + */ +export function getRoleUsersAuthorized(query: UserPageQuery) { + query["authorized"] = 1; // 授权的用户 + return request({ + url: '/api/system/role/get_role_users/', + method: 'get', + params: query, + }); +} +/** + * 当前角色删除授权的用户 + * @param role_id 角色id + * @param user_id 用户id数组 + * @returns + */ +export function removeRoleUser(role_id: number, user_id: Array) { + return request({ + url: `/api/system/role/${role_id}/remove_role_user/`, + method: 'delete', + data: {user_id: user_id}, + }); +} + + +/** + * 当前用户角色添加用户 + * @param role_id 角色id + * @param data 用户id数组 + * @returns + */ +export function addRoleUsers(role_id: number, data: Array) { + return request({ + url: `/api/system/role/${role_id}/add_role_users/`, + method: 'post', + data: {users_id: data}, + }); +} \ No newline at end of file diff --git a/web/src/views/system/role/components/searchUsers/crud.tsx b/web/src/views/system/role/components/searchUsers/crud.tsx new file mode 100644 index 0000000..56fed06 --- /dev/null +++ b/web/src/views/system/role/components/searchUsers/crud.tsx @@ -0,0 +1,193 @@ +import {getRoleUsersAuthorized, removeRoleUser} from './api'; +import { + compute, + dict, + UserPageQuery, + AddReq, + DelReq, + EditReq, + CrudOptions, + CreateCrudOptionsProps, + CreateCrudOptionsRet +} from '@fast-crud/fast-crud'; + +import {auth} from "/@/utils/authFunction"; +import { ref , nextTick} from 'vue'; +import XEUtils from 'xe-utils'; + +export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery) => { + return await getRoleUsersAuthorized(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + return undefined; + }; + const delRequest = async ({ row }: DelReq) => { + return await removeRoleUser(crudExpose.crudRef.value.getSearchFormData().role_id, [row.id]); + }; + const addRequest = async ({ form }: AddReq) => { + return undefined; + }; + + // 记录选中的行 + const selectedRows = ref([]); + + const onSelectionChange = (changed: any) => { + const tableData = crudExpose.getTableData(); + const unChanged = tableData.filter((row: any) => !changed.includes(row)); + // 添加已选择的行 + XEUtils.arrayEach(changed, (item: any) => { + const ids = XEUtils.pluck(selectedRows.value, 'id'); + if (!ids.includes(item.id)) { + selectedRows.value = XEUtils.union(selectedRows.value, [item]); + } + }); + // 剔除未选择的行 + XEUtils.arrayEach(unChanged, (unItem: any) => { + selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id); + }); + }; + const toggleRowSelection = () => { + // 多选后,回显默认勾选 + const tableRef = crudExpose.getBaseTableRef(); + const tableData = crudExpose.getTableData(); + const selected = XEUtils.filter(tableData, (item: any) => { + const ids = XEUtils.pluck(selectedRows.value, 'id'); + return ids.includes(item.id); + }); + + nextTick(() => { + XEUtils.arrayEach(selected, (item) => { + tableRef.toggleRowSelection(item, true); + }); + }); + }; + + return { + selectedRows, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + actionbar: { + buttons: { + add: { + show: auth('role:AuthorizedAdd'), + click: (ctx: any) => { + context!.subUserRef.value.dialog = true; + nextTick(() => { + context!.subUserRef.value.setSearchFormData({ form: { role_id: crudExpose.crudRef.value.getSearchFormData().role_id } }); + context!.subUserRef.value.doRefresh(); + }); + }, + }, + }, + + }, + rowHandle: { + //固定右侧 + fixed: 'left', + width: 120, + show: auth('role:AuthorizedDel'), + buttons: { + view: { + show: false, + }, + edit: { + show: false, + }, + remove: { + iconRight: 'Delete', + show: true, + }, + }, + }, + table: { + rowKey: "id", + onSelectionChange, + onRefreshed: () => toggleRowSelection(), + }, + columns: { + $checked: { + title: "选择", + form: { show: false}, + column: { + show: auth('role:AuthorizedDel'), + type: "selection", + align: "center", + width: "55px", + columnSetDisabled: true, //禁止在列设置中选择 + } + }, + _index: { + title: '序号', + form: { show: false }, + column: { + //type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = crudExpose!.crudBinding.value.pagination; + // @ts-ignore + return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1; + }, + }, + }, + name: { + title: '用户名', + search: { + show: true, + component: { + props: { + clearable: true, + }, + }, + }, + type: 'text', + form: { + show: false, + }, + }, + dept: { + title: '部门', + show: true, + type: 'dict-tree', + column: { + name: 'text', + formatter({value,row,index}){ + return row.dept__name + } + }, + search: { + show: true, + disabled: true, + col:{span: 6}, + component: { + multiple: false, + props: { + checkStrictly: true, + clearable: true, + filterable: true, + }, + }, + }, + form: { + show: false + }, + dict: dict({ + isTree: true, + url: '/api/system/dept/all_dept/', + value: 'id', + label: 'name' + }), + }, + }, + }, + }; +}; diff --git a/web/src/views/system/role/components/searchUsers/index.vue b/web/src/views/system/role/components/searchUsers/index.vue new file mode 100644 index 0000000..f2e8d0b --- /dev/null +++ b/web/src/views/system/role/components/searchUsers/index.vue @@ -0,0 +1,98 @@ + + + diff --git a/web/src/views/system/role/crud.tsx b/web/src/views/system/role/crud.tsx index 22ad8fd..ce8ff5a 100644 --- a/web/src/views/system/role/crud.tsx +++ b/web/src/views/system/role/crud.tsx @@ -1,189 +1,190 @@ -import {CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute} from '@fast-crud/fast-crud'; +import { CreateCrudOptionsProps, CreateCrudOptionsRet, AddReq, DelReq, EditReq, dict, compute } from '@fast-crud/fast-crud'; import * as api from './api'; -import {dictionary} from '/@/utils/dictionary'; -import {columnPermission} from '../../../utils/columnPermission'; -import {successMessage} from '../../../utils/message'; -import {auth} from '/@/utils/authFunction' +import { dictionary } from '/@/utils/dictionary'; +import { successMessage } from '../../../utils/message'; +import { auth } from '/@/utils/authFunction'; +import { nextTick, computed } from 'vue'; -interface CreateCrudOptionsTypes { - output: any; - crudOptions: CrudOptions; -} +/** + * + * @param crudExpose:index传递过来的示例 + * @param context:index传递过来的自定义参数 + * @returns + */ +export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const pageRequest = async (query: any) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; -//此处为crudOptions配置 -export const createCrudOptions = function ({ - crudExpose, - rolePermission, - handleDrawerOpen, - }: { - crudExpose: CrudExpose; - rolePermission: any; - handleDrawerOpen: Function; -}): CreateCrudOptionsTypes { - const pageRequest = async (query: any) => { - return await api.GetList(query); - }; - const editRequest = async ({form, row}: EditReq) => { - form.id = row.id; - return await api.UpdateObj(form); - }; - const delRequest = async ({row}: DelReq) => { - return await api.DelObj(row.id); - }; - const addRequest = async ({form}: AddReq) => { - return await api.AddObj(form); - }; - - //权限判定 - - // @ts-ignore - // @ts-ignore - return { - crudOptions: { - request: { - pageRequest, - addRequest, - editRequest, - delRequest, - }, - pagination: { - show: true - }, - actionbar: { - buttons: { - add: { - show: auth('role:Create') - } - } - }, - rowHandle: { - //固定右侧 - fixed: 'right', - width: 320, - buttons: { - view: { - show: true, - }, - edit: { - show: auth('role:Update'), - }, - remove: { - show: auth('role:Delete'), - }, - permission: { - type: 'primary', - text: '权限配置', - show: auth('role:Permission'), - tooltip: { - placement: 'top', - content: '权限配置', - }, - click: (context: any): void => { - const {row} = context; - handleDrawerOpen(row); - }, - }, - }, - }, - form: { - col: {span: 24}, - labelWidth: '100px', - wrapper: { - is: 'el-dialog', - width: '600px', - }, - }, - columns: { - _index: { - title: '序号', - form: {show: false}, - column: { - type: 'index', - align: 'center', - width: '70px', - columnSetDisabled: true, //禁止在列设置中选择 - }, - }, - id: { - title: 'ID', - type: 'text', - column: {show: false}, - search: {show: false}, - form: {show: false}, - }, - name: { - title: '角色名称', - type: 'text', - search: {show: true}, - column: { - minWidth: 120, - sortable: 'custom', - }, - form: { - rules: [{required: true, message: '角色名称必填'}], - component: { - placeholder: '请输入角色名称', - }, - }, - }, - key: { - title: '权限标识', - type: 'text', - search: {show: false}, - column: { - minWidth: 120, - sortable: 'custom', - columnSetDisabled: true, - }, - form: { - rules: [{required: true, message: '权限标识必填'}], - component: { - placeholder: '输入权限标识', - }, - }, - valueBuilder(context) { - const {row, key} = context - return row[key] - } - }, - sort: { - title: '排序', - search: {show: false}, - type: 'number', - column: { - minWidth: 90, - sortable: 'custom', - }, - form: { - rules: [{required: true, message: '排序必填'}], - value: 1, - }, - }, - status: { - title: '状态', - search: {show: true}, - type: 'dict-radio', - column: { - width: 100, - component: { - name: 'fs-dict-switch', - activeText: '', - inactiveText: '', - style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6', - onChange: compute((context) => { - return () => { - api.UpdateObj(context.row).then((res: APIResponseData) => { - successMessage(res.msg as string); - }); - }; - }), - }, - }, - dict: dict({ - data: dictionary('button_status_bool'), - }), - } - }, - }, - }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + pagination: { + show: true, + }, + actionbar: { + buttons: { + add: { + show: auth('role:Create'), + }, + }, + }, + rowHandle: { + //固定右侧 + fixed: 'right', + width: computed(() => { + if (auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch')){ + return 420; + } + return 320; + }), + buttons: { + view: { + show: true, + }, + edit: { + show: auth('role:Update'), + }, + remove: { + show: auth('role:Delete'), + }, + assignment: { + type: 'primary', + text: '授权用户', + show: auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch'), + click: (ctx: any) => { + const { row } = ctx; + context!.RoleUserDrawer.handleDrawerOpen(row); + nextTick(() => { + context!.RoleUserRef.value.setSearchFormData({ form: { role_id: row.id } }); + context!.RoleUserRef.value.doRefresh(); + }); + }, + }, + permission: { + type: 'primary', + text: '权限配置', + show: auth('role:Permission'), + click: (clickContext: any): void => { + const { row } = clickContext; + context.RoleDrawer.handleDrawerOpen(row); + context.RoleMenuBtn.setState([]); + context.RoleMenuField.setState([]); + }, + }, + }, + }, + form: { + col: { span: 24 }, + labelWidth: '100px', + wrapper: { + is: 'el-dialog', + width: '600px', + }, + }, + columns: { + _index: { + title: '序号', + form: { show: false }, + column: { + type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + }, + }, + id: { + title: 'ID', + column: { show: false }, + search: { show: false }, + form: { show: false }, + }, + name: { + title: '角色名称', + search: { show: true }, + column: { + minWidth: 120, + sortable: 'custom', + }, + form: { + rules: [{ required: true, message: '角色名称必填' }], + component: { + placeholder: '请输入角色名称', + }, + }, + }, + key: { + title: '权限标识', + search: { show: false }, + column: { + minWidth: 120, + sortable: 'custom', + columnSetDisabled: true, + }, + form: { + rules: [{ required: true, message: '权限标识必填' }], + component: { + placeholder: '输入权限标识', + }, + }, + valueBuilder(context) { + const { row, key } = context; + return row[key]; + }, + }, + sort: { + title: '排序', + search: { show: false }, + type: 'number', + column: { + minWidth: 90, + sortable: 'custom', + }, + form: { + rules: [{ required: true, message: '排序必填' }], + value: 1, + }, + }, + status: { + title: '状态', + search: { show: true }, + type: 'dict-radio', + column: { + width: 100, + component: { + name: 'fs-dict-switch', + activeText: '', + inactiveText: '', + style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6', + onChange: compute((context) => { + return () => { + api.UpdateObj(context.row).then((res: APIResponseData) => { + successMessage(res.msg as string); + }); + }; + }), + }, + }, + dict: dict({ + data: dictionary('button_status_bool'), + }), + }, + }, + }, + }; }; diff --git a/web/src/views/system/role/index.vue b/web/src/views/system/role/index.vue index 4b12a38..5d39ff6 100644 --- a/web/src/views/system/role/index.vue +++ b/web/src/views/system/role/index.vue @@ -1,69 +1,43 @@ diff --git a/web/src/views/system/role/stores/RoleDrawerStores.ts b/web/src/views/system/role/stores/RoleDrawerStores.ts new file mode 100644 index 0000000..6e77369 --- /dev/null +++ b/web/src/views/system/role/stores/RoleDrawerStores.ts @@ -0,0 +1,37 @@ +import { defineStore } from 'pinia'; +import { RoleDrawerType } from '../types'; +/** + * 权限配置:抽屉 + */ +const initialState: RoleDrawerType = { + drawerVisible: false, + roleId: undefined, + roleName: undefined, + users: [], +}; + +export const RoleDrawerStores = defineStore('RoleDrawerStores', { + state: (): RoleDrawerType => ({ + ...initialState, + }), + actions: { + /** + * 打开权限修改抽屉 + */ + handleDrawerOpen(row: any) { + this.drawerVisible = true; + this.set_state(row); + }, + set_state(row: any) { + this.roleName = row.name; + this.roleId = row.id; + this.users = row.users; + }, + /** + * 关闭权限修改抽屉 + */ + handleDrawerClose() { + Object.assign(this.$state, initialState); + }, + }, +}); diff --git a/web/src/views/system/role/stores/RoleMenuBtnStores.ts b/web/src/views/system/role/stores/RoleMenuBtnStores.ts new file mode 100644 index 0000000..8ef4610 --- /dev/null +++ b/web/src/views/system/role/stores/RoleMenuBtnStores.ts @@ -0,0 +1,24 @@ +import { defineStore } from 'pinia'; +import { RoleMenuBtnType } from '../types'; +/** + * 权限配置:角色-菜单-按钮 + */ + +export const RoleMenuBtnStores = defineStore('RoleMenuBtnStores', { + state: (): RoleMenuBtnType[] => [], + actions: { + /** + * 初始化 + */ + setState(data: RoleMenuBtnType[]) { + this.$state = data; + this.$state.length = data.length; + }, + updateState(data: RoleMenuBtnType) { + const index = this.$state.findIndex((item) => item.id === data.id); + if (index !== -1) { + this.$state[index] = data; + } + }, + }, +}); diff --git a/web/src/views/system/role/stores/RoleMenuFieldStores.ts b/web/src/views/system/role/stores/RoleMenuFieldStores.ts new file mode 100644 index 0000000..c01c080 --- /dev/null +++ b/web/src/views/system/role/stores/RoleMenuFieldStores.ts @@ -0,0 +1,24 @@ +import { defineStore } from 'pinia'; +import { RoleMenuFieldType, RoleMenuFieldHeaderType } from '../types'; +/** + * 权限配置:角色-菜单-列字段 + */ + +export const RoleMenuFieldStores = defineStore('RoleMenuFieldStores', { + state: (): RoleMenuFieldType[] => [], + actions: { + /** 重置 */ + setState(data: RoleMenuFieldType[]) { + this.$state = data; + this.$state.length = data.length; + }, + }, +}); + +export const RoleMenuFieldHeaderStores = defineStore('RoleMenuFieldHeaderStores', { + state: (): RoleMenuFieldHeaderType[] => [ + { value: 'is_create', label: '新增可见', disabled: 'disabled_create', checked: false }, + { value: 'is_update', label: '编辑可见', disabled: 'disabled_update', checked: false }, + { value: 'is_query', label: '列表可见', disabled: 'disabled_query', checked: false }, + ], +}); diff --git a/web/src/views/system/role/stores/RoleMenuTreeStores.ts b/web/src/views/system/role/stores/RoleMenuTreeStores.ts new file mode 100644 index 0000000..14f28c6 --- /dev/null +++ b/web/src/views/system/role/stores/RoleMenuTreeStores.ts @@ -0,0 +1,21 @@ +import { defineStore } from 'pinia'; +import { RoleMenuTreeType } from '../types'; +/** + * 权限抽屉:角色-菜单 + */ + +export const RoleMenuTreeStores = defineStore('RoleMenuTreeStores', { + state: (): RoleMenuTreeType => ({ + id: 0, + parent: 0, + name: '', + isCheck: false, + is_catalog: false, + }), + actions: { + /** 赋值 */ + setRoleMenuTree(data: RoleMenuTreeType) { + this.$state = data; + }, + }, +}); diff --git a/web/src/views/system/role/stores/RoleUserStores.ts b/web/src/views/system/role/stores/RoleUserStores.ts new file mode 100644 index 0000000..93e5448 --- /dev/null +++ b/web/src/views/system/role/stores/RoleUserStores.ts @@ -0,0 +1,29 @@ +import { defineStore } from 'pinia'; + +/** + * 权限抽屉:角色-用户 + */ + +export const RoleUserStores = defineStore('RoleUserStores', { + state: (): any => ({ + drawerVisible: false, + role_id: undefined, + role_name: undefined, + }), + actions: { + /** + * 打开权限修改抽屉 + */ + handleDrawerOpen(row: any) { + this.drawerVisible = true; + this.role_name = row.name; + this.role_id = row.id; + }, + /** + * 关闭权限修改抽屉 + */ + handleDrawerClose() { + this.drawerVisible = false; + }, + }, +}); diff --git a/web/src/views/system/role/stores/RoleUsersStores.ts b/web/src/views/system/role/stores/RoleUsersStores.ts new file mode 100644 index 0000000..49c216a --- /dev/null +++ b/web/src/views/system/role/stores/RoleUsersStores.ts @@ -0,0 +1,24 @@ +import { defineStore } from 'pinia'; +import { RoleUsersType } from '../types'; +import { getAllUsers } from '../components/api'; +import XEUtils from 'xe-utils'; +/** + * 权限抽屉:角色-用户 + */ + +export const RoleUsersStores = defineStore('RoleUsersStores', { + state: (): RoleUsersType => ({ + all_users: [], + right_users: [], + }), + actions: { + get_all_users() { + getAllUsers().then((res: any) => { + this.$state.all_users = res; + }); + }, + set_right_users(users: any) { + this.$state.right_users = XEUtils.map(users, (item: any) => item.id); + }, + }, +}); diff --git a/web/src/views/system/role/types.ts b/web/src/views/system/role/types.ts index 6b0027c..4005073 100644 --- a/web/src/views/system/role/types.ts +++ b/web/src/views/system/role/types.ts @@ -1,27 +1,99 @@ -import { CrudOptions, CrudExpose } from '@fast-crud/fast-crud'; - -export interface CreateCrudReturnTypes { - crudOptions: CrudOptions; -} - -export interface CreateCrudOptionsTypes { - crudExpose: CrudExpose; - configPermission: Function; -} - +/**角色列表数据类型 */ export interface RoleItemType { - id: string | number; - modifier_name: string; - creator_name: string; - create_datetime: string; - update_datetime: string; - description: string; - modifier: string; - dept_belong_id: string; - name: string; - key: string; - sort: number; - status: boolean; - admin: boolean; - creator: string; -} \ No newline at end of file + id: string | number; + modifier_name: string; + creator_name: string; + create_datetime: string; + update_datetime: string; + description: string; + modifier: string; + dept_belong_id: string; + name: string; + key: string; + sort: number; + status: boolean; + admin: boolean; + creator: string; +} + +export interface UsersType { + id: string | number; + name: string; +} +export interface RoleUsersType { + all_users: UsersType[]; + right_users: UsersType[]; +} + +/** + * 权限配置 抽屉组件参数数据类型 + */ +export interface RoleDrawerType { + /** 是否显示抽屉*/ + drawerVisible: boolean; + /** 角色id*/ + roleId: string | number | undefined; + /** 角色名称*/ + roleName: string | undefined; + /** 用户*/ + users: UsersType[]; +} + +/** + * 菜单数据类型 + */ +export interface RoleMenuTreeType { + id: number | string | undefined; + /** 父级id */ + parent: number | string | undefined; + name: string; + /** 是否选中 */ + isCheck: boolean; + /** 是否是目录 */ + is_catalog: boolean; +} +/** + * 菜单-按钮数据类型 + */ +export interface RoleMenuBtnType { + id: string | number; + menu_btn_pre_id: string | number; + /** 是否选中 */ + isCheck: boolean; + /** 按钮名称 */ + name: string; + /** 数据权限范围 */ + data_range: number | null; + /** 自定义部门 */ + dept: number[]; +} + +/** + * 菜单-列字段数据类型 + */ +export interface RoleMenuFieldType { + id: string | number | boolean; + /** 模型表字段名 */ + field_name: string; + /** 字段显示名 */ + title: string; + /** 是否可查询 */ + is_query: boolean; + /** 是否可创建 */ + is_create: boolean; + /** 是否可更新 */ + is_update: boolean; + [key: string]: string | number | boolean; +} +/** + * 菜单-列字段-标题数据类型 + */ +export interface RoleMenuFieldHeaderType { + value: string; + /** 模型表字段名 */ + label: string; + /** 字段显示名 */ + disabled: string; + /** 是否可查询 */ + checked: boolean; +} diff --git a/web/src/views/system/user/crud.tsx b/web/src/views/system/user/crud.tsx index dd24c40..b93511f 100644 --- a/web/src/views/system/user/crud.tsx +++ b/web/src/views/system/user/crud.tsx @@ -9,27 +9,29 @@ import { CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'; -import {request} from '/@/utils/service'; -import {dictionary} from '/@/utils/dictionary'; -import {successMessage} from '/@/utils/message'; -import {auth} from '/@/utils/authFunction'; -import {SystemConfigStore} from "/@/stores/systemConfig"; -import {storeToRefs} from "pinia"; -import {computed} from "vue"; +import { request } from '/@/utils/service'; +import { dictionary } from '/@/utils/dictionary'; +import { successMessage } from '/@/utils/message'; +import { auth } from '/@/utils/authFunction'; +import { SystemConfigStore } from "/@/stores/systemConfig"; +import { storeToRefs } from "pinia"; +import { computed } from "vue"; import { Md5 } from 'ts-md5'; -import {commonCrudConfig} from "/@/utils/commonCrud"; -export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps): CreateCrudOptionsRet { +import { commonCrudConfig } from "/@/utils/commonCrud"; +import { ElMessageBox } from 'element-plus'; +import {exportData} from "./api"; +export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { const pageRequest = async (query: UserPageQuery) => { return await api.GetList(query); }; - const editRequest = async ({form, row}: EditReq) => { + const editRequest = async ({ form, row }: EditReq) => { form.id = row.id; return await api.UpdateObj(form); }; - const delRequest = async ({row}: DelReq) => { + const delRequest = async ({ row }: DelReq) => { return await api.DelObj(row.id); }; - const addRequest = async ({form}: AddReq) => { + const addRequest = async ({ form }: AddReq) => { return await api.AddObj(form); }; @@ -37,13 +39,13 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) return await api.exportData(query) } - const resetToDefaultPasswordRequest = async (row:EditReq)=>{ + const resetToDefaultPasswordRequest = async (row: EditReq) => { await api.resetToDefaultPassword(row.id) successMessage("重置密码成功") } const systemConfigStore = SystemConfigStore() - const {systemConfig} = storeToRefs(systemConfigStore) + const { systemConfig } = storeToRefs(systemConfigStore) const getSystemConfig = computed(() => { // console.log(systemConfig.value) return systemConfig.value @@ -79,9 +81,10 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) text: "导出",//按钮文字 title: "导出",//鼠标停留显示的信息 show: auth('user:Export'), - click() { - return exportRequest(crudExpose!.getSearchFormData()) - } + click: (ctx: any) => ElMessageBox.confirm( + '确定导出数据吗?', '提示', + { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } + ).then(() => exportData(ctx.row)) } } }, @@ -113,7 +116,7 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) }, //@ts-ignore click: (ctx: any) => { - const {row} = ctx; + const { row } = ctx; resetToDefaultPasswordRequest(row) }, }, @@ -122,7 +125,7 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) columns: { _index: { title: '序号', - form: {show: false}, + form: { show: false }, column: { type: 'index', align: 'center', @@ -176,7 +179,7 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) placeholder: '请输入密码', }, }, - valueResolve({form}) { + valueResolve({ form }) { if (form.password) { form.password = Md5.hashStr(form.password) } @@ -219,7 +222,7 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) }), column: { minWidth: 200, //最小列宽 - formatter({value,row,index}){ + formatter({ value, row, index }) { return row.dept_name_all } }, @@ -235,7 +238,7 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) filterable: true, placeholder: '请选择', props: { - checkStrictly:true, + checkStrictly: true, props: { value: 'id', label: 'name', @@ -257,10 +260,10 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) }), column: { minWidth: 200, //最小列宽 - formatter({value,row,index}){ - const values = row.role_info.map((item:any) => item.name); - return values.join(',') - } + // formatter({ value, row, index }) { + // const values = row.role_info.map((item: any) => item.name); + // return values.join(',') + // } }, form: { rules: [ @@ -333,7 +336,7 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) span: 12, }, }, - component: {props: {color: 'auto'}}, // 自动染色 + component: { props: { color: 'auto' } }, // 自动染色 }, user_type: { title: '用户类型', @@ -382,12 +385,13 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) }, avatar: { title: '头像', - type: 'avatar-cropper', + type: 'avatar-uploader', + align: 'center', form: { show: false, }, column: { - minWidth: 400, //最小列宽 + minWidth: 100, //最小列宽 }, }, ...commonCrudConfig({ diff --git a/web/src/views/system/user/index.vue b/web/src/views/system/user/index.vue index 71085d1..117a559 100644 --- a/web/src/views/system/user/index.vue +++ b/web/src/views/system/user/index.vue @@ -33,6 +33,15 @@ + @@ -50,6 +59,8 @@ import {ref, onMounted, watch, toRaw, h} from 'vue'; import XEUtils from 'xe-utils'; import {getElementLabelLine} from 'element-tree-line'; import importExcel from '/@/components/importExcel/index.vue' +import {getBaseURL} from '/@/utils/baseUrl'; + const ElementTreeLine = getElementLabelLine(h); diff --git a/web/src/views/template/api.ts b/web/src/views/template/api.ts new file mode 100644 index 0000000..1ec69a9 --- /dev/null +++ b/web/src/views/template/api.ts @@ -0,0 +1,50 @@ +import { request,downloadFile } from '/@/utils/service'; +import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; + +export const apiPrefix = '/api/VIEWSETNAME/'; + +export function GetList(query: PageQuery) { + return request({ + url: apiPrefix, + method: 'get', + params: query, + }); +} +export function GetObj(id: InfoReq) { + return request({ + url: apiPrefix + id, + method: 'get', + }); +} + +export function AddObj(obj: AddReq) { + return request({ + url: apiPrefix, + method: 'post', + data: obj, + }); +} + +export function UpdateObj(obj: EditReq) { + return request({ + url: apiPrefix + obj.id + '/', + method: 'put', + data: obj, + }); +} + +export function DelObj(id: DelReq) { + return request({ + url: apiPrefix + id + '/', + method: 'delete', + data: { id }, + }); +} + +export function exportData(params:any){ + return downloadFile({ + url: apiPrefix + 'export_data/', + params: params, + method: 'get' + }) +} \ No newline at end of file diff --git a/web/src/views/template/crud.tsx b/web/src/views/template/crud.tsx new file mode 100644 index 0000000..8ef7a7d --- /dev/null +++ b/web/src/views/template/crud.tsx @@ -0,0 +1,86 @@ +import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, UserPageQuery, CreateCrudOptionsRet } from '@fast-crud/fast-crud'; +import _ from 'lodash-es'; +import * as api from './api'; +import { request } from '/@/utils/service'; +import { auth } from "/@/utils/authFunction"; + +//此处为crudOptions配置 +export default function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsRet { + const pageRequest = async (query: any) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + if (row.id) { + form.id = row.id; + } + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; + + const exportRequest = async (query: UserPageQuery) => { + return await api.exportData(query) + }; + + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + actionbar: { + buttons: { + export: { + // 注释编号:django-vue3-admin-crud210716:注意这个auth里面的值,最好是使用index.vue文件里面的name值并加上请求动作的单词 + show: auth('VIEWSETNAME:Export'), + text: "导出",//按钮文字 + title: "导出",//鼠标停留显示的信息 + click() { + return exportRequest(crudExpose.getSearchFormData()) + // return exportRequest(crudExpose!.getSearchFormData()) // 注意这个crudExpose!.getSearchFormData(),一些低版本的环境是需要添加!的 + } + }, + add: { + show: auth('VIEWSETNAME:Create'), + }, + } + }, + rowHandle: { + //固定右侧 + fixed: 'right', + width: 200, + buttons: { + view: { + type: 'text', + order: 1, + show: auth('VIEWSETNAME:Retrieve') + }, + edit: { + type: 'text', + order: 2, + show: auth('VIEWSETNAME:Update') + }, + copy: { + type: 'text', + order: 3, + show: auth('VIEWSETNAME:Copy') + }, + remove: { + type: 'text', + order: 4, + show: auth('VIEWSETNAME:Delete') + }, + }, + }, + columns: { + // COLUMNS_CONFIG + }, + }, + }; +} \ No newline at end of file diff --git a/web/src/views/template/index.vue b/web/src/views/template/index.vue new file mode 100644 index 0000000..be662d5 --- /dev/null +++ b/web/src/views/template/index.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/web/vite.config.ts b/web/vite.config.ts index ed6dec2..d6ebf26 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -11,6 +11,7 @@ const pathResolve = (dir: string) => { const alias: Record = { '/@': pathResolve('./src/'), + '@great-dream': pathResolve('./node_modules/@great-dream/'), '@views': pathResolve('./src/views'), 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', '@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/') @@ -31,7 +32,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => { server: { host: '0.0.0.0', port: env.VITE_PORT as unknown as number, - open: true, + open: false, hmr: true, proxy: { '/gitee': {