diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..25f1ac39ea55e2d1c684ba7ec2838859565b2ad8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "web/src/views/plugins/dvadmin3-celery-web"] + path = web/src/views/plugins/dvadmin3-celery-web + url = https://gitee.com/wskaudh/dvadmin3-celery-web.git +[submodule "backend/plugins/dvadmin3-celery"] + path = backend/plugins/dvadmin3-celery + url = https://gitee.com/lxy0722/dvadmin3-celery.git diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9bad5790a5799b96f2e164d825c0b1f8ec0c2dfb --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/backend/app/material/__init__.py b/backend/app/material/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/material/admin.py b/backend/app/material/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/backend/app/material/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/app/material/apps.py b/backend/app/material/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..32b36f375056c38f234d0116e658034115effcef --- /dev/null +++ b/backend/app/material/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MaterialConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app.material' diff --git a/backend/app/material/migrations/__init__.py b/backend/app/material/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/material/models.py b/backend/app/material/models.py new file mode 100644 index 0000000000000000000000000000000000000000..f1abdc27dcaf20659ced391cc370a30a3fed5a66 --- /dev/null +++ b/backend/app/material/models.py @@ -0,0 +1,104 @@ +# coding=utf-8 + + +from django.db import models + +# Create your models here. +from dvadmin.utils.models import CoreModel, SoftDeleteModel, table_prefix +from dvadmin.system.models import Users + + +class MaterialBaseInfo(CoreModel, SoftDeleteModel): + type = models.CharField(max_length=255, verbose_name="物料类型", help_text="物料类型") + name = models.CharField(unique=True, max_length=255, verbose_name="物料名称", help_text="物料名称") + code = models.CharField(unique=True, max_length=255, verbose_name="物料编码", help_text="物料编码") + specification = models.CharField(max_length=255, null=True, blank=True, verbose_name="物料规格", help_text="物料规格") + storage_location = models.CharField(max_length=255, null=True, blank=True, verbose_name="存放位置", help_text="存放位置") + safety_stock = models.IntegerField(default=0, verbose_name="安全库存", help_text="安全库存") + + class Meta: + db_table = table_prefix + "material" + verbose_name = "物料基础信息管理" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "-update_datetime") + + +class MaterialInventory(CoreModel): + number = models.IntegerField(default=0, verbose_name="剩余库存", help_text="剩余库存") + SAFETY_STOCK_LEVEL = ( + (0, "安全库存"), + (1, "及时补充库存"), + (2, "紧急补充库存"), + (3, "无库存"), + ) + safety_stock_level = models.IntegerField(default=3, choices=SAFETY_STOCK_LEVEL, null=True, blank=True, + verbose_name="剩余安全库存等级", help_text="剩余安全库存等级") + code = models.OneToOneField( + to="MaterialBaseInfo", + on_delete=models.PROTECT, + # to_field="id", + verbose_name="关联物料编码", + help_text="关联物料编码", + default=None, + related_name="inventory", + ) + + class Meta: + db_table = table_prefix + "material_inventory" + verbose_name = "物料库存数量管理" + verbose_name_plural = verbose_name + ordering = ("number", "-update_datetime") + + +class MaterialRecords(CoreModel): + RECORD_TYPE = ( + (0, "入库"), + (1, "出库"), + ) + record_type = models.IntegerField(choices=RECORD_TYPE, null=True, blank=True, verbose_name="出入库类型", help_text="出入库类型") + receive_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="领用人", help_text="领用人") + number = models.IntegerField(null=True, blank=True, verbose_name="数量", help_text="数量") + code = models.ForeignKey( + to="MaterialBaseInfo", + on_delete=models.PROTECT, + # to_field="code", + verbose_name="关联物料编码", + help_text="关联物料编码", + related_name="records", + ) + + class Meta: + db_table = table_prefix + "material_number_records" + verbose_name = "物料出入库记录" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "-update_datetime") + + +class MaterialApplyFor(CoreModel): + + examine_user = models.CharField(max_length=255, null=True, blank=True, verbose_name="审核人", help_text="审核人") + EXAMINE_TYPE = ( + (0, "审核通过"), + (1, "待审核"), + (2, "审核不通过"), + (3, "已撤回"), + ) + examine_status = models.IntegerField(default=1, choices=EXAMINE_TYPE, verbose_name="审核状态", help_text="审核状态") + examine_opinion = models.CharField(max_length=255, null=True, blank=True, verbose_name="审核意见", help_text="审核意见") + number = models.IntegerField(null=True, verbose_name="申请数量", help_text="申请数量") + reason = models.CharField(max_length=255, null=True, blank=True, verbose_name="申请理由", help_text="申请理由") + url = models.CharField(max_length=255, null=True, blank=True, verbose_name="申请链接", help_text="申请链接") + code = models.ForeignKey( + to="MaterialBaseInfo", + on_delete=models.PROTECT, + # to_field="code", + verbose_name="关联物料编码", + help_text="关联物料编码", + related_name="apply_for", + ) + + class Meta: + db_table = table_prefix + "material_apply_for" + verbose_name = "物料申请" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "-update_datetime") diff --git a/backend/app/material/serializers.py b/backend/app/material/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..aac8161fcd553e91db4613ff9ac355469906b98b --- /dev/null +++ b/backend/app/material/serializers.py @@ -0,0 +1,197 @@ +# coding=utf-8 + +from application import dispatch +from rest_framework import serializers +from dvadmin.utils.json_response import DetailResponse, ErrorResponse + + +from app.material.models import MaterialBaseInfo, MaterialInventory, MaterialRecords, MaterialApplyFor +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.system.models import Users + +RECORD_TYPE = {} +for i in MaterialRecords.RECORD_TYPE: + RECORD_TYPE[i[0]] = i[1] + + +EXAMINE_TYPE = {} +for i in MaterialApplyFor.EXAMINE_TYPE: + EXAMINE_TYPE[i[0]] = i[1] + + +SAFETY_STOCK_LEVEL = {} +for i in MaterialInventory.SAFETY_STOCK_LEVEL: + SAFETY_STOCK_LEVEL[i[0]] = i[1] + + +class MaterialBaseInfoSerializer(CustomModelSerializer): + + class Meta: + model = MaterialBaseInfo + fields = "__all__" + + +class MaterialBaseInfoUpdateSerializer(CustomModelSerializer): + type = serializers.CharField(read_only=True) + code = serializers.CharField(read_only=True) + + class Meta: + model = MaterialBaseInfo + fields = "__all__" + + +class MaterialBaseInfoExportSerializer(CustomModelSerializer): + type = serializers.CharField(read_only=True) + name = serializers.CharField(read_only=True) + code = serializers.CharField(read_only=True) + user_name = serializers.CharField(source='creator.name', read_only=True) + + class Meta: + model = MaterialBaseInfo + fields = "__all__" + + +class MaterialInventoryCreateSerializer(CustomModelSerializer): + + class Meta: + model = MaterialInventory + fields = "__all__" + + +class MaterialInventoryExportSerializer(CustomModelSerializer): + type = serializers.CharField(source='code.type', read_only=True) + name = serializers.CharField(source='code.name', read_only=True) + code = serializers.CharField(source='code.code', read_only=True) + specification = serializers.CharField(source='code.specification', read_only=True) + safety_stock = serializers.CharField(source='code.safety_stock', read_only=True) + storage_location = serializers.CharField(source='code.storage_location', read_only=True) + user_name = serializers.CharField(source='creator.name', read_only=True) + safety_stock_level = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = MaterialInventory + fields = "__all__" + + def get_safety_stock_level(self, instance): + if not hasattr(instance, "safety_stock_level") or instance.safety_stock_level not in SAFETY_STOCK_LEVEL: + return None + + return SAFETY_STOCK_LEVEL[instance.safety_stock_level] + + +class MaterialInventorySerializer(CustomModelSerializer): + type = serializers.CharField(source='code.type', read_only=True) + name = serializers.CharField(source='code.name', read_only=True) + code = serializers.CharField(source='code.code', read_only=True) + specification = serializers.CharField(source='code.specification', read_only=True) + + class Meta: + model = MaterialInventory + fields = "__all__" + + +class MaterialRecordsUpdateSerializer(CustomModelSerializer): + + class Meta: + model = MaterialRecords + fields = "__all__" + + +class MaterialRecordsExportSerializer(CustomModelSerializer): + type = serializers.CharField(source='code.type', read_only=True) + name = serializers.CharField(source='code.name', read_only=True) + code = serializers.CharField(source='code.code', read_only=True) + specification = serializers.CharField(source='code.specification', read_only=True) + safety_stock = serializers.CharField(source='code.safety_stock', read_only=True) + storage_location = serializers.CharField(source='code.storage_location', read_only=True) + user_name = serializers.CharField(source='creator.name', read_only=True) + record_type = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = MaterialRecords + fields = "__all__" + + def get_record_type(self, instance): + if not hasattr(instance, "record_type") or instance.record_type not in RECORD_TYPE: + return None + + return RECORD_TYPE[instance.record_type] + + +class MaterialRecordsSerializer(CustomModelSerializer): + type = serializers.CharField(source='code.type', read_only=True) + name = serializers.CharField(source='code.name', read_only=True) + code = serializers.CharField(source='code.code', read_only=True) + specification = serializers.CharField(source='code.specification', read_only=True) + + class Meta: + model = MaterialRecords + fields = "__all__" + + +class MaterialApplyForSerializer(CustomModelSerializer): + type = serializers.CharField(source='code.type', read_only=True) + name = serializers.CharField(source='code.name', read_only=True) + code = serializers.CharField(source='code.code', read_only=True) + specification = serializers.CharField(source='code.specification', read_only=True) + # 前端重新申请时展示使用 + material = serializers.CharField(source='code.id', read_only=True) + examine_user_name = serializers.SerializerMethodField(method_name='get_examine_user_name', read_only=True) + + class Meta: + model = MaterialApplyFor + fields = "__all__" + + def get_examine_user_name(self, instance): + if not hasattr(instance, "examine_user"): + return None + queryset = ( + Users.objects.filter(id=instance.examine_user) + .values_list("name", flat=True) + .first() + ) + if queryset: + return queryset + return None + + +class MaterialApplyForExportSerializer(CustomModelSerializer): + type = serializers.CharField(source='code.type', read_only=True) + name = serializers.CharField(source='code.name', read_only=True) + code = serializers.CharField(source='code.code', read_only=True) + specification = serializers.CharField(source='code.specification', read_only=True) + safety_stock = serializers.CharField(source='code.safety_stock', read_only=True) + storage_location = serializers.CharField(source='code.storage_location', read_only=True) + user_name = serializers.CharField(source='creator.name', read_only=True) + examine_user_name = serializers.SerializerMethodField(method_name='get_examine_user_name', read_only=True) + examine_status = serializers.SerializerMethodField(method_name='get_examine_status', read_only=True) + + class Meta: + model = MaterialApplyFor + fields = "__all__" + + def get_examine_user_name(self, instance): + if not hasattr(instance, "examine_user"): + return None + queryset = ( + Users.objects.filter(id=instance.examine_user) + .values_list("name", flat=True) + .first() + ) + if queryset: + return queryset + return None + + def get_examine_status(self, instance): + if not hasattr(instance, "examine_status") or instance.examine_status not in EXAMINE_TYPE: + return None + + return EXAMINE_TYPE[instance.examine_status] + + +class MaterialApplyForCreateSerializer(CustomModelSerializer): + + class Meta: + model = MaterialApplyFor + fields = "__all__" + diff --git a/backend/app/material/tests.py b/backend/app/material/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/backend/app/material/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/app/material/urls.py b/backend/app/material/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..53c4e69dcce74845dbef2390842ffbe9388002bc --- /dev/null +++ b/backend/app/material/urls.py @@ -0,0 +1,21 @@ +# coding=utf-8 + +from rest_framework.routers import SimpleRouter +from django.urls import path +from django.http import HttpResponse, JsonResponse +from dvadmin.system.views.clause import PrivacyView, TermsServiceView + +from .views import MaterialBaseInfoViewSet, MaterialInventoryViewSet, MaterialRecordsViewSet, MaterialApplyForViewSet + +router = SimpleRouter() +# 这里进行注册路径,并把视图关联上,这里的api地址以视图名称为后缀,这样方便记忆api/CrudDemoModelViewSet +router.register("base_info", MaterialBaseInfoViewSet) +router.register("inventory", MaterialInventoryViewSet) +router.register("operation_log", MaterialRecordsViewSet) +router.register("apply_for", MaterialApplyForViewSet) + +urlpatterns = [ + +] + +urlpatterns += router.urls diff --git a/backend/app/material/views.py b/backend/app/material/views.py new file mode 100644 index 0000000000000000000000000000000000000000..d0ed4f5c20b2d72d45b52cdae7987300ca085af3 --- /dev/null +++ b/backend/app/material/views.py @@ -0,0 +1,628 @@ +import os +import datetime + +from django.db import transaction +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.filters import BaseFilterBackend + +from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse +from dvadmin.utils.viewset import CustomModelViewSet +from dvadmin.utils.permission import CustomPermission +from dvadmin.utils.filters import DataLevelPermissionsFilter, CoreModelFilterBankend +from dvadmin.system.models import Users, Role, MenuButton, RoleMenuButtonPermission + +from dvadmin.system.views.message_center import websocket_push as message_push, MessageCenterCreateSerializer + +from app.material.serializers import * + + +def websocket_push_designated_user(user_id, title, message, material_type=None, material_name=None, number=None): + """ + 主动推送消息 + """ + try: + data = MessageCenterCreateSerializer(data={ + 'title': title, + 'target_type': 0, + 'target_user': [user_id], + 'content': f'

{message}

' + f'

物料类型:{material_type}

' + f'

物料名称:{material_name}

' + f'

申请数量:{number}

' + }) + data.is_valid(raise_exception=True) + data.save() + + message_push(user_id, + message={ + "sender": 'system', + "contentType": 'MATERIAL', + "content": message, + "unread": 0} + ) + + except: + pass + + +def websocket_push_designated_role(role, title, message): + """ + 主动推送消息 + """ + try: + data = MessageCenterCreateSerializer(data={ + 'title': title, + 'target_type': 1, + 'target_role': role, + 'content': f'

{message}

