diff --git a/README.zh.md b/README.zh.md index dea0106..a95a364 100644 --- a/README.zh.md +++ b/README.zh.md @@ -114,7 +114,7 @@ cd web # 安装依赖 npm install yarn -yarn install --registry=https://registry.npm.taobao.org +yarn install --registry=https://registry.npmmirror.com # 启动服务 yarn build diff --git a/backend/.gitignore b/backend/.gitignore index 6c50cc9..047099f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -98,4 +98,5 @@ media/ __pypackages__/ package-lock.json gunicorn.pid +plugins/* !plugins/__init__.py diff --git a/backend/application/__init__.py b/backend/application/__init__.py index e69de29..b7ddb6f 100644 --- a/backend/application/__init__.py +++ b/backend/application/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/backend/application/celery.py b/backend/application/celery.py index d57b92a..10bce56 100644 --- a/backend/application/celery.py +++ b/backend/application/celery.py @@ -15,7 +15,7 @@ else: from celery import Celery app = Celery(f"application") -app.config_from_object('django.conf:settings') +app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) platforms.C_FORCE_ROOT = True diff --git a/backend/application/settings.py b/backend/application/settings.py index 8b2c8c2..1d0adf7 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = [] # ********** 一键导入插件配置开始 ********** # 例如: # from dvadmin_upgrade_center.settings import * # 升级中心 -# from dvadmin_celery.settings import * # celery 异步任务 +from dvadmin3_celery.settings import * # celery 异步任务 # from dvadmin_third.settings import * # 第三方用户管理 # from dvadmin_ak_sk.settings import * # 秘钥管理管理 # from dvadmin_tenants.settings import * # 租户管理 diff --git a/backend/dvadmin/__init__.py b/backend/dvadmin/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/backend/dvadmin/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/backend/dvadmin/system/fixtures/init_menu.json b/backend/dvadmin/system/fixtures/init_menu.json index 4258872..835a41e 100644 --- a/backend/dvadmin/system/fixtures/init_menu.json +++ b/backend/dvadmin/system/fixtures/init_menu.json @@ -167,19 +167,13 @@ "method": 0 }, { - "name": "查询所有", + "name": "获取所有部门", "value": "dept:SearchAll", "api": "/api/system/dept/all_dept/", "method": 0 }, { - "name": "懒加载查询所有", - "value": "dept:LazySearchAll", - "api": "/api/system/dept/dept_lazy_tree/", - "method": 0 - }, - { - "name": "头信息", + "name": "部门顶部信息", "value": "dept:HeaderInfo", "api": "/api/system/dept/dept_info/", "method": 0 @@ -678,6 +672,53 @@ "model": "ApiWhiteList" } ] + }, + { + "name": "下载中心", + "icon": "ele-Download", + "sort": 9, + "is_link": false, + "is_catalog": false, + "web_path": "/downloadCenter", + "component": "system/downloadCenter/index", + "component_name": "downloadCenter", + "status": true, + "cache": false, + "visible": true, + "parent": 277, + "children": [], + "menu_button": [ + { + "name": "查询", + "value": "Search", + "api": "/api/system/downloadCenter/", + "method": 0 + }, + { + "name": "详情", + "value": "Retrieve", + "api": "/api/system/downloadCenter/{id}/", + "method": 0 + }, + { + "name": "新增", + "value": "Create", + "api": "/api/system/downloadCenter/", + "method": 1 + }, + { + "name": "编辑", + "value": "Update", + "api": "/api/system/downloadCenter/{id}/", + "method": 2 + }, + { + "name": "删除", + "value": "Delete", + "api": "/api/system/downloadCenter/{id}/", + "method": 3 + } + ] } ], "menu_button": [], diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index e0e1841..d9a4274 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -1,5 +1,7 @@ import hashlib import os +from time import time +from pathlib import PurePosixPath from django.contrib.auth.models import AbstractUser, UserManager from django.db import models @@ -595,3 +597,41 @@ class MessageCenterTargetUser(CoreModel): db_table = table_prefix + "message_center_target_user" verbose_name = "消息中心目标用户表" verbose_name_plural = verbose_name + + +def media_file_name_downloadcenter(instance:'DownloadCenter', filename): + h = instance.md5sum + basename, ext = os.path.splitext(filename) + return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower()) + + +class DownloadCenter(CoreModel): + TASK_STATUS_CHOICES = [ + (0, '任务已创建'), + (1, '任务进行中'), + (2, '任务完成'), + (3, '任务失败'), + ] + task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称") + task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载') + file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名") + url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True) + size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小") + md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5") + + def save(self, *args, **kwargs): + if self.url: + if not self.md5sum: # file is new + md5 = hashlib.md5() + for chunk in self.url.chunks(): + md5.update(chunk) + self.md5sum = md5.hexdigest() + if not self.size: + self.size = self.url.size + super(DownloadCenter, self).save(*args, **kwargs) + + class Meta: + db_table = table_prefix + "download_center" + verbose_name = "下载中心" + verbose_name_plural = verbose_name + ordering = ("-create_datetime",) diff --git a/backend/dvadmin/system/signals.py b/backend/dvadmin/system/signals.py new file mode 100644 index 0000000..9728228 --- /dev/null +++ b/backend/dvadmin/system/signals.py @@ -0,0 +1,12 @@ +from django.dispatch import Signal +# 初始化信号 +pre_init_complete = Signal() +detail_init_complete = Signal() +post_init_complete = Signal() +# 租户初始化信号 +pre_tenants_init_complete = Signal() +detail_tenants_init_complete = Signal() +post_tenants_init_complete = Signal() +post_tenants_all_init_complete = Signal() +# 租户创建完成信号 +tenants_create_complete = Signal() diff --git a/backend/dvadmin/system/tasks.py b/backend/dvadmin/system/tasks.py new file mode 100644 index 0000000..0245e63 --- /dev/null +++ b/backend/dvadmin/system/tasks.py @@ -0,0 +1,107 @@ +from hashlib import md5 +from io import BytesIO +from datetime import datetime +from time import sleep + +from openpyxl import Workbook +from openpyxl.worksheet.table import Table, TableStyleInfo +from openpyxl.utils import get_column_letter +from django.core.files.base import ContentFile + +from application.celery import app +from dvadmin.system.models import DownloadCenter + +def is_number(num): + try: + float(num) + return True + except ValueError: + pass + + try: + import unicodedata + unicodedata.numeric(num) + return True + except (TypeError, ValueError): + pass + return False + +def get_string_len(string): + """ + 获取字符串最大长度 + :param string: + :return: + """ + length = 4 + if string is None: + return length + if is_number(string): + return length + for char in string: + length += 2.1 if ord(char) > 256 else 1 + return round(length, 1) if length <= 50 else 50 + +@app.task +def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict): + instance = DownloadCenter.objects.get(pk=dcid) + instance.task_status = 1 + instance.save() + sleep(2) + try: + wb = Workbook() + ws = wb.active + header_data = ["序号", *export_field_label.values()] + hidden_header = ["#", *export_field_label.keys()] + df_len_max = [get_string_len(ele) for ele in header_data] + row = get_column_letter(len(export_field_label) + 1) + column = 1 + ws.append(header_data) + for index, results in enumerate(data): + results_list = [] + for h_index, h_item in enumerate(hidden_header): + for key, val in results.items(): + if key == h_item: + if val is None or val == "": + results_list.append("") + elif isinstance(val, datetime): + val = val.strftime("%Y-%m-%d %H:%M:%S") + results_list.append(val) + else: + results_list.append(val) + # 计算最大列宽度 + result_column_width = get_string_len(val) + if h_index != 0 and result_column_width > df_len_max[h_index]: + df_len_max[h_index] = result_column_width + ws.append([index + 1, *results_list]) + column += 1 + #  更新列宽 + for index, width in enumerate(df_len_max): + ws.column_dimensions[get_column_letter(index + 1)].width = width + tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器 + style = TableStyleInfo( + name="TableStyleLight11", + showFirstColumn=True, + showLastColumn=True, + showRowStripes=True, + showColumnStripes=True, + ) + tab.tableStyleInfo = style + ws.add_table(tab) + stream = BytesIO() + wb.save(stream) + stream.seek(0) + s = md5() + while True: + chunk = stream.read(1024) + if not chunk: + break + s.update(chunk) + stream.seek(0) + instance.md5sum = s.hexdigest() + instance.file_name = filename + instance.url.save(filename, ContentFile(stream.read())) + instance.task_status = 2 + except Exception as e: + instance.task_status = 3 + instance.description = str(e)[:250] + instance.save() diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py index 8d21507..c9c12e9 100644 --- a/backend/dvadmin/system/urls.py +++ b/backend/dvadmin/system/urls.py @@ -18,6 +18,7 @@ from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermi from dvadmin.system.views.system_config import SystemConfigViewSet from dvadmin.system.views.user import UserViewSet from dvadmin.system.views.menu_field import MenuFieldViewSet +from dvadmin.system.views.download_center import DownloadCenterViewSet system_url = routers.SimpleRouter() system_url.register(r'menu', MenuViewSet) @@ -35,6 +36,8 @@ system_url.register(r'message_center', MessageCenterViewSet) system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet) system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet) system_url.register(r'column', MenuFieldViewSet) +system_url.register(r'login_log', LoginLogViewSet) +system_url.register(r'download_center', DownloadCenterViewSet) urlpatterns = [ @@ -44,8 +47,8 @@ urlpatterns = [ path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})), path('system_config/get_table_data//', SystemConfigViewSet.as_view({'get': 'get_table_data'})), 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('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('clause/privacy.html', PrivacyView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()), diff --git a/backend/dvadmin/system/views/area.py b/backend/dvadmin/system/views/area.py index dfa2353..7a19eb0 100644 --- a/backend/dvadmin/system/views/area.py +++ b/backend/dvadmin/system/views/area.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- +import pypinyin from django.db.models import Q from rest_framework import serializers from dvadmin.system.models import Area +from dvadmin.utils.field_permission import FieldPermissionMixin from dvadmin.utils.json_response import SuccessResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet @@ -14,13 +16,21 @@ class AreaSerializer(CustomModelSerializer): """ pcode_count = serializers.SerializerMethodField(read_only=True) hasChild = serializers.SerializerMethodField() + pcode_info = serializers.SerializerMethodField() + + def get_pcode_info(self, instance): + pcode = Area.objects.filter(code=instance.pcode_id).values("name", "code") + return pcode + def get_pcode_count(self, instance: Area): return Area.objects.filter(pcode=instance).count() + def get_hasChild(self, instance): hasChild = Area.objects.filter(pcode=instance.code) if hasChild: return True return False + class Meta: model = Area fields = "__all__" @@ -32,12 +42,24 @@ class AreaCreateUpdateSerializer(CustomModelSerializer): 地区管理 创建/更新时的列化器 """ + def to_internal_value(self, data): + pinyin = ''.join([''.join(i) for i in pypinyin.pinyin(data["name"], style=pypinyin.NORMAL)]) + data["level"] = 1 + data["pinyin"] = pinyin + data["initials"] = pinyin[0].upper() if pinyin else "#" + pcode = data["pcode"] if 'pcode' in data else None + if pcode: + pcode = Area.objects.get(pk=pcode) + data["pcode"] = pcode.code + data["level"] = pcode.level + 1 + return super().to_internal_value(data) + class Meta: model = Area fields = '__all__' -class AreaViewSet(CustomModelViewSet): +class AreaViewSet(CustomModelViewSet, FieldPermissionMixin): """ 地区管理接口 list:查询 @@ -48,21 +70,28 @@ class AreaViewSet(CustomModelViewSet): """ queryset = Area.objects.all() serializer_class = AreaSerializer + create_serializer_class = AreaCreateUpdateSerializer + update_serializer_class = AreaCreateUpdateSerializer extra_filter_class = [] - def get_queryset(self): + def list(self, request, *args, **kwargs): self.request.query_params._mutable = True params = self.request.query_params - pcode = params.get('pcode', None) - page = params.get('page', None) - limit = params.get('limit', None) - if page: - del params['page'] - if limit: - del params['limit'] - if params and pcode: - queryset = self.queryset.filter(enable=True, pcode=pcode) - else: + known_params = {'page', 'limit', 'pcode'} + # 使用集合操作检查是否有未知参数 + other_params_exist = any(param not in known_params for param in params) + if other_params_exist: queryset = self.queryset.filter(enable=True) - return queryset - + else: + pcode = params.get('pcode', None) + params['limit'] = 999 + if params and pcode: + queryset = self.queryset.filter(enable=True, pcode=pcode) + else: + queryset = self.queryset.filter(enable=True, level=1) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True, request=request) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True, request=request) + return SuccessResponse(data=serializer.data, msg="获取成功") diff --git a/backend/dvadmin/system/views/dept.py b/backend/dvadmin/system/views/dept.py index 5c5105b..de48b0d 100644 --- a/backend/dvadmin/system/views/dept.py +++ b/backend/dvadmin/system/views/dept.py @@ -10,6 +10,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users +from dvadmin.utils.filters import DataLevelPermissionsFilter from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet @@ -124,33 +125,7 @@ class DeptViewSet(CustomModelViewSet): data = serializer.data return SuccessResponse(data=data) - @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[]) - def dept_lazy_tree(self, request, *args, **kwargs): - parent = self.request.query_params.get('parent') - is_superuser = request.user.is_superuser - if is_superuser: - queryset = Dept.objects.values('id', 'name', 'parent') - else: - role_ids = request.user.role.values_list('id', flat=True) - data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True) - user_dept_id = request.user.dept.id - dept_list = [user_dept_id] - data_range_list = list(set(data_range)) - for item in data_range_list: - if item in [0, 2]: - dept_list = [user_dept_id] - elif item == 1: - dept_list = Dept.recursion_all_dept(dept_id=user_dept_id) - elif item == 3: - dept_list = Dept.objects.values_list('id', flat=True) - elif item == 4: - dept_list = request.user.role.values_list('dept', flat=True) - else: - dept_list = [] - queryset = Dept.objects.filter(id__in=dept_list).values('id', 'name', 'parent') - return DetailResponse(data=queryset, msg="获取成功") - - @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[]) + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) def all_dept(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent') diff --git a/backend/dvadmin/system/views/download_center.py b/backend/dvadmin/system/views/download_center.py new file mode 100644 index 0000000..2587d74 --- /dev/null +++ b/backend/dvadmin/system/views/download_center.py @@ -0,0 +1,46 @@ +from rest_framework import serializers +from django.conf import settings +from django_filters.rest_framework import FilterSet, CharFilter + +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.utils.viewset import CustomModelViewSet +from dvadmin.system.models import DownloadCenter + + +class DownloadCenterSerializer(CustomModelSerializer): + url = serializers.SerializerMethodField(read_only=True) + + def get_url(self, instance): + 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()) + return (f'{prefix}/media/{str(instance.url)}') + return f'media/{str(instance.url)}' + + class Meta: + model = DownloadCenter + fields = "__all__" + read_only_fields = ["id"] + + +class DownloadCenterFilterSet(FilterSet): + task_name = CharFilter(field_name='task_name', lookup_expr='icontains') + file_name = CharFilter(field_name='file_name', lookup_expr='icontains') + + class Meta: + model = DownloadCenter + fields = ['task_status', 'task_name', 'file_name'] + + +class DownloadCenterViewSet(CustomModelViewSet): + queryset = DownloadCenter.objects.all() + serializer_class = DownloadCenterSerializer + filter_class = DownloadCenterFilterSet + permission_classes = [] + + def get_queryset(self): + 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 c122f03..c595699 100644 --- a/backend/dvadmin/system/views/file_list.py +++ b/backend/dvadmin/system/views/file_list.py @@ -15,8 +15,8 @@ class FileSerializer(CustomModelSerializer): url = serializers.SerializerMethodField(read_only=True) def get_url(self, instance): - # return 'media/' + str(instance.url) - return instance.file_url or (f'media/{str(instance.url)}') + base_url = f"{self.request.scheme}://{self.request.get_host()}/" + return base_url + (instance.file_url or (f'media/{str(instance.url)}')) class Meta: model = FileList diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index e04743c..1996906 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -4,6 +4,7 @@ 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.db.models import Q from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ from drf_yasg import openapi @@ -83,11 +84,18 @@ class LoginSerializer(TokenObtainPairSerializer): else: self.image_code and self.image_code.delete() raise CustomValidationError("图片验证码错误") - - user = Users.objects.get(username=attrs['username']) + try: + user = Users.objects.get( + Q(username=attrs['username']) | Q(email=attrs['username']) | Q(mobile=attrs['username'])) + except Users.DoesNotExist: + raise CustomValidationError("您登录的账号不存在") + except Users.MultipleObjectsReturned: + raise CustomValidationError("您登录的账号存在多个,请联系管理员检查登录账号唯一性") if not user.is_active: raise CustomValidationError("账号已被锁定,联系管理员解锁") try: + # 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误 + attrs['username'] = user.username data = super().validate(attrs) data["name"] = self.user.name data["userId"] = self.user.id @@ -114,6 +122,7 @@ class LoginSerializer(TokenObtainPairSerializer): user.login_error_count += 1 if user.login_error_count >= 5: user.is_active = False + user.save() raise CustomValidationError("账号已被锁定,联系管理员解锁") user.save() count = 5 - user.login_error_count diff --git a/backend/dvadmin/system/views/login_log.py b/backend/dvadmin/system/views/login_log.py index d362cea..4d8bf74 100644 --- a/backend/dvadmin/system/views/login_log.py +++ b/backend/dvadmin/system/views/login_log.py @@ -7,6 +7,7 @@ @Remark: 按钮权限管理 """ from dvadmin.system.models import LoginLog +from dvadmin.utils.field_permission import FieldPermissionMixin from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet @@ -22,7 +23,7 @@ class LoginLogSerializer(CustomModelSerializer): read_only_fields = ["id"] -class LoginLogViewSet(CustomModelViewSet): +class LoginLogViewSet(CustomModelViewSet, FieldPermissionMixin): """ 登录日志接口 list:查询 @@ -33,4 +34,4 @@ class LoginLogViewSet(CustomModelViewSet): """ queryset = LoginLog.objects.all() serializer_class = LoginLogSerializer - extra_filter_class = [] + # extra_filter_class = [] diff --git a/backend/dvadmin/system/views/menu.py b/backend/dvadmin/system/views/menu.py index 28dce62..c0c6b65 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) + queryset = self.queryset.filter(status=1).order_by("id") 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) + queryset = Menu.objects.filter(id__in=menu_list).order_by("id") 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/menu_button.py b/backend/dvadmin/system/views/menu_button.py index f91839a..d290bb1 100644 --- a/backend/dvadmin/system/views/menu_button.py +++ b/backend/dvadmin/system/views/menu_button.py @@ -10,12 +10,14 @@ from django.db.models import F from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated -from dvadmin.system.models import MenuButton, RoleMenuButtonPermission +from dvadmin.system.models import MenuButton, RoleMenuButtonPermission, Menu from dvadmin.utils.json_response import DetailResponse, SuccessResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet + + class MenuButtonSerializer(CustomModelSerializer): """ 菜单按钮-序列化器 @@ -92,17 +94,15 @@ class MenuButtonViewSet(CustomModelViewSet): """ menu_obj = Menu.objects.filter(id=request.data['menu']).first() result_list = [ - {'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api{menu_obj.web_path}/', - 'method': 1}, - {'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api{menu_obj.web_path}/{{id}}/', - 'method': 3}, - {'menu': menu_obj.id, 'name': '修改', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api{menu_obj.web_path}/{{id}}/', - 'method': 2}, - {'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api{menu_obj.web_path}/', - 'method': 0}, - {'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api{menu_obj.web_path}/{{id}}/', - 'method': 0}] + {'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api/{menu_obj.component_name}/', 'method': 1}, + {'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 3}, + {'menu': menu_obj.id, 'name': '编辑', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 2}, + {'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api/{menu_obj.component_name}/', 'method': 0}, + {'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 0}, + {'menu': menu_obj.id, 'name': '复制', 'value': f'{menu_obj.component_name}:Copy', 'api': f'/api/{menu_obj.component_name}/', 'method': 1}, + {'menu': menu_obj.id, 'name': '导入', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api/{menu_obj.component_name}/import_data/', 'method': 1}, + {'menu': menu_obj.id, 'name': '导出', 'value': f'{menu_obj.component_name}:Export', 'api': f'/api{menu_obj.component_name}/export_data/', 'method': 1},] serializer = self.get_serializer(data=result_list, many=True) serializer.is_valid(raise_exception=True) serializer.save() - return SuccessResponse(serializer.data, msg="批量创建成功") + return SuccessResponse(serializer.data, msg="批量创建成功") \ No newline at end of file diff --git a/backend/dvadmin/system/views/message_center.py b/backend/dvadmin/system/views/message_center.py index 96b23c3..db91b75 100644 --- a/backend/dvadmin/system/views/message_center.py +++ b/backend/dvadmin/system/views/message_center.py @@ -36,6 +36,8 @@ class MessageCenterSerializer(CustomModelSerializer): return serializer.data def get_user_info(self, instance, parsed_query): + if instance.target_type in (1,2,3): + return [] users = instance.target_user.all() # You can do what ever you want in here # `parsed_query` param is passed to BookSerializer to allow further querying @@ -80,6 +82,9 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer): """ 目标用户序列化器-序列化器 """ + role_info = DynamicSerializerMethodField() + user_info = DynamicSerializerMethodField() + dept_info = DynamicSerializerMethodField() is_read = serializers.SerializerMethodField() def get_is_read(self, instance): @@ -90,6 +95,44 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer): return queryset.is_read return False + def get_role_info(self, instance, parsed_query): + roles = instance.target_role.all() + # You can do what ever you want in here + # `parsed_query` param is passed to BookSerializer to allow further querying + from dvadmin.system.views.role import RoleSerializer + serializer = RoleSerializer( + roles, + many=True, + parsed_query=parsed_query + ) + return serializer.data + + def get_user_info(self, instance, parsed_query): + if instance.target_type in (1,2,3): + return [] + users = instance.target_user.all() + # You can do what ever you want in here + # `parsed_query` param is passed to BookSerializer to allow further querying + from dvadmin.system.views.user import UserSerializer + serializer = UserSerializer( + users, + many=True, + parsed_query=parsed_query + ) + return serializer.data + + def get_dept_info(self, instance, parsed_query): + dept = instance.target_dept.all() + # You can do what ever you want in here + # `parsed_query` param is passed to BookSerializer to allow further querying + from dvadmin.system.views.dept import DeptSerializer + serializer = DeptSerializer( + dept, + many=True, + parsed_query=parsed_query + ) + return serializer.data + class Meta: model = MessageCenter fields = "__all__" diff --git a/backend/dvadmin/system/views/role_menu_button_permission.py b/backend/dvadmin/system/views/role_menu_button_permission.py index 604ebe2..3ee596d 100644 --- a/backend/dvadmin/system/views/role_menu_button_permission.py +++ b/backend/dvadmin/system/views/role_menu_button_permission.py @@ -6,9 +6,11 @@ @Created on: 2021/6/3 003 0:30 @Remark: 菜单按钮管理 """ -from django.db.models import F, Subquery, OuterRef, Exists +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, \ @@ -172,59 +174,103 @@ 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_premission(self, request): - """ - 角色授权获取: - :param request: role - :return: menu,btns,columns - """ + def get_role_permission(self, request): params = request.query_params - role = params.get('role', None) - if role is None: - return ErrorResponse(msg="未获取到角色信息") + # 需要授权的角色信息 + 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: - queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all() + menu_queryset = Menu.objects.prefetch_related('menuPermission').prefetch_related( + 'menufield_set') else: - role_id = request.user.role.values_list('id', flat=True) - menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True) - queryset = Menu.objects.filter(status=1, is_catalog=True, id__in=menu_list).values('name', 'id').all() - serializer = RoleMenuSerializer(queryset, many=True, request=request) - data = serializer.data - return DetailResponse(data=data) - # data = [] - # if is_superuser: - # queryset = Menu.objects.filter(status=1, is_catalog=False).values('name', 'id').all() - # else: - # role_id = request.user.role.values_list('id', flat=True) - # menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True) - # queryset = Menu.objects.filter(status=1, is_catalog=False, id__in=menu_list).values('name', 'id') - # for item in queryset: - # parent_list = Menu.get_all_parent(item['id']) - # names = [d["name"] for d in parent_list] - # completeName = "/".join(names) - # isCheck = RoleMenuPermission.objects.filter( - # menu__id=item['id'], - # role__id=role, - # ).exists() - # mbCheck = RoleMenuButtonPermission.objects.filter( - # menu_button=OuterRef("pk"), - # role__id=role, - # ) - # btns = MenuButton.objects.filter( - # menu__id=item['id'], - # ).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck', - # data_range=F('menu_button_permission__data_range')) - # dicts = { - # 'name': completeName, - # 'id': item['id'], - # 'isCheck': isCheck, - # 'btns': btns, - # - # } - # data.append(dicts) - # return DetailResponse(data=data) + 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) @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) def set_role_premission(self, request, pk): @@ -238,27 +284,21 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): RoleMenuPermission.objects.filter(role=pk).delete() RoleMenuButtonPermission.objects.filter(role=pk).delete() for item in body: - for menu in item["menus"]: - if menu.get('isCheck'): - menu_parent = Menu.get_all_parent(menu.get('id')) - role_menu_permission_list = [] - for d in menu_parent: - role_menu_permission_list.append(RoleMenuPermission(role_id=pk, menu_id=d["id"])) - RoleMenuPermission.objects.bulk_create(role_menu_permission_list) - # RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id')) - for btn in menu.get('btns'): - if btn.get('isCheck'): - data_range = btn.get('data_range', 0) or 0 - instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'), - data_range=data_range) - instance.dept.set(btn.get('dept', [])) - for col in menu.get('columns'): - FieldPermission.objects.update_or_create(role_id=pk, field_id=col.get('id'), - defaults={ - 'is_query': col.get('is_query'), - 'is_create': col.get('is_create'), - 'is_update': col.get('is_update') - }) + 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="授权成功") @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) @@ -291,86 +331,45 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): 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": '自定义数据权限' - } + {"value": 0, "label": '仅本人数据权限'}, + {"value": 1, "label": '本部门及以下数据权限'}, + {"value": 2, "label": '本部门数据权限'}, + {"value": 3, "label": '全部数据权限'}, + {"value": 4, "label": '自定义数据权限'} ] return DetailResponse(data=data) else: - data = [] + params = request.query_params + data = [{"value": 0, "label": '仅本人数据权限'}] role_list = request.user.role.values_list('id', flat=True) - if params := request.query_params: - if menu_button_id := params.get('menu_button', None): - role_queryset = RoleMenuButtonPermission.objects.filter( - role__in=role_list, menu_button__id=menu_button_id - ).values_list('data_range', flat=True) - data_range_list = list(set(role_queryset)) - for item in data_range_list: - if item == 0: - data = [{ - "value": 0, - "label": '仅本人数据权限' - }] - elif item == 1: - data = [{ - "value": 0, - "label": '仅本人数据权限' - }, { - "value": 1, - "label": '本部门及以下数据权限' - }, - { - "value": 2, - "label": '本部门数据权限' - }] - elif item == 2: - data = [{ - "value": 0, - "label": '仅本人数据权限' - }, - { - "value": 2, - "label": '本部门数据权限' - }] - elif item == 3: - data = [{ - "value": 0, - "label": '仅本人数据权限' - }, - { - "value": 3, - "label": '全部数据权限' - }, ] - elif item == 4: - data = [{ - "value": 0, - "label": '仅本人数据权限' - }, - { - "value": 4, - "label": '自定义数据权限' - }] - else: - data = [] - return DetailResponse(data=data) - return ErrorResponse(msg="参数错误") + # 权限页面进入初始化获取所有的数据权限范围 + 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) + + 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=['get'], detail=False, permission_classes=[IsAuthenticated]) def role_to_dept_all(self, request): @@ -379,23 +378,23 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): :param request: :return: """ - params = request.query_params is_superuser = request.user.is_superuser - if is_superuser: - queryset = Dept.objects.values('id', 'name', 'parent') - else: - if not params: - return ErrorResponse(msg="参数错误") - menu_button = params.get('menu_button') - if menu_button is None: - return ErrorResponse(msg="参数错误") - role_list = request.user.role.values_list('id', flat=True) - queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values( - dept_id=F('dept__id'), - name=F('dept__name'), - parent=F('dept__parent') - ) - return DetailResponse(data=queryset) + params = request.query_params + # 当前登录用户的角色 + role_list = request.user.role.values_list('id', flat=True) + + menu_button_id = params.get('menu_button') + # 当前登录用户角色可以分配的自定义部门权限 + dept_checked_disabled = RoleMenuButtonPermission.objects.filter( + role_id__in=role_list, menu_button_id=menu_button_id + ).values_list('dept', flat=True) + dept_list = Dept.objects.values('id', 'name', 'parent') + + data = [] + for dept in dept_list: + 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): diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index c6c002e..68de06b 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -119,7 +119,6 @@ class UserUpdateSerializer(CustomModelSerializer): """ 更改激活状态 """ - print(111, value) if value: self.initial_data["login_error_count"] = 0 return value @@ -331,6 +330,10 @@ class UserViewSet(CustomModelViewSet): if not verify_password: old_pwd_md5 = hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest() verify_password = check_password(str(old_pwd_md5), request.user.password) + # 创建用户时、自定义密码无法修改问题 + if not verify_password: + 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.save() @@ -407,9 +410,12 @@ class UserViewSet(CustomModelViewSet): queryset = self.filter_queryset(self.get_queryset()) else: queryset = self.filter_queryset(self.get_queryset()) + # print(queryset.values('id','name','dept__id')) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True, request=request) + # print(serializer.data) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True, request=request) + return SuccessResponse(data=serializer.data, msg="获取成功") diff --git a/backend/dvadmin/utils/field_permission.py b/backend/dvadmin/utils/field_permission.py index 20b4cb9..7c259f6 100644 --- a/backend/dvadmin/utils/field_permission.py +++ b/backend/dvadmin/utils/field_permission.py @@ -1,4 +1,7 @@ # -*- 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 @@ -35,4 +38,34 @@ class FieldPermissionMixin: 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 diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py index 05c0dfb..d09a8a1 100644 --- a/backend/dvadmin/utils/filters.py +++ b/backend/dvadmin/utils/filters.py @@ -37,11 +37,11 @@ class CoreModelFilterBankend(BaseFilterBackend): if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]): create_filter = Q() if create_datetime_after and create_datetime_before: - create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=create_datetime_before) + create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=f'{create_datetime_before} 23:59:59') elif create_datetime_after: create_filter &= Q(create_datetime__gte=create_datetime_after) elif create_datetime_before: - create_filter &= Q(create_datetime__lte=create_datetime_before) + create_filter &= Q(create_datetime__lte=f'{create_datetime_before} 23:59:59') # 更新时间范围过滤条件 update_filter = Q() diff --git a/backend/dvadmin/utils/import_export_mixin.py b/backend/dvadmin/utils/import_export_mixin.py index 44f51cc..74e85fd 100644 --- a/backend/dvadmin/utils/import_export_mixin.py +++ b/backend/dvadmin/utils/import_export_mixin.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import datetime from urllib.parse import quote from django.db import transaction @@ -11,8 +12,10 @@ from rest_framework.decorators import action from rest_framework.request import Request from dvadmin.utils.import_export import import_to_data -from dvadmin.utils.json_response import DetailResponse +from dvadmin.utils.json_response import DetailResponse, SuccessResponse from dvadmin.utils.request_util import get_verbose_name +from dvadmin.system.tasks import async_export_data +from dvadmin.system.models import DownloadCenter class ImportSerializerMixin: @@ -301,6 +304,17 @@ class ExportSerializerMixin: assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ 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, + self.export_field_label + ) + return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载") + except: + pass # 导出excel 表 response = HttpResponse(content_type="application/msexcel") response["Access-Control-Expose-Headers"] = f"Content-Disposition" diff --git a/backend/dvadmin/utils/middleware.py b/backend/dvadmin/utils/middleware.py index f4717a4..6029c5a 100644 --- a/backend/dvadmin/utils/middleware.py +++ b/backend/dvadmin/utils/middleware.py @@ -32,6 +32,14 @@ class ApiLoggingMiddleware(MiddlewareMixin): request.request_path = get_request_path(request) def __handle_response(self, request, response): + + # 判断有无log_id属性,使用All记录时,会出现此情况 + if request.request_data.get('log_id', None) is None: + return + + # 移除log_id,不记录此ID + log_id = request.request_data.pop('log_id') + # request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性 body = getattr(request, 'request_data', {}) # 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数) @@ -60,7 +68,7 @@ class ApiLoggingMiddleware(MiddlewareMixin): 'status': True if response.data.get('code') in [2000, ] else False, 'json_result': {"code": response.data.get('code'), "msg": response.data.get('msg')}, } - operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=self.operation_log_id) + operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=log_id) if not operation_log.request_modular and settings.API_MODEL_MAP.get(request.request_path, None): operation_log.request_modular = settings.API_MODEL_MAP[request.request_path] operation_log.save() @@ -71,7 +79,8 @@ class ApiLoggingMiddleware(MiddlewareMixin): if self.methods == 'ALL' or request.method in self.methods: log = OperationLog(request_modular=get_verbose_name(view_func.cls.queryset)) log.save() - self.operation_log_id = log.id + # self.operation_log_id = log.id + request.request_data['log_id'] = log.id return diff --git a/backend/dvadmin/utils/models.py b/backend/dvadmin/utils/models.py index fb0c2ba..b387ea4 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -6,13 +6,14 @@ @Created on: 2021/5/31 031 22:08 @Remark: 公共基础model类 """ +from datetime import datetime from importlib import import_module -from django.apps import apps -from django.db import models -from django.conf import settings - from application import settings +from django.apps import apps +from django.conf import settings +from django.db import models +from rest_framework.request import Request table_prefix = settings.TABLE_PREFIX # 数据库表名前缀 @@ -60,8 +61,24 @@ class SoftDeleteModel(models.Model): """ 重写删除方法,直接开启软删除 """ - self.is_deleted = True - self.save(using=using) + if soft_delete: + self.is_deleted = True + self.save(using=using) + # 级联软删除关联对象 + for related_object in self._meta.related_objects: + related_model = getattr(self, related_object.get_accessor_name()) + # 处理一对多和多对多的关联对象 + if related_object.one_to_many or related_object.many_to_many: + related_objects = related_model.all() + elif related_object.one_to_one: + related_objects = [related_model] + else: + continue + + for obj in related_objects: + obj.delete(soft_delete=True) + else: + super().delete(using=using, *args, **kwargs) class CoreModel(models.Model): @@ -87,6 +104,111 @@ class CoreModel(models.Model): verbose_name = '核心模型' verbose_name_plural = verbose_name + def get_request_user(self, request: Request): + if getattr(request, "user", None): + return request.user + return None + + def get_request_user_id(self, request: Request): + if getattr(request, "user", None): + return getattr(request.user, "id", None) + return None + + def get_request_user_name(self, request: Request): + if getattr(request, "user", None): + return getattr(request.user, "name", None) + return None + + def get_request_user_username(self, request: Request): + if getattr(request, "user", None): + return getattr(request.user, "username", None) + return None + + def common_insert_data(self, request: Request): + data = { + 'create_datetime': datetime.now(), + 'creator': self.get_request_user(request) + } + return {**data, **self.common_update_data(request)} + + def common_update_data(self, request: Request): + return { + 'update_datetime': datetime.now(), + 'modifier': self.get_request_user_username(request) + } + + exclude_fields = [ + '_state', + 'pk', + 'id', + 'create_datetime', + 'update_datetime', + 'creator', + 'creator_id', + 'creator_pk', + 'creator_name', + 'modifier', + 'modifier_id', + 'modifier_pk', + 'modifier_name', + 'dept_belong_id', + ] + + def get_exclude_fields(self): + return self.exclude_fields + + def get_all_fields(self): + return self._meta.fields + + def get_all_fields_names(self): + return [field.name for field in self.get_all_fields()] + + def get_need_fields_names(self): + return [field.name for field in self.get_all_fields() if field.name not in self.exclude_fields] + + def to_data(self): + """将模型转化为字典(去除不包含字段)(注意与to_dict_data区分)。 + """ + res = {} + for field in self.get_need_fields_names(): + field_value = getattr(self, field) + res[field] = field_value.id if (issubclass(field_value.__class__, CoreModel)) else field_value + return res + + @property + def DATA(self): + return self.to_data() + + def to_dict_data(self): + """需要导出的字段(去除不包含字段)(注意与to_data区分) + """ + return {field: getattr(self, field) for field in self.get_need_fields_names()} + + @property + def DICT_DATA(self): + return self.to_dict_data() + + def insert(self, request): + """插入模型 + """ + assert self.pk is None, f'模型{self.__class__.__name__}还没有保存到数据中,不能手动指定ID' + validated_data = {**self.common_insert_data(request), **self.DICT_DATA} + return self.__class__._default_manager.create(**validated_data) + + def update(self, request, update_data: dict[str, any] = None): + """更新模型 + """ + assert isinstance(update_data, dict), 'update_data必须为字典' + validated_data = {**self.common_insert_data(request), **update_data} + for key, value in validated_data.items(): + # 不允许修改id,pk,uuid字段 + if key in ['id', 'pk', 'uuid']: + continue + if hasattr(self, key): + setattr(self, key, value) + self.save() + return self + def get_all_models_objects(model_name=None): """ @@ -97,16 +219,9 @@ def get_all_models_objects(model_name=None): if not settings.ALL_MODELS_OBJECTS: all_models = apps.get_models() for item in list(all_models): - table = { - "tableName": item._meta.verbose_name, - "table": item.__name__, - "tableFields": [] - } + table = {"tableName": item._meta.verbose_name, "table": item.__name__, "tableFields": []} for field in item._meta.fields: - fields = { - "title": field.verbose_name, - "field": field.name - } + fields = {"title": field.verbose_name, "field": field.name} table['tableFields'].append(fields) settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item}) if model_name: @@ -117,25 +232,20 @@ def get_all_models_objects(model_name=None): def get_model_from_app(app_name): """获取模型里的字段""" model_module = import_module(app_name + '.models') + exclude_models = getattr(model_module, 'exclude_models', []) filter_model = [ - getattr(model_module, item) for item in dir(model_module) - if item != 'CoreModel' and issubclass(getattr(model_module, item).__class__, models.base.ModelBase) + value for key, value in model_module.__dict__.items() + if key != 'CoreModel' + and isinstance(value, type) + and issubclass(value, models.Model) + and key not in exclude_models ] model_list = [] for model in filter_model: if model.__name__ == 'AbstractUser': continue - fields = [ - {'title': field.verbose_name, 'name': field.name, 'object': field} - for field in model._meta.fields - ] - model_list.append({ - 'app': app_name, - 'verbose': model._meta.verbose_name, - 'model': model.__name__, - 'object': model, - 'fields': fields - }) + fields = [{'title': field.verbose_name, 'name': field.name, 'object': field} for field in model._meta.fields] + model_list.append({'app': app_name, 'verbose': model._meta.verbose_name, 'model': model.__name__, 'object': model, 'fields': fields}) return model_list diff --git a/backend/dvadmin/utils/permission.py b/backend/dvadmin/utils/permission.py index 8716264..688ab54 100644 --- a/backend/dvadmin/utils/permission.py +++ b/backend/dvadmin/utils/permission.py @@ -44,6 +44,35 @@ class AnonymousUserPermission(BasePermission): return True +class SuperuserPermission(BasePermission): + """ + 超级管理员权限类 + """ + + def has_permission(self, request, view): + if isinstance(request.user, AnonymousUser): + return False + # 判断是否是超级管理员 + if request.user.is_superuser: + return True + + +class AdminPermission(BasePermission): + """ + 普通管理员权限类 + """ + + def has_permission(self, request, view): + if isinstance(request.user, AnonymousUser): + return False + # 判断是否是超级管理员 + is_superuser = request.user.is_superuser + # 判断是否是管理员角色 + is_admin = request.user.role.values_list('admin', flat=True) + if is_superuser or True in is_admin: + return True + + def ReUUID(api): """ 将接口的uuid替换掉 @@ -81,8 +110,9 @@ class CustomPermission(BasePermission): # ********# if not hasattr(request.user, "role"): return False - role_id_list = request.user.role.values_list('id',flat=True) - userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口 + role_id_list = request.user.role.values_list('id', flat=True) + userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values( + permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口 ApiList = [ str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str( item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')] diff --git a/backend/dvadmin/utils/serializers.py b/backend/dvadmin/utils/serializers.py index 5dd9527..b0c411b 100644 --- a/backend/dvadmin/utils/serializers.py +++ b/backend/dvadmin/utils/serializers.py @@ -26,7 +26,6 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer): # 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖 modifier_field_id = "modifier" modifier_name = serializers.SerializerMethodField(read_only=True) - dept_belong_id = serializers.IntegerField(required=False, allow_null=True) def get_modifier_name(self, instance): if not hasattr(instance, "modifier"): @@ -52,7 +51,7 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer): format="%Y-%m-%d %H:%M:%S", required=False, read_only=True ) update_datetime = serializers.DateTimeField( - format="%Y-%m-%d %H:%M:%S", required=False + format="%Y-%m-%d %H:%M:%S", required=False, read_only=True ) def __init__(self, instance=None, data=empty, request=None, **kwargs): @@ -71,11 +70,11 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer): validated_data[self.creator_field_id] = self.request.user if ( - self.dept_belong_id_field_name in self.fields.fields - and validated_data.get(self.dept_belong_id_field_name, None) is None + self.dept_belong_id_field_name in self.fields.fields + and validated_data.get(self.dept_belong_id_field_name, None) is None ): validated_data[self.dept_belong_id_field_name] = getattr( - self.request.user, "dept_id", None + self.request.user, "dept_id", validated_data.get(self.dept_belong_id_field_name, None) ) return super().create(validated_data) diff --git a/backend/main.py b/backend/main.py index 2408243..c0419d9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,5 +9,9 @@ from application.settings import LOGGING if __name__ == '__main__': multiprocessing.freeze_support() - uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=4, + workers = 4 + if os.sys.platform.startswith('win'): + # Windows操作系统 + workers = None + uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=workers, log_config=LOGGING) diff --git a/backend/requirements.txt b/backend/requirements.txt index 6309e7c..3395719 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,32 +1,31 @@ -Django==4.2.7 +Django==4.2.14 django-comment-migrate==0.1.7 -django-cors-headers==4.3.0 -django-filter==23.3 +django-cors-headers==4.4.0 +django-filter==24.2 django-ranged-response==0.2.0 -djangorestframework==3.14.0 -django-restql==0.15.3 -django-simple-captcha==0.5.20 -django-timezone-field==6.0.1 -djangorestframework-simplejwt==5.3.0 +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 drf-yasg==1.21.7 mysqlclient==2.2.0 -pypinyin==0.49.0 +pypinyin==0.51.0 ua-parser==0.18.0 -pyparsing==3.1.1 -openpyxl==3.1.2 -requests==2.31.0 -typing-extensions==4.8.0 -tzlocal==5.1 -channels==3.0.5 -channels-redis==4.1.0 +pyparsing==3.1.2 +openpyxl==3.1.5 +requests==2.32.3 +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.6.0 +whitenoise==6.7.0 psycopg2==2.9.9 -uvicorn==0.23.2 -gunicorn==21.2.0 -gevent==23.9.1 -Pillow==10.1.0 -dvadmin-celery==1.0.5 -pyinstaller==6.8.0 \ No newline at end of file +uvicorn==0.30.3 +gunicorn==22.0.0 +gevent==24.2.1 +Pillow==10.4.0 +pyinstaller==6.9.0 \ No newline at end of file diff --git a/backend/static/logo.icns b/backend/static/logo.icns new file mode 100644 index 0000000..99572ef Binary files /dev/null and b/backend/static/logo.icns differ diff --git a/docker_env/django/Dockerfile b/docker_env/django/Dockerfile index 427ed0c..cad6fed 100644 --- a/docker_env/django/Dockerfile +++ b/docker_env/django/Dockerfile @@ -6,4 +6,4 @@ RUN awk 'BEGIN { cmd="cp -i ./conf/env.example.py ./conf/env.py "; print "n" | RUN sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.1'|g" ./conf/env.py RUN sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.1'|g" ./conf/env.py RUN python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt -CMD ["/backend/docker_start.sh"] +CMD ["sh","docker_start.sh"] diff --git a/docker_env/nginx/my-80.conf b/docker_env/nginx/my-80.conf index e50bdc4..98d33bd 100644 --- a/docker_env/nginx/my-80.conf +++ b/docker_env/nginx/my-80.conf @@ -7,6 +7,10 @@ server { index index.html index.htm; root /usr/share/nginx/html; try_files $uri $uri/ /index.html; + # 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况 + if ($request_uri ~* "^/$|^/index.html|^/index.htm") { + add_header Cache-Control "no-store"; + } } location ~ ^/api/ { diff --git a/docker_env/nginx/my.conf b/docker_env/nginx/my.conf index 178d979..dd6b333 100644 --- a/docker_env/nginx/my.conf +++ b/docker_env/nginx/my.conf @@ -11,6 +11,10 @@ server { real_ip_header X-Forwarded-For; root /usr/share/nginx/html; index index.html index.php index.htm; + # 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况 + if ($request_uri ~* "^/$|^/index.html|^/index.htm") { + add_header Cache-Control "no-store"; + } } location /api/ { diff --git a/web/.gitignore b/web/.gitignore index aa1baf2..8cef918 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -21,3 +21,5 @@ pnpm-debug.log* *.njsproj *.sln *.sw? +# 构建版本文件,无需上传git +public/version-build diff --git a/web/package.json b/web/package.json index 49d3d8a..7022283 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "django-vue3-admin", - "version": "3.0.3", + "version": "3.0.4", "description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus", "license": "MIT", "scripts": { @@ -10,32 +10,32 @@ "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" }, "dependencies": { - "@element-plus/icons-vue": "^2.0.10", - "@fast-crud/fast-crud": "^1.20.1", - "@fast-crud/fast-extends": "^1.20.1", - "@fast-crud/ui-element": "^1.20.1", - "@fast-crud/ui-interface": "^1.20.1", - "@iconify/vue": "^4.1.1", - "@types/lodash": "^4.14.202", - "@vitejs/plugin-vue-jsx": "^3.0.0", + "@element-plus/icons-vue": "^2.3.1", + "@fast-crud/fast-crud": "^1.21.2", + "@fast-crud/fast-extends": "^1.21.2", + "@fast-crud/ui-element": "^1.21.2", + "@fast-crud/ui-interface": "^1.21.2", + "@iconify/vue": "^4.1.2", + "@types/lodash": "^4.17.7", + "@vitejs/plugin-vue-jsx": "^4.0.1", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", - "autoprefixer": "^10.4.14", - "axios": "^1.2.1", - "countup.js": "^2.3.2", - "cropperjs": "^1.5.13", + "autoprefixer": "^10.4.20", + "axios": "^1.7.4", + "countup.js": "^2.8.0", + "cropperjs": "^1.6.2", "e-icon-picker": "2.1.1", - "echarts": "^5.4.1", + "echarts": "^5.5.1", "echarts-gl": "^2.0.9", "echarts-wordcloud": "^2.1.0", - "element-plus": "^2.5.5", + "element-plus": "^2.8.0", "element-tree-line": "^0.2.1", "font-awesome": "^4.7.0", - "js-cookie": "^3.0.1", - "js-table2excel": "^1.0.3", + "js-cookie": "^3.0.5", + "js-table2excel": "^1.1.2", "jsplumb": "^2.15.6", "lodash-es": "^4.17.21", - "mitt": "^3.0.0", + "mitt": "^3.0.1", "nprogress": "^0.2.0", "pinia": "^2.0.28", "pinia-plugin-persist": "^1.0.0", @@ -49,31 +49,31 @@ "tailwindcss": "^3.2.7", "ts-md5": "^1.3.1", "upgrade": "^1.1.0", - "vue": "^3.2.45", + "vue": "^3.4.38", "vue-clipboard3": "^2.0.0", "vue-cropper": "^1.0.8", "vue-grid-layout": "^3.0.0-beta1", - "vue-i18n": "^9.2.2", - "vue-router": "^4.1.6", - "vxe-table": "^4.4.1", - "xe-utils": "^3.5.7" + "vue-i18n": "^9.14.0", + "vue-router": "^4.4.3", + "vxe-table": "^4.6.18", + "xe-utils": "^3.5.30" }, "devDependencies": { - "@types/node": "^18.11.13", - "@types/nprogress": "^0.2.0", - "@types/sortablejs": "^1.15.0", + "@types/node": "^18.19.42", + "@types/nprogress": "^0.2.3", + "@types/sortablejs": "^1.15.8", "@typescript-eslint/eslint-plugin": "^5.46.0", "@typescript-eslint/parser": "^5.46.0", - "@vitejs/plugin-vue": "^4.0.0", - "@vue/compiler-sfc": "^3.2.45", - "eslint": "^8.54.0", - "eslint-plugin-vue": "^9.8.0", + "@vitejs/plugin-vue": "^5.1.2", + "@vue/compiler-sfc": "^3.4.38", + "eslint": "^9.9.0", + "eslint-plugin-vue": "^9.27.0", "prettier": "^2.8.1", "sass": "^1.56.2", "typescript": "^4.9.4", - "vite": "^4.0.0", + "vite": "^5.4.1", "vite-plugin-vue-setup-extend": "^0.4.0", - "vue-eslint-parser": "^9.1.0" + "vue-eslint-parser": "^9.4.3" }, "browserslist": [ "> 1%", diff --git a/web/src/components/tableSelector/index.vue b/web/src/components/tableSelector/index.vue index 7ad3cad..8e8c91a 100644 --- a/web/src/components/tableSelector/index.vue +++ b/web/src/components/tableSelector/index.vue @@ -1,203 +1,211 @@ diff --git a/web/src/i18n/pages/login/zh-cn.ts b/web/src/i18n/pages/login/zh-cn.ts index 6ab9813..07fd82e 100644 --- a/web/src/i18n/pages/login/zh-cn.ts +++ b/web/src/i18n/pages/login/zh-cn.ts @@ -9,7 +9,7 @@ export default { two4: '友情链接', }, account: { - accountPlaceholder1: '请输入登录账号', + accountPlaceholder1: '请输入登录账号/邮箱/手机号', accountPlaceholder2: '请输入登录密码', accountPlaceholder3: '请输入验证码', accountBtnText: '登 录', diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 3f564eb..f98dcf3 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -13,6 +13,7 @@ import {initBackEndControlRoutes, setRouters} from '/@/router/backEnd'; import {useFrontendMenuStore} from "/@/stores/frontendMenu"; import {useTagsViewRoutes} from "/@/stores/tagsViewRoutes"; import {toRaw} from "vue"; +import {checkVersion} from "/@/utils/upgrade"; /** * 1、前端控制路由时:isRequestRoutes 为 false,需要写 roles,需要走 setFilterRoute 方法。 @@ -93,8 +94,12 @@ export function formatTwoStageRoutes(arr: any) { return newArr; } +const frameOutRoutes = staticRoutes.map(item => item.path) + // 路由加载前 router.beforeEach(async (to, from, next) => { + // 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新 + await checkVersion() NProgress.configure({showSpinner: false}); if (to.meta.title) NProgress.start(); const token = Session.get('token'); @@ -109,8 +114,9 @@ router.beforeEach(async (to, from, next) => { } else if (token && to.path === '/login') { next('/home'); NProgress.done(); + }else if(token && frameOutRoutes.includes(to.path) ){ + next() } else { - const storesRoutesList = useRoutesList(pinia); const {routesList} = storeToRefs(storesRoutesList); if (routesList.value.length === 0) { diff --git a/web/src/utils/columnPermission.ts b/web/src/utils/columnPermission.ts index 4a9d9f1..adff4e1 100644 --- a/web/src/utils/columnPermission.ts +++ b/web/src/utils/columnPermission.ts @@ -22,33 +22,18 @@ export const handleColumnPermission = async (func: Function, crudOptions: any,ex } } const columns = crudOptions.columns; - const excludeColumns = ['_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn) + const excludeColumns = ['checked','_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn) for (let col in columns) { - if (excludeColumns.includes(col)) { - continue - }else{ - if (columns[col].column) { - columns[col].column.show = false - } else { - columns[col]['column'] = { - show: false - } - } - columns[col].addForm = { - show: false - } - columns[col].editForm = { - show: false - } - } - for (let item of res.data) { if (excludeColumns.includes(item.field_name)) { continue } else if(item.field_name === col) { - columns[col].column.show = item['is_query'] // 如果列表不可见,则禁止在列设置中选择 - if(!item['is_query'])columns[col].column.columnSetDisabled = true + // 只有列表不可见,才修改列配置,这样才不影响默认的配置 + if(!item['is_query']){ + columns[col].column.show = false + columns[col].column.columnSetDisabled = true + } columns[col].addForm = { show: item['is_create'] } diff --git a/web/src/utils/loading.ts b/web/src/utils/loading.ts index 5fd020c..9dce4bd 100644 --- a/web/src/utils/loading.ts +++ b/web/src/utils/loading.ts @@ -1,5 +1,7 @@ import { nextTick } from 'vue'; import '/@/theme/loading.scss'; +import { showUpgrade } from "/@/utils/upgrade"; + /** * 页面全局 Loading @@ -9,6 +11,8 @@ import '/@/theme/loading.scss'; export const NextLoading = { // 创建 loading start: () => { + // 显示升级提示 + showUpgrade() const bodys: Element = document.body; const div = document.createElement('div'); div.setAttribute('class', 'loading-next'); diff --git a/web/src/utils/service.ts b/web/src/utils/service.ts index 0dfe45a..afb30cc 100644 --- a/web/src/utils/service.ts +++ b/web/src/utils/service.ts @@ -10,6 +10,7 @@ import { errorLog, errorCreate } from './tools.ts'; import { Local, Session } from '/@/utils/storage'; import qs from 'qs'; import { getBaseURL } from './baseUrl'; +import { successMessage } from './message.js'; /** * @description 创建请求实例 */ @@ -82,7 +83,7 @@ function createService() { ElMessageBox.alert(dataAxios.msg, '提示', { confirmButtonText: 'OK', callback: (action: Action) => { - window.location.reload(); + // window.location.reload(); }, }); errorCreate(`${dataAxios.msg}: ${response.config.url}`); @@ -204,6 +205,8 @@ export const requestForMock = createRequestFunction(serviceForMock); * @param filename */ export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) { + // return request({ url: url, method: method, params: params }) + // .then((res: any) => successMessage(res.msg)); request({ url: url, method: method, @@ -211,6 +214,8 @@ export const downloadFile = function ({ url, params, method, filename = '文件 responseType: 'blob' // headers: {Accept: 'application/vnd.openxmlformats-officedocument'} }).then((res: any) => { + // console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载 + if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载'); const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1]) const fileName = xlsxName || `${filename}.xlsx` if (res) { diff --git a/web/src/utils/upgrade.ts b/web/src/utils/upgrade.ts new file mode 100644 index 0000000..f21c778 --- /dev/null +++ b/web/src/utils/upgrade.ts @@ -0,0 +1,55 @@ +import axios from "axios"; +import * as process from "process"; +import {Local, Session} from '/@/utils/storage'; +import {ElNotification} from "element-plus"; +import fs from "fs"; + +// 是否显示升级提示信息框 +const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade'; +const VERSION_KEY = 'DVADMIN3_VERSION' +const VERSION_FILE_NAME = 'version-build' + +export function showUpgrade () { + const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false + if (isShowUpgrade) { + Session.remove(IS_SHOW_UPGRADE_SESSION_KEY) + ElNotification({ + title: '新版本升级', + message: "检测到系统新版本,正在更新中!不用担心,更新很快的哦!", + type: 'success', + duration: 5000, + }); + } +} + +// 生产环境前端版本校验, +export async function checkVersion(){ + if (process.env.NODE_ENV === 'development') { + // 开发环境无需校验前端版本 + return + } + // 获取线上版本号 t为时间戳,防止缓存 + await axios.get(`${import.meta.env.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then(res => { + const {status, data} = res || {} + if (status === 200) { + // 获取当前版本号 + const localVersion = Local.get(VERSION_KEY) + // 将当前版本号持久缓存至本地 + Local.set(VERSION_KEY, data) + // 当用户本地存在版本号并且和线上版本号不一致时,进行页面刷新操作 + if (localVersion && localVersion !== data) { + // 本地缓存版本号和线上版本号不一致,弹出升级提示框 + // 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框 + Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true) + window.location.reload() + + } + } + }) +} + +export function generateVersionFile (){ + // 生成版本文件到public目录下version文件中 + const version = `${process.env.npm_package_version}.${new Date().getTime()}`; + fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version); +} diff --git a/web/src/views/plugins/dvadmin_form_flow/install-render.js b/web/src/views/plugins/dvadmin_form_flow/install-render.js deleted file mode 100644 index bd79ece..0000000 --- a/web/src/views/plugins/dvadmin_form_flow/install-render.js +++ /dev/null @@ -1,46 +0,0 @@ -import axios from 'axios' - -import VFormRender from '@/components/form-render/index.vue' -import ContainerItems from '@/components/form-render/container-item/index' - -import {registerIcon} from '@/utils/el-icons' -import 'virtual:svg-icons-register' -import '@/iconfont/iconfont.css' - -import { installI18n } from '@/utils/i18n' -import { loadExtension } from '@/extension/extension-loader' - -VFormRender.install = function (app) { - installI18n(app) - loadExtension(app) - - app.use(ContainerItems) - registerIcon(app) - app.component(VFormRender.name, VFormRender) -} - -const components = [ - VFormRender -] - -const install = (app) => { - installI18n(app) - loadExtension(app) - - app.use(ContainerItems) - registerIcon(app) - components.forEach(component => { - app.component(component.name, component) - }) - - window.axios = axios -} - -if (typeof window !== 'undefined' && window.Vue) { /* scriptʽʱֵaxios */ - //window.axios = axios -} - -export default { - install, - VFormRender -} diff --git a/web/src/views/plugins/dvadmin_form_flow/install.js b/web/src/views/plugins/dvadmin_form_flow/install.js deleted file mode 100644 index 02154d4..0000000 --- a/web/src/views/plugins/dvadmin_form_flow/install.js +++ /dev/null @@ -1,73 +0,0 @@ -import axios from 'axios' - -import VFormDesigner from '@/components/form-designer/index.vue' -import VFormRender from '@/components/form-render/index.vue' - -import Draggable from '@/../lib/vuedraggable/dist/vuedraggable.umd.js' -import {registerIcon} from '@/utils/el-icons' -import 'virtual:svg-icons-register' -import '@/iconfont/iconfont.css' - -import ContainerWidgets from '@/components/form-designer/form-widget/container-widget/index' -import ContainerItems from '@/components/form-render/container-item/index' - -import { addDirective } from '@/utils/directive' -import { installI18n } from '@/utils/i18n' -import { loadExtension } from '@/extension/extension-loader' - - -VFormDesigner.install = function (app) { - addDirective(app) - installI18n(app) - loadExtension(app) - - app.use(ContainerWidgets) - app.use(ContainerItems) - - registerIcon(app) - app.component('draggable', Draggable) - app.component(VFormDesigner.name, VFormDesigner) -} - -VFormRender.install = function (app) { - installI18n(app) - loadExtension(app) - - app.use(ContainerItems) - - registerIcon(app) - app.component(VFormRender.name, VFormRender) -} - -const components = [ - VFormDesigner, - VFormRender -] - -const install = (app) => { - addDirective(app) - installI18n(app) - loadExtension(app) - - app.use(ContainerWidgets) - app.use(ContainerItems) - - registerIcon(app) - app.component('draggable', Draggable) - - components.forEach(component => { - app.component(component.name, component) - }) - - window.axios = axios -} - -if (typeof window !== 'undefined' && window.Vue) { /* scriptʽʱֵaxios */ - //window.axios = axios -} - -export default { - install, - VFormDesigner, - VFormRender -} diff --git a/web/src/views/plugins/dvadmin_form_flow/src/index.ts b/web/src/views/plugins/dvadmin_form_flow/src/index.ts deleted file mode 100644 index c57cc05..0000000 --- a/web/src/views/plugins/dvadmin_form_flow/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -// -import DVAFormDesigner from './components/DVAFormDesigner.vue' - -// 浽һ -const components = [ - DVAFormDesigner -] - -// install -const install = function (Vue) { - - if (install.installed) return - install.installed = true - // бעȫ - components.map(component => { - Vue.component(component.name, component) //component.name ˴ʹõvueļе name - }) -} - -if (typeof window !== 'undefined' && window.Vue) { - install(window.Vue) -} - -export default { - // Ķ߱һ install - install, - // б - ...components -} diff --git a/web/src/views/system/areas/api.ts b/web/src/views/system/areas/api.ts index bad818b..f256058 100644 --- a/web/src/views/system/areas/api.ts +++ b/web/src/views/system/areas/api.ts @@ -39,3 +39,9 @@ export function DelObj(id: DelReq) { data: { id }, }); } +export function GetPermission() { + return request({ + url: apiPrefix + 'field_permission/', + method: 'get', + }); +} diff --git a/web/src/views/system/areas/crud.tsx b/web/src/views/system/areas/crud.tsx index 01d1860..1a1e47b 100644 --- a/web/src/views/system/areas/crud.tsx +++ b/web/src/views/system/areas/crud.tsx @@ -1,244 +1,202 @@ import * as api from './api'; -import { - dict, - UserPageQuery, - AddReq, - DelReq, - EditReq, - compute, - CreateCrudOptionsProps, - CreateCrudOptionsRet -} from '@fast-crud/fast-crud'; -import {dictionary} from '/@/utils/dictionary'; -import {successMessage} from '/@/utils/message'; -import {auth} from "/@/utils/authFunction"; +import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'; +import { dictionary } from '/@/utils/dictionary'; +import { successMessage } from '/@/utils/message'; +import { auth } from '/@/utils/authFunction'; +import tableSelector from '/@/components/tableSelector/index.vue'; +import { shallowRef } from 'vue'; -export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps): CreateCrudOptionsRet { - const pageRequest = async (query: UserPageQuery) => { - return await api.GetList(query); - }; - const editRequest = async ({form, row}: EditReq) => { - form.id = row.id; - return await api.UpdateObj(form); - }; - const delRequest = async ({row}: DelReq) => { - return await api.DelObj(row.id); - }; - const addRequest = async ({form}: AddReq) => { - return await api.AddObj(form); - }; +export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; - /** - * 懒加载 - * @param row - * @returns {Promise} - */ - const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => { - pageRequest({pcode: tree.code}).then((res: APIResponseData) => { - resolve(res.data); - }); - }; + /** + * 懒加载 + * @param row + * @returns {Promise} + */ + const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => { + pageRequest({ pcode: tree.code }).then((res: APIResponseData) => { + resolve(res.data); + }); + }; - return { - crudOptions: { - request: { - pageRequest, - addRequest, - editRequest, - delRequest, - }, - actionbar: { - buttons: { - add: { - show: auth('area:Create'), - } - } - }, - rowHandle: { - //固定右侧 - fixed: 'right', - width: 200, - buttons: { - view: { - show: false, - }, - edit: { - iconRight: 'Edit', - type: 'text', - show: auth('area:Update') - }, - remove: { - iconRight: 'Delete', - type: 'text', - show: auth('area:Delete') - }, - }, - }, - pagination: { - show: false, - }, - table: { - rowKey: 'id', - lazy: true, - load: loadContentMethod, - treeProps: {children: 'children', hasChildren: 'hasChild'}, - }, - columns: { - _index: { - title: '序号', - form: {show: false}, - column: { - type: 'index', - align: 'center', - width: '70px', - columnSetDisabled: true, //禁止在列设置中选择 - }, - }, - // pcode: { - // title: '父级地区', - // show: false, - // search: { - // show: true, - // }, - // type: 'dict-tree', - // form: { - // component: { - // showAllLevels: false, // 仅显示最后一级 - // props: { - // elProps: { - // clearable: true, - // showAllLevels: false, // 仅显示最后一级 - // props: { - // checkStrictly: true, // 可以不需要选到最后一级 - // emitPath: false, - // clearable: true, - // }, - // }, - // }, - // }, - // }, - // }, - name: { - title: '名称', - search: { - show: true, - }, - treeNode: true, - type: 'input', - column: { - minWidth: 120, - }, - form: { - rules: [ - // 表单校验规则 - {required: true, message: '名称必填项'}, - ], - component: { - placeholder: '请输入名称', - }, - }, - }, - code: { - title: '地区编码', - search: { - show: true, - }, - type: 'input', - column: { - minWidth: 90, - }, - form: { - rules: [ - // 表单校验规则 - {required: true, message: '地区编码必填项'}, - ], - component: { - placeholder: '请输入地区编码', - }, - }, - }, - pinyin: { - title: '拼音', - search: { - disabled: true, - }, - type: 'input', - column: { - minWidth: 120, - }, - form: { - rules: [ - // 表单校验规则 - {required: true, message: '拼音必填项'}, - ], - component: { - placeholder: '请输入拼音', - }, - }, - }, - level: { - title: '地区层级', - search: { - disabled: true, - }, - type: 'input', - column: { - minWidth: 100, - }, - form: { - disabled: false, - rules: [ - // 表单校验规则 - {required: true, message: '拼音必填项'}, - ], - component: { - placeholder: '请输入拼音', - }, - }, - }, - initials: { - title: '首字母', - column: { - minWidth: 100, - }, - form: { - rules: [ - // 表单校验规则 - {required: true, message: '首字母必填项'}, - ], - - component: { - placeholder: '请输入首字母', - }, - }, - }, - enable: { - title: '是否启用', - search: { - show: true, - }, - type: 'dict-radio', - column: { - minWidth: 90, - component: { - name: 'fs-dict-switch', - activeText: '', - inactiveText: '', - style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6', - onChange: compute((context) => { - return () => { - api.UpdateObj(context.row).then((res: APIResponseData) => { - successMessage(res.msg as string); - }); - }; - }), - }, - }, - dict: dict({ - data: dictionary('button_status_bool'), - }), - }, - }, - }, - }; + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + actionbar: { + buttons: { + add: { + show: auth('area:Create'), + }, + }, + }, + rowHandle: { + //固定右侧 + fixed: 'right', + width: 200, + buttons: { + view: { + show: false, + }, + edit: { + iconRight: 'Edit', + type: 'text', + show: auth('area:Update'), + }, + remove: { + iconRight: 'Delete', + type: 'text', + show: auth('area:Delete'), + }, + }, + }, + pagination: { + show: false, + }, + table: { + rowKey: 'id', + lazy: true, + load: loadContentMethod, + treeProps: { children: 'children', hasChildren: 'hasChild' }, + }, + columns: { + _index: { + title: '序号', + form: { show: false }, + column: { + type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + }, + }, + name: { + title: '名称', + search: { + show: true, + }, + treeNode: true, + type: 'input', + column: { + minWidth: 120, + }, + form: { + rules: [ + // 表单校验规则 + { required: true, message: '名称必填项' }, + ], + component: { + placeholder: '请输入名称', + }, + }, + }, + pcode: { + title: '父级地区', + search: { + disabled: true, + }, + width: 130, + type: 'table-selector', + form: { + component: { + name: shallowRef(tableSelector), + vModel: 'modelValue', + displayLabel: compute(({ row }) => { + if (row) { + return row.pcode_info; + } + return null; + }), + tableConfig: { + url: '/api/system/area/', + label: 'name', + value: 'id', + isTree: true, + isMultiple: false, + lazy: true, + load: loadContentMethod, + treeProps: { children: 'children', hasChildren: 'hasChild' }, + columns: [ + { + prop: 'name', + label: '地区', + width: 150, + }, + { + prop: 'code', + label: '地区编码', + }, + ], + }, + }, + }, + column: { + show: false, + }, + }, + code: { + title: '地区编码', + search: { + show: true, + }, + type: 'input', + column: { + minWidth: 90, + }, + form: { + rules: [ + // 表单校验规则 + { required: true, message: '地区编码必填项' }, + ], + component: { + placeholder: '请输入地区编码', + }, + }, + }, + enable: { + title: '是否启用', + search: { + show: true, + }, + type: 'dict-radio', + column: { + minWidth: 90, + component: { + name: 'fs-dict-switch', + activeText: '', + inactiveText: '', + style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6', + onChange: compute((context) => { + return () => { + api.UpdateObj(context.row).then((res: APIResponseData) => { + successMessage(res.msg as string); + }); + }; + }), + }, + }, + dict: dict({ + data: dictionary('button_status_bool'), + }), + }, + }, + }, + }; }; diff --git a/web/src/views/system/areas/index.vue b/web/src/views/system/areas/index.vue index 51c6c77..8201f7e 100644 --- a/web/src/views/system/areas/index.vue +++ b/web/src/views/system/areas/index.vue @@ -5,14 +5,21 @@ diff --git a/web/src/views/system/dept/components/DeptUserCom/api.ts b/web/src/views/system/dept/components/DeptUserCom/api.ts index 28602c7..b395c39 100644 --- a/web/src/views/system/dept/components/DeptUserCom/api.ts +++ b/web/src/views/system/dept/components/DeptUserCom/api.ts @@ -5,13 +5,13 @@ type GetListType = PageQuery & { show_all: string }; export const apiPrefix = '/api/system/user/'; -export function GetDept(query: PageQuery) { - return request({ - url: '/api/system/dept/dept_lazy_tree/', - method: 'get', - params: query, - }); -} +// export function GetDept(query: PageQuery) { +// return request({ +// url: '/api/system/dept/dept_all/', +// method: 'get', +// params: query, +// }); +// } export function GetList(query: GetListType) { return request({ diff --git a/web/src/views/system/dept/components/DeptUserCom/crud.tsx b/web/src/views/system/dept/components/DeptUserCom/crud.tsx index 8bb2b3c..bcee5b0 100644 --- a/web/src/views/system/dept/components/DeptUserCom/crud.tsx +++ b/web/src/views/system/dept/components/DeptUserCom/crud.tsx @@ -220,7 +220,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp label: 'name', }), column: { - minWidth: 150, //最小列宽 + minWidth: 200, //最小列宽 + formatter({value,row,index}){ + return row.dept_name_all + } }, form: { rules: [ @@ -259,7 +262,11 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp label: 'name', }), column: { - minWidth: 100, //最小列宽 + minWidth: 200, //最小列宽 + formatter({value,row,index}){ + const values = row.role_info.map((item:any) => item.name); + return values.join(',') + } }, form: { rules: [ @@ -382,6 +389,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp form: { show: false, }, + column:{ + width:150, + showOverflowTooltip: true, + } }, }, }, diff --git a/web/src/views/system/dept/components/DeptUserCom/index.vue b/web/src/views/system/dept/components/DeptUserCom/index.vue index fb50ff9..f52db55 100644 --- a/web/src/views/system/dept/components/DeptUserCom/index.vue +++ b/web/src/views/system/dept/components/DeptUserCom/index.vue @@ -277,7 +277,8 @@ const { resetCrudOptions } = useCrud({ padding: 0 10px; border-radius: 8px 0 0 8px; box-sizing: border-box; - background-color: #fff; + color: var(--next-bg-topBarColor); + background-color: var(--el-fill-color-blank);; } .dept-user-com-table { height: calc(100% - 200px); diff --git a/web/src/views/system/dept/index.vue b/web/src/views/system/dept/index.vue index 60b803b..52df92b 100644 --- a/web/src/views/system/dept/index.vue +++ b/web/src/views/system/dept/index.vue @@ -133,7 +133,7 @@ onMounted(() => { } .dept-left { - background-color: #fff; + background-color: var(--el-fill-color-blank);; border-radius: 0 8px 8px 0; padding: 10px; } diff --git a/web/src/views/system/downloadCenter/api.ts b/web/src/views/system/downloadCenter/api.ts new file mode 100644 index 0000000..4baa884 --- /dev/null +++ b/web/src/views/system/downloadCenter/api.ts @@ -0,0 +1,49 @@ +import { request } from '/@/utils/service'; +import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; + +export const apiPrefix = '/api/system/download_center/'; + +export function GetPermission() { + return request({ + url: apiPrefix + 'field_permission/', + method: 'get', + }); +} + +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 }, + }); +} diff --git a/web/src/views/system/downloadCenter/crud.tsx b/web/src/views/system/downloadCenter/crud.tsx new file mode 100644 index 0000000..79639c0 --- /dev/null +++ b/web/src/views/system/downloadCenter/crud.tsx @@ -0,0 +1,160 @@ +import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud'; +import * as api from './api'; +import { dictionary } from '/@/utils/dictionary'; +import { successMessage } from '../../../utils/message'; +import { auth } from '/@/utils/authFunction' + +interface CreateCrudOptionsTypes { + output: any; + crudOptions: CrudOptions; +} + +//此处为crudOptions配置 +export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose; }): 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: false + } + } + }, + toolbar: { + buttons: { + export: { + show: false + } + } + }, + rowHandle: { + //固定右侧 + fixed: 'right', + width: 120, + buttons: { + view: { + show: false + }, + edit: { + show: false + }, + remove: { + show: false + }, + download: { + show: compute(ctx => ctx.row.task_status === 2), + text: '下载文件', + type: 'warning', + click: (ctx) => window.open(ctx.row.url, '_blank') + } + }, + }, + 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, //禁止在列设置中选择 + }, + }, + task_name: { + title: '任务名', + type: 'text', + column: { + minWidth: 160, + align: 'left' + }, + search: { + show: true + } + }, + file_name: { + title: '文件名', + type: 'text', + column: { + minWidth: 160, + align: 'left' + }, + search: { + show: true + } + }, + size: { + title: '文件大小(b)', + type: 'number', + column: { + width: 100 + } + }, + task_status: { + title: '任务状态', + type: 'dict-select', + dict: dict({ + data: [ + { label: '任务已创建', value: 0 }, + { label: '任务进行中', value: 1 }, + { label: '任务完成', value: 2 }, + { label: '任务失败', value: 3 }, + ] + }), + column: { + width: 120 + }, + search: { + show: true + } + }, + create_datetime: { + title: '创建时间', + column: { + width: 160 + } + }, + update_datetime: { + title: '创建时间', + column: { + width: 160 + } + } + }, + }, + }; +}; diff --git a/web/src/views/system/downloadCenter/index.vue b/web/src/views/system/downloadCenter/index.vue new file mode 100644 index 0000000..2ea8a54 --- /dev/null +++ b/web/src/views/system/downloadCenter/index.vue @@ -0,0 +1,42 @@ + + + diff --git a/web/src/views/system/log/loginLog/api.ts b/web/src/views/system/log/loginLog/api.ts index defefc4..273aa90 100644 --- a/web/src/views/system/log/loginLog/api.ts +++ b/web/src/views/system/log/loginLog/api.ts @@ -39,3 +39,10 @@ export function DelObj(id: DelReq) { data: { id }, }); } + +export function GetPermission() { + return request({ + url: apiPrefix + 'field_permission/', + method: 'get', + }); +} diff --git a/web/src/views/system/log/loginLog/index.vue b/web/src/views/system/log/loginLog/index.vue index 618e6cf..924429b 100644 --- a/web/src/views/system/log/loginLog/index.vue +++ b/web/src/views/system/log/loginLog/index.vue @@ -5,14 +5,21 @@ diff --git a/web/src/views/system/menu/components/MenuButtonCom/api.ts b/web/src/views/system/menu/components/MenuButtonCom/api.ts index d91f920..612c49d 100644 --- a/web/src/views/system/menu/components/MenuButtonCom/api.ts +++ b/web/src/views/system/menu/components/MenuButtonCom/api.ts @@ -48,3 +48,10 @@ export function BatchAdd(obj: AddReq) { }); } +export function BatchDelete(keys: any) { + return request({ + url: apiPrefix + 'multiple_delete/', + method: 'delete', + data: { keys }, + }); +} diff --git a/web/src/views/system/menu/components/MenuButtonCom/crud.tsx b/web/src/views/system/menu/components/MenuButtonCom/crud.tsx index 908cf2a..79675fb 100644 --- a/web/src/views/system/menu/components/MenuButtonCom/crud.tsx +++ b/web/src/views/system/menu/components/MenuButtonCom/crud.tsx @@ -4,6 +4,8 @@ import {auth} from '/@/utils/authFunction' import {request} from '/@/utils/service'; import { successNotification } from '/@/utils/message'; import { ElMessage } from 'element-plus'; +import { nextTick, ref } from 'vue'; +import XEUtils from 'xe-utils'; //此处为crudOptions配置 export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet { const pageRequest = async () => { @@ -22,7 +24,42 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti const addRequest = async ({form}: AddReq) => { return await api.AddObj({...form, ...{menu: context!.selectOptions.value.id}}); }; + // 记录选中的行 + 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: { pagination:{ show:false @@ -84,6 +121,11 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti editRequest, delRequest, }, + table: { + rowKey: 'id', //设置你的主键id, 默认rowKey=id + onSelectionChange, + onRefreshed: () => toggleRowSelection(), + }, form: { col: {span: 24}, labelWidth: '100px', @@ -93,6 +135,16 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti }, }, columns: { + $checked: { + title: '选择', + form: { show: false }, + column: { + type: 'selection', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + }, + }, _index: { title: '序号', form: {show: false}, diff --git a/web/src/views/system/menu/components/MenuButtonCom/index.vue b/web/src/views/system/menu/components/MenuButtonCom/index.vue index 9dddaf9..ec55e35 100644 --- a/web/src/views/system/menu/components/MenuButtonCom/index.vue +++ b/web/src/views/system/menu/components/MenuButtonCom/index.vue @@ -1,19 +1,72 @@ diff --git a/web/src/views/system/menu/components/MenuFormCom/index.vue b/web/src/views/system/menu/components/MenuFormCom/index.vue index 5fa4aba..d958a62 100644 --- a/web/src/views/system/menu/components/MenuFormCom/index.vue +++ b/web/src/views/system/menu/components/MenuFormCom/index.vue @@ -10,21 +10,12 @@ - + - + @@ -35,12 +26,14 @@ - + - + @@ -48,46 +41,45 @@ - + - + + + + + + + + + + + - - - - - - - - - - - +
- + - + @@ -96,7 +88,8 @@ - +
@@ -111,6 +104,7 @@ diff --git a/web/src/views/system/personal/index.vue b/web/src/views/system/personal/index.vue index 05b5d12..6c7d6b4 100644 --- a/web/src/views/system/personal/index.vue +++ b/web/src/views/system/personal/index.vue @@ -153,7 +153,7 @@ center > - + diff --git a/web/src/views/system/role/components/PermissionComNew/api.ts b/web/src/views/system/role/components/PermissionComNew/api.ts index be37ef5..0243ab6 100644 --- a/web/src/views/system/role/components/PermissionComNew/api.ts +++ b/web/src/views/system/role/components/PermissionComNew/api.ts @@ -1,15 +1,17 @@ import { request } from "/@/utils/service"; - +import XEUtils from "xe-utils"; /** * 获取角色的授权列表 * @param roleId * @param query */ -export function getRolePremission(query:object) { +export function getRolePermission(query:object) { return request({ - url: '/api/system/role_menu_button_permission/get_role_premission/', + url: '/api/system/role_menu_button_permission/get_role_permission/', method: 'get', params:query + }).then((res:any)=>{ + return XEUtils.toArrayTree(res.data, {key: 'id', parentKey: 'parent',children: 'children',strict: false}) }) } @@ -26,16 +28,25 @@ export function setRolePremission(roleId:any,data:object) { }) } -export function getDataPermissionRange() { +export function getDataPermissionRange(query:object) { + return request({ + url: '/api/system/role_menu_button_permission/data_scope/', + method: 'get', + params:query + }) +} + +export function getDataPermissionRangeAll() { return request({ url: '/api/system/role_menu_button_permission/data_scope/', method: 'get', }) } -export function getDataPermissionDept() { +export function getDataPermissionDept(query:object) { return request({ url: '/api/system/role_menu_button_permission/role_to_dept_all/', - method: 'get' + method: 'get', + params:query }) } diff --git a/web/src/views/system/role/components/PermissionComNew/index.vue b/web/src/views/system/role/components/PermissionComNew/index.vue index 058f5fc..b1da73f 100644 --- a/web/src/views/system/role/components/PermissionComNew/index.vue +++ b/web/src/views/system/role/components/PermissionComNew/index.vue @@ -1,392 +1,451 @@ diff --git a/web/src/views/system/role/components/PermissionComNew/types.ts b/web/src/views/system/role/components/PermissionComNew/types.ts index 6426810..5afc5c0 100644 --- a/web/src/views/system/role/components/PermissionComNew/types.ts +++ b/web/src/views/system/role/components/PermissionComNew/types.ts @@ -1,36 +1,29 @@ export interface DataPermissionRangeType { - label: string; - value: number; + label: string; + value: number; } export interface CustomDataPermissionDeptType { - id: number; - name: string; - patent: number; - children: 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 MenusType{ - id: string; - name: string; - isCheck: boolean; - radio: string; - btns: { id:number,name: string; value: string; isCheck: boolean; data_range: number; dept:object; name:string }[]; - columns: { [key: string]: boolean | string; }[] + 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; - menus:MenusType[]; + 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/user/api.ts b/web/src/views/system/user/api.ts index 78a15ec..02a2ac6 100644 --- a/web/src/views/system/user/api.ts +++ b/web/src/views/system/user/api.ts @@ -5,7 +5,7 @@ export const apiPrefix = '/api/system/user/'; export function GetDept(query: PageQuery) { return request({ - url: "/api/system/dept/dept_lazy_tree/", + url: "/api/system/dept/all_dept/", method: 'get', params: query, }); diff --git a/web/src/views/system/user/crud.tsx b/web/src/views/system/user/crud.tsx index 7281525..dd24c40 100644 --- a/web/src/views/system/user/crud.tsx +++ b/web/src/views/system/user/crud.tsx @@ -218,7 +218,10 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) label: 'name' }), column: { - minWidth: 150, //最小列宽 + minWidth: 200, //最小列宽 + formatter({value,row,index}){ + return row.dept_name_all + } }, form: { rules: [ @@ -253,7 +256,11 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) label: 'name', }), column: { - minWidth: 100, //最小列宽 + minWidth: 200, //最小列宽 + formatter({value,row,index}){ + const values = row.role_info.map((item:any) => item.name); + return values.join(',') + } }, form: { rules: [ diff --git a/web/src/views/system/user/index.vue b/web/src/views/system/user/index.vue index 4ea5033..71085d1 100644 --- a/web/src/views/system/user/index.vue +++ b/web/src/views/system/user/index.vue @@ -98,7 +98,6 @@ const getData = () => { const result = XEUtils.toArrayTree(responseData, { parentKey: 'parent', children: 'children', - strict: true, }); data.value = result; diff --git a/web/src/views/system/whiteList/crud.tsx b/web/src/views/system/whiteList/crud.tsx index 7104cda..33059c9 100644 --- a/web/src/views/system/whiteList/crud.tsx +++ b/web/src/views/system/whiteList/crud.tsx @@ -200,11 +200,9 @@ export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps) component: { span: 24, props: { - elProps: { - allowCreate: true, - filterable: true, - clearable: true, - }, + allowCreate: true, + filterable: true, + clearable: true, }, }, itemProps: { diff --git a/web/vite.config.ts b/web/vite.config.ts index c86cfc4..ed6dec2 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -3,6 +3,7 @@ import { resolve } from 'path'; import { defineConfig, loadEnv, ConfigEnv } from 'vite'; import vueSetupExtend from 'vite-plugin-vue-setup-extend'; import vueJsx from '@vitejs/plugin-vue-jsx' +import { generateVersionFile } from "/@/utils/upgrade"; const pathResolve = (dir: string) => { return resolve(__dirname, '.', dir); @@ -17,6 +18,8 @@ const alias: Record = { const viteConfig = defineConfig((mode: ConfigEnv) => { const env = loadEnv(mode.mode, process.cwd()); + // 当Vite构建时,生成版本文件 + generateVersionFile() return { plugins: [vue(), vueJsx(), vueSetupExtend()], root: process.cwd(),