diff --git a/README.zh.md b/README.zh.md index dea0106..07615aa 100644 --- a/README.zh.md +++ b/README.zh.md @@ -88,7 +88,19 @@ github地址:[https://github.com/huge-dream/django-vue3-admin](https://github. 13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。 ## 插件市场 🔌 -更新中... +1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html) + +2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html) + +3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html) + +4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html) + +5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git) + +6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线) + + ## 仓库分支说明 💈 主分支:master(稳定版本) @@ -114,7 +126,7 @@ cd web # 安装依赖 npm install yarn -yarn install --registry=https://registry.npm.taobao.org +yarn install --registry=https://registry.npmmirror.com # 启动服务 yarn build @@ -210,5 +222,19 @@ docker-compose up -d --build ![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png) +## 审批流插件 + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/97fbbf29673edfd66a1edd49237791bb.png) + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/c43aa51278cbc478287c718d22397479.png) + + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/9732a5cca9c1166d1a65c35e313ab90d.png) + + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/3ca9dd0801ce76d21435abcc8a3d505a.png) + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/a87a8d2329ef66880af5b0f16c5ff823.png) + diff --git a/backend/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..c719c46 100644 --- a/backend/application/celery.py +++ b/backend/application/celery.py @@ -1,6 +1,8 @@ import functools import os +from celery.signals import task_postrun + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') from django.conf import settings @@ -15,7 +17,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 @@ -38,3 +40,12 @@ def retry_base_task_error(): return wrapper return wraps + + +@task_postrun.connect +def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras): + periodic_task_name = kwargs.get('periodic_task_name') + if periodic_task_name: + from django_celery_results.models import TaskResult + # 更新 TaskResult 表中的 periodic_task_name 字段 + TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name) diff --git a/backend/application/dispatch.py b/backend/application/dispatch.py index 3bd364c..101b3ab 100644 --- a/backend/application/dispatch.py +++ b/backend/application/dispatch.py @@ -2,6 +2,10 @@ # -*- coding: utf-8 -*- from django.conf import settings from django.db import connection +from django.core.cache import cache +from dvadmin.utils.validator import CustomValidationError + +dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis def is_tenants_mode(): @@ -68,6 +72,9 @@ def init_dictionary(): :return: """ try: + if dispatch_db_type == 'redis': + cache.set(f"init_dictionary", _get_all_dictionary()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -88,7 +95,9 @@ def init_system_config(): :return: """ try: - + if dispatch_db_type == 'redis': + cache.set(f"init_system_config", _get_all_system_config()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -107,6 +116,9 @@ def refresh_dictionary(): 刷新字典配置 :return: """ + if dispatch_db_type == 'redis': + cache.set(f"init_dictionary", _get_all_dictionary()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -122,6 +134,9 @@ def refresh_system_config(): 刷新系统配置 :return: """ + if dispatch_db_type == 'redis': + cache.set(f"init_system_config", _get_all_system_config()) + return if is_tenants_mode(): from django_tenants.utils import tenant_context, get_tenant_model @@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + init_dictionary_data = cache.get(f"init_dictionary") + if not init_dictionary_data: + refresh_dictionary() + return cache.get(f"init_dictionary") or {} if not settings.DICTIONARY_CONFIG: refresh_dictionary() if is_tenants_mode(): @@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + dictionary_config = cache.get(f"init_dictionary") + if not dictionary_config: + refresh_dictionary() + dictionary_config = cache.get(f"init_dictionary") + return dictionary_config.get(key) dictionary_config = get_dictionary_config(schema_name) return dictionary_config.get(key) @@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ - children = get_dictionary_values(key, schema_name) or [] - for ele in children: + res = get_dictionary_values(key, schema_name) or [] + for ele in res.get('children'): if ele.get("value") == str(name): return ele.get("label") return "" @@ -187,6 +213,11 @@ def get_system_config(schema_name=None): :param schema_name: 对应字典配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + init_dictionary_data = cache.get(f"init_system_config") + if not init_dictionary_data: + refresh_system_config() + return cache.get(f"init_system_config") or {} if not settings.SYSTEM_CONFIG: refresh_system_config() if is_tenants_mode(): @@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None): :param schema_name: 对应系统配置的租户schema_name值 :return: """ + if dispatch_db_type == 'redis': + system_config = cache.get(f"init_system_config") + if not system_config: + refresh_system_config() + system_config = cache.get(f"init_system_config") + return system_config.get(key) system_config = get_system_config(schema_name) return system_config.get(key) +def get_system_config_values_to_dict(key, schema_name=None): + """ + 获取系统配置数据并转换为字典 **仅限于数组类型系统配置 + :param key: 对应系统配置的key值(字典编号) + :param schema_name: 对应系统配置的租户schema_name值 + :return: + """ + values_dict = {} + config_values = get_system_config_values(key, schema_name) + if not isinstance(config_values, list): + raise CustomValidationError("该方式仅限于数组类型系统配置") + for ele in get_system_config_values(key, schema_name): + values_dict[ele.get('key')] = ele.get('value') + return values_dict + + def get_system_config_label(key, name, schema_name=None): """ 获取获取系统配置label值 diff --git a/backend/application/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/application/urls.py b/backend/application/urls.py index cb5a899..641b85c 100644 --- a/backend/application/urls.py +++ b/backend/application/urls.py @@ -50,7 +50,7 @@ schema_view = get_schema_view( license=openapi.License(name="BSD License"), ), public=True, - permission_classes=(permissions.AllowAny,), + permission_classes=(permissions.IsAuthenticated,), generator_class=CustomOpenAPISchemaGenerator, ) # 前端页面映射 diff --git a/backend/dvadmin/system/fixtures/initSerializer.py b/backend/dvadmin/system/fixtures/initSerializer.py index 9ed094f..f983aea 100644 --- a/backend/dvadmin/system/fixtures/initSerializer.py +++ b/backend/dvadmin/system/fixtures/initSerializer.py @@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer): """ 初始化获取数信息(用于生成初始化json文件) """ + role_key = serializers.SerializerMethodField() + dept_key = serializers.SerializerMethodField() + + def get_dept_key(self, obj): + if obj.dept: + return obj.dept.key + else: + return None + + def get_role_key(self, obj): + if obj.role.all(): + return [role.key for role in obj.role.all()] + else: + return [] def save(self, **kwargs): instance = super().save(**kwargs) @@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer): model = Users fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id', - 'password', 'last_login', 'is_superuser'] + 'password', 'last_login', 'is_superuser', 'role_key' ,'dept_key'] read_only_fields = ['id'] extra_kwargs = { 'creator': {'write_only': True}, @@ -175,15 +189,21 @@ class RoleMenuInitSerializer(CustomModelSerializer): """ 初始化角色菜单(用于生成初始化json文件) """ - role__key = serializers.CharField(max_length=100, required=True) - menu__web_path = serializers.CharField(max_length=100, required=True) - menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True) + role__key = serializers.CharField(source='role.key') + menu__web_path = serializers.CharField(source='menu.web_path') + menu__component_name = serializers.CharField(source='menu.component_name', allow_blank=True) + + def update(self, instance, validated_data): + init_data = self.initial_data + role_id = Role.objects.filter(key=init_data['role__key']).first() + menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first() + validated_data['role'] = role_id + validated_data['menu'] = menu_id + return super().update(instance, validated_data) + def create(self, validated_data): init_data = self.initial_data - validated_data.pop('menu__web_path') - validated_data.pop('menu__component_name') - validated_data.pop('role__key') role_id = Role.objects.filter(key=init_data['role__key']).first() menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first() validated_data['role'] = role_id @@ -192,7 +212,7 @@ class RoleMenuInitSerializer(CustomModelSerializer): class Meta: model = RoleMenuPermission - fields = ['role__key', 'menu__web_path', 'menu__component_name', 'creator', 'dept_belong_id'] + fields = ['role__key', 'menu__web_path', 'menu__component_name','creator', 'dept_belong_id'] read_only_fields = ["id"] extra_kwargs = { 'role': {'required': False}, @@ -206,14 +226,22 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer): """ 初始化角色菜单按钮(用于生成初始化json文件) """ - role__key = serializers.CharField(max_length=100, required=True) - menu_button__value = serializers.CharField(max_length=100, required=True) + role__key = serializers.CharField(source='role.key') + menu_button__value = serializers.CharField(source='menu_button.value') data_range = serializers.CharField(max_length=100, required=False) + def update(self, instance, validated_data): + init_data = self.initial_data + role_id = Role.objects.filter(key=init_data['role__key']).first() + menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first() + validated_data['role'] = role_id + validated_data['menu_button'] = menu_button_id + instance = super().create(validated_data) + instance.dept.set([]) + return super().update(instance, validated_data) + def create(self, validated_data): init_data = self.initial_data - validated_data.pop('menu_button__value') - validated_data.pop('role__key') role_id = Role.objects.filter(key=init_data['role__key']).first() menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first() validated_data['role'] = role_id @@ -223,7 +251,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer): return instance def save(self, **kwargs): - if self.instance and self.initial_data.get('reset'): + if not self.instance or self.initial_data.get('reset'): return super().save(**kwargs) return self.instance diff --git a/backend/dvadmin/system/fixtures/init_menu.json b/backend/dvadmin/system/fixtures/init_menu.json index b683f0a..835a41e 100644 --- a/backend/dvadmin/system/fixtures/init_menu.json +++ b/backend/dvadmin/system/fixtures/init_menu.json @@ -672,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/management/commands/generate_init_json.py b/backend/dvadmin/system/management/commands/generate_init_json.py index 7d6eb08..e0de3e3 100644 --- a/backend/dvadmin/system/management/commands/generate_init_json.py +++ b/backend/dvadmin/system/management/commands/generate_init_json.py @@ -10,7 +10,7 @@ django.setup() from django.core.management.base import BaseCommand from application.settings import BASE_DIR -from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig +from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig, RoleMenuButtonPermission, RoleMenuPermission from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \ MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \ RoleMenuInitSerializer, RoleMenuButtonInitSerializer @@ -57,6 +57,12 @@ class Command(BaseCommand): def generate_system_config(self): self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True)) + def generate_role_menu(self): + self.serializer_data(RoleMenuInitSerializer, RoleMenuPermission.objects.all()) + + def generate_role_menu_button(self): + self.serializer_data(RoleMenuButtonInitSerializer, RoleMenuButtonPermission.objects.all()) + def handle(self, *args, **options): generate_name = options.get('generate_name') generate_name_dict = { @@ -67,6 +73,8 @@ class Command(BaseCommand): "api_white_list": self.generate_api_white_list, "dictionary": self.generate_dictionary, "system_config": self.generate_system_config, + "role_menu": self.generate_role_menu, + "role_menu_button": self.generate_role_menu_button, } if not generate_name: for ele in generate_name_dict.keys(): diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index e0e1841..c70f256 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 @@ -7,8 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from application import dispatch from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models - -class Role(CoreModel): +from dvadmin3_flow.base_model import FlowBaseModel +class Role(CoreModel,FlowBaseModel): name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称") key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符") sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序") @@ -71,6 +73,7 @@ class Users(CoreModel, AbstractUser): help_text="关联部门", ) login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数") + pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数") objects = CustomUserManager() def set_password(self, raw_password): @@ -119,6 +122,27 @@ class Dept(CoreModel): help_text="上级部门", ) + @classmethod + def _recursion(cls, instance, parent, result): + new_instance = getattr(instance, parent, None) + res = [] + data = getattr(instance, result, None) + if data: + res.append(data) + if new_instance: + array = cls._recursion(new_instance, parent, result) + res += array + return res + + @classmethod + def get_region_name(cls, obj): + """ + 获取某个用户的递归所有部门名称 + """ + dept_name_all = cls._recursion(obj, "parent", "name") + dept_name_all.reverse() + return "/".join(dept_name_all) + @classmethod def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None): """ @@ -405,6 +429,18 @@ class FileList(CoreModel): mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型") size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小") md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5") + UPLOAD_METHOD_CHOIDES = ( + (0, '默认上传'), + (1, '文件选择器上传'), + ) + upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式') + FILE_TYPE_CHOIDES = ( + (0, '图片'), + (1, '视频'), + (2, '音频'), + (3, '其他'), + ) + file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型') def save(self, *args, **kwargs): if not self.md5sum: # file is new @@ -595,3 +631,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 a41e048..0ee2cb8 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) @@ -36,6 +37,7 @@ system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionView 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 = [ @@ -47,7 +49,7 @@ urlpatterns = [ path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), # path('login_log//', LoginLogViewSet.as_view({'get': 'retrieve'})), - path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), + # path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), path('clause/privacy.html', PrivacyView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()), ] diff --git a/backend/dvadmin/system/views/area.py b/backend/dvadmin/system/views/area.py index 3a72728..7a19eb0 100644 --- a/backend/dvadmin/system/views/area.py +++ b/backend/dvadmin/system/views/area.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import pypinyin from django.db.models import Q from rest_framework import serializers @@ -15,6 +16,11 @@ 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() @@ -36,6 +42,18 @@ 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__' @@ -52,20 +70,28 @@ class AreaViewSet(CustomModelViewSet, FieldPermissionMixin): """ 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/download_center.py b/backend/dvadmin/system/views/download_center.py new file mode 100644 index 0000000..4fa88bb --- /dev/null +++ b/backend/dvadmin/system/views/download_center.py @@ -0,0 +1,49 @@ +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 = [] + extra_filter_class = [] + + def get_queryset(self): + if self.request.user.is_superuser: + return super().get_queryset() + return super().get_queryset().filter(creator=self.request.user) diff --git a/backend/dvadmin/system/views/file_list.py b/backend/dvadmin/system/views/file_list.py index c595699..c0fed8d 100644 --- a/backend/dvadmin/system/views/file_list.py +++ b/backend/dvadmin/system/views/file_list.py @@ -1,12 +1,15 @@ import hashlib import mimetypes +import django_filters +from django.conf import settings +from django.db import connection from rest_framework import serializers from rest_framework.decorators import action from application import dispatch from dvadmin.system.models import FileList -from dvadmin.utils.json_response import DetailResponse +from dvadmin.utils.json_response import DetailResponse, SuccessResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet @@ -15,8 +18,17 @@ class FileSerializer(CustomModelSerializer): url = serializers.SerializerMethodField(read_only=True) def get_url(self, instance): - base_url = f"{self.request.scheme}://{self.request.get_host()}/" - return base_url + (instance.file_url or (f'media/{str(instance.url)}')) + if self.request.query_params.get('prefix'): + if settings.ENVIRONMENT in ['local']: + prefix = 'http://127.0.0.1:8000' + elif settings.ENVIRONMENT in ['test']: + prefix = 'http://{host}/api'.format(host=self.request.get_host()) + else: + prefix = 'https://{host}/api'.format(host=self.request.get_host()) + if instance.file_url: + return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}" + return (f'{prefix}/media/{str(instance.url)}') + return instance.file_url or (f'media/{str(instance.url)}') class Meta: model = FileList @@ -35,6 +47,8 @@ class FileSerializer(CustomModelSerializer): validated_data['md5sum'] = md5.hexdigest() validated_data['engine'] = file_engine validated_data['mime_type'] = file.content_type + ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None) + validated_data['file_type'] = 3 if ft is None else ft if file_backup: validated_data['url'] = file if file_engine == 'oss': @@ -64,6 +78,22 @@ class FileSerializer(CustomModelSerializer): return super().create(validated_data) +class FileAllSerializer(CustomModelSerializer): + + class Meta: + model = FileList + fields = ['id', 'name'] + + +class FileFilter(django_filters.FilterSet): + name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名") + mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型") + + class Meta: + model = FileList + fields = ['name', 'mime_type', 'upload_method', 'file_type'] + + class FileViewSet(CustomModelViewSet): """ 文件管理接口 @@ -75,5 +105,22 @@ class FileViewSet(CustomModelViewSet): """ queryset = FileList.objects.all() serializer_class = FileSerializer - filter_fields = ['name', ] - permission_classes = [] \ No newline at end of file + filter_class = FileFilter + permission_classes = [] + + @action(methods=['GET'], detail=False) + def get_all(self, request): + data1 = self.get_serializer(self.get_queryset(), many=True).data + data2 = [] + if dispatch.is_tenants_mode(): + from django_tenants.utils import schema_context + with schema_context('public'): + data2 = self.get_serializer(FileList.objects.all(), many=True).data + return DetailResponse(data=data2+data1) + + def list(self, request, *args, **kwargs): + if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode(): + from django_tenants.utils import schema_context + with schema_context('public'): + return super().list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index 4023a82..3b1209d 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -4,12 +4,15 @@ from datetime import datetime, timedelta from captcha.views import CaptchaStore, captcha_image from django.contrib import auth from django.contrib.auth import login +from django.contrib.auth.hashers import check_password, make_password from django.db.models import Q from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import serializers +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.views import TokenObtainPairView @@ -97,16 +100,17 @@ class LoginSerializer(TokenObtainPairSerializer): # 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误 attrs['username'] = user.username data = super().validate(attrs) + data["username"] = self.user.username data["name"] = self.user.name data["userId"] = self.user.id data["avatar"] = self.user.avatar data['user_type'] = self.user.user_type + data['pwd_change_count'] = self.user.pwd_change_count dept = getattr(self.user, 'dept', None) if dept: data['dept_info'] = { 'dept_id': dept.id, 'dept_name': dept.name, - } role = getattr(self.user, 'role', None) if role: @@ -124,6 +128,7 @@ class LoginSerializer(TokenObtainPairSerializer): user.is_active = False user.save() raise CustomValidationError("账号已被锁定,联系管理员解锁") + user.save() count = 5 - user.login_error_count raise CustomValidationError(f"账号/密码错误;重试{count}次后将被锁定~") diff --git a/backend/dvadmin/system/views/menu.py b/backend/dvadmin/system/views/menu.py index 28dce62..33ed979 100644 --- a/backend/dvadmin/system/views/menu.py +++ b/backend/dvadmin/system/views/menu.py @@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet): """用于前端获取当前角色的路由""" user = request.user if user.is_superuser: - queryset = self.queryset.filter(status=1) + queryset = self.queryset.filter(status=1).order_by("sort") else: role_list = user.role.values_list('id', flat=True) menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True) - queryset = Menu.objects.filter(id__in=menu_list) + queryset = Menu.objects.filter(id__in=menu_list).order_by("sort") serializer = WebRouterSerializer(queryset, many=True, request=request) data = serializer.data return SuccessResponse(data=data, total=len(data), msg="获取成功") diff --git a/backend/dvadmin/system/views/message_center.py b/backend/dvadmin/system/views/message_center.py index d46209f..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 @@ -106,6 +108,8 @@ class MessageCenterTargetUserListSerializer(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 diff --git a/backend/dvadmin/system/views/role.py b/backend/dvadmin/system/views/role.py index a5b5a6f..f396860 100644 --- a/backend/dvadmin/system/views/role.py +++ b/backend/dvadmin/system/views/role.py @@ -10,22 +10,29 @@ from rest_framework import serializers from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated -from dvadmin.system.models import Role, Menu, MenuButton, Dept +from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users from dvadmin.system.views.dept import DeptSerializer from dvadmin.system.views.menu import MenuSerializer from dvadmin.system.views.menu_button import MenuButtonSerializer from dvadmin.utils.crud_mixin import FastCrudMixin from dvadmin.utils.field_permission import FieldPermissionMixin -from dvadmin.utils.json_response import SuccessResponse, DetailResponse +from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.viewset import CustomModelViewSet +from dvadmin.utils.permission import CustomPermission class RoleSerializer(CustomModelSerializer): """ 角色-序列化器 """ + users = serializers.SerializerMethodField() + + @staticmethod + def get_users(instance): + users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name') + return users class Meta: model = Role @@ -101,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer): fields = '__all__' - class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): """ 角色管理接口 @@ -116,3 +122,82 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): create_serializer_class = RoleCreateUpdateSerializer update_serializer_class = RoleCreateUpdateSerializer search_fields = ['name', 'key'] + + @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) + def set_role_users(self, request, pk): + """ + 设置 角色-用户 + :param request: + :return: + """ + data = request.data + direction = data.get('direction') + movedKeys = data.get('movedKeys') + role = Role.objects.get(pk=pk) + if direction == "left": + # left : 移除用户权限 + role.users_set.remove(*movedKeys) + else: + # right : 添加用户权限 + role.users_set.add(*movedKeys) + serializer = RoleSerializer(role) + return DetailResponse(data=serializer.data, msg="更新成功") + + @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission]) + def get_role_users(self, request): + """ + 获取角色已授权、未授权的用户 + 已授权的用户:1 + 未授权的用户:0 + """ + role_id = request.query_params.get('role_id', None) + + if not role_id: + return ErrorResponse(msg="请选择角色") + + if request.query_params.get('authorized', 0) == "1": + queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True) + else: + queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True) + + if name := request.query_params.get('name', None): + queryset = queryset.filter(name__icontains=name) + + if dept := request.query_params.get('dept', None): + queryset = queryset.filter(dept=dept) + + page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name')) + if page is not None: + return self.get_paginated_response(page) + + return SuccessResponse(data=page) + + @action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission]) + def remove_role_user(self, request, pk): + """ + 角色-删除用户 + """ + user_id = request.data.get('user_id', None) + + if not user_id: + return ErrorResponse(msg="请选择用户") + + role = self.get_object() + role.users_set.remove(*user_id) + + return SuccessResponse(msg="删除成功") + + @action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission]) + def add_role_users(self, request, pk): + """ + 角色-添加用户 + """ + users_id = request.data.get('users_id', None) + + if not users_id: + return ErrorResponse(msg="请选择用户") + + role = self.get_object() + role.users_set.add(*users_id) + + return DetailResponse(msg="添加成功") diff --git a/backend/dvadmin/system/views/role_menu_button_permission.py b/backend/dvadmin/system/views/role_menu_button_permission.py index 3ee596d..c041887 100644 --- a/backend/dvadmin/system/views/role_menu_button_permission.py +++ b/backend/dvadmin/system/views/role_menu_button_permission.py @@ -6,24 +6,20 @@ @Created on: 2021/6/3 003 0:30 @Remark: 菜单按钮管理 """ -from django.db.models import F, Subquery, OuterRef, Exists, BooleanField, Q, Case, Value, When -from django.db.models.functions import Coalesce from rest_framework import serializers from rest_framework.decorators import action -from rest_framework.fields import ListField from rest_framework.permissions import IsAuthenticated -from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \ - MenuField -from dvadmin.system.views.menu import MenuSerializer -from dvadmin.utils.json_response import DetailResponse, ErrorResponse +from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \ + MenuField, FieldPermission +from dvadmin.utils.json_response import DetailResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet class RoleMenuButtonPermissionSerializer(CustomModelSerializer): """ - 菜单按钮-序列化器 + 角色-菜单-按钮-权限 查询序列化 """ class Meta: @@ -34,7 +30,7 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer): class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer): """ - 初始化菜单按钮-序列化器 + 角色-菜单-按钮-权限 创建/修改序列化 """ menu_button__name = serializers.CharField(source='menu_button.name', read_only=True) menu_button__value = serializers.CharField(source='menu_button.value', read_only=True) @@ -45,63 +41,99 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer): read_only_fields = ["id"] -class RoleButtonPermissionSerializer(CustomModelSerializer): +class RoleMenuSerializer(CustomModelSerializer): """ - 角色按钮权限 + 角色-菜单 序列化 """ isCheck = serializers.SerializerMethodField() - data_range = serializers.SerializerMethodField() def get_isCheck(self, instance): params = self.request.query_params + data = self.request.data + return RoleMenuPermission.objects.filter( + menu_id=instance.id, + role_id=params.get('roleId', data.get('roleId')), + ).exists() + + class Meta: + model = Menu + fields = ["id", "name", "parent", "is_catalog", "isCheck"] + + +class RoleMenuButtonSerializer(CustomModelSerializer): + """ + 角色-菜单-按钮 序列化 + """ + isCheck = serializers.SerializerMethodField() + data_range = serializers.SerializerMethodField() + role_menu_btn_perm_id = serializers.SerializerMethodField() + dept = serializers.SerializerMethodField() + + def get_isCheck(self, instance): + params = self.request.query_params + data = self.request.data return RoleMenuButtonPermission.objects.filter( - menu_button__id=instance['id'], - role__id=params.get('role'), + menu_button_id=instance.id, + role_id=params.get('roleId', data.get('roleId')), ).exists() def get_data_range(self, instance): - params = self.request.query_params - obj = RoleMenuButtonPermission.objects.filter( - menu_button__id=instance['id'], - role__id=params.get('role'), - ).first() + obj = self.get_role_menu_btn_prem(instance) if obj is None: return None return obj.data_range + def get_role_menu_btn_perm_id(self, instance): + obj = self.get_role_menu_btn_prem(instance) + if obj is None: + return None + return obj.id + + def get_dept(self, instance): + obj = self.get_role_menu_btn_prem(instance) + if obj is None: + return None + return obj.dept.all().values_list('id', flat=True) + + def get_role_menu_btn_prem(self, instance): + params = self.request.query_params + data = self.request.data + obj = RoleMenuButtonPermission.objects.filter( + menu_button_id=instance.id, + role_id=params.get('roleId', data.get('roleId')), + ).first() + return obj + class Meta: model = MenuButton - fields = ['id', 'name', 'value', 'isCheck', 'data_range'] - - -class RoleFieldPermissionSerializer(CustomModelSerializer): - class Meta: - model = FieldPermission - fields = "__all__" + fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept'] class RoleMenuFieldSerializer(CustomModelSerializer): + """ + 角色-菜单-字段 序列化 + """ is_query = serializers.SerializerMethodField() is_create = serializers.SerializerMethodField() is_update = serializers.SerializerMethodField() def get_is_query(self, instance): params = self.request.query_params - queryset = instance.menu_field.filter(role=params.get('role')).first() + queryset = instance.menu_field.filter(role=params.get('roleId')).first() if queryset: return queryset.is_query return False def get_is_create(self, instance): params = self.request.query_params - queryset = instance.menu_field.filter(role=params.get('role')).first() + queryset = instance.menu_field.filter(role=params.get('roleId')).first() if queryset: return queryset.is_create return False def get_is_update(self, instance): params = self.request.query_params - queryset = instance.menu_field.filter(role=params.get('role')).first() + queryset = instance.menu_field.filter(role=params.get('roleId')).first() if queryset: return queryset.is_update return False @@ -111,54 +143,6 @@ class RoleMenuFieldSerializer(CustomModelSerializer): fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update'] -class RoleMenuSerializer(CustomModelSerializer): - menus = serializers.SerializerMethodField() - - def get_menus(self, instance): - menu_list = Menu.objects.filter(parent=instance['id']).values('id', 'name') - serializer = RoleMenuPermissionSerializer(menu_list, many=True, request=self.request) - return serializer.data - - class Meta: - model = Menu - fields = ['id', 'name', 'menus'] - - -class RoleMenuPermissionSerializer(CustomModelSerializer): - """ - 菜单和按钮权限 - """ - # name = serializers.SerializerMethodField() - isCheck = serializers.SerializerMethodField() - btns = serializers.SerializerMethodField() - columns = serializers.SerializerMethodField() - - # def get_name(self, instance): - # parent_list = Menu.get_all_parent(instance['id']) - # names = [d["name"] for d in parent_list] - # return "/".join(names) - def get_isCheck(self, instance): - params = self.request.query_params - return RoleMenuPermission.objects.filter( - menu__id=instance['id'], - role__id=params.get('role'), - ).exists() - - def get_btns(self, instance): - btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value') - serializer = RoleButtonPermissionSerializer(btn_list, many=True, request=self.request) - return serializer.data - - def get_columns(self, instance): - col_list = MenuField.objects.filter(menu=instance['id']) - serializer = RoleMenuFieldSerializer(col_list, many=True, request=self.request) - return serializer.data - - class Meta: - model = Menu - fields = ['id', 'name', 'isCheck', 'btns', 'columns'] - - class RoleMenuButtonPermissionViewSet(CustomModelViewSet): """ 菜单按钮接口 @@ -174,202 +158,111 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer extra_filter_class = [] - # @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - # def get_role_premission(self, request): - # """ - # 角色授权获取: - # :param request: role - # :return: menu,btns,columns - # """ - # params = request.query_params - # is_superuser = request.user.is_superuser - # if is_superuser: - # queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all() - # else: - # role_id = request.user.role.values_list('id', flat=True) - # menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('menu__id', flat=True) - # queryset = Menu.objects.filter(status=1, is_catalog=True, id__in=menu_list).values('name', 'id').all() - # serializer = RoleMenuSerializer(queryset, many=True, request=request) - # data = serializer.data - # return DetailResponse(data=data) + @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) + def get_role_menu(self, request): + """ + 获取 角色-菜单 + :param request: + :return: + """ + menu_queryset = Menu.objects.all() + serializer = RoleMenuSerializer(menu_queryset, many=True, request=request) + return DetailResponse(data=serializer.data) + + @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) + def set_role_menu(self, request): + """ + 设置 角色-菜单 + :param request: + :return: + """ + data = request.data + roleId = data.get('roleId') + menuId = data.get('menuId') + isCheck = data.get('isCheck') + if isCheck: + # 添加权限:创建关联记录 + instance = RoleMenuPermission.objects.create(role_id=roleId, menu_id=menuId) + else: + # 删除权限:移除关联记录 + RoleMenuPermission.objects.filter(role_id=roleId, menu_id=menuId).delete() + menu_instance = Menu.objects.get(id=menuId) + serializer = RoleMenuSerializer(menu_instance, request=request) + return DetailResponse(data=serializer.data, msg="更新成功") @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - def get_role_permission(self, request): + def get_role_menu_btn_field(self, request): + """ + 获取 角色-菜单-按钮-列字段 + :param request: + :return: + """ params = request.query_params - # 需要授权的角色信息 - current_role = params.get('role', None) - # 当前登录用户的角色 - role_list = request.user.role.values_list('id', flat=True) - if current_role is None: - return ErrorResponse(msg='参数错误') - is_superuser = request.user.is_superuser - if is_superuser: - menu_queryset = Menu.objects.prefetch_related('menuPermission').prefetch_related( - 'menufield_set') - else: - role_id_list = request.user.role.values_list('id', flat=True) - menu_list = RoleMenuPermission.objects.filter(role__in=role_id_list).values_list('menu__id', flat=True) - - # 当前角色已授权的菜单 - menu_queryset = Menu.objects.filter(id__in=menu_list).prefetch_related('menuPermission').prefetch_related( - 'menufield_set') - result = [] - for menu_item in menu_queryset: - isCheck = RoleMenuPermission.objects.filter( - menu_id=menu_item.id, - role_id=current_role - ).exists() - dicts = { - 'name': menu_item.name, - 'id': menu_item.id, - 'parent': menu_item.parent.id if menu_item.parent else None, - 'isCheck': isCheck, - 'btns': [], - 'columns': [] - } - for mb_item in menu_item.menuPermission.all(): - rolemenubuttonpermission_queryset = RoleMenuButtonPermission.objects.filter( - menu_button_id=mb_item.id, - role_id=current_role - ).first() - dicts['btns'].append( - { - 'id': mb_item.id, - 'name': mb_item.name, - 'value': mb_item.value, - 'data_range': rolemenubuttonpermission_queryset.data_range - if rolemenubuttonpermission_queryset - else None, - 'isCheck': bool(rolemenubuttonpermission_queryset), - 'dept': rolemenubuttonpermission_queryset.dept.all().values_list('id', flat=True) - if rolemenubuttonpermission_queryset - else [], - } - ) - for column_item in menu_item.menufield_set.all(): - # 需要授权角色已拥有的列权限 - fieldpermission_queryset = column_item.menu_field.filter(role_id=current_role).first() - is_query = fieldpermission_queryset.is_query if fieldpermission_queryset else False - is_create = fieldpermission_queryset.is_create if fieldpermission_queryset else False - is_update = fieldpermission_queryset.is_update if fieldpermission_queryset else False - # 当前登录用户角色可分配的列权限 - fieldpermission_queryset_disabled = column_item.menu_field.filter(role_id__in=role_list).first() - disabled_query = fieldpermission_queryset_disabled.is_query if fieldpermission_queryset_disabled else True - disabled_create = fieldpermission_queryset_disabled.is_create if fieldpermission_queryset_disabled else True - disabled_update = fieldpermission_queryset_disabled.is_update if fieldpermission_queryset_disabled else True - - dicts['columns'].append({ - 'id': column_item.id, - 'field_name': column_item.field_name, - 'title': column_item.title, - 'is_query': is_query, - 'is_create': is_create, - 'is_update': is_update, - 'disabled_query': False if is_superuser else not disabled_query, - 'disabled_create': False if is_superuser else not disabled_create, - 'disabled_update': False if is_superuser else not disabled_update, - }) - result.append(dicts) - return DetailResponse(data=result) + menuId = params.get('menuId', None) + menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId) + menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request) + menu_field_queryset = MenuField.objects.filter(menu_id=menuId) + menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request) + return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data}) @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) - def set_role_premission(self, request, pk): + def set_role_menu_field(self, request, pk): """ - 对角色的菜单和按钮及按钮范围授权: - :param request: - :param pk: role - :return: + 设置 角色-菜单-列字段 """ - body = request.data - RoleMenuPermission.objects.filter(role=pk).delete() - RoleMenuButtonPermission.objects.filter(role=pk).delete() - for item in body: - if item.get('isCheck'): - RoleMenuPermission.objects.create(role_id=pk, menu_id=item["id"]) - for btn in item.get('btns'): - if btn.get('isCheck'): - data_range = btn.get('data_range', 0) or 0 - instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'), - data_range=data_range) - instance.dept.set(btn.get('dept', [])) - for col in item.get('columns'): - FieldPermission.objects.update_or_create(role_id=pk, field_id=col.get('id'), - defaults={ - 'is_query': col.get('is_query'), - 'is_create': col.get('is_create'), - 'is_update': col.get('is_update') - }) - return DetailResponse(msg="授权成功") + data = request.data + for col in data: + FieldPermission.objects.update_or_create( + role_id=pk, field_id=col.get('id'), + defaults={ + 'is_create': col.get('is_create'), + 'is_update': col.get('is_update'), + 'is_query': col.get('is_query'), + }) - @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - def role_menu_get_button(self, request): - """ - 当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用 - :param request: - :return: - """ - if params := request.query_params: - if menu_id := params.get('menu', None): - is_superuser = request.user.is_superuser - if is_superuser: - queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name') - else: - role_list = request.user.role.values_list('id', flat=True) - queryset = RoleMenuButtonPermission.objects.filter( - role__in=role_list, menu_button__menu=menu_id - ).values(btn_id=F('menu_button__id'), name=F('menu_button__name')) - return DetailResponse(data=queryset) - return ErrorResponse(msg="参数错误") + return DetailResponse(data=[], msg="更新成功") - @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) - def data_scope(self, request): + @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) + def set_role_menu_btn(self, request): """ - 获取数据权限范围:角色授权页面使用 - :param request: - :return: + 设置 角色-菜单-按钮 """ - is_superuser = request.user.is_superuser - if is_superuser: - data = [ - {"value": 0, "label": '仅本人数据权限'}, - {"value": 1, "label": '本部门及以下数据权限'}, - {"value": 2, "label": '本部门数据权限'}, - {"value": 3, "label": '全部数据权限'}, - {"value": 4, "label": '自定义数据权限'} - ] - return DetailResponse(data=data) + data = request.data + isCheck = data.get('isCheck', None) + roleId = data.get('roleId', None) + btnId = data.get('btnId', None) + data_range = data.get('data_range', None) or 0 # 默认仅本人权限 + dept = data.get('dept', None) or [] # 默认空部门 + + if isCheck: + # 添加权限:创建关联记录 + instance = RoleMenuButtonPermission.objects.create(role_id=roleId, + menu_button_id=btnId, + data_range=data_range) + # 自定义部门权限 + if data_range == 4 and dept: + instance.dept.set(dept) else: - params = request.query_params - data = [{"value": 0, "label": '仅本人数据权限'}] - role_list = request.user.role.values_list('id', flat=True) - # 权限页面进入初始化获取所有的数据权限范围 - role_queryset = RoleMenuButtonPermission.objects.filter( - role__in=role_list - ).values_list('data_range', flat=True) - # 通过按钮小齿轮获取指定按钮的权限 - if menu_button_id := params.get('menu_button', None): - role_queryset = RoleMenuButtonPermission.objects.filter( - role__in=role_list, menu_button__id=menu_button_id - ).values_list('data_range', flat=True) + # 删除权限:移除关联记录 + RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() + menu_btn_instance = MenuButton.objects.get(id=btnId) + serializer = RoleMenuButtonSerializer(menu_btn_instance, request=request) + return DetailResponse(data=serializer.data, msg="更新成功") - data_range_list = list(set(role_queryset)) - for item in data_range_list: - if item == 0: - data = data - elif item == 1: - data.extend([ - {"value": 1, "label": '本部门及以下数据权限'}, - {"value": 2, "label": '本部门数据权限'} - ]) - elif item == 2: - data.extend([{"value": 2, "label": '本部门数据权限'}]) - elif item == 3: - data.extend([{"value": 3, "label": '全部数据权限'}]) - elif item == 4: - data.extend([{"value": 4, "label": '自定义数据权限'}]) - else: - data = [] - return DetailResponse(data=data) + @action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated]) + def set_role_menu_btn_data_range(self, request): + """ + 设置 角色-菜单-按钮-权限 + """ + data = request.data + instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id')) + instance.data_range = data.get('data_range') + instance.dept.add(*data.get('dept')) + if not data.get('dept'): + instance.dept.clear() + instance.save() + serializer = RoleMenuButtonPermissionSerializer(instance, request=request) + return DetailResponse(data=serializer.data, msg="更新成功") @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) def role_to_dept_all(self, request): @@ -395,55 +288,3 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled data.append(dept) return DetailResponse(data=data) - - @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) - def menu_to_button(self, request): - """ - 根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用 - :param request: - :return: - """ - params = request.query_params - menu_id = params.get('menu', None) - if menu_id is None: - return ErrorResponse(msg="未获取到参数") - is_superuser = request.user.is_superuser - if is_superuser: - queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values( - 'id', - 'data_range', - 'menu_button', - 'menu_button__name', - 'menu_button__value' - ) - return DetailResponse(data=queryset) - else: - if params: - - role_id = params.get('role', None) - if role_id is None: - return ErrorResponse(msg="未获取到参数") - queryset = RoleMenuButtonPermission.objects.filter(role=role_id, menu_button__menu=menu_id).values( - 'id', - 'data_range', - 'menu_button', - 'menu_button__name', - 'menu_button__value' - ) - return DetailResponse(data=queryset) - return ErrorResponse(msg="未获取到参数") - - @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) - def role_to_menu(self, request): - """ - 获取角色对应的按钮权限 - :param request: - :return: - """ - params = request.query_params - role_id = params.get('role', None) - if role_id is None: - return ErrorResponse(msg="未获取到参数") - queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct() - - return DetailResponse(data=queryset) diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index 116d71f..c31540c 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -286,6 +286,7 @@ class UserViewSet(CustomModelViewSet): "dept": user.dept_id, "is_superuser": user.is_superuser, "role": user.role.values_list('id', flat=True), + "pwd_change_count":user.pwd_change_count } if hasattr(connection, 'tenant'): result['tenant_id'] = connection.tenant and connection.tenant.id @@ -319,7 +320,6 @@ class UserViewSet(CustomModelViewSet): """密码修改""" data = request.data old_pwd = data.get("oldPassword") - print(old_pwd) new_pwd = data.get("newPassword") new_pwd2 = data.get("newPassword2") if old_pwd is None or new_pwd is None or new_pwd2 is None: @@ -330,13 +330,33 @@ 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.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) + request.user.pwd_change_count += 1 request.user.save() return DetailResponse(data=None, msg="修改成功") else: return ErrorResponse(msg="旧密码不正确") + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated]) + def login_change_password(self, request, *args, **kwargs): + """初次登录进行密码修改""" + data = request.data + new_pwd = data.get("password") + new_pwd2 = data.get("password_regain") + if new_pwd != new_pwd2: + return ErrorResponse(msg="两次密码不匹配") + else: + request.user.password = make_password(new_pwd) + request.user.pwd_change_count += 1 + request.user.save() + return DetailResponse(data=None, msg="修改成功") + @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) def reset_to_default_password(self, request,pk): """恢复默认密码""" diff --git a/backend/dvadmin/utils/field_permission.py b/backend/dvadmin/utils/field_permission.py index 20b4cb9..8702684 100644 --- a/backend/dvadmin/utils/field_permission.py +++ b/backend/dvadmin/utils/field_permission.py @@ -5,34 +5,39 @@ from rest_framework.permissions import IsAuthenticated from dvadmin.system.models import FieldPermission, MenuField from dvadmin.utils.json_response import DetailResponse -from dvadmin.utils.models import get_custom_app_models + + +def merge_permission(data): + """ + 合并权限 + """ + result = {} + for item in data: + field_name = item.pop('field_name') + if field_name not in result: + result[field_name] = item + else: + for key, value in item.items(): + result[field_name][key] = result[field_name][key] or value + return result class FieldPermissionMixin: - @action(methods=['get'], detail=False,permission_classes=[IsAuthenticated]) + @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) def field_permission(self, request): """ 获取字段权限 """ - finded = False - for model in get_custom_app_models(): - if model['object'] is self.serializer_class.Meta.model: - finded = True - break - if finded: - break - if finded is False: - return [] + model = self.serializer_class.Meta.model.__name__ user = request.user - if user.is_superuser==1: - data = MenuField.objects.filter( model=model['model']).values('field_name') - for item in data: - item['is_create'] = True - item['is_query'] = True - item['is_update'] = True + # 创建一个默认字典来存储最终的结果 + if user.is_superuser == 1: + data = MenuField.objects.filter(model=model).values('field_name') + result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data} else: roles = request.user.role.values_list('id', flat=True) - data= FieldPermission.objects.filter( - field__model=model['model'],role__in=roles - ).values( 'is_create', 'is_query', 'is_update',field_name=F('field__field_name')) - return DetailResponse(data=data) \ No newline at end of file + data = FieldPermission.objects.filter( + field__model=model, role__in=roles + ).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name')) + result = merge_permission(data) + return DetailResponse(data=result) diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py index 05c0dfb..f61fc62 100644 --- a/backend/dvadmin/utils/filters.py +++ b/backend/dvadmin/utils/filters.py @@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend from django_filters.utils import get_model_field from rest_framework.filters import BaseFilterBackend from django_filters.conf import settings -from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission +from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton from dvadmin.utils.models import CoreModel class CoreModelFilterBankend(BaseFilterBackend): @@ -33,15 +33,15 @@ class CoreModelFilterBankend(BaseFilterBackend): create_datetime_after = request.query_params.get('create_datetime_after', None) create_datetime_before = request.query_params.get('create_datetime_before', None) update_datetime_after = request.query_params.get('update_datetime_after', None) - update_datetime_before = request.query_params.get('update_datetime_after', None) + update_datetime_before = request.query_params.get('update_datetime_before', None) if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]): create_filter = Q() if create_datetime_after and create_datetime_before: - 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() @@ -149,13 +149,16 @@ class DataLevelPermissionsFilter(BaseFilterBackend): if _pk: # 判断是否是单例查询 re_api = re.sub(_pk,'{id}', api) role_id_list = request.user.role.values_list('id', flat=True) - role_permission_list=RoleMenuButtonPermission.objects.filter( - role__in=role_id_list, - role__status=1, - menu_button__api=re_api, - menu_button__method=method).values( - 'data_range' - ) + # 修复权限获取bug + menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True) + role_permission_list = [] + if menu_button_ids: + role_permission_list=RoleMenuButtonPermission.objects.filter( + role__in=role_id_list, + role__status=1, + menu_button_id__in=menu_button_ids).values( + 'data_range' + ) dataScope_list = [] # 权限范围列表 for ele in role_permission_list: # 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据 @@ -340,7 +343,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): from timezone_field import TimeZoneField # 不进行 过滤的model 类 - if isinstance(field, (models.JSONField, TimeZoneField)): + if isinstance(field, (models.JSONField, TimeZoneField, models.FileField)): continue # warn if the field doesn't exist. if field is None: diff --git a/backend/dvadmin/utils/import_export.py b/backend/dvadmin/utils/import_export.py index 2bd6e1e..a3bccd9 100644 --- a/backend/dvadmin/utils/import_export.py +++ b/backend/dvadmin/utils/import_export.py @@ -86,4 +86,5 @@ def import_to_data(file_url, field_data, m2m_fields=None): else: array[key] = cell_value tables.append(array) - return tables + data = [i for i in tables if len(i) != 0] + return data diff --git a/backend/dvadmin/utils/import_export_mixin.py b/backend/dvadmin/utils/import_export_mixin.py index 44f51cc..e188ff5 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,16 @@ 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: + 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)}数据导出任务', dept_belong_id=request.user.dept_id).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 7ed2f92..b387ea4 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -61,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): @@ -216,9 +232,13 @@ 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: 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/dvadmin/utils/viewset.py b/backend/dvadmin/utils/viewset.py index b85007a..42948b1 100644 --- a/backend/dvadmin/utils/viewset.py +++ b/backend/dvadmin/utils/viewset.py @@ -6,6 +6,8 @@ @Created on: 2021/6/1 001 22:57 @Remark: 自定义视图集 """ +import copy + from django.db import transaction from django_filters import DateTimeFromToRangeFilter from django_filters.rest_framework import FilterSet @@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi kwargs.setdefault('context', self.get_serializer_context()) # 全部以可见字段为准 can_see = self.get_menu_field(serializer_class) - # 排除掉序列化器级的字段 - # sub_set = set(serializer_class._declared_fields.keys()) - set(can_see) - # for field in sub_set: - # serializer_class._declared_fields.pop(field) + # 排除掉序列化器级的字段(排除字段权限中未授权的字段) # if not self.request.user.is_superuser: - # serializer_class.Meta.fields = can_see + # exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see) + # for field in exclude_set: + # serializer_class._declared_fields.pop(field) + # meta = copy.deepcopy(serializer_class.Meta) + # meta.fields = list(can_see) + # serializer_class.Meta = meta # 在分页器中使用 self.request.permission_fields = can_see if isinstance(self.request.data, list): @@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi def get_menu_field(self, serializer_class): """获取字段权限""" - finded = False - for model in get_custom_app_models(): - if model['object'] is serializer_class.Meta.model: - finded = True - break - if finded is False: + + if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()): return [] - return MenuField.objects.filter(model=model['model'] - ).values('field_name', 'title') + + # 匿名用户没有角色 + ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__) + if hasattr(self.request.user, 'role'): + roles = self.request.user.role.values_list('id', flat=True) + ret = ret.filter(is_query=True, role__in=roles) + + return ret.values_list('field__field_name', flat=True) def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data, request=request) @@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi instance.delete() return DetailResponse(data=[], msg="删除成功") - keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING) - + keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING)) @swagger_auto_schema(request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['keys'], diff --git a/backend/requirements.txt b/backend/requirements.txt index 2e7c6cb..9cae2cc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,32 +1,32 @@ -Django==4.2.7 +Django==4.2.14 django-comment-migrate==0.1.7 django-cors-headers==4.4.0 -django-filter==23.3 +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 +dvadmin3-celery==3.1.6 \ No newline at end of file diff --git a/crud-gen.sh b/crud-gen.sh new file mode 100644 index 0000000..51fe5f3 --- /dev/null +++ b/crud-gen.sh @@ -0,0 +1,87 @@ +if ! [ -f ".env" ];then + echo ".env file not found" + exit 1 +fi + +if [ -z "$3" ]; then + echo "Use: $0 " + exit 1 +fi + + +DIR=./web/src/views/$1/$2 + + +# 设置数据库连接信息 +HOST="177.10.0.13" +USER="root" +PASSWORD=$(cat .env | grep MYSQL_PASSWORD | sed 's/^.*MYSQL_PASSWORD=//g') +DATABASE="django-vue3-admin" +TABLE=$3 +TARGET_FILE="./web/src/views/$1/$2/crud.tsx" + + +# 表是否存在 +TABLE_EXISTS=$(mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "SHOW TABLES LIKE '$TABLE';" -N | grep "$TABLE" | wc -l) + +if [ "$TABLE_EXISTS" -eq 0 ]; then + echo "Table $TABLE does not exist in database $DATABASE." + exit 1 +fi + +mkdir -p $DIR +cp -r ./web/src/views/template/* $DIR +sed -i "s/VIEWSETNAME/$2/g" $DIR/* + +sed -n -e :a -e '1,5!{P;N;D;};N;ba' -i $TARGET_FILE + +# 查询表结构 +QUERY="SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$DATABASE' AND TABLE_NAME = '$TABLE' ORDER BY ORDINAL_POSITION;" + +# 使用 MySQL 查询获取字段信息,并生成 fast-crud 配置 +mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "$QUERY" -N | while read COLUMN_NAME DATA_TYPE COLUMN_COMMENT IS_NULLABLE; do + # 映射 MySQL 数据类型到 fast-crud 类型 + case "$DATA_TYPE" in + "int"|"bigint"|"smallint"|"mediumint"|"tinyint"|"decimal"|"float"|"double") + TYPE="number" + ;; + "date"|"datetime"|"timestamp") + TYPE="date" + ;; + *) + TYPE="text" + ;; + esac + + echo " $COLUMN_NAME: { + title: '$COLUMN_NAME', + type: '$TYPE', + search: { show: true }, + column: { + minWidth: 120, + sortable: 'custom', + }, + form: {" >> $TARGET_FILE + + if [ "$IS_NULLABLE" = "NO" ]; then + echo " helper: { + render() { + return
$COLUMN_NAME 是必填的
; + } + }, + rules: [{ + required: true, message: '$COLUMN_NAME 是必填的' + }]," >> $TARGET_FILE + fi + + echo " component: { + placeholder: '请输入 $COLUMN_NAME', + }, + }, + }," >> $TARGET_FILE +done + +echo " }, + }, + }; +}" >> $TARGET_FILE diff --git a/docker_env/web/Dockerfile b/docker_env/web/Dockerfile index dc0cdb8..73fc4e7 100644 --- a/docker_env/web/Dockerfile +++ b/docker_env/web/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine +FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:18.20-alpine WORKDIR /web/ COPY web/. . RUN yarn install --registry=https://registry.npmmirror.com diff --git a/init.sh b/init.sh index 8c377ed..ab828cb 100644 --- a/init.sh +++ b/init.sh @@ -1,5 +1,6 @@ #!/bin/bash ENV_FILE=".env" +HOST="177.10.0.13" # 检查 .env 文件是否存在 if [ -f "$ENV_FILE" ]; then echo "$ENV_FILE 文件已存在。" @@ -15,17 +16,60 @@ else echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。" awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }' - sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.13'|g" ./backend/conf/env.py + sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '$HOST'|g" ./backend/conf/env.py sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py echo "初始化密码创建成功" fi +echo "正在启动容器..." docker-compose up -d -docker exec dvadmin3-django python manage.py makemigrations -docker exec dvadmin3-django python manage.py migrate -docker exec dvadmin3-django python manage.py init -echo "欢迎使用dvadmin3项目" -echo "登录地址:http://ip:8080" -echo "如访问不到,请检查防火墙配置" + +if [ $? -ne 0 ]; then + echo "docker-compose up -d 执行失败!" + exit 1 +fi + +MYSQL_PORT=3306 +REDIS_PORT=6379 + +check_mysql() { + if nc -z "$HOST" "$MYSQL_PORT" >/dev/null 2>&1; then + echo "MySQL 服务正在运行在 $HOST:$MYSQL_PORT" + return 0 + else + return 1 + fi +} + +check_redis() { + if nc -z "$HOST" "$REDIS_PORT" >/dev/null 2>&1; then + echo "Redis 服务正在运行在 $HOST:$REDIS_PORT" + return 0 + else + return 1 + fi +} + +i=1 +while [ $i -le 8 ]; do + if check_mysql || check_redis; then + echo "正在迁移数据..." + docker exec dvadmin3-django python3 manage.py makemigrations + docker exec dvadmin3-django python3 manage.py migrate + echo "正在初始化数据..." + docker exec dvadmin3-django python3 manage.py init + echo "欢迎使用dvadmin3项目" + echo "登录地址:http://ip:8080" + echo "如访问不到,请检查防火墙配置" + exit 0 + else + echo "第 $i 次尝试:MySQL 或 REDIS服务未运行,等待 2 秒后重试..." + sleep 2 + fi + i=$((i+1)) +done + +echo "尝试 5 次后,MySQL 或 REDIS服务仍未运行" +exit 1 diff --git a/web/.env b/web/.env index 0d828a7..a77f485 100644 --- a/web/.env +++ b/web/.env @@ -1,6 +1,6 @@ # port 端口号 VITE_PORT = 8080 -VITE_API_URL = 'http://dvadmin3api.django.icu:8001' +VITE_API_URL = 'http://127.0.0.1:8000' # open 运行 npm run dev 时自动打开浏览器 VITE_OPEN = false diff --git a/web/.env.development b/web/.env.development index 1c3ca5d..dc36b29 100644 --- a/web/.env.development +++ b/web/.env.development @@ -2,7 +2,7 @@ ENV = 'development' # 本地环境接口地址 -VITE_API_URL = 'http://127.0.0.1:8000' +VITE_API_URL = 'http://127.0.0.1:8001' # 是否启用按钮权限 VITE_PM_ENABLED = true diff --git a/web/README.md b/web/README.md index e70cc71..86d36b1 100644 --- a/web/README.md +++ b/web/README.md @@ -49,6 +49,10 @@ 👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2) +## 给框架点赞 + + + ## 交流 diff --git a/web/flowH5.config.ts b/web/flowH5.config.ts new file mode 100644 index 0000000..505f760 --- /dev/null +++ b/web/flowH5.config.ts @@ -0,0 +1,77 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import path, {resolve} from 'path'; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import vueSetupExtend from "vite-plugin-vue-setup-extend"; +import { terser } from 'rollup-plugin-terser'; +import postcss from 'rollup-plugin-postcss'; +import pxtorem from 'postcss-pxtorem'; +const pathResolve = (dir: string) => { + return resolve(__dirname, '.', dir); +}; +export default defineConfig({ + build: { + // outDir: '../backend/static/previewer', + lib: { + entry: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'), // 库的入口文件 + name: 'previewer', // 库的全局变量名称 + fileName: (format) => `index.${format}.js`, // 输出文件名格式 + }, + rollupOptions: { + input:{ + previewer: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'), + }, + external: ['vue','xe-utils'], // 指定外部依赖 + output:{ + // dir: '../backend/static/previewer', // 输出目录 + entryFileNames: 'index.[format].js', // 入口文件名格式 + format: 'commonjs', + globals: { + vue: 'Vue' + }, + chunkFileNames: `[name].[hash].js` + }, + plugins: [ + terser({ + compress: { + drop_console: false, // 确保不移除 console.log + }, + }), + postcss({ + plugins: [ + pxtorem({ + rootValue: 37.5, + unitPrecision: 5, + propList: ['*'], + selectorBlackList: [], + replace: true, + mediaQuery: false, + minPixelValue: 0, + exclude: /node_modules/i, + }), + ], + }), + ], + }, + }, + plugins: [ + vue(), + vueJsx(), + vueSetupExtend(), + ], + resolve: { + alias: { + '/@': path.resolve(__dirname, 'src'), // '@' 别名指向 'src' 目录 + '@views': pathResolve('./src/views'), + '/src':path.resolve(__dirname, 'src') + }, + }, + css:{ + postcss:{ + + } + }, + define: { + 'process.env': {} + } +}); \ No newline at end of file diff --git a/web/package.json b/web/package.json index 49d3d8a..ac60e3f 100644 --- a/web/package.json +++ b/web/package.json @@ -1,41 +1,45 @@ { "name": "django-vue3-admin", - "version": "3.0.3", + "version": "3.1.0", "description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus", "license": "MIT", "scripts": { "dev": "vite --force", + "build:dev": "vite build --mode development", "build": "vite build", "build:local": "vite build --mode local_prod", "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" }, "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", + "@great-dream/dvadmin3-celery-web": "^3.1.3", + "@iconify/vue": "^4.1.2", + "@types/lodash": "^4.17.7", + "@vitejs/plugin-vue-jsx": "^4.0.1", "@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", + "date-holidays": "^3.24.1", "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", + "lunar-javascript": "^1.7.1", + "mitt": "^3.0.1", "nprogress": "^0.2.0", "pinia": "^2.0.28", "pinia-plugin-persist": "^1.0.0", @@ -49,31 +53,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/App.vue b/web/src/App.vue index 56f585e..c13df04 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -11,7 +11,7 @@ @@ -172,7 +174,6 @@ defineExpose({ .user-info-head { position: relative; display: inline-block; - height: 120px; } .user-info-head:hover:after { diff --git a/web/src/components/calendar/index.vue b/web/src/components/calendar/index.vue new file mode 100644 index 0000000..9c44e60 --- /dev/null +++ b/web/src/components/calendar/index.vue @@ -0,0 +1,403 @@ + + + + + + \ No newline at end of file diff --git a/web/src/components/fileSelector/fileItem.vue b/web/src/components/fileSelector/fileItem.vue new file mode 100644 index 0000000..73ef6f0 --- /dev/null +++ b/web/src/components/fileSelector/fileItem.vue @@ -0,0 +1,84 @@ + + + \ No newline at end of file diff --git a/web/src/components/fileSelector/index.vue b/web/src/components/fileSelector/index.vue new file mode 100644 index 0000000..c8319aa --- /dev/null +++ b/web/src/components/fileSelector/index.vue @@ -0,0 +1,521 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/fileSelector/types.ts b/web/src/components/fileSelector/types.ts new file mode 100644 index 0000000..e428015 --- /dev/null +++ b/web/src/components/fileSelector/types.ts @@ -0,0 +1,7 @@ +export const SHOW = { + IMAGE: 0b1000, // 图片 + VIDEO: 0b0100, // 视频 + AUDIO: 0b0010, // 音频 + OTHER: 0b0001, // 其他 + ALL: 0b1111, // 全部 +}; diff --git a/web/src/components/tableSelector/index.vue b/web/src/components/tableSelector/index.vue index 7ad3cad..d827a75 100644 --- a/web/src/components/tableSelector/index.vue +++ b/web/src/components/tableSelector/index.vue @@ -1,203 +1,239 @@ diff --git a/web/src/i18n/pages/login/en.ts b/web/src/i18n/pages/login/en.ts index 28c6983..ad93b86 100644 --- a/web/src/i18n/pages/login/en.ts +++ b/web/src/i18n/pages/login/en.ts @@ -3,6 +3,7 @@ export default { label: { one1: 'User name login', two2: 'Mobile number', + changePwd: 'Change The Password', }, link: { one3: 'Third party login', diff --git a/web/src/i18n/pages/login/zh-cn.ts b/web/src/i18n/pages/login/zh-cn.ts index 07fd82e..ccd60cb 100644 --- a/web/src/i18n/pages/login/zh-cn.ts +++ b/web/src/i18n/pages/login/zh-cn.ts @@ -3,6 +3,7 @@ export default { label: { one1: '账号密码登录', two2: '手机号登录', + changePwd: '密码修改', }, link: { one3: '第三方登录', @@ -12,6 +13,8 @@ export default { accountPlaceholder1: '请输入登录账号/邮箱/手机号', accountPlaceholder2: '请输入登录密码', accountPlaceholder3: '请输入验证码', + accountPlaceholder4:'请输入新密码', + accountPlaceholder5:'请再次输入新密码', accountBtnText: '登 录', }, mobile: { diff --git a/web/src/i18n/pages/login/zh-tw.ts b/web/src/i18n/pages/login/zh-tw.ts index 13ad013..e3cd9e6 100644 --- a/web/src/i18n/pages/login/zh-tw.ts +++ b/web/src/i18n/pages/login/zh-tw.ts @@ -3,6 +3,7 @@ export default { label: { one1: '用戶名登入', two2: '手機號登入', + changePwd: '密码修改', }, link: { one3: '協力廠商登入', diff --git a/web/src/layout/component/aside.vue b/web/src/layout/component/aside.vue index cfd863d..f953682 100644 --- a/web/src/layout/component/aside.vue +++ b/web/src/layout/component/aside.vue @@ -17,13 +17,15 @@ import { useRoutesList } from '/@/stores/routesList'; import { useThemeConfig } from '/@/stores/themeConfig'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import mittBus from '/@/utils/mitt'; - +import { useRoute } from 'vue-router'; +const route = useRoute(); // 引入组件 const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue')); const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue')); // 定义变量内容 const layoutAsideScrollbarRef = ref(); +const routesIndex = ref(0); const stores = useRoutesList(); const storesThemeConfig = useThemeConfig(); const storesTagsViewRoutes = useTagsViewRoutes(); @@ -83,10 +85,36 @@ const closeLayoutAsideMobileMode = () => { if (clientWidth < 1000) themeConfig.value.isCollapse = false; document.body.setAttribute('class', ''); }; +const findFirstLevelIndex = (data, path) => { + for (let index = 0; index < data.length; index++) { + const item = data[index]; + // 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径 + if (item.children && item.children.length > 0) { + // 检查子菜单中是否有匹配的路径 + const childIndex = item.children.findIndex((child) => child.path === path); + if (childIndex !== -1) { + return index; // 返回当前一级菜单的索引 + } + // 递归查找子菜单 + const foundIndex = findFirstLevelIndex(item.children, path); + if (foundIndex !== null) { + return index; // 返回找到的索引 + } + } + } + return null; // 找不到路径时返回 null +}; // 设置/过滤路由(非静态路由/是否显示在菜单中) -const setFilterRoutes = () => { +const setFilterRoutes = (path='') => { if (themeConfig.value.layout === 'columns') return false; - state.menuList = filterRoutesFun(routesList.value); + let { layout, isClassicSplitMenu } = themeConfig.value; + if (layout === 'classic' && isClassicSplitMenu) { + // 获取当前地址的索引,不用从参数选取 + routesIndex.value = findFirstLevelIndex(routesList.value,path || route.path) || 0 + state.menuList = filterRoutesFun(routesList.value[routesIndex.value].children || [routesList.value[routesIndex.value]]); + } else { + state.menuList = filterRoutesFun(routesList.value); + } }; // 路由过滤递归函数 const filterRoutesFun = (arr: T[]): T[] => { @@ -122,7 +150,8 @@ onBeforeMount(() => { let { layout, isClassicSplitMenu } = themeConfig.value; if (layout === 'classic' && isClassicSplitMenu) { state.menuList = []; - state.menuList = res.children; + // state.menuList = res.children; + setFilterRoutes(res.path); } }); mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => { diff --git a/web/src/layout/navBars/breadcrumb/index.vue b/web/src/layout/navBars/breadcrumb/index.vue index 29bd1bc..04e575d 100644 --- a/web/src/layout/navBars/breadcrumb/index.vue +++ b/web/src/layout/navBars/breadcrumb/index.vue @@ -102,6 +102,5 @@ onUnmounted(() => { display: flex; align-items: center; background: var(--next-bg-topBar); - border-bottom: 1px solid var(--next-border-color-light); } diff --git a/web/src/layout/navBars/breadcrumb/user.vue b/web/src/layout/navBars/breadcrumb/user.vue index 61793c9..351bf25 100644 --- a/web/src/layout/navBars/breadcrumb/user.vue +++ b/web/src/layout/navBars/breadcrumb/user.vue @@ -37,7 +37,7 @@
- + @@ -250,6 +250,7 @@ onMounted(() => { //消息中心的未读数量 import { messageCenterStore } from '/@/stores/messageCenter'; +import {getBaseURL} from "/@/utils/baseUrl"; const messageCenter = messageCenterStore(); diff --git a/web/src/layout/navMenu/horizontal.vue b/web/src/layout/navMenu/horizontal.vue index 650fb14..e7fcfca 100644 --- a/web/src/layout/navMenu/horizontal.vue +++ b/web/src/layout/navMenu/horizontal.vue @@ -1,8 +1,8 @@