' + }) + data.is_valid(raise_exception=True) + data.save() + for user in GetPermissionUsers().get_permission_users(role): + message_push(user, + message={ + "sender": 'system', + "contentType": 'MATERIAL', + "title": title, + "content": message, + "unread": 1} + ) + except: + pass + + +class GetPermissionUsers: + + def get_permission_role(self, url, method): + queryset = RoleMenuButtonPermission.objects.filter(menu_button__api=url, menu_button__method=method) + + return queryset.values_list("role_id", flat=True) + + def get_permission_users(self, role_id): + users = Users.objects.filter(role__in=role_id, is_superuser=False, is_active=True) + + return users.values_list("id", flat=True) + + def get_users(self, api_url, method): + roles = self.get_permission_role(api_url, method) + if not roles: + return [] + + return self.get_permission_users(roles) + + +class CustomModelFilterBanked(BaseFilterBackend): + """ + 自定义过滤器 + """ + + def filter_queryset(self, request, queryset, view): + + # 库存模型过滤已经删数据 + if isinstance(view, MaterialInventoryViewSet): + queryset = queryset.exclude(code__is_deleted=True) + + if (query_material_type := request.query_params.get('type', '')) != '': + queryset = queryset.filter(code__type__icontains=query_material_type) + + if (query_material_name := request.query_params.get('name', '')) != '': + queryset = queryset.filter(code__name__icontains=query_material_name) + + if query_creator_name := request.query_params.get('creator_name', '') != '': + queryset = queryset.filter(creator__name=query_creator_name) + + return queryset + + +class MaterialBaseInfoViewSet(CustomModelViewSet): + """ + 物料基本信息接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + get_material_type:获取所有物料类型 + get_material_name:获取所有物料类型名称 + get_id:根据id获取物料信息 + """ + queryset = MaterialBaseInfo.objects.all() + serializer_class = MaterialBaseInfoSerializer + update_serializer_class = MaterialBaseInfoUpdateSerializer + export_field_label = { + 'type': "物料类型", + 'name': "物料名称", + 'code': "物料编码", + 'safety_stock': "安全库存", + 'storage_location': "存放位置", + 'user_name': "创建人", + 'create_datetime': "创建时间", + 'update_datetime': "更新时间", + } + export_serializer_class = MaterialBaseInfoExportSerializer + + @transaction.atomic + def create(self, request, *args, **kwargs): + # 获取物料类型 + material_type = request.data.get('type', None) + + if material_type is None: + return ErrorResponse(msg='缺少type(物料类型)参数') + + try: + if int(request.data.get('safety_stock', 0)) <= 0: + return ErrorResponse(msg='安全库存必须大于0') + + except: + return ErrorResponse(msg='type参数类型错误') + + # 当天 00:00:00:00 - 23:59:59:999999 + today = datetime.date.today() + today = datetime.datetime(today.year, today.month, today.day, 0, 0, 0, 0) + tomorrow = datetime.datetime(today.year, today.month, today.day, 23, 59, 59, 999999) + # tomorrow = today + datetime.timedelta(hours=23, minutes=59, seconds=59) + + # 删选当天创建的数据, 删除的数据不进行过滤 + count = MaterialBaseInfo.objects.filter(create_datetime__range=(today, tomorrow), is_deleted=None) + + # 生成物料编码 + material_code = str(count.count() + 1).zfill(4) + + # 补充物料编码 + request.data['code'] = f"HY-{today.strftime('%Y%m%d')}-{material_code}" + + # 判断是否有删除记录 + queryset = MaterialBaseInfo.objects.filter(name=request.data["name"], is_deleted=True) + if queryset.count() > 0: + + instance = queryset[0] + + """ + 没有设置的字段设置为None + """ + data = {} + for key in ["type", "name", "safety_stock", "storage_location", "safety_stock"]: + data[key] = request.data.get(key, None) + + for key, value in data.items(): + instance.__setattr__(key, value) + + instance.is_deleted = False # 设置未删除 + instance.save() + + return DetailResponse(data=instance.DICT_DATA, msg="新增成功") + + data = super().create(request, *args, **kwargs) + + # 关联属性 + serializer = MaterialInventoryCreateSerializer(request.user, + data={ + "code": data.data['data']['id'], + "number": 0, + "safety_stock_level": 3 + }, + request=request) + + serializer.is_valid(raise_exception=True) + serializer.create(serializer.validated_data) + + return data + + def update(self, request, *args, **kwargs): + """ + 更新时检查是否变更了安全库存数量 + """ + try: + if (safety_stock := int(request.data.get('safety_stock', 0))) <= 0: + return ErrorResponse(msg='安全库存必须大于0') + + except: + return ErrorResponse(msg='type参数类型错误') + + # 先获取原有数据 + instance = self.get_object() + + # 保存变更数据 + data = super().update(request, *args, **kwargs) + + # 变更安全库存数量需要变更安全库存等级 + if safety_stock != instance.safety_stock: + # 获取库存数据对象 + inventory_obj = MaterialInventory.objects.get(code=instance.id) + # 变更等级 + inventory_obj.safety_stock_level = MaterialInventoryViewSet.inventory_level(inventory_obj.number, + safety_stock) + # 保存 + inventory_obj.save() + + if getattr(inventory_obj, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + inventory_obj._prefetched_objects_cache = {} + + return data + + def list(self, request, *args, **kwargs): + """" + 前端code字段多页面共用,查询时返回关联的id字段 + """ + if code := request.query_params.get("code", ""): + queryset = self.get_queryset().filter(id=code) + + 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) + return SuccessResponse(data=serializer.data) + + return super().list(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + """ + 物料删除 + """ + instance = self.get_object() + + if instance.inventory.number > 0: + return ErrorResponse(msg=f'该物料还有库存: {instance.inventory.number},禁止删除!!!') + + instance.delete() + return DetailResponse(data=[], msg="删除成功") + + @action(methods=['delete'], detail=False, permission_classes=[CustomPermission]) + def multiple_delete(self, request, *args, **kwargs): + + request_data = request.data + keys = request_data.get('keys', None) + if not keys: + return ErrorResponse(msg="未获取到keys字段") + + instances = [] + err_msg = [] + for instance in self.get_queryset().filter(id__in=keys): + instances.append(instance) + if instance.inventory.number > 0: + err_msg.append(f'物料编码:{instance.code}还有库存: {instance.inventory.number},禁止删除!!!') + # return ErrorResponse(msg=f'该物料:{instance.name}还有库存: {instance.inventory.number},禁止删除!!!') + if err_msg: + return ErrorResponse(msg=err_msg) + + for instance in instances: + instance.delete() + + return DetailResponse(data=[], msg=f"{len(instances)}条记录已删除") + + @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) + def get_material_type(self, request, *args, **kwargs): + data = [] + info = [] + queryset = MaterialBaseInfo.objects.filter(is_deleted=None).order_by('type') + for value in queryset.values('id', 'type'): + if value['type'] not in info: + data.append(value) + info.append(value['type']) + return SuccessResponse(data=data) + + @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) + def get_material_type_name(self, request, *args, **kwargs): + queryset = MaterialBaseInfo.objects.filter(is_deleted=None).order_by('name') + if 'type' in request.query_params: + queryset = queryset.filter(type=request.query_params['type']) + + return SuccessResponse(data=queryset.values('id', "type", 'name', 'code')) + + @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated], url_path='byIds') + def get_id(self, request, *args, **kwargs): + queryset = MaterialBaseInfo.objects.filter(is_deleted=None).filter(id__in=list(request.query_params.values())) + serializer = self.get_serializer(queryset, many=True) + return DetailResponse(data=serializer.data) + + +class MaterialInventoryViewSet(CustomModelViewSet): + """ + 物料库存管理接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + get_all_user:获取所有用户 + """ + queryset = MaterialInventory.objects.all() + serializer_class = MaterialInventorySerializer + extra_filter_class = [CoreModelFilterBankend, DataLevelPermissionsFilter, CustomModelFilterBanked] + export_field_label = { + 'type': "物料类型", + 'name': "物料名称", + 'code': "物料编码", + "number": "剩余库存", + 'safety_stock': "安全库存", + "safety_stock_level": "剩余库存等级", + 'storage_location': "存放位置", + 'user_name': "创建人", + 'create_datetime': "创建时间", + 'update_datetime': "更新时间", + } + export_serializer_class = MaterialInventoryExportSerializer + + def create(self, request, *args, **kwargs): + + return ErrorResponse(msg='禁止手动创建库存记录') + + def update(self, request, *args, **kwargs): + if request.data.get('number', None) is None: + return ErrorResponse(msg='缺少number(数量)参数') + + if request.data.get('record_type', None) is None: + return ErrorResponse(msg='缺少record_type(操作类型)参数') + + try: + number = int(request.data['number']) + if request.data['number'] <= 0: + return ErrorResponse(msg='number(数量)参数必须大于0') + except ValueError: + return ErrorResponse(msg='number(数量)参数必须为整数') + + ######################################################################################################### + instance = self.get_object() + + # 判断是入库还是出库 + if request.data['record_type'] == 0: + instance.number += number + else: + + if instance.number - number < 0: + return ErrorResponse(msg=f'库存数量不足, 剩余库存数量:{instance.number}') + if request.data.get('receive_name', None) is None: + return ErrorResponse(msg='缺少receive_name(领用人)参数') + + instance.number -= number + + # 库存等级 + instance.safety_stock_level = self.inventory_level(instance.number, instance.code.safety_stock) + ######################################################################################################### + + # 操作记录 + request.data['code'] = instance.code.id + serializer = MaterialRecordsUpdateSerializer(request.user, data=request.data, request=request) + serializer.is_valid(raise_exception=True) + serializer.create(serializer.validated_data) + ######################################################################################################### + # 保存数据 + instance.save() + ######################################################################################################### + + partial = kwargs.pop('partial', False) + serializer = self.get_serializer(instance, data=request.data, request=request, partial=partial) + + serializer.is_valid(raise_exception=True) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + return DetailResponse(data=serializer.data, msg="更新成功") + + @staticmethod + def inventory_level(inventory, safety_stock): + level = 3 + if inventory >= safety_stock * 2: + level = 0 + elif safety_stock <= inventory <= safety_stock * 2: + level = 1 + elif 1 <= inventory <= safety_stock: + level = 2 + return level + + # path('inventory/get_all_user/', MaterialInventoryViewSet.as_view({'get': 'get_all_user'})), + + @action(methods=['get'], detail=False, permission_classes=[]) + def get_all_user(self, request, *args, **kwargs): + from dvadmin.system.models import Users + + return SuccessResponse(data=Users.objects.all().exclude(is_superuser=True).values('id', 'name', 'dept__parent')) + + +class MaterialRecordsViewSet(CustomModelViewSet): + """ + 物料出入库记录接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + """ + queryset = MaterialRecords.objects.all() + serializer_class = MaterialRecordsSerializer + extra_filter_class = [CoreModelFilterBankend, DataLevelPermissionsFilter, CustomModelFilterBanked] + export_field_label = { + 'type': "物料类型", + 'name': "物料名称", + 'code': "物料编码", + 'safety_stock': "安全库存", + 'storage_location': "存放位置", + "number": "数量", + "record_type": "操作类型", + "receive_name": "领用人", + 'user_name': "创建人", + 'create_datetime': "创建时间", + 'update_datetime': "更新时间", + } + export_serializer_class = MaterialRecordsExportSerializer + + +class MaterialApplyForViewSet(CustomModelViewSet): + """ + 物料申请接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + revocation:撤销申请 + examine:审核申请 + dismissal:驳回申请 + """ + + queryset = MaterialApplyFor.objects.all() + serializer_class = MaterialApplyForSerializer + create_serializer_class = MaterialApplyForCreateSerializer + extra_filter_class = [CoreModelFilterBankend, DataLevelPermissionsFilter, CustomModelFilterBanked] + export_field_label = { + 'type': "物料类型", + 'name': "物料名称", + 'code': "物料编码", + 'safety_stock': "安全库存", + 'storage_location': "存放位置", + "number": "申请数量", + 'user_name': "申请人", + 'reason': "申请原因", + "examine_status": "审核状态", + "examine_user_name": "审核人", + 'url': "申请链接", + 'create_datetime': "创建时间", + 'update_datetime': "更新时间", + } + export_serializer_class = MaterialApplyForExportSerializer + + def create(self, request, *args, **kwargs): + + material_id = request.data.get('id', -1) + if material_id is None: + return ErrorResponse(msg='申请的物料不存在,请先创建物料, 再提交申请') + if material_id == -1: + return ErrorResponse(msg='缺少id(主键)参数') + + request.data['code'] = material_id + + # 删除该属性,前端用于表单申请字段,后端实际并无该字段 + # del request.data['material'] + + data = super().create(request, *args, **kwargs) + + websocket_push_designated_role( + GetPermissionUsers().get_permission_role('/api/material/apply_for/operation/examine/', 2), + title='物料申请通知', + message=f'{request.user.name}提交了新的物料申请, 请及时查看', + ) + + return DetailResponse(data=data.data, msg="申请成功") + + def update(self, request, *args, **kwargs): + + if request.data['examine_status'] not in [1, 2, 3]: + return ErrorResponse(msg='当前状态不允许重新申请') + + request.data['examine_status'] = 1 # 设置为待审核状态 + request.data['examine_opinion'] = None # 设置审核意见为None + request.data['examine_user'] = None # 设置审核人为None + + partial = kwargs.pop('partial', False) + + instance = self.get_object() + if instance.creator.id != request.user.id: + return ErrorResponse(msg='只能由“申请用户”重新修改提交') + + serializer = self.get_serializer(instance, data=request.data, request=request, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + websocket_push_designated_role( + GetPermissionUsers().get_permission_role('/api/material/apply_for/operation/examine/', 2), + title='物料重新提交通知', + message=f'{request.user.name}重新提交了物料申请, 请及时查看', + ) + + return DetailResponse(data=serializer.data, msg="重新提交成功") + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + + # 超级管理员允许删除 + if instance.examine_status != 1 and request.user.is_superuser is False: + return ErrorResponse(msg='当前状态不允许删除') + + # 判断是否为申请人或者超级管理员 + if instance.creator.id != request.user.id and request.user.is_superuser is False: + return ErrorResponse(msg='申请单必须由申请人删除') + + return super().destroy(request, *args, **kwargs) + + @action(methods=['put'], detail=False, permission_classes=[CustomPermission], url_path='operation/revocation') + def revocation(self, request, *args, **kwargs): + + queryset = self.queryset.get(id=request.data.get('id', None)) + if queryset.examine_status != 1: + return ErrorResponse(msg='该申请单状态不处于“待审核”,无法撤销') + + # 判断是否为申请人或者超级管理员 + if queryset.creator.id != request.user.id and request.user.is_superuser is False: + return ErrorResponse(msg='申请单必须由申请人撤销') + + queryset.examine_status = 3 + queryset.save() + + return DetailResponse(msg='撤销成功') + + @action(methods=['put'], detail=False, permission_classes=[CustomPermission], url_path='operation/examine') + def examine(self, request, *args, **kwargs): + queryset = self.queryset.filter(id__in=list(request.data.get('ids', []))) + if queryset.count() == 0: + return ErrorResponse(msg='审核失败, 没有找到该记录') + + for obj in queryset: + if obj.examine_status != 1: + return ErrorResponse(msg=f'该申请单不处于“待审核”状态,物料名称: {obj.code.name}') + + obj.examine_status = 0 + obj.examine_opinion = "通过" + obj.examine_user = request.user.id + + # 一条数据不满足则不保存 + for obj in queryset: + websocket_push_designated_user(obj.creator.id, + title='物料申请审核通知', + message=f'您提交的物料申请已通过审核!', + material_type=obj.code.type, + material_name=obj.code.name, + number=obj.number, + ) + obj.save() + + return DetailResponse(msg=f'审核成功') + + @action(methods=['put'], detail=False, permission_classes=[CustomPermission], url_path='operation/dismissal') + def dismissal(self, request, *args, **kwargs): + queryset = self.queryset.filter(id__in=list(request.data.get('ids', []))) + if queryset.count() == 0: + return ErrorResponse(msg='审核失败, 没有找到该记录') + + for obj in queryset: + if obj.examine_status != 1: + return ErrorResponse(msg=f'该申请单不处于“待审核”状态,物料名称: {obj.code.name}') + + obj.examine_status = 2 + obj.examine_user = request.user.id + obj.examine_opinion = request.data['examine_opinion'] + + # 一条数据不满足则不保存 + for obj in queryset: + websocket_push_designated_user(obj.creator.id, + title='物料申请审核通知', + message=f'您提交的物料申请已被驳回!驳回原因:{request.data["examine_opinion"]}!', + material_type=obj.code.type, + material_name=obj.code.name, + number=obj.number + ) + + obj.save() + + return DetailResponse(msg='驳回申请成功') diff --git a/backend/app/test_tool/__init__.py b/backend/app/test_tool/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/test_tool/admin.py b/backend/app/test_tool/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/backend/app/test_tool/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/app/test_tool/apps.py b/backend/app/test_tool/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..395cfd214dd680f4d586d448972264d1d109c0c3 --- /dev/null +++ b/backend/app/test_tool/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TestAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app.test_tool' diff --git a/backend/app/test_tool/migrations/__init__.py b/backend/app/test_tool/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/test_tool/models.py b/backend/app/test_tool/models.py new file mode 100644 index 0000000000000000000000000000000000000000..f93c30bfdb76d7236f0776dc71e8059a6b8d9b71 --- /dev/null +++ b/backend/app/test_tool/models.py @@ -0,0 +1,137 @@ +# coding=utf-8 + +import hashlib +import os +import datetime + +from django.db import models + +# Create your models here. +from dvadmin.utils.models import CoreModel, SoftDeleteModel, table_prefix + + +def media_file_name(instance, filename): + h = instance.md5sum + return os.path.join("files", f"{datetime.date.today()}", h[-8:], filename) + + +class TestToolsFileListModel(CoreModel): + name = models.CharField(max_length=200, null=True, blank=True, verbose_name="名称", help_text="名称") + url = models.FileField(upload_to=media_file_name, null=True, blank=True, ) + file_url = models.CharField(max_length=255, blank=True, verbose_name="文件地址", help_text="文件地址") + engine = models.CharField(max_length=100, default='local', blank=True, verbose_name="引擎", help_text="引擎") + mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型") + size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小") + md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5") + version = models.CharField(max_length=36, blank=True, verbose_name="工具版本", help_text="工具版本") + update_comment = models.TextField(blank=True, verbose_name="工具更新说明", help_text="工具更新说明") + forced_update = models.BooleanField(default=False, blank=True, verbose_name="强制更新", help_text="强制更新") + platform = models.CharField(max_length=36, blank=True, verbose_name="工具平台", help_text="工具平台") + status = models.BooleanField(default=True, verbose_name="启用状态", help_text="启用状态") + 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 + 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 + if not self.file_url: + url = media_file_name(self, self.name) + self.file_url = f'media/{url}' + super(TestToolsFileListModel, self).save(*args, **kwargs) + + class Meta: + db_table = table_prefix + "test_tools_list" + verbose_name = "产测工具管理" + verbose_name_plural = verbose_name + ordering = ("-version",) + # constraints = [ + # models.UniqueConstraint(fields=['version'], name='tool_version_unique_together', + # violation_error_message='当前版本已经存在,请勿重复上传') + # ] + + +class TestToolLicenseModel(SoftDeleteModel, CoreModel): + license = models.CharField(max_length=200, verbose_name="license", help_text="license") + uuid = models.CharField(max_length=200, unique=True, verbose_name="uuid", help_text="uuid") + comment = models.TextField(max_length=999, blank=True, verbose_name="备注", help_text="备注") + status = models.BooleanField(default=True, verbose_name="启用状态", help_text="启用状态") + + class Meta: + db_table = table_prefix + "test_tool_license" + verbose_name = "产测工具授权管理" + verbose_name_plural = verbose_name + ordering = ("-create_datetime",) + constraints = [ + models.UniqueConstraint(fields=['uuid'], name='tool_uuid_unique_together', + violation_error_message='当前uuid已经存在,请勿重复上传') + ] + + +class TestToolLogModel(CoreModel): + name = models.CharField(max_length=200, null=True, blank=True, verbose_name="文件名称", help_text="文件名称") + url = models.FileField(upload_to=media_file_name, null=True, blank=True, ) + file_url = models.CharField(max_length=255, blank=True, verbose_name="文件地址", help_text="文件地址") + engine = models.CharField(max_length=100, default='local', blank=True, verbose_name="引擎", help_text="引擎") + mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型") + size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小") + md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5") + problem_description = models.CharField(max_length=255, null=True, blank=True, verbose_name="问题描述", help_text="问题描述") + platform = models.CharField(max_length=36, blank=True, verbose_name="工具平台", help_text="工具平台") + version = models.CharField(max_length=36, blank=True, verbose_name="工具版本", help_text="工具版本") + mes_info = models.CharField(max_length=255, null=True, blank=True, verbose_name="MES信息", help_text="MES信息") + os_info = models.TextField(blank=True, verbose_name="系统信息", help_text="系统信息") + network_info = models.TextField(blank=True, verbose_name="网络信息", help_text="网络信息") + cpu_info = models.TextField(blank=True, verbose_name="CPU信息", help_text="CPU信息") + memory_info = models.TextField(blank=True, verbose_name="内存信息", help_text="内存信息") + disk_info = models.TextField(blank=True, verbose_name="磁盘信息", help_text="磁盘信息") + script = models.TextField(blank=True, verbose_name="使用脚本", help_text="使用脚本") + 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='文件类型') + + class Meta: + db_table = table_prefix + "test_tool_log" + verbose_name = "产测工具log" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "-update_datetime") + + def save(self, *args, **kwargs): + 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 + if not self.file_url: + url = media_file_name(self, self.name) + self.file_url = f'media/{url}' + super(TestToolLogModel, self).save(*args, **kwargs) diff --git a/backend/app/test_tool/serializers.py b/backend/app/test_tool/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..15613da5d873a474647ee0e12b74774f516dcd67 --- /dev/null +++ b/backend/app/test_tool/serializers.py @@ -0,0 +1,27 @@ +# coding=utf-8 + + +from app.test_tool.models import TestToolsFileListModel, TestToolLicenseModel, TestToolLogModel +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.system.views.file_list import FileSerializer + + +class TestToolModelSerializer(FileSerializer): + + class Meta: + model = TestToolsFileListModel + fields = "__all__" + + +class TestToolLicenseModelSerializer(CustomModelSerializer): + + class Meta: + model = TestToolLicenseModel + fields = "__all__" + + +class TestToolLogModelSerializer(FileSerializer): + + class Meta: + model = TestToolLogModel + fields = "__all__" diff --git a/backend/app/test_tool/tests.py b/backend/app/test_tool/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/backend/app/test_tool/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/app/test_tool/urls.py b/backend/app/test_tool/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a9a02a4718904c70b330563a70f404e2d13e7e3c --- /dev/null +++ b/backend/app/test_tool/urls.py @@ -0,0 +1,24 @@ +# coding=utf-8 + +from rest_framework.routers import SimpleRouter +from django.urls import path +from django.http import HttpResponse, JsonResponse +from dvadmin.system.views.clause import PrivacyView, TermsServiceView + +from .views import ToolFileViewSet, ToolLicenseViewSet, ToolLogViewSet + +router = SimpleRouter() +# 这里进行注册路径,并把视图关联上,这里的api地址以视图名称为后缀,这样方便记忆api/CrudDemoModelViewSet +router.register("testTool", ToolFileViewSet) +router.register("license", ToolLicenseViewSet) +router.register("log", ToolLogViewSet) + +urlpatterns = [ + + path('testTool/latestVersion/', ToolFileViewSet.as_view({'get': 'get_latest_version'})), + path('testTool/forcedUpdateVersion/', ToolFileViewSet.as_view({'get': 'get_forced_update_version'})), + path('testTool/historyUpdateNote/', ToolFileViewSet.as_view({'get': 'get_history_update_note'})), + path('license/get_license/', ToolLicenseViewSet.as_view({'get': 'get_license'})), +] + +urlpatterns += router.urls diff --git a/backend/app/test_tool/views.py b/backend/app/test_tool/views.py new file mode 100644 index 0000000000000000000000000000000000000000..dad3c82c2c6fbe94ac2d22d650ec37f034e3f7a4 --- /dev/null +++ b/backend/app/test_tool/views.py @@ -0,0 +1,297 @@ +import os + + +from cryptography.fernet import Fernet + +from application import dispatch +from app.test_tool.models import TestToolsFileListModel, TestToolLicenseModel, TestToolLogModel +from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse + +from dvadmin.utils.viewset import CustomModelViewSet +from dvadmin.utils.filters import DataLevelPermissionsFilter, CoreModelFilterBankend + +from dvadmin.utils.permission import CustomPermission + +from app.test_tool.serializers import TestToolModelSerializer, TestToolLicenseModelSerializer, TestToolLogModelSerializer + + +class ToolFileViewSet(CustomModelViewSet): + """ + 产测工具管理接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + """ + + queryset = TestToolsFileListModel.objects.all() + serializer_class = TestToolModelSerializer + # filter_fields = ['name', 'version', 'update_comment', 'platform', 'status'] + # filter_fields = '__all__' # 某些字段不允许模塑搜索 + permission_classes = [] + custom_extra_filter_class = [CoreModelFilterBankend] + + def filter_queryset(self, queryset): + for backend in set(set(self.filter_backends) | set(self.custom_extra_filter_class or [])): + + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def get_latest_version(self, request): + """ + 查询最新的工具版本信息 + """ + + if request.query_params.get('platform', None) is None: + return ErrorResponse(data=[{"platform:": "windows | macOSX"}], msg="platform字段为空") + + queryset = self.filter_queryset(self.get_queryset()).exclude(status=False) + + if not queryset: + return DetailResponse(data=[], msg="暂无新版本") + + serializer = self.get_serializer(queryset[0], request=request) + + return DetailResponse(data=serializer.data, msg="获取成功") + + def get_forced_update_version(self, request): + """ + 查询最新的强制更新工具版本信息 + """ + if request.query_params.get('platform', None) is None: + return ErrorResponse(data=[], msg="platform字段为空>>>windows | macOSX") + + queryset = self.filter_queryset(self.get_queryset()).exclude(status=False).exclude(forced_update=False) + # queryset = ToolFileViewSet.queryset.filter(forced_update=True, + # platform=request.query_params.get('platform'), + # status=True) + if not queryset: + return DetailResponse(data=[], msg="暂无强制更新版本") + + serializer = self.get_serializer(queryset[0], request=request) + + return DetailResponse(data=serializer.data, msg="获取成功") + + def get_history_update_note(self, request): + """ + 查询历史更新说明 + """ + + if request.query_params.get('platform', None) is None: + return ErrorResponse(data=[], msg="platform字段为空>>>windows | macOSX") + + queryset = self.filter_queryset(self.get_queryset()).exclude(status=False) + if request.query_params.get('start_version', None) is not None: + queryset = queryset.filter(version__gt=request.query_params.get('start_version')) + + if request.query_params.get('end_version', None) is not None: + queryset = queryset.filter(version__lte=request.query_params.get('end_version')) + + if not queryset: + return DetailResponse(data=[], msg="暂无更新版本说明") + + update_note = "" + for i in queryset: + update_note += f"{i.version}\r\n{i.update_comment}\r\n\r\n\r\n" + + return DetailResponse(data={"update_comment": update_note}, msg="获取成功") + + def destroy(self, request, *args, **kwargs): + # 删除时检查权限 + if CustomPermission().has_permission(request, ToolFileViewSet) is not True: + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + instance = self.get_object() + + # 配置是否删除本地文件, 前端配置 + if dispatch.get_system_config_values("tool.remove_is_deleted"): + try: + os.remove(instance.file_url) + except FileNotFoundError: # 删除文件的目的就是为了删除文件,所以找不到文件就认为删除了 + pass + + instance.delete() + return DetailResponse(data=[], msg="删除成功") + + def create(self, request, *args, **kwargs): + + # 上传文件时检查权限 + if CustomPermission().has_permission(request, ToolFileViewSet) is not True: + return ErrorResponse(data=[], msg="您没有该操作的执行权限") + + if 'update_comment' not in request.data or \ + 'version' not in request.data or \ + 'forced_update' not in request.data or \ + 'platform' not in request.data or \ + 'file' not in request.data: + raise KeyError("缺少必要参数") + + version = request.data.get('version', None) + platform = request.data.get('platform', None) + + if self.filter_queryset(self.get_queryset()).filter(version=version, platform=platform): + return ErrorResponse(data=[], msg=f"{platform}系统已经存在版本号:{version}的产测工具.请勿重复上传") + + return super().create(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + + # 更新时检查权限 + if CustomPermission().has_permission(request, ToolFileViewSet) is not True: + + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + return super().update(request, *args, **kwargs) + + +class ToolLicenseViewSet(CustomModelViewSet): + """ + 产测工具lisense管理接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + """ + + queryset = TestToolLicenseModel.objects.all() + serializer_class = TestToolLicenseModelSerializer + filter_fields = "__all__" + permission_classes = [] + custom_extra_filter_class = [CoreModelFilterBankend] + + _fernet = Fernet(b'fLoKjwCwE-NOsb2LGHNMLMa9ZHW000DFXgQrQ6UaruY=') # 产测工具密钥 + + def filter_queryset(self, queryset): + """ + 自定义过滤器去除权限过滤器, 用在非标准接口 + """ + for backend in set(set(self.filter_backends) | set(self.custom_extra_filter_class or [])): + + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def parse_license(self, uuid, uuid_license): + + data = self._fernet.decrypt(uuid_license).decode() + return data == uuid + + def generate_license(self, uuid, uuid_license): + + # 如果license 已经存在且和现有的UUID校验一致通过,直接返回license + if uuid_license and self.parse_license(uuid, uuid_license): + return uuid_license + + data = self._fernet.encrypt(f'{uuid}'.encode()).decode() + return data + + def get_license(self, request): + """ + 获取产测工具license + """ + if request.query_params.get('uuid', None) is None: + return ErrorResponse(data=[], msg="uuid字段为空") + + queryset = self.filter_queryset(self.get_queryset()) # 不进行筛选状态 + if not queryset: + return DetailResponse(data=[], msg="暂无license") + + serializer = self.get_serializer(queryset[0], request=request) + + return DetailResponse(data=serializer.data, msg="获取成功") + + def destroy(self, request, *args, **kwargs): + # 删除时检查权限 + if CustomPermission().has_permission(request, ToolLicenseViewSet) is not True: + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + return super().destroy(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + # 更新时检查权限 + if CustomPermission().has_permission(request, ToolLicenseViewSet) is not True: + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + request.data['license'] = self.generate_license(request.data['uuid'], request.data['license']) + + return super().update(request, *args, **kwargs) + + def create(self, request, *args, **kwargs): + + # 创建时检查权限,, 产测工具创建为工具特有 + if (request.data.get('comment', '') != '产测工具创建' + and CustomPermission().has_permission(request, ToolLicenseViewSet) is not True): + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + # 获取是否启动自动授权 + request.data['status'] = dispatch.get_system_config_values("tool.license_auto_authorized") + + request.data['license'] = self.generate_license(request.data['uuid'], request.data.get('license', '')) + + queryset = TestToolLicenseModel.objects.filter(is_deleted=None, uuid=request.data['uuid']) + + if queryset: # 软删除恢复数据 + instance = queryset[0] + instance.is_deleted = False + partial = kwargs.pop('partial', False) + serializer = self.get_serializer(instance, data=request.data, request=request, partial=partial) + serializer.is_valid(raise_exception=True) + + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + return DetailResponse(data=serializer.data, msg="创建成功") + + return super().create(request, *args, **kwargs) + + +class ToolLogViewSet(CustomModelViewSet): + """ + 产测工具log接口 + list:查询 + create:新增 + update:修改 + retrieve:单例 + destroy:删除 + """ + queryset = TestToolLogModel.objects.all() + serializer_class = TestToolLogModelSerializer + + permission_classes = [] + + def list(self, request, *args, **kwargs): + if CustomPermission().has_permission(request, ToolLicenseViewSet) is not True: + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + return super().list(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + if CustomPermission().has_permission(request, ToolLicenseViewSet) is not True: + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + return super().update(request, *args, **kwargs) + + def retrieve(self, request, *args, **kwargs): + if CustomPermission().has_permission(request, ToolLicenseViewSet) is not True: + return ErrorResponse(data=[], msg="您没有该操作的执行权限。") + + return super().retrieve(request, *args, **kwargs) + + def create(self, request, *args, **kwargs): + """ + 不做权限校验,数据主要来自产测工具 + """ + return super().create(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + + return ErrorResponse(data=[], msg="该操作被禁止!") + + def multiple_delete(self, request, *args, **kwargs): + + return ErrorResponse(data=[], msg="该操作被禁止!") diff --git a/backend/application/settings.py b/backend/application/settings.py index e6bdec5eaa5fa6bc0778592ace87c8da995262ef..1d27dab102562fefb67547c9703e500f4737d8ca 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -60,8 +60,18 @@ INSTALLED_APPS = [ "captcha", "channels", "dvadmin.system", + 'django_celery_beat', + 'django_celery_results', + 'dvadmin3_celery', ] +MY_APPS = [ + 'app.test_tool', + 'app.material' +] + +INSTALLED_APPS += MY_APPS + MIDDLEWARE = [ "dvadmin.utils.middleware.HealthCheckMiddleware", "django.middleware.security.SecurityMiddleware", @@ -155,7 +165,7 @@ STATICFILES_DIRS = [ MEDIA_ROOT = "media" # 项目下的目录 MEDIA_URL = "/media/" # 跟STATIC_URL类似,指定用户可以通过这个url找到文件 -#添加以下代码以后就不用写{% load staticfiles %},可以直接引用 +# 添加以下代码以后就不用写{% load staticfiles %},可以直接引用 STATICFILES_FINDERS = ( "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder" @@ -369,8 +379,8 @@ CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.math_challenge" # 加减乘除验证 DEFAULT_AUTO_FIELD = "django.db.models.AutoField" API_LOG_ENABLE = True -# API_LOG_METHODS = 'ALL' # ['POST', 'DELETE'] -API_LOG_METHODS = ["POST", "UPDATE", "DELETE", "PUT"] # ['POST', 'DELETE'] +API_LOG_METHODS = 'ALL' # ['POST', 'DELETE'] +# API_LOG_METHODS = ["POST", "UPDATE", "DELETE", "PUT"] # ['POST', 'DELETE'] API_MODEL_MAP = { "/token/": "登录模块", "/api/login/": "登录模块", @@ -404,11 +414,27 @@ 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 * # 租户管理 -#from dvadmin_social_auth.settings import * -#from dvadmin_uniapp.settings import * +# from dvadmin_social_auth.settings import * +# from dvadmin_uniapp.settings import * # ... # ********** 一键导入插件配置结束 ********** + + +CACHES = { # 配置缓存 + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": f'{REDIS_URL}/1', # 库名可自选1~16 + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + }, +} +CELERY_BROKER_URL = f'{REDIS_URL}/2' # 库名可自选1~16 +# BROKER_URL = f'{REDIS_URL}/2' # 库名可自选1~16 +CELERY_RESULT_BACKEND = 'django-db' # celery结果存储到数据库中 +CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' # Backend数据库 diff --git a/backend/application/urls.py b/backend/application/urls.py index cb5a89997adb091fb3f8b071a6c7be814583e766..c82ef237b97dcf652267f752208e8d7766da30c8 100644 --- a/backend/application/urls.py +++ b/backend/application/urls.py @@ -115,8 +115,16 @@ urlpatterns = ( # 前端页面映射 path('web/', web_view, name='web_view'), path('web/', serve_web_files, name='serve_web_files'), + path(r'api/dvadmin_celery/', include('dvadmin3_celery.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_URL) + [re_path(ele.get('re_path'), include(ele.get('include'))) for ele in settings.PLUGINS_URL_PATTERNS] ) + +MY_URLS = [ + path("api/tool/", include("app.test_tool.urls"), name="test_tool"), + path("api/material/", include("app.material.urls"), name="material"), +] + +urlpatterns += MY_URLS diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index 3b1209d73c0a3ab9fd5676a1695b9cf1f0e2fccb..46a268eaa422f99f8573a80c1ad5a31ce0a945ab 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -124,6 +124,7 @@ class LoginSerializer(TokenObtainPairSerializer): return {"code": 2000, "msg": "请求成功", "data": data} except Exception as e: user.login_error_count += 1 + user.save() if user.login_error_count >= 5: user.is_active = False user.save() diff --git a/backend/dvadmin/system/views/role_menu_button_permission.py b/backend/dvadmin/system/views/role_menu_button_permission.py index 723cd2a6a4af20c6d1c2b983c7d0e17d4b7383c4..81891c04b153e289553e348d79c3fe42d313f431 100644 --- a/backend/dvadmin/system/views/role_menu_button_permission.py +++ b/backend/dvadmin/system/views/role_menu_button_permission.py @@ -12,7 +12,7 @@ from rest_framework.permissions import IsAuthenticated from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \ MenuField, FieldPermission -from dvadmin.utils.json_response import DetailResponse +from dvadmin.utils.json_response import DetailResponse, ErrorResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet @@ -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/utils/models.py b/backend/dvadmin/utils/models.py index b387ea4b21c5d68e2541359f83435d0a41a74dab..5ca0035306c910012ab380a05f741f242290afaf 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -26,18 +26,28 @@ class SoftDeleteManager(models.Manager): """支持软删除""" def __init__(self, *args, **kwargs): - self.__add_is_del_filter = False + # self.__add_is_del_filter = False + self.__get_all = False # 不筛选数据 super(SoftDeleteManager, self).__init__(*args, **kwargs) def filter(self, *args, **kwargs): # 考虑是否主动传入is_deleted - if not kwargs.get('is_deleted') is None: - self.__add_is_del_filter = True + if "is_deleted" in kwargs and kwargs.get('is_deleted') is None: + self.__get_all = True + + # 删除is_deleted字段,防止其他过滤器为None条件过滤 + kwargs.pop('is_deleted') + else: + self.__get_all = False + return super(SoftDeleteManager, self).filter(*args, **kwargs) def get_queryset(self): - if self.__add_is_del_filter: - return SoftDeleteQuerySet(self.model, using=self._db).exclude(is_deleted=False) + # 不进行筛选删除标记 + if self.__get_all: + self.__get_all = False # 仅允许一次 + return SoftDeleteQuerySet(self.model) + return SoftDeleteQuerySet(self.model).exclude(is_deleted=True) def get_by_natural_key(self, name): diff --git a/backend/requirements.txt b/backend/requirements.txt index 3395719bc4a8703d46ef4804b143346da6bc5808..6507bbdfaf2aae132583c9d8b65a0d8b080d5f4a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,31 +1,37 @@ -Django==4.2.14 +async-timeout==5.0.1 +Django==4.2.7 +django-celery-beat==2.7.0 +django-celery-results==2.5.1 django-comment-migrate==0.1.7 -django-cors-headers==4.4.0 -django-filter==24.2 +django-cors-headers==4.3.0 +django-filter==23.3 django-ranged-response==0.2.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 +django-redis==5.4.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 drf-yasg==1.21.7 mysqlclient==2.2.0 -pypinyin==0.51.0 +pypinyin==0.49.0 ua-parser==0.18.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 +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 websockets==11.0.3 user-agents==2.2.0 six==1.16.0 -whitenoise==6.7.0 +whitenoise==6.6.0 psycopg2==2.9.9 -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 +uvicorn==0.23.2 +eventlet==0.38.0 +gunicorn==21.2.0 +gevent==23.9.1 +Pillow==10.1.0 +dvadmin-celery==1.0.5 +cryptography==42.0.8 \ No newline at end of file diff --git a/web/.env b/web/.env index 0d828a796e2ea128786d272e30bac3ddf9f004a6..682bb753c5c6189dbb382bc7cc858b33c2e1b96a 100644 --- a/web/.env +++ b/web/.env @@ -1,5 +1,5 @@ # port 端口号 -VITE_PORT = 8080 +VITE_PORT = 80 VITE_API_URL = 'http://dvadmin3api.django.icu:8001' # open 运行 npm run dev 时自动打开浏览器 VITE_OPEN = false diff --git a/web/.env.production b/web/.env.production index 998aa5e15fe3f7cd5a3765c4c1b82dd2a629b2f0..54ec19921eb3597c1591620fa48ef40060243b5d 100644 --- a/web/.env.production +++ b/web/.env.production @@ -2,7 +2,7 @@ ENV = 'production' # 线上环境接口地址 -VITE_API_URL = '/api' # docker-compose部署不需要修改,nginx容器自动代理了这个地址 +VITE_API_URL = 'http://172.16.17.169:8000' # docker-compose部署不需要修改,nginx容器自动代理了这个地址 # 是否启用按钮权限 VITE_PM_ENABLED = true diff --git a/web/index.html b/web/index.html index 9515e84b71affad9064130809ddf03581b3cf9cc..5deba5ec85942e2ded66acde0d58b42759a2a5f4 100644 --- a/web/index.html +++ b/web/index.html @@ -6,25 +6,25 @@ - - django-vue-admin + + 海盈实业
diff --git a/web/package.json b/web/package.json index 7022283db3a8fdcd04bbc01b6483dc5f202bc09b..11d4aaf8ec0e6658e07301f7f75f43bb58ef2efe 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "axios": "^1.7.4", "countup.js": "^2.8.0", "cropperjs": "^1.6.2", + "dvadmin3-celery-web": "^1.0.2", "e-icon-picker": "2.1.1", "echarts": "^5.5.1", "echarts-gl": "^2.0.9", diff --git a/web/src/App.vue b/web/src/App.vue index 56f585e22b7aa9b7a27581bea53edc4f4454d015..6326581c09fc4171c49943862549d584e762c71a 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -106,6 +106,17 @@ watch( import { messageCenterStore } from '/@/stores/messageCenter'; const wsReceive = (message: any) => { const data = JSON.parse(message.data); + if (data.contentType === 'MATERIAL') { + ElNotification({ + title: data.title, + message: data.content, + type: 'info', + position: 'bottom-right', + duration: 0, + }); + return + } + const { unread } = data; const messageCenter = messageCenterStore(); messageCenter.setUnread(unread); diff --git a/web/src/assets/logo/logo_backgroud.jpeg b/web/src/assets/logo/logo_backgroud.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a9866ed0f8fcaab85dc9522a7a34b5033cea05db Binary files /dev/null and b/web/src/assets/logo/logo_backgroud.jpeg differ diff --git a/web/src/assets/logo/logo_title.png b/web/src/assets/logo/logo_title.png new file mode 100644 index 0000000000000000000000000000000000000000..49b4cb5fd4ce86bec32a92a9d756b584c7505163 Binary files /dev/null and b/web/src/assets/logo/logo_title.png differ diff --git a/web/src/components/avatarSelector/index.vue b/web/src/components/avatarSelector/index.vue index f979cb09091a54f9f6da9016a9fd3d83a5a236db..2ad853e9b1f2b100a3e982470ec139ae381e8da4 100644 --- a/web/src/components/avatarSelector/index.vue +++ b/web/src/components/avatarSelector/index.vue @@ -95,7 +95,10 @@ function modalOpened() { }); } /** 覆盖默认上传行为 */ -function requestUpload() {} +function requestUpload() { + + console.log(123) +} /** 向左旋转 */ function rotateLeft() { proxy.$refs.cropper.rotateLeft(); diff --git a/web/src/layout/footer/index.vue b/web/src/layout/footer/index.vue index 67e6e5c07b00a4a9be9f1c42449528f7a3892a22..adc0ce8d1c8792fda8bf68ade00fca3f147ab7a5 100644 --- a/web/src/layout/footer/index.vue +++ b/web/src/layout/footer/index.vue @@ -1,7 +1,8 @@ diff --git a/web/src/layout/logo/index.vue b/web/src/layout/logo/index.vue index 1a656bbf4eb1e9cd3837f48e058462699e4b478a..23b815c644b78261f6c852e981507bc65f6a09c2 100644 --- a/web/src/layout/logo/index.vue +++ b/web/src/layout/logo/index.vue @@ -15,6 +15,8 @@ import { useThemeConfig } from '/@/stores/themeConfig'; import logoMini from '/@/assets/logo-mini.svg'; import { SystemConfigStore } from "/@/stores/systemConfig"; import _ from "lodash-es"; +import { getBaseURL } from "/@/utils/baseUrl"; + // 定义变量内容 const storesThemeConfig = useThemeConfig(); const { themeConfig } = storeToRefs(storesThemeConfig); @@ -38,7 +40,7 @@ const getSystemConfig = computed(() => { const siteLogo = computed(() => { if (!_.isEmpty(getSystemConfig.value['login.site_logo'])) { - return getSystemConfig.value['login.site_logo'] + return getBaseURL(getSystemConfig.value['login.site_logo']) } return logoMini }); diff --git a/web/src/layout/navBars/breadcrumb/breadcrumb.vue b/web/src/layout/navBars/breadcrumb/breadcrumb.vue index 7dfaca67f9dbf621eaa998a7c870f544e15fdb97..3c93058a2d71de83d48f34b5427edf1e4b636fc7 100644 --- a/web/src/layout/navBars/breadcrumb/breadcrumb.vue +++ b/web/src/layout/navBars/breadcrumb/breadcrumb.vue @@ -74,9 +74,9 @@ const getBreadcrumbList = (arr: RouteItems) => { arr.forEach((item: RouteItem) => { state.routeSplit.forEach((v: string, k: number, arrs: string[]) => { if (state.routeSplitFirst === item.path) { - state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`; + // state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`; state.breadcrumbList.push(item); - state.routeSplitIndex++; + // state.routeSplitIndex++; if (item.children) getBreadcrumbList(item.children); } }); diff --git a/web/src/layout/navBars/breadcrumb/user.vue b/web/src/layout/navBars/breadcrumb/user.vue index 61793c974ad275979387c36293f24ad9dcc3d931..c948a8700b3f143986a38407122bf30ef375b419 100644 --- a/web/src/layout/navBars/breadcrumb/user.vue +++ b/web/src/layout/navBars/breadcrumb/user.vue @@ -93,7 +93,7 @@ {{ $t('message.user.dropdown1') }} {{ $t('message.user.dropdown2') }} - {{ $t('message.user.dropdown6') }} + {{ $t('message.user.dropdown5') }} diff --git a/web/src/layout/upgrade/index.vue b/web/src/layout/upgrade/index.vue index 9419fcc916b680d62f7d71f829c7098b5c331169..ec744ad183e108395ba6fa9e20ed02c11f461b1a 100644 --- a/web/src/layout/upgrade/index.vue +++ b/web/src/layout/upgrade/index.vue @@ -81,7 +81,7 @@ const delayShow = () => { }; // 页面加载时 onMounted(() => { - delayShow(); + // delayShow(); // 不显示更新 setTimeout(() => { state.btnTxt = t('message.upgrade.btnTwo'); }, 200); diff --git a/web/src/settings.ts b/web/src/settings.ts index 61881d3163c51dbac6cb2ead4b0c98ea4bcc686d..a3d2db7d1efbb28c0171e1170bbbb779c2be034c 100644 --- a/web/src/settings.ts +++ b/web/src/settings.ts @@ -13,6 +13,11 @@ import {FsExtendsEditor, FsExtendsUploader } from '@fast-crud/fast-extends'; import '@fast-crud/fast-extends/dist/style.css'; import {successNotification} from '/@/utils/message'; import XEUtils from "xe-utils"; +import { compute, } from '@fast-crud/fast-crud'; +import {CrudExpose } from '@fast-crud/fast-crud'; + +// import context from '@fast-crud/ui-interface/dist/d/context'; + export default { async install(app: any, options: any) { // 先安装ui @@ -90,7 +95,7 @@ export default { action: `/api/system/file/`, name: "file", withCredentials: false, - uploadRequest: async ({ action, file, onProgress }: { action: string; file: any; onProgress: Function }) => { + uploadRequest: async ({ action, file, onProgress}: { action: string; file: any; onProgress: Function}) => { // @ts-ignore const data = new FormData(); data.append("file", file); @@ -115,6 +120,7 @@ export default { ...ret.data }; } + // onprogress() }, valueBuilder(context: any){ const { row, key } = context diff --git a/web/src/stores/themeConfig.ts b/web/src/stores/themeConfig.ts index 356e584a31916ad47ff8baa5c3d1be62e94f7337..a31d4bc2b389f7d6b43ce94ed1f639280a42835a 100644 --- a/web/src/stores/themeConfig.ts +++ b/web/src/stores/themeConfig.ts @@ -137,11 +137,11 @@ export const useThemeConfig = defineStore('themeConfig', { * 全局网站标题 / 副标题 */ // 网站主标题(菜单导航、浏览器当前网页标题) - globalTitle: 'DVAdmin', + globalTitle: '海盈实业', // 网站副标题(登录页顶部文字) - globalViceTitle: 'DVAdmin', + globalViceTitle: '海盈实业', // 网站副标题(登录页顶部文字) - globalViceTitleMsg: '企业级快速开发平台', + globalViceTitleMsg: '后台管理系统', // 默认初始语言,可选值"",默认 zh-cn globalI18n: 'zh-cn', // 默认全局组件大小,可选值"",默认 'large' diff --git a/web/src/utils/customCrudMaterial.ts b/web/src/utils/customCrudMaterial.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec399772f6ec78f6d4905e4db1cb1318c83c7a45 --- /dev/null +++ b/web/src/utils/customCrudMaterial.ts @@ -0,0 +1,165 @@ +import {dict, useTypes} from "@fast-crud/fast-crud"; + + +//添加自定义字段类型,使用type:'time2',你就可以省略下面的配置 +//不要写在页面里,这个是全局的,要写在vue.use(FastCrud)之后 +const { addTypes, getTypes } = useTypes() + +addTypes({ + 'custom':{ //如果与官方字段类型同名,将会覆盖官方的字段类型 + form: { component: { name: 'el-input' } }, + // column:{ component: {name: 'a-input'} }, // text=无类型 + search: { component: { name: 'fs-dict-select' } } + } +}) + +export const customCrudConfig = (crudExpose: {}) => { + return { + + type: { + title: '物料类型', + type: 'custom', + column: { + show: true, + align: 'center', + sortable: true, + width: 200, + }, + + search: { + show: true, + + }, + dict: dict({ + // isTree: true, + url: '/api/material/base_info/get_material_type/', + value: 'type', + label: 'type', + cache: true, + }), + form: { + show: true, + component: { + placeholder: '请输入物料类型', + filterable: true, + props: { + allowCreate: true, + filterable: true, + clearable: true, + }, + }, + rules: [ + // 表单校验规则 + { + required: true, + message: '请填写物料名称', + }, + ], + valueChange({ form, value, getComponentRef }) { + form.name = ""; // 将“name”的值置空 + form.code = ""; // 将“code”的值置空 + getComponentRef("name").reloadDict(); // 触发“city”重新加载字典 + getComponentRef("code").reloadDict(); // 触发“city”重新加载字典 + // if (value) { + // getComponentRef("name").reloadDict(); // 触发“city”重新加载字典 + // getComponentRef("code").reloadDict(); // 触发“city”重新加载字典 + // } + } + }, + }, + name: { + title: '物料名称', + search: { + show: true, + }, + type: 'custom', + dict: dict({ + value: 'name', + label: 'name', + cache: true, + prototype: true, + url({ form }) { + if (form && form.type != null) { + // 本数据字典的url是通过前一个select的选项决定的 + return `/api/material/base_info/get_material_type_name/?limit=999&type=${form.type}`; + } + return "/api/material/base_info/get_material_type_name/?limit=999"; // 返回undefined 将不加载字典 + }, + }), + column:{ + show: true, + align: 'center', + sortable: true, + width: 200, + + }, + form: { + show: true, + component: { + placeholder: '请填写物料名称', + filterable: true, + props: { + allowCreate: true, + filterable: true, + clearable: true, + }, + }, + rules: [ + // 表单校验规则 + { + required: true, + message: '请填写物料名称', + }, + ], + valueChange({ form, value, getComponentRef }) { + if (value) { + form.code = ''; // 清空物料编码, 防止不一致导致查询失效 + } + } + }, + }, + code: { + title: '物料编码', + search: { + show: true, + }, + column:{ + show: true, + align: 'center', + sortable: true, + width: 200, + }, + type: 'custom', + dict: dict({ + value: 'id', + label: 'code', + cache: true, + prototype: true, + url({ form }) { + if (form && form.type != null) { + // 本数据字典的url是通过前一个select的选项决定的 + return `/api/material/base_info/get_material_type_name/?limit=999&type=${form.type}`; + } + return "/api/material/base_info/get_material_type_name/?limit=999"; // 返回undefined 将不加载字典 + }, + }), + form: { + show: true, + component: { + placeholder: '请填写物料编码', + filterable: true, + props: { + allowCreate: true, + filterable: true, + clearable: true, + }, + }, + valueChange({ form, value, getComponentRef }) { + if (value) { + form.name = ''; // 清空物料名称, 防止不一致导致查询失效 + } + } + }, + }, + } +} diff --git a/web/src/views/fileList/api.ts b/web/src/views/fileList/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c5708b1b8db7c49cea171aee36ced8297935a06 --- /dev/null +++ b/web/src/views/fileList/api.ts @@ -0,0 +1,41 @@ +import { request } from '/@/utils/service'; +import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; + +export const apiPrefix = '/api/system/file/'; +export function GetList(query: UserPageQuery) { + return request({ + url: apiPrefix, + method: 'get', + params: query, + }); +} +export function GetObj(id: InfoReq) { + return request({ + url: apiPrefix + id, + method: 'get', + }); +} + +export function AddObj(obj: AddReq) { + return request({ + url: apiPrefix, + method: 'post', + data: obj, + }); +} + +export function UpdateObj(obj: EditReq) { + return request({ + url: apiPrefix + obj.id + '/', + method: 'put', + data: obj, + }); +} + +export function DelObj(id: DelReq) { + return request({ + url: apiPrefix + id + '/', + method: 'delete', + data: { id }, + }); +} diff --git a/web/src/views/fileList/crud.tsx b/web/src/views/fileList/crud.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5c6a0d5a634cf7d8910b623388e0a28a688597de --- /dev/null +++ b/web/src/views/fileList/crud.tsx @@ -0,0 +1,132 @@ +import * as api from './api'; +import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'; + +export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const pageRequest = async (query: UserPageQuery) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; + return { + crudOptions: { + actionbar: { + buttons: { + add: { + show: false, + }, + }, + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + rowHandle: { + //固定右侧 + fixed: 'right', + width: 200, + show:false, + buttons: { + view: { + show: false, + }, + edit: { + iconRight: 'Edit', + type: 'text', + }, + remove: { + iconRight: 'Delete', + type: 'text', + }, + }, + }, + columns: { + _index: { + title: '序号', + form: { show: false }, + column: { + //type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = crudExpose!.crudBinding.value.pagination; + return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1; + }, + }, + }, + search: { + title: '关键词', + column: { + show: false, + }, + search: { + show: true, + component: { + props: { + clearable: true, + }, + placeholder: '请输入关键词', + }, + }, + form: { + show: false, + component: { + props: { + clearable: true, + }, + }, + }, + }, + name: { + title: '文件名称', + search: { + show: true, + }, + type: 'input', + column:{ + minWidth: 120, + }, + form: { + component: { + placeholder: '请输入文件名称', + }, + }, + }, + url: { + title: '文件地址', + type: 'file-uploader', + search: { + disabled: true, + }, + column:{ + minWidth: 200, + }, + }, + md5sum: { + title: '文件MD5', + search: { + disabled: true, + }, + column:{ + minWidth: 120, + }, + form: { + disabled: false, + }, + }, + }, + }, + }; +}; diff --git a/web/src/views/fileList/index.vue b/web/src/views/fileList/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..119c73a03a9f0b14a0f2439c933f3fdc0c0ae813 --- /dev/null +++ b/web/src/views/fileList/index.vue @@ -0,0 +1,26 @@ + + + diff --git a/web/src/views/materialApplyFor/api.ts b/web/src/views/materialApplyFor/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..a383261fc3f3c8110c0b8da9426a427b42f514ce --- /dev/null +++ b/web/src/views/materialApplyFor/api.ts @@ -0,0 +1,60 @@ +import { Stream } from 'stream'; +import { request, downloadFile } from '/@/utils/service'; +import { PageQuery, UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; +import { getBaseURL } from '/@/utils/baseUrl' + +export const apiPrefix = 'api/material/apply_for/'; +export function GetList(query: UserPageQuery) { + return request({ + url: apiPrefix, + method: 'get', + params: query, + }); +} +export function GetObj(id: InfoReq) { + return request({ + url: apiPrefix + id, + method: 'get', + }); +} +export function AddObj(obj: AddReq) { + return request({ + url: apiPrefix, + method: 'post', + data: obj, + }); +} + +export function UpdateObj(obj: EditReq) { + return request({ + url: apiPrefix + obj.id + '/', + method: 'put', + data: obj, + }); +} + +export function DelObj(id: DelReq) { + return request({ + url: apiPrefix + id + '/', + method: 'delete', + data: { id }, + }); +} + + +export function exportData(params: any) { + return downloadFile({ + url: apiPrefix + 'export_data/', + params: params, + method: 'get', + }); +} + + +export function UpdateStatue(id: any, status: any) { + return request({ + url: apiPrefix + 'revocation/', + method: 'put', + data: {"id": id, "status": status}, + }); +} \ No newline at end of file diff --git a/web/src/views/materialApplyFor/crud.tsx b/web/src/views/materialApplyFor/crud.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7baba5a087bb730773f642f2020dc79c663abe22 --- /dev/null +++ b/web/src/views/materialApplyFor/crud.tsx @@ -0,0 +1,787 @@ +import * as api from './api'; +import { ElMessageBox, ElNotification} from 'element-plus'; +import { errorMessage, successMessage, successNotification, errorNotification,} from '/@/utils/message'; +import { request } from '/@/utils/service'; +import {dict, UserPageQuery, AddReq, DelReq, EditReq, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'; +import {dictionary} from '/@/utils/dictionary'; + +import {auth} from "/@/utils/authFunction"; + +import {customCrudConfig} from "/@/utils/customCrudMaterial"; + +import {createCrudOptions as UserCreateCrudOptions} from '/@/views/user/crud'; + +import {createCrudOptions as MaterialBasecreateCrudOptions} from '/@/views/materialBase/crud'; +import * as materialBaseTableApi from '/@/views/materialBase/api'; + +import * as UsereApi from '/@/views/system/personal/api'; + +import { ref } from "vue"; +import { useCompute } from "@fast-crud/fast-crud"; +import { fa } from 'element-plus/es/locale'; + + +var UserInfo = {id: -1, is_superuser: false} + +export const getUserInfo = function () { + UsereApi.GetUserInfo({}).then((res: any) => { + UserInfo = res.data + }); +}; + +export const createCrudOptions = function ({ crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet { + const {compute} = useCompute() + const pageRequest = async (query: UserPageQuery) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; + const exportRequest = async (query: UserPageQuery) => { + return await api.exportData(query); + }; + + const updateRequest = async (data: {}, operation="") => { + await request({ + url: "api/material/apply_for/operation/" + operation + "/", + method: 'put', + data: data} + ).then((res: any) => { + if (res) { + if (res["code"] === 2000) { + successNotification(res["msg"]) + } + else { + errorNotification(res["msg"]) + } + } + }) + // 刷新 + crudExpose.doRefresh() + }; + + const selectedIds = ref([]); + + const onSelectionChange = (changed) => { + selectedIds.value = changed.map((item) => item.id); + }; + + return { + crudOptions: { + actionbar: { + buttons: { + add: { + icon: "CirclePlusFilled", + show: auth('materialApply:Create'), + text: "申请", + }, + export: { + icon: "Upload", + style:{backgroundColor: "#6495ED", color: "white"}, + text: '导出', //按钮文字 + title: '导出', //鼠标停留显示的信息 + show: auth('materialApply:Export'), + click() { + return exportRequest(crudExpose!.getSearchFormData()); + }, + }, + examine: { + text: "审核通过", + icon: "SuccessFilled", + size: "Big", + show: auth('materialApply:Examine'), + style:{backgroundColor: "#32CD32", color: "white"}, + click: async () => { + if (selectedIds.value?.length > 0) { + await ElMessageBox.confirm(`确定要批量审核这${selectedIds.value.length}条记录吗`, "确认"); + updateRequest({"ids": selectedIds.value}, "examine") + selectedIds.value = []; + } else { + errorMessage("请先勾选记录"); + } + } + }, + dismissal: { + text: "驳回申请", + icon: "CircleCloseFilled", + show: auth('materialApply:Dismissal'), + style:{backgroundColor: "#00BFFF", color: "white"}, + click: async () => { + if (selectedIds.value?.length > 0) { + const {value} = await ElMessageBox.prompt("请输入驳回原因!",`确定要 “驳回” 这${selectedIds.value.length}条记录吗? `); + if (value == "" || value == null) { + errorMessage("请输入驳回原因!") + return + } + updateRequest({"ids": selectedIds.value, "examine_opinion": value}, "dismissal") + + selectedIds.value = []; + } else { + errorMessage("请先勾选记录"); + } + } + }, + }, + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + form: { + wrapper:{ + width: 500, + }, + col: { + span: 24 + } + }, + viewForm: { + wrapper:{ + width: "800", + }, + col: { + span: 12 + } + }, + addForm: { + columns: { + type: { + show: true, + component: { + name: 'fs-dict-select', + cache: false, + props: { + allowCreate: true, + filterable: true, + clearable: true, + }, + }, + rules: [ + ], + valueChange({ form, value, getComponentRef }) { + form.name = ""; // 将“name”的值置空 + getComponentRef("name").reloadDict(); // 触发“city”重新加载字典 + }, + }, + name: { + show: true, + component: { + name: 'fs-dict-select', + cache: false, + props: { + allowCreate: true, + filterable: true, + clearable: true, + }, + }, + valueChange({ form, value, getComponentRef }) { + const data = getComponentRef(("name")).curDict.dataMap[value] + if (data !== undefined) { + // 设置默认值 + form.type = {"value": data.type, "label": data.type} + form.id = data.id + return true + } + form.id = null + } + }, + id: { + show: false, + value: null, + }, + code: { + show: false + }, + material: { + show: false, + } + } + }, + editForm: { + columns: { + type: { + show: false + }, + name: { + show: false + }, + code: { + show: false + }, + material: { + show: true, + component: { + readonly: true, + // disabled: true + }, + } + } + }, + rowHandle: { + //固定右侧 + fixed: 'right', + align: 'center', + // width: 300, + show: true, + buttons: { + edit: { + order: 2, + iconRight: 'Edit', + text: "重新申请", + // size: "small", + style:{backgroundColor: "#8A2BE2", color: "white"}, + show: compute(({row})=>{ + if (auth('materialApply:Update') === false) { + return false + } + if (row.creator !== UserInfo.id && UserInfo.is_superuser !== true) { + return false + } + return true + }), + }, + remove: { + order: 4, + iconRight: 'Delete', + text: "删除申请", + // size: "small", + show: compute(({row})=>{ + if (auth('materialApply:Delete') === false) { + return false + } + if (row.creator !== UserInfo.id && UserInfo.is_superuser !== true) { + return false + } + return true + }), + }, + view: { + order: 5, + iconRight: 'View', + text: "查看详情", + // size: "small", + style: {backgroundColor: "#9FB6CD", color: "white"}, + show: auth('materialApply:Retrieve'), + }, + examine: { + order: 0, + iconRight: 'SuccessFilled', + // dropdown: true, + text: "审核通过", + title: "审核通过", + show: auth('materialApply:Examine'), + // size: "small", + style:{backgroundColor: "#32CD32", color: "white"}, + click: async ({row}) => { + if (row.examine_status != 1){ + errorMessage('当前状态不允许审核') + return false + } + await ElMessageBox.confirm("您确定要审核该记录吗?", "审核提示") + updateRequest({"ids": [row.id]}, "examine") + } + }, + dismissal: { + order: 1, + iconRight: 'CircleCloseFilled', + // dropdown: true, + // type: 'text', + text: "驳回申请", + title: "驳回", + show: auth('materialApply:Dismissal'), + // size: "small", + style:{backgroundColor: "#00BFFF", color: "white"}, + click: async ({row}) => { + if (row.examine_status != 1){ + errorMessage('当前状态不允许审核') + return false + } + const {value} = await ElMessageBox.prompt("请输入驳回原因!", "审核提示"); + if (value == "" || value == null) { + errorMessage("请输入驳回原因!") + return + } + updateRequest({"ids": [row.id], "examine_opinion": value}, "dismissal") + } + }, + revocation: { + order: 3, + iconRight: 'BottomLeft', + // type: 'text', + text: "撤销申请", + title: "撤销申请", + // size: "small", + show: compute(({row})=>{ + if (auth('materialApply:Revocation') === false) { + return false + } + if (row.creator !== UserInfo.id && UserInfo.is_superuser !== true) { + return false + } + return true + }), + style:{backgroundColor: "#EEC900", color: "white"}, + click: async ({row}) => { + if (row.examine_status !== 1){ + errorMessage('当前状态不允许撤销') + return false + } + await ElMessageBox.confirm("您确定要撤回该记录吗?", "撤回提示") + updateRequest({"id": row.id}, "revocation") + } + }, + }, + dropdown: { + show: false, + more: { + //更多按钮配置 + show: false, + type: "text", + text: "审核", + iconRight: "View", + icon: "" + } + } + }, + table: { + editable: { + mode: "cell", + }, + rowKey: "id", + onSelectionChange + }, + columns: { + $checked: { + title: "选择", + form: { show: false}, + column: { + show: auth('materialApply:Examine') || auth('materialApply:Dismissal') , + type: "selection", + align: "center", + width: "55px", + columnSetDisabled: true, //禁止在列设置中选择 + selectable(row, index) { + return row.examine_status === 1; //设置第一行不允许选择 + // return true; //设置第一行不允许选择 + } + } + }, + _index: { + title: '序号', + form: { show: false }, + column: { + //type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = crudExpose!.crudBinding.value.pagination; + return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1; + }, + fixed: "left", + }, + }, + material: { + title: "申请物料", + order: 0, + type: "table-select", + dict: dict({ + value: "id", + label: "name", + getNodesByValues: async (values: any[]) => { + const data = await materialBaseTableApi.GetByIds(values) + return data.data; + } + }), + search: { + show: false, + }, + column:{ + show: false, + columnSetDisabled: true, //禁止在列设置中选择 + }, + form: { + show: false, + rules: [ + // 表单校验规则 + { + required: true, + message: '请填写申请物料', + }, + ], + component: { + placeholder: '请填写申请物料', + multiple: false, + rowKey: "id", //element-plus 必传 + createCrudOptions: MaterialBasecreateCrudOptions, + crudOptionsOverride: { + columns: { + $checked: { + column:{ + show: false, + columnSetDisabled: true, //禁止在列设置中选择 + }, + }, + }, + actionbar: { + buttons: { + add: { + show: false + } + }, + }, + rowHandle: { + + // show: false, + buttons: { + edit: { + show: false + }, + remove: { + show: false + } + + }, + // fixed: "right" + } + } + }, + }, + }, + ...customCrudConfig(), + specification: { + title: '物料规格', + search: { + show: false, + }, + type: 'text', + column:{ + align: 'center', + sortable: true, + width: 200, + show: false + }, + form: { + show: false, + component: { + placeholder: '请输入物料规格', + }, + }, + }, + number: { + title: '申请数量', + search: { + show: false, + }, + type: ['number'], + column:{ + align: 'center', + sortable: true, + width: 120, + }, + form: { + show: true, + rules: [ + // 表单校验规则 + { + type: 'number', + min: 1, + required: true, + message: '数量需要大于0', + }, + ], + component: { + placeholder: '请填写出库数量', + }, + }, + }, + examine_status: { + title: '审核状态', + search: { + show: true, + }, + type: 'dict-select', + dict: dict({ + data: dictionary('material_examine_status'), + }), + column:{ + align: 'center', + sortable: true, + width: 120, + }, + form: { + show: false + }, + viewForm: { + show: true + } + }, + reason: { + title: '申请理由', + search: { + show: false, + }, + type: ['textarea', 'colspan'], + column:{ + align: 'center', + sortable: true, + width: 150, + }, + form: { + show: true + }, + }, + url: { + title: '申请链接', + search: { + show: false, + }, + type: 'link', + column:{ + align: 'center', + width: 200, + component: { + on: { + // 注意:必须要on前缀 + onClick({row}) { + if (row.url) { + window.open(row.url); + } + } + } + } + }, + form: { + show: true + }, + }, + examine_opinion: { + title: '审核意见', + search: { + show: false, + }, + type: 'text', + column:{ + align: 'center', + sortable: true, + width: 150, + }, + form: { + show: false + }, + viewForm: { + show: true + } + }, + creator_name: { + title: '申请人', + type: "table-select", + dict: dict({ + value: "name", + label: "name", + // getNodesByValues: async (values: any[]) => { + // // if (1 != (1 % values)) { + // // return + // // } + // console.log(1, values, 2) + // const data = await UserTableApi.GetObj(values) + // console.log(data.data) + // return data.data; + // } + }), + search: { + show: true + }, + column: { + width: 100, + show: true, + type: 'text', + }, + form: { + show: false, + component: { + placeholder: '申请人', + multiple: false, + rowKey: "name", //element-plus 必传 + createCrudOptions: UserCreateCrudOptions, + crudOptionsOverride: { + form: { + wrapper: { + draggable: false, + // width: '400px', //antdv对话框的宽度 + // width: 400, + }, + }, + + rowHandle: { + show: false, + // fixed: "right" + } + } + }, + }, + viewForm: { + show: true + }, + }, + examine_user_name: { + title: '审核人', + search: { + show: false, + }, + type: 'text', + column:{ + align: 'center', + sortable: true, + width: 120, + }, + form: { + show: false + }, + viewForm: { + show: true + } + }, + create_datetime: { + title: '申请时间', + type: 'datetime', + search: { + show: true, + col: {span: 8}, + component: { + type: 'datetimerange', + props: { + 'start-placeholder': '开始时间', + 'end-placeholder': '结束时间', + 'value-format': 'YYYY-MM-DD HH:mm:ss', + 'picker-options': { + shortcuts: [{ + text: '最近一周', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近一个月', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近三个月', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); + picker.$emit('pick', [start, end]); + } + }] + } + } + }, + valueResolve(context: any) { + const {key, value} = context + //value解析,就是把组件的值转化为后台所需要的值 + //在form表单点击保存按钮后,提交到后台之前执行转化 + if (value) { + context.form.create_datetime_after = value[0] + context.form.create_datetime_before = value[1] + } + // ↑↑↑↑↑ 注意这里是form,不是row + } + }, + column: { + width: 160, + show: true, + sortable: true, + }, + form: { + show: false, + }, + viewForm: { + show: true + }, + }, + update_datetime: { + title: '最后更新时间', + type: 'datetime', + search: { + show: true, + col: {span: 8}, + component: { + type: 'datetimerange', + props: { + 'start-placeholder': '开始时间', + 'end-placeholder': '结束时间', + 'value-format': 'YYYY-MM-DD HH:mm:ss', + 'picker-options': { + shortcuts: [{ + text: '最近一周', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近一个月', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近三个月', + onClick(picker) { + const end = new Date(); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); + picker.$emit('pick', [start, end]); + } + }] + } + } + }, + valueResolve(context: any) { + const {key, value} = context + //value解析,就是把组件的值转化为后台所需要的值 + //在form表单点击保存按钮后,提交到后台之前执行转化 + if (value) { + context.form.create_datetime_after = value[0] + context.form.create_datetime_before = value[1] + } + // ↑↑↑↑↑ 注意这里是form,不是row + } + }, + column: { + width: 160, + show: true, + sortable: true, + }, + form: { + show: false, + }, + viewForm: { + show: true + }, + } + }, + }, + }; +}; diff --git a/web/src/views/materialApplyFor/index.vue b/web/src/views/materialApplyFor/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..0bc0d3c8ef9e505b316f9117e6949b298ad2ed0f --- /dev/null +++ b/web/src/views/materialApplyFor/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/views/materialBase/api.ts b/web/src/views/materialBase/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca41d82feb5683653a13b8f3e4f802e46590b8e1 --- /dev/null +++ b/web/src/views/materialBase/api.ts @@ -0,0 +1,70 @@ +import { Stream } from 'stream'; +import { request, downloadFile } from '/@/utils/service'; +import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; +import { getBaseURL } from '/@/utils/baseUrl' + +export const apiPrefix = 'api/material/base_info/'; +export function GetList(query: UserPageQuery) { + return request({ + url: apiPrefix, + method: 'get', + params: query, + }); +} +export function GetObj(id: InfoReq) { + return request({ + url: apiPrefix + id, + method: 'get', + }); +} + +export function GetByIds(ids: any) { + return request({ + url: apiPrefix + "byIds/", + method: 'get', + params: ids, + }); +} + +export function AddObj(obj: AddReq) { + return request({ + url: apiPrefix, + method: 'post', + data: obj, + }); +} + +export function UpdateObj(obj: EditReq) { + return request({ + url: apiPrefix + obj.id + '/', + method: 'put', + data: obj, + }); +} + +export function DelObj(id: DelReq) { + return request({ + url: apiPrefix + id + '/', + method: 'delete', + data: { id }, + }); +} + + +export function exportData(params: any) { + return downloadFile({ + url: apiPrefix + 'export_data/', + params: params, + method: 'get', + }); +} + + +export function MultipleDelObj(data: {}) { + return request({ + url: apiPrefix + 'multiple_delete/', + method: 'delete', + data: data, + }); +} + diff --git a/web/src/views/materialBase/crud.tsx b/web/src/views/materialBase/crud.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f6166b39c1d625c3ab5ab154c171b665a143fc81 --- /dev/null +++ b/web/src/views/materialBase/crud.tsx @@ -0,0 +1,286 @@ +import * as api from './api'; +import { ElMessageBox} from 'element-plus'; +import { errorMessage, successMessage, successNotification } from '/@/utils/message'; +import {dictionary} from '/@/utils/dictionary'; +import {compute, dict, UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet,FormWrapperContext } from '@fast-crud/fast-crud'; + +import {auth} from "/@/utils/authFunction"; + +import {commonCrudConfig} from "/@/utils/commonCrud"; +import {customCrudConfig} from "/@/utils/customCrudMaterial"; + +import { ref } from "vue"; +import { useCompute } from "@fast-crud/fast-crud"; + + +export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { + + const pageRequest = async (query: UserPageQuery) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; + + const exportRequest = async (query: UserPageQuery) => { + return await api.exportData(query); + }; + + const MultipleDelRequest = async (data: {}) => { + return await api.MultipleDelObj(data); + }; + + const selectedIds = ref([]); + + const onSelectionChange = (changed) => { + selectedIds.value = changed.map((item) => item.id); + }; + + return { + crudOptions: { + actionbar: { + buttons: { + add: { + icon: "CirclePlusFilled", + show: auth('materialBaseInfo:Create'), + text: '添加', + }, + export: { + icon: "Upload", + style:{backgroundColor: "#6495ED", color: "white"}, + text: '导出', //按钮文字 + title: '导出', //鼠标停留显示的信息 + show: auth('materialBaseInfo:Export'), + click: async () => { + return await exportRequest(crudExpose!.getSearchFormData()); + }, + }, + m_delete: { + icon: "DeleteFilled", + style:{backgroundColor: "#FF2520", color: "white"}, + text: '批量删除', //按钮文字 + title: '批量删除', //鼠标停留显示的信息 + show: auth('materialBaseInfo:MultipleDelete'), + click: async () => { + if (selectedIds.value?.length < 1) { + errorMessage("请先勾选记录"); + return + } + await ElMessageBox.confirm(`确定要删除这${selectedIds.value.length}条记录吗`, "确认"); + const req = await MultipleDelRequest({"keys": selectedIds.value}) + selectedIds.value = []; + successNotification(req.msg) + crudExpose.doRefresh() + }, + }, + }, + }, + // tabs: { + // show: true, + // name:'status', //对应查询字段key + // defaultOption:{ + // show:true, //是否显示默认页签 + // value: '', //点击此页签时,对应的查询value + // label: "全部" //页签显示名称 + // } + // }, + // editForm: { + // wrapper:{ + // title:"生成License", + // buttons: { + // ok: {show: true, text: "生成"}, // fs-button配置 + + // }, + // }, + // }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + rowHandle: { + //固定右侧 + fixed: 'right', + align: 'center', + width: 300, + show: true, + buttons: { + + view: { + iconRight: 'View', + // type: 'text', + show: auth('materialBaseInfo:Search'), + }, + edit: { + iconRight: 'Edit', + // type: 'text', + text: '编辑', + show: auth('materialBaseInfo:Update'), + }, + remove: { + iconRight: 'Delete', + // type: 'text', + show: auth('materialBaseInfo:Delete') + } + }, + }, + viewForm: { + col: { + span: 12, //双例模式 + }, + wrapper: { + width: 1000 + }, + columns: { + code: { + show: true + } + } + }, + form: { + col: { + span: 24, //单例模式 + }, + wrapper: { + width: 400 + }, + }, + addForm: { + columns: { + // 默认事件,当字段值改变会清空其他字段内容 + type: { + valueChange({ form, value, getComponentRef }) { + + } + }, + name: { + valueChange({ form, value, getComponentRef }) { + + } + }, + code: { + show: false + } + } + }, + editForm: { + columns: { + code: { + show: false + } + } + }, + table: { + rowKey: "id", + onSelectionChange + }, + columns: { + $checked: { + title: "选择", + form: { show: false}, + column: { + show: auth('materialBaseInfo:MultipleDelete'), + type: "selection", + align: "center", + width: "55px", + columnSetDisabled: true, //禁止在列设置中选择 + // selectable(row, index) { + // // return row.examine_status === 1; //设置第一行不允许选择 + // return true; //设置第一行不允许选择 + // } + } + }, + _index: { + title: '序号', + form: { show: false }, + column: { + //type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = crudExpose!.crudBinding.value.pagination; + return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1; + }, + fixed: "left", + }, + }, + ...customCrudConfig(crudExpose), + specification: { + title: '物料规格', + search: { + show: true, + }, + type: 'text', + column:{ + align: 'center', + sortable: true, + width: 300, + }, + form: { + show: true, + component: { + placeholder: '请输入物料规格', + }, + }, + }, + storage_location: { + title: '存放位置', + search: { + show: false, + }, + type: 'text', + column:{ + align: 'center', + sortable: true, + }, + form: { + show: true, + component: { + placeholder: '请输入存放位置', + }, + }, + }, + safety_stock: { + title: '安全库存', + search: { + show: false, + }, + type: 'number', + column:{ + align: 'center', + sortable: true, + width: 150, + }, + form: { + show: true, + rules: [ + // 表单校验规则 + { + type: 'number', + min: 1, + required: true, + message: '安全库存必须大于0', + } + ], + component: { + placeholder: '请输入安全库存', + }, + }, + }, + ...commonCrudConfig() + }, + + }, + }; +}; diff --git a/web/src/views/materialBase/index.vue b/web/src/views/materialBase/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..8c0894682f815b14d8caabe36c2c6840982de723 --- /dev/null +++ b/web/src/views/materialBase/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/views/materialInventory/api.ts b/web/src/views/materialInventory/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..d04cbebe90b2f77aa247018ee6776e465406669b --- /dev/null +++ b/web/src/views/materialInventory/api.ts @@ -0,0 +1,52 @@ +import { Stream } from 'stream'; +import { request, downloadFile } from '/@/utils/service'; +import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; +import { getBaseURL } from '/@/utils/baseUrl' + +export const apiPrefix = 'api/material/inventory/'; +export function GetList(query: UserPageQuery) { + return request({ + url: apiPrefix, + method: 'get', + params: query, + }); +} +export function GetObj(id: InfoReq) { + return request({ + url: apiPrefix + id, + method: 'get', + }); +} +export function AddObj(obj: AddReq) { + return request({ + url: apiPrefix, + method: 'post', + data: obj, + }); +} + +export function UpdateObj(obj: EditReq) { + console.log(obj) + return request({ + url: apiPrefix + obj.id + '/', + method: 'put', + data: obj, + }); +} + +export function DelObj(id: DelReq) { + return request({ + url: apiPrefix + id + '/', + method: 'delete', + data: { id }, + }); +} + + +export function exportData(params: any) { + return downloadFile({ + url: apiPrefix + 'export_data/', + params: params, + method: 'get', + }); +} \ No newline at end of file diff --git a/web/src/views/materialInventory/crud.tsx b/web/src/views/materialInventory/crud.tsx new file mode 100644 index 0000000000000000000000000000000000000000..79f9d4b2ce0722b6d49bc3a086d8de6b39929cc8 --- /dev/null +++ b/web/src/views/materialInventory/crud.tsx @@ -0,0 +1,329 @@ +import * as api from './api'; +import { errorMessage, successMessage } from '/@/utils/message'; +import tableSelector from '/@/components/tableSelector/index.vue'; +import { shallowRef, computed } from 'vue'; + +import {dictionary} from '/@/utils/dictionary'; +import {compute, dict, UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet,FormWrapperContext } from '@fast-crud/fast-crud'; + +import {auth} from "/@/utils/authFunction"; + +import {commonCrudConfig} from "/@/utils/commonCrud"; +import {customCrudConfig} from "/@/utils/customCrudMaterial"; + +import {createCrudOptions as UserCreateCrudOptions} from '/@/views/user/crud'; + + +export const createCrudOptions = function ({ crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet { + + const pageRequest = async (query: UserPageQuery) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; + const exportRequest = async (query: UserPageQuery) => { + return await api.exportData(query); + }; + + const inRequest = async ( row : any) => { + row.type = 0 + row.number = 1 + return await api.UpdateObj(row); + }; + const outRequest = async ( row : any) => { + row.id = row.id; + row.type = 1 + row.number = 1 + row.receive_name = row.creator + return await api.UpdateObj(row); + }; + + return { + crudOptions: { + actionbar: { + buttons: { + add: { + show: false, + }, + export: { + icon: "Upload", + style:{backgroundColor: "#6495ED", color: "white"}, + text: '导出', //按钮文字 + title: '导出', //鼠标停留显示的信息 + show: auth('materialInventory:Export'), + click() { + return exportRequest(crudExpose!.getSearchFormData()); + }, + }, + }, + }, + // tabs: { + // show: true, + // name:'status', //对应查询字段key + // defaultOption:{ + // show:true, //是否显示默认页签 + // value: '', //点击此页签时,对应的查询value + // label: "全部" //页签显示名称 + // } + // }, + // editForm: { + // wrapper:{ + // title:"生成License", + // buttons: { + // ok: {show: true, text: "生成"}, // fs-button配置 + + // }, + // }, + // }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + rowHandle: { + //固定右侧 + fixed: 'right', + align: 'center', + // width: 300, + show: true, + buttons: { + edit: { + show: false + }, + remove: { + show: false + }, + view: { + show: false, + }, + in: { + icon: "CirclePlusFilled", + style:{backgroundColor: "#32CD32", color: "white"}, + text: '入库', + show: auth('materialInventory:Update'), + click(ctx) { + let row = JSON.parse(JSON.stringify({"record_type": 0, 'id': ctx.row.id})); + crudExpose.openEdit({row},{ + columns: { + number: { + title: "入库数量", + col: {span: 24}, + }, + receive_name: { + show: false + }, + description: { + col: {span: 24}, + show: true, + }, + }, + wrapper:{ + title:"物料入库" + " 【" + ctx.row.type + "】"+ "-【" + ctx.row.name + "】" + } + }) + }, + }, + out: { + icon: "RemoveFilled", + style:{backgroundColor: "#00BFFF", color: "white"}, + text: '出库', + size: "100", + show: auth('materialInventory:Update'), + click: (ctx: any) => { + let row = JSON.parse(JSON.stringify({"record_type": 1, 'id': ctx.row.id})); + crudExpose.openEdit({row},{ + columns: { + number: { + order: 0, + title: "出库数量", + col: {span: 24}, + }, + receive_name: { + show: true, + col: {span: 24}, + order: 1, + }, + description: { + col: {span: 24}, + show: true, + order: 2, + }, + }, + wrapper:{title:"物料出库" + " 【" + ctx.row.type + "】"+ "-【" + ctx.row.name + "】"} + }) + }, + } + }, + }, + editForm: { + wrapper:{ + width: 400, + }, + columns: { + type: { + show: false + }, + name: { + show: false + }, + code: { + show:false + } + } + }, + columns: { + _index: { + title: '序号', + form: { show: false }, + column: { + //type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = crudExpose!.crudBinding.value.pagination; + return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1; + }, + fixed: "left", + }, + }, + ...customCrudConfig(), + specification: { + title: '物料规格', + search: { + show: false, + }, + type: 'text', + column:{ + align: 'center', + sortable: true, + }, + form: { + show: false, + component: { + placeholder: '请输入物料规格', + }, + }, + }, + number: { + title: '剩余库存', + search: { + show: false, + }, + type: ['number', 'colspan'], + column:{ + align: 'center', + sortable: true, + }, + form: { + show: true, + rules: [ + // 表单校验规则 + { + type: 'number', + min: 1, + required: true, + message: '数量需要大于0', + }, + ], + component: { + placeholder: '请填写出库数量', + }, + }, + }, + safety_stock_level: { + title: '安全库存等级', + search: { + show: false, + }, + type: 'dict-select', + dict: dict({ + data: dictionary('material_leval'), + }), + column:{ + align: 'center', + sortable: true, + }, + form: { + show: false + }, + }, + ...commonCrudConfig({ + description: { + form: true, + table: false, + search: false, + } + }), + // 只能在这定义类型,无法改动 + receive_name: { + show: false, + type: "table-select", + dict: dict({ + value: "name", + label: "name", + }), + title: '领用人', + column: { + show: false, + columnSetDisabled: true, //禁止在列设置中选择 + }, + form: { + show: false, + rules: [ + // 表单校验规则 + { + required: true, + message: '请输入领用人', + }, + ], + component: { + placeholder: '申请人', + multiple: false, + rowKey: "name", //element-plus 必传 + createCrudOptions: UserCreateCrudOptions, + dialog: { + width: "700px", + }, + crudOptionsOverride: { + search: { + columns: { + username: { + search: { + show: false, + }, + }, + }, + buttons: { + reset: { + show: false + } + }, + }, + rowHandle: { + show: false, + // fixed: "right" + }, + toolbar: { + show: false + }, + } + }, + }, + }, + }, + + }, + }; +}; diff --git a/web/src/views/materialInventory/index.vue b/web/src/views/materialInventory/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..0f409b74d65ed0b35b9bf2af2fb8d70f49359ec6 --- /dev/null +++ b/web/src/views/materialInventory/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/views/materialOperationlog/api.ts b/web/src/views/materialOperationlog/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f4b367bf7a92094e82b6f633f7e7816c2c51623 --- /dev/null +++ b/web/src/views/materialOperationlog/api.ts @@ -0,0 +1,51 @@ +import { Stream } from 'stream'; +import { request, downloadFile } from '/@/utils/service'; +import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; +import { getBaseURL } from '/@/utils/baseUrl' + +export const apiPrefix = 'api/material/operation_log/'; +export function GetList(query: UserPageQuery) { + return request({ + url: apiPrefix, + method: 'get', + params: query, + }); +} +export function GetObj(id: InfoReq) { + return request({ + url: apiPrefix + id, + method: 'get', + }); +} +export function AddObj(obj: AddReq) { + return request({ + url: apiPrefix, + method: 'post', + data: obj, + }); +} + +export function UpdateObj(obj: EditReq) { + return request({ + url: apiPrefix + obj.id + '/', + method: 'put', + data: obj, + }); +} + +export function DelObj(id: DelReq) { + return request({ + url: apiPrefix + id + '/', + method: 'delete', + data: { id }, + }); +} + + +export function exportData(params: any) { + return downloadFile({ + url: apiPrefix + 'export_data/', + params: params, + method: 'get', + }); +} \ No newline at end of file diff --git a/web/src/views/materialOperationlog/crud.tsx b/web/src/views/materialOperationlog/crud.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e76d55bd9ede06f24c452e862f8a751828a267ab --- /dev/null +++ b/web/src/views/materialOperationlog/crud.tsx @@ -0,0 +1,201 @@ +import * as api from './api'; +import { errorMessage, successMessage } from '/@/utils/message'; +import {dictionary} from '/@/utils/dictionary'; +import tableSelector from '/@/components/tableSelector/index.vue'; +import { shallowRef, computed } from 'vue'; +import {compute, dict, UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet,FormWrapperContext } from '@fast-crud/fast-crud'; + +import {auth} from "/@/utils/authFunction"; + +import {commonCrudConfig} from "/@/utils/commonCrud"; +import {customCrudConfig} from "/@/utils/customCrudMaterial"; + +import {createCrudOptions as T} from '/@/views/user/crud'; +import * as textTableApi from "/@/views/user/api"; + + +export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { + + const pageRequest = async (query: UserPageQuery) => { + return await api.GetList(query); + }; + const editRequest = async ({ form, row }: EditReq) => { + form.id = row.id; + return await api.UpdateObj(form); + }; + const delRequest = async ({ row }: DelReq) => { + return await api.DelObj(row.id); + }; + const addRequest = async ({ form }: AddReq) => { + return await api.AddObj(form); + }; + const exportRequest = async (query: UserPageQuery) => { + return await api.exportData(query); + }; + + return { + crudOptions: { + actionbar: { + buttons: { + add: { + show: false, + text: '添加', + }, + export: { + icon: "Upload", + style:{backgroundColor: "#6495ED", color: "white"}, + text: '导出', //按钮文字 + title: '导出', //鼠标停留显示的信息 + show: auth('materialOperationLog:Export'), + click() { + return exportRequest(crudExpose!.getSearchFormData()); + }, + }, + }, + }, + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + tabs: { + show: true, + name:'record_type', //对应查询字段key + defaultOption:{ + show:true, //是否显示默认页签 + value: '', //点击此页签时,对应的查询value + label: "全部记录" //页签显示名称 + } + }, + rowHandle: { + //固定右侧 + fixed: 'right', + align: 'center', + width: 150, + show: true, + buttons: { + + view: { + iconRight: 'View', + // type: 'text', + show: auth('materialOperationLog:Retrieve'), + }, + edit: { + show: false, + }, + remove: { + show: false + } + }, + }, + columns: { + _index: { + title: '序号', + form: { show: false }, + column: { + //type: 'index', + align: 'center', + width: '70px', + columnSetDisabled: true, //禁止在列设置中选择 + formatter: (context) => { + //计算序号,你可以自定义计算规则,此处为翻页累加 + let index = context.index ?? 1; + let pagination = crudExpose!.crudBinding.value.pagination; + return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1; + }, + fixed: "left", + }, + }, + ...customCrudConfig(), + specification: { + title: '物料规格', + search: { + show: false, + }, + type: 'text', + column:{ + align: 'center', + sortable: true, + width: 150, + }, + }, + record_type: { + title: '操作', + search: { + show: false, + }, + type: 'dict-select', + dict: dict({ + data: dictionary('material_in_out'), + }), + column:{ + align: 'center', + sortable: true, + }, + }, + number: { + title: '数量', + search: { + show: false, + }, + type: 'number', + column:{ + align: 'center', + sortable: true, + }, + }, + receive_name: { + title: "领用人", + search: { show: true }, + column:{ + align: 'center', + sortable: true, + }, + type: "table-select", + dict: dict({ + value: "name", + label: "name", + // getNodesByValues: async (values: any[]) => { + // return await textTableApi.GetObj(values); + // } + }), + form: { + component: { + multiple: false, + rowKey: "name", //element-plus 必传 + createCrudOptions: T, + crudOptionsOverride: { + form: { + wrapper: { + draggable: true, + // width: '400px', //antdv对话框的宽度 + width: 400, + }, + }, + + rowHandle: { + show: false, + // fixed: "right" + } + } + } + } + }, + ...commonCrudConfig({ + create_datetime: { + form: false, + table: true, + search: true, + }, + description: { + form: false, + table: true, + search: false, + } + }), + }, + + }, + }; +}; diff --git a/web/src/views/materialOperationlog/index.vue b/web/src/views/materialOperationlog/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..3675b2381013dd7b0cf9bd2169cc7f107dd02ea6 --- /dev/null +++ b/web/src/views/materialOperationlog/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/views/plugins/dvadmin3-celery-web b/web/src/views/plugins/dvadmin3-celery-web new file mode 160000 index 0000000000000000000000000000000000000000..02d8bd0442f178b5880acecebcc543869e4aae0d --- /dev/null +++ b/web/src/views/plugins/dvadmin3-celery-web @@ -0,0 +1 @@ +Subproject commit 02d8bd0442f178b5880acecebcc543869e4aae0d diff --git a/web/src/views/system/config/components/formContent.vue b/web/src/views/system/config/components/formContent.vue index b4a21413bf7c04a34f986bb6deadcd74cf330e3b..9d9e8419fa9050e09e90d7ab8f4496788e4fccfd 100644 --- a/web/src/views/system/config/components/formContent.vue +++ b/web/src/views/system/config/components/formContent.vue @@ -290,8 +290,20 @@ const getInit = () => { formList.value = data; const _formData: any = {}; for (const item of data) { + const key = item.key; if (item.value) { + /////////////////////////////////////// + //修改后未显示后端地址头,加上地址头 + if ( typeof(item.value) === 'object') { + for (const sub_item of item.value){ + if (sub_item.hasOwnProperty('url')) { + sub_item.url = getBaseURL(sub_item.url) + } + } + + } + /////////////////////////////////////// _formData[key] = item.value; } else { if ([5, 12, 14].indexOf(item.form_item_type) !== -1) { @@ -433,7 +445,7 @@ const handleUploadSuccess = (response: any, file: any, fileList: any, imgKey: an const that = this; const { code, msg } = response; if (code === 2000) { - const { url } = response.data; + const { file_url } = response.data; const { name } = file; const type = isImage(name); if (!type) { @@ -446,7 +458,7 @@ const handleUploadSuccess = (response: any, file: any, fileList: any, imgKey: an // console.log(len) const dict = { name: name, - url: getBaseURL() + url, + url: file_url, }; formData[imgKey].push(dict); } diff --git a/web/src/views/system/login/component/account.vue b/web/src/views/system/login/component/account.vue index 64d870c08801ca38353bfad94cd26695932404b9..8499b2c05f8102b649708e5772588dbedb1233e1 100644 --- a/web/src/views/system/login/component/account.vue +++ b/web/src/views/system/login/component/account.vue @@ -234,9 +234,36 @@ export default defineComponent({ + diff --git a/web/src/views/system/login/index.vue b/web/src/views/system/login/index.vue index 0f4d9c86186777cfc8bb71bd38e7e01a9ea8f874..110035266b7f343da71db8c6abd530649da19426 100644 --- a/web/src/views/system/login/index.vue +++ b/web/src/views/system/login/index.vue @@ -154,8 +154,10 @@ onMounted(() => { animation: logoAnimation 0.3s ease; img { - width: 52px; - height: 52px; + width: 30%; + height: 30%; + // width: 52px; + // height: 52px; } .login-left-logo-text { diff --git a/web/src/views/system/menu/components/MenuButtonCom/crud.tsx b/web/src/views/system/menu/components/MenuButtonCom/crud.tsx index 79675fba30975199db2fc467e36ccfa9e1e27a44..3b8320e7cc0a10ee01421f5cabb30904c5cff946 100644 --- a/web/src/views/system/menu/components/MenuButtonCom/crud.tsx +++ b/web/src/views/system/menu/components/MenuButtonCom/crud.tsx @@ -100,7 +100,7 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti rowHandle: { //固定右侧 fixed: 'right', - width: 200, + width: 220, buttons: { view: { show: false, @@ -113,6 +113,24 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti remove: { show: auth('btn:Delete') }, + copy: { + text: '复制', //按钮显示名称 + // type: 'text', //按钮类型 + show: auth('btn:Copy'), + order: 4, //排序,这个看自己喜欢排在什么位置了 + style:{color: "#606266"}, + title: '复制', + click: (context: any): void => { + // 这里必须拿到了context里面的row属性并且赋值给newrow,接下来做一个深拷贝出来一个全新与源对象无关系的对象row + let newrow = context.row; + let row = JSON.parse(JSON.stringify(newrow)); + // 官方调用openAdd: (context: OpenEditContext, formOpts?: OpenDialogProps) => Promise;其中formOpts就是指form的选择配置 + // 第一个参数只能传命名为row的参数,别的名称好像不行。 + // 第二个参数是传了form的title名称进去{wrapper:{title:"复制数据"}},如果要传其它参数,可根据情况而定 + crudExpose.openAdd({row},{wrapper:{title:"复制数据"}}) + + }, + }, }, }, request: { diff --git a/web/src/views/system/menu/components/MenuTreeCom/index.vue b/web/src/views/system/menu/components/MenuTreeCom/index.vue index 91a9988e14dbaed3e52d814803931b924bcca079..127c0e759977a43d72004e5ee982a470d0185bbc 100644 --- a/web/src/views/system/menu/components/MenuTreeCom/index.vue +++ b/web/src/views/system/menu/components/MenuTreeCom/index.vue @@ -142,6 +142,7 @@ const handleTreeLoad = (node: Node, resolve: Function) => { * 树的点击事件 */ const handleNodeClick = (record: MenuTreeItemType, node: Node) => { + console.log(record, node) treeSelectMenu.value = record; treeSelectNode.value = node; emit('treeClick', record); diff --git a/web/src/views/system/messageCenter/crud.tsx b/web/src/views/system/messageCenter/crud.tsx index dddc1a65a928433bc6de25cb503d1cc7561f989e..fc70481a5d8942640857c7681c50d1260eb5d2ba 100644 --- a/web/src/views/system/messageCenter/crud.tsx +++ b/web/src/views/system/messageCenter/crud.tsx @@ -49,6 +49,24 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat return tabActivted.value !== 'receive' && auth('messageCenter:Create'); }), }, + all_mark_read: { + title: "当前页面全部标记为已读", + text: '全部标记为已读', //按钮文字 + show: computed(() => { + return tabActivted.value === 'receive' && auth('messageCenter:Create'); + }), + click: async () => { + const data = crudExpose.getTableData() + + for (var i=0; i @@ -17,7 +17,7 @@ import { useFs } from '@fast-crud/fast-crud'; import createCrudOptions from './crud'; //tab选择 -const tabActivted = ref('send'); +const tabActivted = ref('receive'); const onTabClick = (tab: any) => { const { paneName } = tab; tabActivted.value = paneName; diff --git a/web/src/views/system/role/components/RoleMenuBtn.vue b/web/src/views/system/role/components/RoleMenuBtn.vue index 8d0f1b41f38692f7ea39e94f351e1ccfa552ef43..4e8fdb67b405a9f05a310f42bcc11dc8632d86c7 100644 --- a/web/src/views/system/role/components/RoleMenuBtn.vue +++ b/web/src/views/system/role/components/RoleMenuBtn.vue @@ -1,9 +1,39 @@