diff --git a/README.zh.md b/README.zh.md index a95a364a07f1ce5d12952bb1d6facfa01600a520..e3e104827df74a6889d4175e6057b1a689b15954 100644 --- a/README.zh.md +++ b/README.zh.md @@ -54,16 +54,21 @@ ## 交流 - 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦 - - 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦 - - django-vue-admin交流01群(已满):812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi) - django-vue-admin交流02群(已满):687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi) -- django-vue-admin交流03群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) +- django-vue-admin交流03群(已满):442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) +- django-vue-admin交流04群:442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) + + + +## 给框架点赞 -- 二维码 +
+ + +
- ## 源码地址 @@ -88,7 +93,19 @@ github地址:[https://github.com/huge-dream/django-vue3-admin](https://github. 13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html):基于Django-Vue-Admin框架开发的应用和插件。 ## 插件市场 🔌 -更新中... +1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html) + +2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html) + +3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html) + +4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html) + +5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git) + +6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线) + + ## 仓库分支说明 💈 主分支:master(稳定版本) @@ -210,5 +227,19 @@ docker-compose up -d --build ![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png) +## 审批流插件 + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/97fbbf29673edfd66a1edd49237791bb.png) + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/c43aa51278cbc478287c718d22397479.png) + + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/9732a5cca9c1166d1a65c35e313ab90d.png) + + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/3ca9dd0801ce76d21435abcc8a3d505a.png) + +![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/a87a8d2329ef66880af5b0f16c5ff823.png) + diff --git a/backend/.gitignore b/backend/.gitignore index 047099f9068f4a8308f66bfe075a69791f6818f6..6c50cc968f99592116ca575d38654a3694d906e5 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -98,5 +98,4 @@ media/ __pypackages__/ package-lock.json gunicorn.pid -plugins/* !plugins/__init__.py diff --git a/backend/application/celery.py b/backend/application/celery.py index 10bce56c5f643ba19c776409ee2d5482e2fc3560..c719c46aafc55f0ef22529a1936f0a6a17efeb20 100644 --- a/backend/application/celery.py +++ b/backend/application/celery.py @@ -1,6 +1,8 @@ import functools import os +from celery.signals import task_postrun + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') from django.conf import settings @@ -38,3 +40,12 @@ def retry_base_task_error(): return wrapper return wraps + + +@task_postrun.connect +def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras): + periodic_task_name = kwargs.get('periodic_task_name') + if periodic_task_name: + from django_celery_results.models import TaskResult + # 更新 TaskResult 表中的 periodic_task_name 字段 + TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name) diff --git a/backend/application/settings.py b/backend/application/settings.py index e6bdec5eaa5fa6bc0778592ace87c8da995262ef..1d0adf79b43c695cbb576518c213ea2837303a07 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 dvadmin3_celery.settings import * # celery 异步任务 +from dvadmin3_celery.settings import * # celery 异步任务 # from dvadmin_third.settings import * # 第三方用户管理 # from dvadmin_ak_sk.settings import * # 秘钥管理管理 # from dvadmin_tenants.settings import * # 租户管理 diff --git a/backend/dvadmin/system/fixtures/initSerializer.py b/backend/dvadmin/system/fixtures/initSerializer.py index 9ed094fd07b84306ac4f0786f83975d5abfa57dd..f983aeaffbfd6cb01c6535e75a9c72442b547fc1 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/management/commands/generate_init_json.py b/backend/dvadmin/system/management/commands/generate_init_json.py index b0743448e20c0cf80555a1e6df9bff1bfdc93078..6ce83b00ba34877483f421d20a72f119ca38e748 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/urls.py b/backend/dvadmin/system/urls.py index c9c12e9afc90d76336279c837c2d0be1e2567def..0ee2cb8c7b3f4ded0cdd69e3ffa66d5ef744a27e 100644 --- a/backend/dvadmin/system/urls.py +++ b/backend/dvadmin/system/urls.py @@ -49,7 +49,7 @@ urlpatterns = [ path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), # path('login_log//', LoginLogViewSet.as_view({'get': 'retrieve'})), - path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), + # path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), path('clause/privacy.html', PrivacyView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()), ] diff --git a/backend/dvadmin/system/views/role.py b/backend/dvadmin/system/views/role.py index 07a1637615dc75ef2afea90adc9ee19a441a8368..f3968601ff9be75ea46e25cee8efa1bbefd68d6d 100644 --- a/backend/dvadmin/system/views/role.py +++ b/backend/dvadmin/system/views/role.py @@ -10,16 +10,17 @@ 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): @@ -107,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer): fields = '__all__' - class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): """ 角色管理接口 @@ -141,4 +141,63 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): # right : 添加用户权限 role.users_set.add(*movedKeys) serializer = RoleSerializer(role) - return DetailResponse(data=serializer.data, msg="更新成功") \ No newline at end of file + 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 723cd2a6a4af20c6d1c2b983c7d0e17d4b7383c4..c041887fb7557f1249ea1b59c342fcbb83ff2d27 100644 --- a/backend/dvadmin/system/views/role_menu_button_permission.py +++ b/backend/dvadmin/system/views/role_menu_button_permission.py @@ -231,9 +231,17 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet): 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: # 添加权限:创建关联记录 - RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId) + 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: # 删除权限:移除关联记录 RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index 16fcbe9429ff4463d9d5efd39b718109035c5d61..464e77dc64733974fbfffb7ee9761627d3b915b3 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -336,7 +336,7 @@ class UserViewSet(CustomModelViewSet): 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(new_pwd) + 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="修改成功") diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py index da808ace31b75cf11e7259e2e2f4c24543bab036..f61fc62ed214d878693ee6f936b07209e331b101 100644 --- a/backend/dvadmin/utils/filters.py +++ b/backend/dvadmin/utils/filters.py @@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend from django_filters.utils import get_model_field from rest_framework.filters import BaseFilterBackend from django_filters.conf import settings -from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission +from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton from dvadmin.utils.models import CoreModel class CoreModelFilterBankend(BaseFilterBackend): @@ -33,7 +33,7 @@ class CoreModelFilterBankend(BaseFilterBackend): create_datetime_after = request.query_params.get('create_datetime_after', None) create_datetime_before = request.query_params.get('create_datetime_before', None) update_datetime_after = request.query_params.get('update_datetime_after', None) - update_datetime_before = request.query_params.get('update_datetime_after', None) + update_datetime_before = request.query_params.get('update_datetime_before', None) if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]): create_filter = Q() if create_datetime_after and create_datetime_before: @@ -149,13 +149,16 @@ class DataLevelPermissionsFilter(BaseFilterBackend): if _pk: # 判断是否是单例查询 re_api = re.sub(_pk,'{id}', api) role_id_list = request.user.role.values_list('id', flat=True) - role_permission_list=RoleMenuButtonPermission.objects.filter( - role__in=role_id_list, - role__status=1, - menu_button__api=re_api, - menu_button__method=method).values( - 'data_range' - ) + # 修复权限获取bug + menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True) + role_permission_list = [] + if menu_button_ids: + role_permission_list=RoleMenuButtonPermission.objects.filter( + role__in=role_id_list, + role__status=1, + menu_button_id__in=menu_button_ids).values( + 'data_range' + ) dataScope_list = [] # 权限范围列表 for ele in role_permission_list: # 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据 diff --git a/backend/dvadmin/utils/viewset.py b/backend/dvadmin/utils/viewset.py index b85007a0a03c36a68571f37a1c964f4f03a77e2d..42948b1afb7a80fbe6fd1e8175a6345157365598 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 3395719bc4a8703d46ef4804b143346da6bc5808..9cae2cca7e0fe4085fa49dec92d9f5203dd10aea 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -28,4 +28,5 @@ uvicorn==0.30.3 gunicorn==22.0.0 gevent==24.2.1 Pillow==10.4.0 -pyinstaller==6.9.0 \ No newline at end of file +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 0000000000000000000000000000000000000000..51fe5f3593dc055e8240cbdbe0aa30e96f9a11ef --- /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 dc0cdb8b8ee85a1d04e18a7dfdb3a14916a2cffe..73fc4e7cd6d3d3cfd06d93260d2cdba4abfc5d20 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 8c377ed6f0d6c19d0a47cb941495d64f20b217d1..ab828cb56d912697f883f9c7699e0dc1490130b7 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 0d828a796e2ea128786d272e30bac3ddf9f004a6..a77f48577b0acf8fbdb15e7a5b49e3d87fb8f426 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 1c3ca5db362e1f7935aded834e08f0a46ed640a6..dc36b291b2cdf3991de42c0cb05648f397f5e173 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 e70cc71da1995900a70d45a53017b86fde915a0c..86d36b1ee45b46b4db71a740ee495d329e756422 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/package.json b/web/package.json index ca4c3067e397d9b46437faab6fecb285f043d2a6..61d8bc49da6b53bce7b88b574d4b192e8c89414f 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,7 @@ "license": "MIT", "scripts": { "dev": "vite --force", + "build:dev":"vite build --mode development", "build": "vite build", "build:local": "vite build --mode local_prod", "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" @@ -15,6 +16,7 @@ "@fast-crud/fast-extends": "^1.21.2", "@fast-crud/ui-element": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2", + "@great-dream/dvadmin3-celery-web": "^3.1.3", "@iconify/vue": "^4.1.2", "@types/lodash": "^4.17.7", "@vitejs/plugin-vue-jsx": "^4.0.1", diff --git a/web/src/App.vue b/web/src/App.vue index 56f585e22b7aa9b7a27581bea53edc4f4454d015..c13df045b37daea76ba83127e15361baf2ff0fa5 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/fileSelector/index.vue b/web/src/components/fileSelector/index.vue index 1e1862c348f8084092665c86a73af860b8e87d90..c8319aa423142a47c1965eafccbb5091e3c01666 100644 --- a/web/src/components/fileSelector/index.vue +++ b/web/src/components/fileSelector/index.vue @@ -8,7 +8,29 @@ -
+
+ + + + +
+
+
+ + + +
+
+
+
+
@@ -24,10 +46,11 @@
- + +
+
{ while (!target.dataset.id) target = target.parentElement as HTMLElement; let fileId = target.dataset.id; if (props.multiple) { + if (!!!data.value) data.value = []; if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; } else { target.classList.add('active'); flat = 1; } if (data.value.length) { @@ -327,8 +352,12 @@ const clearState = () => { // all数据不能清,因为all只会在挂载的时候赋值一次 // listAllData.value = []; }; -const clear = () => { data.value = null; onDataChange(null); } - +const clear = () => { data.value = null; onDataChange(null); }; +const clearOne = (item: any) => { + let _l = (JSON.parse(JSON.stringify(data.value)) as any[]).filter((i: any) => i !== item) + data.value = _l; + onDataChange(_l); +}; // 网络文件部分 const netLoading = ref(false); @@ -386,7 +415,15 @@ watch( const { ui } = useUi(); const formValidator = ui.formItem.injectFormItemContext(); const onDataChange = (value: any) => { - emit('update:modelValue', value); + let _v = null; + if (value) { + if (typeof value === 'string') _v = value.replace(/\\/g, '/'); + else { + _v = []; + for (let i of value) _v.push(i.replace(/\\/g, '/')); + } + } + emit('update:modelValue', _v); formValidator.onChange(); formValidator.onBlur(); }; @@ -394,7 +431,8 @@ const onDataChange = (value: any) => { defineExpose({ data, onDataChange, selectVisiable, clearState, clear }); onMounted(() => { - if (props.multiple && props.inputType !== 'selector') + + if (props.multiple && !['selector', 'image'].includes(props.inputType)) throw new Error('FileSelector组件属性multiple为true时inputType必须为selector'); listRequestAll(); console.log('fileselector tenentmdoe', isTenentMode); @@ -475,4 +513,9 @@ onMounted(() => { top: 2px; cursor: pointer; } + +.itemList { + border: 1px solid #dcdfe6; + border-radius: 8px; +} \ No newline at end of file diff --git a/web/src/components/tableSelector/index.vue b/web/src/components/tableSelector/index.vue index 8e8c91a9dc56db58087acc8c4ae2af414919c625..d827a751c70554113a27942963c25980d6a9d61e 100644 --- a/web/src/components/tableSelector/index.vue +++ b/web/src/components/tableSelector/index.vue @@ -3,6 +3,7 @@ popper-class="popperClass" class="tableSelector" multiple + :collapseTags="props.tableConfig.collapseTags" @remove-tag="removeTag" v-model="data" placeholder="请选择" @@ -18,20 +19,22 @@ - + diff --git a/web/src/layout/navBars/breadcrumb/user.vue b/web/src/layout/navBars/breadcrumb/user.vue index 61793c974ad275979387c36293f24ad9dcc3d931..351bf259095fb9d00ac43ccd659dc0c73248d878 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 650fb1485f7a5b98b535574799050c5ff2c06c28..e7fcfca0737c7d6f9347b20cff9839d9e43a7810 100644 --- a/web/src/layout/navMenu/horizontal.vue +++ b/web/src/layout/navMenu/horizontal.vue @@ -1,8 +1,8 @@