소스 검색

批次下载

flower_bs 4 달 전
부모
커밋
a353662041

+ 3 - 1
.gitignore

@@ -7,6 +7,8 @@ __pycache__/
 
 # Ignore pycharm project files
 /logs/
-templates/src/boot/axios_request.js
+
 
 /bigscreen/
+templates/dist/spa/statics/baseurl.txt
+templates/public/statics/baseurl.txt

+ 47 - 0
bound/files.py

@@ -71,3 +71,50 @@ class FileDetailRenderCN(CSVStreamingRenderer):
     labels = detail_cn_data_header()
 
 
+def BatchFile_headers_list():
+    return [
+        'id',
+        'bound_number',
+        'bound_batch_order',
+        'warehouse_name',
+        'goods_code',
+        'goods_desc',
+        'goods_std',
+        'goods_unit',
+        'goods_qty',
+        'goods_in_qty',
+        'goods_in_location_qty',
+        'goods_out_qty',
+        'check_status',
+        'creater',
+        'create_time',
+        'update_time'
+    ]
+
+
+def BatchFile_cn_data_header_list():
+    return dict([
+        ('id', '序号'),
+        ('bound_number', '管理批次'),
+        ('bound_batch_order', '批号'),
+        ('warehouse_name', '仓库名称'),
+        ('goods_code', '存货编码'),
+        ('goods_desc', '存货名称'),
+        ('goods_std', '规格型号'),
+        ('goods_unit', '单位'),
+        ('goods_qty', '计划数目'),
+        ('goods_in_qty', '已入库/组盘数目'),
+        ('goods_in_location_qty', '在库'),
+        ('goods_out_qty', '已出库数目'),
+        ('check_status', '质检状态'),
+        ('creater', '创建人'),
+        ('create_time', '创建时间'),
+        ('update_time', '更新时间')
+    ])
+
+
+class BatchFileRenderCN(CSVStreamingRenderer):
+    header = BatchFile_headers_list()
+    labels = BatchFile_cn_data_header_list()
+
+

+ 1 - 0
bound/urls.py

@@ -29,6 +29,7 @@ re_path(r'^outdetail/(?P<pk>\d+)/$', views.OutBoundDetailViewSet.as_view({
 }), name="outbounddetail_1"),
 
 path(r'batch/', views.BoundBatchViewSet.as_view({"get": "list", "post": "create" }), name="boundbatch"), 
+path(r'batch/filelist/', views.BatchFileDownloadView.as_view({"get": "list"}), name="batchfilelist"),
 path(r'batch/container/', views.BatchContainerAPIView.as_view(), name="batchcontainer"), 
 
 path(r'batch/count/', views.MaterialStatisticsViewSet.as_view({"get": "list" }), name="materialstatistics"), 

+ 56 - 1
bound/views.py

@@ -9,8 +9,10 @@ from rest_framework.response import Response
 from rest_framework.exceptions import APIException
 from django.utils import timezone
 from django.db.models import Sum
+from django.http import StreamingHttpResponse
+from rest_framework.settings import api_settings
 from .models import BoundListModel, BoundDetailModel,BoundBatchModel, BatchOperateLogModel, OutBatchModel,OutBoundDetailModel,MaterialStatistics,OutBoundDemandModel
-# from .files import FileListRenderCN, FileDetailRenderCN
+from .files import BatchFileRenderCN
 
 from .serializers import BoundListGetSerializer,BoundListPostSerializer,BoundBatchGetSerializer,BoundBatchPostSerializer,BoundDetailGetSerializer,BoundDetailPostSerializer
 from .serializers import OutBoundDetailGetSerializer,OutBoundDetailPostSerializer,OutBatchGetSerializer,OutBatchPostSerializer,BatchLogGetSerializer
@@ -755,6 +757,59 @@ class BoundBatchViewSet(viewsets.ModelViewSet):
         )
         return Response({"code": 200,"message": "操作成功","data": "批次进出库数目:{batch_in_qty}"}, status=200)
 
+
+class BatchFileDownloadView(viewsets.ModelViewSet):
+    """批次文件下载视图类"""
+    renderer_classes = (BatchFileRenderCN, ) + tuple(api_settings.DEFAULT_RENDERER_CLASSES)
+    filter_backends = [DjangoFilterBackend, OrderingFilter, ]
+    ordering_fields = ['id', "create_time", "update_time", ]
+    filter_class = BoundBatchFilter
+
+    def get_project(self):
+        try:
+            id = self.kwargs.get('pk')
+            return id
+        except:
+            return None
+
+    def get_queryset(self):
+        id = self.get_project()
+        if self.request.user:
+            if id is None:
+                queryset = BoundBatchModel.objects.filter(is_delete=False)
+                # 支持 goods_in_location_qty__gt 过滤器,用于只下载在库数量大于0的批次
+                if self.request.query_params.get('goods_in_location_qty__gt'):
+                    queryset = queryset.filter(goods_in_location_qty__gt=0)
+                return queryset
+            else:
+                return BoundBatchModel.objects.filter(id=id, is_delete=False)
+        else:
+            return BoundBatchModel.objects.none()
+
+    def get_serializer_class(self):
+        if self.action in ['list']:
+            return BoundBatchGetSerializer
+        else:
+            return self.http_method_not_allowed(request=self.request)
+
+    def get_render(self, data):
+        return BatchFileRenderCN().render(data)
+
+    def list(self, request, *args, **kwargs):
+        from datetime import datetime
+        dt = datetime.now()
+        data = (
+            BoundBatchGetSerializer(instance).data
+            for instance in self.filter_queryset(self.get_queryset())
+        )
+        renderer = self.get_render(data)
+        response = StreamingHttpResponse(
+            renderer,
+            content_type="text/csv"
+        )
+        response['Content-Disposition'] = "attachment; filename='batch_剩余量_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        return response
+
    
 
 # 入库明细类视图      

+ 7 - 0
staff/urls.py

@@ -12,6 +12,10 @@ urlpatterns = [
         'patch': 'partial_update',
         'delete': 'destroy'
     }), name="staff_1"),
+    # 修改验证码(密码)
+    path('change-check-code/<int:pk>/', views.APIViewSet.as_view({
+        'post': 'change_check_code'
+    }), name="staff-change-check-code"),
     
     # 角色管理路由
     path('role/', views.RoleViewSet.as_view({
@@ -41,6 +45,9 @@ urlpatterns = [
     path('role-permissions/', views.RolePermissionViewSet.as_view({
         'get': 'list'
     }), name="role-permissions-list"),
+    path('role-permissions/reset-default/', views.RolePermissionViewSet.as_view({
+        'post': 'reset_default_permissions'
+    }), name="role-permissions-reset"),
     path('role-permissions/<str:pk>/', views.RolePermissionViewSet.as_view({
         'get': 'retrieve',
         'put': 'update'

+ 400 - 16
staff/views.py

@@ -1,4 +1,5 @@
 from rest_framework import viewsets
+from rest_framework.decorators import action
 from .models import ListModel, TypeListModel
 from . import serializers
 from utils.page import MyPageNumberPagination
@@ -140,17 +141,32 @@ class APIViewSet(viewsets.ModelViewSet):
             check_code = random.randint(1000, 9999)
             data['check_code'] = check_code
             
-            # 创建用户
-            user = User.objects.create_user(
-                username=str(data['staff_name']),
-                password=str(check_code)
-            )
+            # 创建/更新 Django auth 用户
+            username = str(data['staff_name'])
+            password = str(check_code)
+            try:
+                user = User.objects.get(username=username)
+                user.set_password(password)
+                user.save()
+            except User.DoesNotExist:
+                user = User.objects.create_user(username=username, password=password)
             ip = request.META.get('HTTP_X_FORWARDED_FOR') if request.META.get(
             'HTTP_X_FORWARDED_FOR') else request.META.get('REMOTE_ADDR')
-            Users.objects.create(user_id=user.id, name=str(data['name']),
-                                                 openid=app_code, appid=app_code,
-                                                 t_code=Md5.md5(str(timezone.now())),
-                                                 developer=1, ip=ip)
+            # 创建/更新用户档案
+            users_defaults = dict(
+                name=str(data['staff_name']),
+                openid=app_code, appid=app_code,
+                t_code=Md5.md5(str(timezone.now())),
+                developer=1, ip=ip
+            )
+            profile, created = Users.objects.get_or_create(user_id=user.id, defaults=users_defaults)
+            if not created:
+                # 同步基础字段
+                profile.name = users_defaults['name']
+                profile.openid = users_defaults['openid']
+                profile.appid = users_defaults['appid']
+                profile.ip = users_defaults['ip']
+                profile.save()
             
             serializer = self.get_serializer(data=data)
             serializer.is_valid(raise_exception=True)
@@ -172,12 +188,77 @@ class APIViewSet(viewsets.ModelViewSet):
             role, created = Role.objects.get_or_create(name=role_name)
             data['role'] = role.id
         
+        old_staff_name = qs.staff_name
         serializer = self.get_serializer(qs, data=data)
         serializer.is_valid(raise_exception=True)
         serializer.save()
+        # 同步 Django auth 用户与用户档案
+        new_staff_name = serializer.instance.staff_name
+        if old_staff_name != new_staff_name:
+            try:
+                auth_user = User.objects.get(username=str(old_staff_name))
+                auth_user.username = str(new_staff_name)
+                auth_user.save()
+                # 同步 userprofile.Users
+                profile = Users.objects.filter(user_id=auth_user.id).first()
+                if profile:
+                    profile.name = str(new_staff_name)
+                    profile.save()
+            except User.DoesNotExist:
+                # 如不存在旧账号,则以新名称创建
+                auth_user = User.objects.create_user(username=str(new_staff_name), password=str(qs.check_code))
+                Users.objects.get_or_create(user_id=auth_user.id, defaults=dict(
+                    name=str(new_staff_name), openid=qs.appid, appid=qs.appid,
+                    t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
+                ))
         headers = self.get_success_headers(serializer.data)
         return Response(serializer.data, status=200, headers=headers)
 
+    def change_check_code(self, request, pk=None):
+        """
+        修改员工验证码(密码)
+        - 普通员工:只能修改自己的验证码
+        - 管理/主管/管理员/经理:可修改任意员工验证码
+        请求体: { "new_check_code": 1234 }
+        """
+        target_staff = self.get_object()
+        current_staff = ListModel.objects.filter(openid=self.request.auth.openid, is_delete=False).first()
+
+        if current_staff is None:
+            raise APIException({"detail": "当前用户不存在或未登录"})
+
+        privileged_types = ['admin', '主管', '管理员', '经理']
+        can_modify_any = str(current_staff.staff_type) in privileged_types
+
+        if not can_modify_any and current_staff.id != target_staff.id:
+            raise APIException({"detail": "无权限修改他人验证码"})
+
+        new_code = request.data.get('new_check_code')
+        try:
+            new_code_int = int(new_code)
+        except (TypeError, ValueError):
+            raise APIException({"detail": "new_check_code 必须为4位数字"})
+
+        if new_code_int < 0 or new_code_int > 9999:
+            raise APIException({"detail": "new_check_code 必须为0-9999之间的数字"})
+
+        # 更新staff表
+        target_staff.check_code = new_code_int
+        target_staff.error_check_code_counter = 0
+        target_staff.is_lock = False
+        target_staff.save()
+
+        # 同步更新Django auth用户密码(用户名为staff_name)
+        try:
+            auth_user = User.objects.get(username=str(target_staff.staff_name))
+            auth_user.set_password(str(new_code_int))
+            auth_user.save()
+        except User.DoesNotExist:
+            # 若不存在则创建,保持行为幂等
+            User.objects.create_user(username=str(target_staff.staff_name), password=str(new_code_int))
+
+        return Response({"message": "验证码已更新", "id": target_staff.id, "check_code": target_staff.check_code})
+
     def partial_update(self, request, pk):
         qs = self.get_object()
         if qs.openid != self.request.auth.openid:
@@ -191,9 +272,27 @@ class APIViewSet(viewsets.ModelViewSet):
                 role, created = Role.objects.get_or_create(name=role_name)
                 data['role'] = role.id
             
+            old_staff_name = qs.staff_name
             serializer = self.get_serializer(qs, data=data, partial=True)
             serializer.is_valid(raise_exception=True)
             serializer.save()
+            # 同步 Django auth 用户与用户档案
+            new_staff_name = serializer.instance.staff_name
+            if old_staff_name != new_staff_name:
+                try:
+                    auth_user = User.objects.get(username=str(old_staff_name))
+                    auth_user.username = str(new_staff_name)
+                    auth_user.save()
+                    profile = Users.objects.filter(user_id=auth_user.id).first()
+                    if profile:
+                        profile.name = str(new_staff_name)
+                        profile.save()
+                except User.DoesNotExist:
+                    auth_user = User.objects.create_user(username=str(new_staff_name), password=str(qs.check_code))
+                    Users.objects.get_or_create(user_id=auth_user.id, defaults=dict(
+                        name=str(new_staff_name), openid=qs.appid, appid=qs.appid,
+                        t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
+                    ))
             headers = self.get_success_headers(serializer.data)
             return Response(serializer.data, status=200, headers=headers)
 
@@ -241,6 +340,252 @@ class RolePermissionViewSet(viewsets.ViewSet):
         roles = Role.objects.values_list('name', flat=True).distinct()
         return Response(list(roles))
     
+    def reset_default_permissions(self, request):
+        """恢复默认权限配置"""
+        # 从 test_permission.py 导入配置
+        PAGES = [
+            {"primary_page": "stock", "path": "/stock/management", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "stock", "path": "/stock/stocklist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "stock", "path": "/stock/stockbinlist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "stock", "path": "/stock/emptybin", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "stock", "path": "/stock/occupiedbin", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "stock", "path": "/stock/binset", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "stock", "path": "/stock/handcount", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "erp", "path": "/erp/erpasn", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "erp", "path": "/erp/erpasnmaterial", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "erp", "path": "/erp/erpdnmaterial", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "erp", "path": "/erp/erpasnaudit", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "erp", "path": "/erp/erpdn", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "erp", "path": "/erp/erpsortstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/asn", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/predeliverystock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/preloadstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/presortstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/sortstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/shortage", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/more", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "inbound", "path": "/inbound/asnfinish", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "container", "path": "/container/containerlist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "container", "path": "/container/containerdetail", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "container", "path": "/container/containercategory", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "container", "path": "/container/containeroperate", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "dashboard", "path": "/dashboard/inboundAndOutbound", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "dashboard", "path": "/dashboard/flows_statements", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "dashboard", "path": "/dashboard/flows", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "dashboard", "path": "/dashboard/flows_complex", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "dashboard", "path": "/dashboard/batchlog", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "dashboard", "path": "/dashboard/ContainerDetailLogModel", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "dashboard", "path": "/dashboard/MaterialChangeHistory", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/detaillog", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/batchoperatelog", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/countbatchlog", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/presortstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/sortstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/shortage", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/containerDetail", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/batch", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "count", "path": "/count/asnfinish", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/dn", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/freshorder", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/neworder", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/pickstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/pickedstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/pickinglist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/shippedstock", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/backorder", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/pod", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "outbound", "path": "/outbound/container_check", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodslist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodsunit", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodsclass", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodsbrand", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodscolor", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodsspecs", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodsshape", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "goods", "path": "/goods/goodsorigin", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "taskpage", "path": "/taskpage/task", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "warehouse", "path": "/warehouse/warehouseset", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "warehouse", "path": "/warehouse/department", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "warehouse", "path": "/warehouse/boundcodetype", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "warehouse", "path": "/warehouse/boundtype", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "warehouse", "path": "/warehouse/boundbusiness", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "warehouse", "path": "/warehouse/status", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "warehouse", "path": "/warehouse/product", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "staff", "path": "/permission/roles", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "staff", "path": "/staff/stafflist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "staff", "path": "/staff/stafflist_check_code", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "staff", "path": "/staff/stafftype", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "uploadcenter", "path": "/uploadcenter/initializeupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "uploadcenter", "path": "/uploadcenter/addupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadinbound", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadoutbound", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadstocklist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadgoodslist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadbinlist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+        ]
+        
+        ROLES = {
+            "管理员": {
+                "description": "系统管理员,拥有所有权限",
+                "page_access": "all",
+                "component_access": "all"
+            },
+            "经理": {
+                "description": "部门经理,拥有大部分管理权限",
+                "page_access": [
+                    "/stock/management", "/stock/stockbinlist", "/stock/stocklist",
+                    "/erp/erpasn", "/erp/erpasnmaterial", "/erp/erpdnmaterial", "/erp/erpdn", "/erp/erpsortstock",
+                    "/inbound/asn", "/inbound/predeliverystock", "/inbound/sortstock",
+                    "/container/containerlist", "/container/containerdetail", "/container/containercategory", "/container/containeroperate",
+                    "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
+                    "/taskpage/task",
+                    "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
+                    "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
+                    "/warehouse/department", "/warehouse/boundcodetype", "/warehouse/boundtype", "/warehouse/boundbusiness", "/warehouse/status", "/warehouse/product",
+                    "/staff/stafflist", "/staff/stafflist_check_code", "/staff/stafftype"
+                ],
+                "component_access": "all"
+            },
+            "主管": {
+                "description": "仓库主管,负责日常运营管理",
+                "page_access": [
+                    "/stock/management", "/stock/stockbinlist", "/stock/stocklist",
+                    "/erp/erpasn", "/erp/erpasnmaterial", "/erp/erpdnmaterial", "/erp/erpdn", "/erp/erpsortstock",
+                    "/inbound/asn", "/inbound/predeliverystock", "/inbound/sortstock",
+                    "/container/containerlist", "/container/containerdetail", "/container/containercategory",
+                    "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
+                    "/taskpage/task",
+                    "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
+                    "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
+                    "/staff/stafflist", "/staff/stafftype"
+                ],
+                "component_access": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]
+            },
+            "操作员": {
+                "description": "仓库操作员,负责具体操作",
+                "page_access": [
+                    "/stock/management", "/stock/stockbinlist", "/stock/stocklist",
+                    "/erp/erpasn", "/erp/erpasnmaterial", "/erp/erpdnmaterial", "/erp/erpdn", "/erp/erpsortstock",
+                    "/inbound/asn", "/inbound/predeliverystock", "/inbound/sortstock",
+                    "/container/containerlist", "/container/containercategory",
+                    "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
+                    "/taskpage/task",
+                    "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
+                    "/dashboard/flows_statements", "/dashboard/flows"
+                ],
+                "component_access": ["view", "edit", "add", "download", "confirm"]
+            },
+            "查看员": {
+                "description": "数据查看员,只能查看数据",
+                "page_access": [
+                    "/stock/management", "/stock/stockbinlist",
+                    "/erp/erpasn", "/erp/erpasnmaterial", "/erp/erpdnmaterial", "/erp/erpdn", "/erp/erpsortstock",
+                    "/inbound/asn", "/inbound/predeliverystock", "/inbound/sortstock",
+                    "/container/containerlist", "/container/containercategory",
+                    "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
+                    "/taskpage/task",
+                    "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
+                    "/dashboard/flows_statements", "/dashboard/flows"
+                ],
+                "component_access": ["view", "download"]
+            }
+        }
+        
+        try:
+            # 1. 创建所有权限
+            created_perm_count = 0
+            for page_info in PAGES:
+                primary_page = page_info.get("primary_page")
+                page_path = page_info["path"]
+                components = page_info["components"]
+                
+                # 创建页面访问权限
+                page_permission, created = Permission.objects.get_or_create(
+                    primary_page=primary_page,
+                    page=page_path,
+                    component=None,
+                    defaults={
+                        "name": f"{page_path} 页面访问",
+                        "description": f"访问 {page_path} 页面的权限",
+                        "enabled": True
+                    }
+                )
+                if created:
+                    created_perm_count += 1
+                
+                # 创建组件权限
+                for component in components:
+                    comp_permission, created = Permission.objects.get_or_create(
+                        primary_page=primary_page,
+                        page=page_path,
+                        component=component,
+                        defaults={
+                            "name": f"{page_path} - {component} 组件",
+                            "description": f"在 {page_path} 页面使用 {component} 组件的权限",
+                            "enabled": True
+                        }
+                    )
+                    if created:
+                        created_perm_count += 1
+            
+            # 2. 恢复所有角色的默认权限
+            all_permissions = Permission.objects.all()
+            role_count = 0
+            permission_count = 0
+            
+            for role_name, role_config in ROLES.items():
+                role, created = Role.objects.get_or_create(
+                    name=role_name,
+                    defaults={"description": role_config["description"]}
+                )
+                
+                if role:
+                    role_count += 1
+                    
+                    if role_config["page_access"] == "all":
+                        # 分配所有权限
+                        role.permissions.set(all_permissions)
+                        permission_count += all_permissions.count()
+                    else:
+                        # 分配特定权限
+                        assigned_permissions = []
+                        
+                        for page_path in role_config["page_access"]:
+                            # 获取页面权限
+                            page_perms = all_permissions.filter(
+                                page=page_path,
+                                component=None
+                            )
+                            assigned_permissions.extend(page_perms)
+                            
+                            # 获取组件权限
+                            comp_perms = all_permissions.filter(
+                                page=page_path,
+                                component__isnull=False
+                            )
+                            
+                            if role_config["component_access"] == "all":
+                                assigned_permissions.extend(comp_perms)
+                            else:
+                                for perm in comp_perms:
+                                    if perm.component in role_config["component_access"]:
+                                        assigned_permissions.append(perm)
+                        
+                        role.permissions.set(assigned_permissions)
+                        permission_count += len(assigned_permissions)
+            
+            return Response({
+                "message": "默认权限已恢复成功",
+                "created_permissions": created_perm_count,
+                "roles_updated": role_count,
+                "permissions_assigned": permission_count
+            })
+        except Exception as e:
+            return Response({
+                "error": "恢复默认权限失败",
+                "detail": str(e)
+            }, status=500)
+    
     def retrieve(self, request, pk=None):
         """获取特定角色的权限配置"""
         try:
@@ -252,22 +597,61 @@ class RolePermissionViewSet(viewsets.ViewSet):
             return Response({"error": "Role not found"}, status=404)
     
     def update(self, request, pk=None):
-        """更新角色权限"""
+        """更新角色权限(增量更新,只更新当前页面的权限)"""
         try:
             role = Role.objects.get(name=pk)
             permissions_data = request.data.get('permissions', [])
             
-            # 清除现有权限
-            role.permissions.clear()
+            if not permissions_data:
+                return Response({"message": "No permissions to update"}, status=400)
+            
+            # 获取要更新的页面(从第一个权限中获取,因为所有权限应该属于同一页面)
+            target_page = permissions_data[0].get('page') if permissions_data else None
+            if not target_page:
+                return Response({"error": "Page is required"}, status=400)
             
-            # 添加新权限
+            # 获取当前页面已关联的权限,从角色中移除
+            existing_perms = role.permissions.filter(page=target_page)
+            # 从角色的权限中移除当前页面的所有权限
+            role.permissions.remove(*existing_perms)
+            
+            # 添加/更新当前页面的权限
+            # 注意:这里只控制角色与权限的关联关系,不修改权限对象本身的 enabled 状态
+            new_perms = []
             for perm_data in permissions_data:
+                # 确保所有权限都属于同一页面
+                if perm_data.get('page') != target_page:
+                    continue
+                
+                # 如果 enabled 为 False,表示该角色不应该拥有此权限,跳过不添加
+                if not perm_data.get('enabled', True):
+                    continue
+                
+                component = perm_data.get('component')
+                # 处理 component 为 null 的情况
+                if component is None or component == '':
+                    component = None
+                    
+                # 获取或创建权限对象(不修改 enabled 状态)
                 perm, created = Permission.objects.get_or_create(
                     page=perm_data['page'],
-                    component=perm_data.get('component'),
-                    defaults={'enabled': perm_data['enabled']}
+                    component=component,
+                    defaults={
+                        'name': perm_data.get('name', f"{perm_data['page']}-{perm_data.get('component', 'page')}"),
+                        'enabled': True,  # 新创建的权限默认启用
+                        'primary_page': perm_data.get('primary_page', ''),
+                        'description': perm_data.get('description', '')
+                    }
                 )
-                role.permissions.add(perm)
+                
+                # 不修改已存在权限的 enabled 状态,保持原有状态
+                
+                # 只将 enabled 为 True 的权限添加到角色
+                new_perms.append(perm)
+            
+            # 批量添加新权限到角色(只添加 enabled 为 True 的权限)
+            if new_perms:
+                role.permissions.add(*new_perms)
             
             return Response({"message": "Permissions updated successfully"})
         except Role.DoesNotExist:

+ 1 - 1
templates/dist/spa/statics/baseurl.txt

@@ -1 +1 @@
-http://192.168.31.27:8008
+http://localhost:8008

+ 1 - 1
templates/public/statics/baseurl.txt

@@ -1 +1 @@
-http://192.168.31.27:8008
+http://localhost:8008

+ 734 - 0
templates/src/boot/axios_request.js

@@ -0,0 +1,734 @@
+import Vue from 'vue'
+import axios from 'axios'
+import { SessionStorage, LocalStorage, Notify, Loading } from 'quasar'
+import { i18n } from './i18n'
+import Bus from './bus.js'
+
+function getBaseUrl (name) {
+  const xhr = new XMLHttpRequest()
+  const okStatus = document.location.protocol === 'file:' ? 0 : 200
+  xhr.open('GET', '../../statics/' + name, false)
+  console.log('xhr.open', xhr.open)
+  xhr.overrideMimeType('text/html; charset=utf-8')
+  xhr.send(null)
+  return xhr.status === okStatus ? xhr.responseText : null
+}
+// 注意以后修改成服务器的地址
+// 获取本机ip地址
+// const ip = getLocalIP()
+// function getLocalIP () {
+//   const interfaces = require('os').networkInterfaces()
+//   for (const key in interfaces) {
+//     for (const alias of interfaces[key]) {
+//       if (alias.family === 'IPv4' && !alias.internal) {
+//         return alias.address
+//       }
+//     }
+//   }
+// }
+// console.log('当前IP地址:', ip)
+// // const baseurl = 'http://localhost:8008'
+// const baseurl = 'http://' + ip + ':8008'
+// const baseurl = 'http://192.168.31.107:8008'
+const baseurl = getBaseUrl('baseurl.txt')
+
+const axiosInstance = axios.create({
+  baseURL: baseurl
+})
+
+const axiosInstanceVersion = axios.create({
+  baseURL: baseurl
+})
+
+const axiosInstanceAuth = axios.create({
+  baseURL: baseurl
+})
+
+const axiosInstanceAuthScan = axios.create({
+  baseURL: baseurl
+})
+
+var lang = LocalStorage.getItem('lang')
+if (LocalStorage.has('lang')) {
+  lang = lang || 'zh-hans'
+} else {
+  LocalStorage.set('lang', 'zh-hans')
+  lang = 'zh-hans'
+}
+
+const axiosFile = axios.create({
+  baseURL: baseurl
+})
+
+axiosInstanceAuth.interceptors.request.use(
+  function (config) {
+    const auth = LocalStorage.getItem('auth')
+    const login = SessionStorage.getItem('axios_check')
+    if (auth || login) {
+      config.headers.post['Content-Type'] = 'application/json, charset="utf-8"'
+      config.headers.patch['Content-Type'] =
+        'application/json, charset="utf-8"'
+      config.headers.put['Content-Type'] = 'application/json, charset="utf-8"'
+      config.headers.token = LocalStorage.getItem('openid')
+      config.headers.appid = LocalStorage.getItem('appid')
+
+      config.headers.operator = LocalStorage.getItem('login_id')
+      config.headers.language = lang
+      if (
+        config.method === 'post' ||
+        config.method === 'patch' ||
+        config.method === 'put' ||
+        config.method === 'delete'
+      ) {
+        Loading.show()
+      }
+      return config
+    } else {
+      Loading.hide()
+      Bus.$emit('needLogin', true)
+    }
+  },
+  function (error) {
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosInstanceAuth.interceptors.response.use(
+  function (response) {
+    if (response.data.detail) {
+      if (response.data.detail !== 'success') {
+        Notify.create({
+          message: response.data.detail,
+          icon: 'close',
+          color: 'negative',
+          timeout: 1500
+        })
+      }
+    }
+    if (response.data.results) {
+      var sslcheck = baseurl.split(':')
+      if (response.data.next !== null) {
+        if (sslcheck.length === 2) {
+          var nextlinkcheck = response.data.next.toString().split(sslcheck[1])
+          response.data.next = nextlinkcheck[1]
+        } else {
+          var nextlinkcheck1 = response.data.next
+            .toString()
+            .split(sslcheck[1] + ':' + sslcheck[2])
+          response.data.next = nextlinkcheck1[1]
+        }
+      } else {
+        response.data.next = null
+      }
+      if (response.data.previous !== null) {
+        if (sslcheck.length === 2) {
+          var previouslinkcheck = response.data.previous
+            .toString()
+            .split(sslcheck[1])
+          response.data.previous = previouslinkcheck[1]
+        } else {
+          var previouslinkcheck1 = response.data.previous
+            .toString()
+            .split(sslcheck[1] + ':' + sslcheck[2])
+          response.data.previous = previouslinkcheck1[1]
+        }
+      } else {
+        response.data.previous = null
+      }
+      Loading.hide()
+      return response.data
+    }
+    Loading.hide()
+    return response.data
+  },
+  function (error) {
+    const defaultNotify = {
+      message: '未知错误',
+      icon: 'close',
+      color: 'negative',
+      timeout: 1500
+    }
+    if (
+      error.code === 'ECONNABORTED' ||
+      error.message.indexOf('timeout') !== -1 ||
+      error.message === 'Network Error'
+    ) {
+      defaultNotify.message = i18n.t('notice.network_error')
+      Notify.create(defaultNotify)
+      Loading.hide()
+      return Promise.reject(error)
+    }
+    switch (error.response.status) {
+      case 400:
+        defaultNotify.message = i18n.t('notice.400')
+        Notify.create(defaultNotify)
+        break
+      case 401:
+        defaultNotify.message = i18n.t('notice.401')
+        Notify.create(defaultNotify)
+        break
+      case 403:
+        defaultNotify.message = i18n.t('notice.403')
+        Notify.create(defaultNotify)
+        break
+      case 404:
+        defaultNotify.message = i18n.t('notice.404')
+        Notify.create(defaultNotify)
+        break
+      case 405:
+        defaultNotify.message = i18n.t('notice.405')
+        Notify.create(defaultNotify)
+        break
+      case 408:
+        defaultNotify.message = i18n.t('notice.408')
+        Notify.create(defaultNotify)
+        break
+      case 409:
+        defaultNotify.message = i18n.t('notice.409')
+        Notify.create(defaultNotify)
+        break
+      case 410:
+        defaultNotify.message = i18n.t('notice.410')
+        Notify.create(defaultNotify)
+        break
+      case 500:
+        defaultNotify.message = i18n.t('notice.500')
+        Notify.create(defaultNotify)
+        break
+      case 501:
+        defaultNotify.message = i18n.t('notice.501')
+        Notify.create(defaultNotify)
+        break
+      case 502:
+        defaultNotify.message = i18n.t('notice.502')
+        Notify.create(defaultNotify)
+        break
+      case 503:
+        defaultNotify.message = i18n.t('notice.503')
+        Notify.create(defaultNotify)
+        break
+      case 504:
+        defaultNotify.message = i18n.t('notice.504')
+        Notify.create(defaultNotify)
+        break
+      case 505:
+        defaultNotify.message = i18n.t('notice.505')
+        Notify.create(defaultNotify)
+        break
+      default:
+        Notify.create(defaultNotify)
+        break
+    }
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosInstanceAuthScan.interceptors.request.use(
+  function (config) {
+    // 从 LocalStorage 获取认证信息
+    const auth = LocalStorage.getItem('auth')
+    // 从 SessionStorage 获取 axios 检查标志
+    const login = SessionStorage.getItem('axios_check')
+    // 如果存在认证信息或登录标志,则设置请求头并显示加载状态
+    if (auth || login) {
+      config.headers.post['Content-Type'] = 'application/json, charset="utf-8"'
+      config.headers.patch['Content-Type'] =
+        'application/json, charset="utf-8"'
+      config.headers.put['Content-Type'] = 'application/json, charset="utf-8"'
+      config.headers.token = LocalStorage.getItem('openid')
+      config.headers.appid = LocalStorage.getItem('appid')
+      config.headers.operator = LocalStorage.getItem('login_id')
+      config.headers.language = lang
+      // 对于 POST, PATCH, PUT 和 DELETE 方法,显示加载状态
+      if (
+        config.method === 'post' ||
+        config.method === 'patch' ||
+        config.method === 'put' ||
+        config.method === 'delete'
+      ) {
+        Loading.show()
+      }
+      return config
+    } else {
+      // 隐藏加载状态并发出需要登录的事件
+      Loading.hide()
+      Bus.$emit('needLogin', true)
+    }
+  },
+  function (error) {
+    // 隐藏加载状态并拒绝请求
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosInstanceAuthScan.interceptors.response.use(
+  function (response) {
+    if (response.data.results) {
+      var sslcheck = baseurl.split(':')
+      if (response.data.next !== null) {
+        if (sslcheck.length === 2) {
+          var nextlinkcheck = response.data.next.toString().split(sslcheck[1])
+          response.data.next = nextlinkcheck[1]
+        } else {
+          var nextlinkcheck1 = response.data.next
+            .toString()
+            .split(sslcheck[1] + ':' + sslcheck[2])
+          response.data.next = nextlinkcheck1[1]
+        }
+      } else {
+        response.data.next = null
+      }
+      if (response.data.previous !== null) {
+        if (sslcheck.length === 2) {
+          var previouslinkcheck = response.data.previous
+            .toString()
+            .split(sslcheck[1])
+          response.data.previous = previouslinkcheck[1]
+        } else {
+          var previouslinkcheck1 = response.data.previous
+            .toString()
+            .split(sslcheck[1] + ':' + sslcheck[2])
+          response.data.previous = previouslinkcheck1[1]
+        }
+      } else {
+        response.data.previous = null
+      }
+      Loading.hide()
+      return response.data
+    }
+    Loading.hide()
+    return response.data
+  },
+  function (error) {
+    const defaultNotify = {
+      message: i18n.t('notice.unknow_error'),
+      icon: 'close',
+      color: 'negative',
+      timeout: 1500
+    }
+    if (
+      error.code === 'ECONNABORTED' ||
+      error.message.indexOf('timeout') !== -1 ||
+      error.message === 'Network Error'
+    ) {
+      defaultNotify.message = i18n.t('notice.network_error')
+      Notify.create(defaultNotify)
+      Loading.hide()
+      return Promise.reject(error)
+    }
+    switch (error.response.status) {
+      case 400:
+        defaultNotify.message = i18n.t('notice.400')
+        Notify.create(defaultNotify)
+        break
+      case 401:
+        defaultNotify.message = i18n.t('notice.401')
+        Notify.create(defaultNotify)
+        break
+      case 403:
+        defaultNotify.message = i18n.t('notice.403')
+        Notify.create(defaultNotify)
+        break
+      case 404:
+        defaultNotify.message = i18n.t('notice.404')
+        Notify.create(defaultNotify)
+        break
+      case 405:
+        defaultNotify.message = i18n.t('notice.405')
+        Notify.create(defaultNotify)
+        break
+      case 408:
+        defaultNotify.message = i18n.t('notice.408')
+        Notify.create(defaultNotify)
+        break
+      case 409:
+        defaultNotify.message = i18n.t('notice.409')
+        Notify.create(defaultNotify)
+        break
+      case 410:
+        defaultNotify.message = i18n.t('notice.410')
+        Notify.create(defaultNotify)
+        break
+      case 500:
+        defaultNotify.message = i18n.t('notice.500')
+        Notify.create(defaultNotify)
+        break
+      case 501:
+        defaultNotify.message = i18n.t('notice.501')
+        Notify.create(defaultNotify)
+        break
+      case 502:
+        defaultNotify.message = i18n.t('notice.502')
+        Notify.create(defaultNotify)
+        break
+      case 503:
+        defaultNotify.message = i18n.t('notice.503')
+        Notify.create(defaultNotify)
+        break
+      case 504:
+        defaultNotify.message = i18n.t('notice.504')
+        Notify.create(defaultNotify)
+        break
+      case 505:
+        defaultNotify.message = i18n.t('notice.505')
+        Notify.create(defaultNotify)
+        break
+      default:
+        Notify.create(defaultNotify)
+        break
+    }
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosInstance.interceptors.request.use(
+  function (config) {
+    config.headers.post['Content-Type'] = 'application/json, charset="utf-8"'
+    config.headers.language = lang
+    if (
+      config.method === 'post' ||
+      config.method === 'patch' ||
+      config.method === 'put' ||
+      config.method === 'delete'
+    ) {
+      Loading.show()
+    }
+    return config
+  },
+  function (error) {
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosInstance.interceptors.response.use(
+  function (response) {
+    if (response.data.detail) {
+      if (response.data.detail !== 'success') {
+        Notify.create({
+          message: response.data.detail,
+          icon: 'close',
+          color: 'negative',
+          timeout: 1500
+        })
+      }
+    }
+    Loading.hide()
+    return response.data
+  },
+  function (error) {
+    const defaultNotify = {
+      message: i18n.t('notice.network_error'),
+      icon: 'close',
+      color: 'negative',
+      timeout: 1500
+    }
+    if (
+      error.code === 'ECONNABORTED' ||
+      error.message.indexOf('timeout') !== -1 ||
+      error.message === 'Network Error'
+    ) {
+      defaultNotify.message = i18n.t('notice.network_error')
+      Notify.create(defaultNotify)
+      Loading.hide()
+      return Promise.reject(error)
+    }
+    switch (error.response.status) {
+      case 400:
+        defaultNotify.message = i18n.t('notice.400')
+        Notify.create(defaultNotify)
+        break
+      case 401:
+        defaultNotify.message = i18n.t('notice.401')
+        Notify.create(defaultNotify)
+        break
+      case 403:
+        defaultNotify.message = i18n.t('notice.403')
+        Notify.create(defaultNotify)
+        break
+      case 404:
+        defaultNotify.message = i18n.t('notice.404')
+        Notify.create(defaultNotify)
+        break
+      case 405:
+        defaultNotify.message = i18n.t('notice.405')
+        Notify.create(defaultNotify)
+        break
+      case 408:
+        defaultNotify.message = i18n.t('notice.408')
+        Notify.create(defaultNotify)
+        break
+      case 409:
+        defaultNotify.message = i18n.t('notice.409')
+        Notify.create(defaultNotify)
+        break
+      case 410:
+        defaultNotify.message = i18n.t('notice.410')
+        Notify.create(defaultNotify)
+        break
+      case 500:
+        defaultNotify.message = i18n.t('notice.500')
+        Notify.create(defaultNotify)
+        break
+      case 501:
+        defaultNotify.message = i18n.t('notice.501')
+        Notify.create(defaultNotify)
+        break
+      case 502:
+        defaultNotify.message = i18n.t('notice.502')
+        Notify.create(defaultNotify)
+        break
+      case 503:
+        defaultNotify.message = i18n.t('notice.503')
+        Notify.create(defaultNotify)
+        break
+      case 504:
+        defaultNotify.message = i18n.t('notice.504')
+        Notify.create(defaultNotify)
+        break
+      case 505:
+        defaultNotify.message = i18n.t('notice.505')
+        Notify.create(defaultNotify)
+        break
+      default:
+        Notify.create(defaultNotify)
+        break
+    }
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosInstanceVersion.interceptors.request.use(
+  function (config) {
+    const auth = LocalStorage.getItem('auth')
+    const login = SessionStorage.getItem('axios_check')
+    if (auth || login) {
+      if (
+        config.method === 'post' ||
+        config.method === 'patch' ||
+        config.method === 'put' ||
+        config.method === 'delete'
+      ) {
+        Loading.show()
+      }
+      return config
+    } else {
+      Loading.hide()
+      Bus.$emit('needLogin', true)
+    }
+  },
+  function (error) {
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosInstanceVersion.interceptors.response.use(
+  function (response) {
+    Loading.hide()
+    return response.data
+  },
+  function (error) {
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosFile.interceptors.request.use(
+  function (config) {
+    const auth = LocalStorage.getItem('auth')
+    const login = SessionStorage.getItem('axios_check')
+    if (auth || login) {
+      config.headers.get['Content-Type'] = 'application/vnd.ms-excel'
+      config.headers.token = LocalStorage.getItem('openid')
+      config.headers.appid = LocalStorage.getItem('appid')
+      config.headers.operator = LocalStorage.getItem('login_id')
+      config.headers.language = lang
+      if (
+        config.method === 'post' ||
+        config.method === 'patch' ||
+        config.method === 'put' ||
+        config.method === 'delete'
+      ) {
+        Loading.show()
+      }
+      return config
+    } else {
+      Loading.hide()
+      Bus.$emit('needLogin', true)
+    }
+  },
+  function (error) {
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+axiosFile.interceptors.response.use(
+  function (response) {
+    if (response.data.detail) {
+      if (response.data.detail !== 'success') {
+        Notify.create({
+          message: response.data.detail,
+          icon: 'close',
+          color: 'negative',
+          timeout: 1500
+        })
+      }
+    }
+    Loading.hide()
+    return response
+  },
+  function (error) {
+    const defaultNotify = {
+      message: i18n.t('notice.network_error'),
+      icon: 'close',
+      color: 'negative',
+      timeout: 1500
+    }
+    if (
+      error.code === 'ECONNABORTED' ||
+      error.message.indexOf('timeout') !== -1 ||
+      error.message === 'Network Error'
+    ) {
+      defaultNotify.message = i18n.t('notice.network_error')
+      Notify.create(defaultNotify)
+      Loading.hide()
+      return Promise.reject(error)
+    }
+    switch (error.response.status) {
+      case 400:
+        defaultNotify.message = i18n.t('notice.400')
+        Notify.create(defaultNotify)
+        break
+      case 401:
+        defaultNotify.message = i18n.t('notice.401')
+        Notify.create(defaultNotify)
+        break
+      case 403:
+        defaultNotify.message = i18n.t('notice.403')
+        Notify.create(defaultNotify)
+        break
+      case 404:
+        defaultNotify.message = i18n.t('notice.404')
+        Notify.create(defaultNotify)
+        break
+      case 405:
+        defaultNotify.message = i18n.t('notice.405')
+        Notify.create(defaultNotify)
+        break
+      case 408:
+        defaultNotify.message = i18n.t('notice.408')
+        Notify.create(defaultNotify)
+        break
+      case 409:
+        defaultNotify.message = i18n.t('notice.409')
+        Notify.create(defaultNotify)
+        break
+      case 410:
+        defaultNotify.message = i18n.t('notice.410')
+        Notify.create(defaultNotify)
+        break
+      case 500:
+        defaultNotify.message = i18n.t('notice.500')
+        Notify.create(defaultNotify)
+        break
+      case 501:
+        defaultNotify.message = i18n.t('notice.501')
+        Notify.create(defaultNotify)
+        break
+      case 502:
+        defaultNotify.message = i18n.t('notice.502')
+        Notify.create(defaultNotify)
+        break
+      case 503:
+        defaultNotify.message = i18n.t('notice.503')
+        Notify.create(defaultNotify)
+        break
+      case 504:
+        defaultNotify.message = i18n.t('notice.504')
+        Notify.create(defaultNotify)
+        break
+      case 505:
+        defaultNotify.message = i18n.t('notice.505')
+        Notify.create(defaultNotify)
+        break
+      default:
+        Notify.create(defaultNotify)
+        break
+    }
+    Loading.hide()
+    return Promise.reject(error)
+  }
+)
+
+function getauth (url) {
+  return axiosInstanceAuth.get(url)
+}
+
+function get (url) {
+  return axiosInstance.get(url)
+}
+
+function versioncheck (url) {
+  return axiosInstanceVersion.get(url)
+}
+
+function post (url, data) {
+  return axiosInstance.post(url, data)
+}
+
+function postauth (url, data) {
+  return axiosInstanceAuth.post(url, data)
+}
+
+function putauth (url, data) {
+  return axiosInstanceAuth.put(url, data)
+}
+
+function patchauth (url, data) {
+  return axiosInstanceAuth.patch(url, data)
+}
+
+function deleteauth (url) {
+  return axiosInstanceAuth.delete(url)
+}
+
+function ViewPrintAuth (url) {
+  return axiosInstanceAuth.get(url)
+}
+
+function scangetauth (url) {
+  return axiosInstanceAuthScan.get(url)
+}
+
+function scanpostauth (url, data) {
+  return axiosInstanceAuthScan.post(url, data)
+}
+
+function getfile (url) {
+  return axiosFile.get(url)
+}
+
+Vue.prototype.$axios = axios
+
+export {
+  baseurl,
+  get,
+  versioncheck,
+  post,
+  getauth,
+  postauth,
+  putauth,
+  deleteauth,
+  patchauth,
+  ViewPrintAuth,
+  getfile,
+  scangetauth,
+  scanpostauth
+}

+ 126 - 3
templates/src/pages/count/batch.vue

@@ -58,6 +58,36 @@
 
           <q-space />
 
+          <div class="flex items-center q-mr-md">
+            <q-toggle
+              v-model="onlyDownloadInStock"
+              label="只下载在库批次"
+              color="primary"
+              dense
+            >
+              <q-tooltip
+                content-class="bg-amber text-black shadow-4"
+                :offset="[10, 10]"
+                content-style="font-size: 12px"
+                >只下载在库数量大于0的批次(发完了的批次不下载)</q-tooltip
+              >
+            </q-toggle>
+            <q-btn
+              label="下载批次剩余量"
+              icon="cloud_download"
+              color="primary"
+              @click="downloadBatchData()"
+              class="q-ml-md"
+            >
+              <q-tooltip
+                content-class="bg-amber text-black shadow-4"
+                :offset="[10, 10]"
+                content-style="font-size: 12px"
+                >下载批次在库数量</q-tooltip
+              >
+            </q-btn>
+          </div>
+
           <div class="flex items-center">
             <div class="q-mr-md">{{ $t("download_center.createTime") }}</div>
             <q-input
@@ -492,8 +522,8 @@
 <router-view />
 
 <script>
-import { getauth, postauth, putauth, deleteauth } from 'boot/axios_request'
-import { date, LocalStorage } from 'quasar'
+import { getauth, postauth, putauth, deleteauth, getfile } from 'boot/axios_request'
+import { date, exportFile, LocalStorage } from 'quasar'
 import containercard from 'components/containercard.vue'
 
 
@@ -518,6 +548,7 @@ export default {
       pathname: 'bound/batch/',
       pathname_previous: '',
       pathname_next: '',
+      downloadUrl: 'bound/batch/filelist/',
       separator: 'cell',
       loading: false,
       height: '',
@@ -658,7 +689,8 @@ export default {
       checkQuantity: 0,
       userComponentPermissions: [], // 用户权限
       login_mode: LocalStorage.getItem('login_mode'), // 登录模式
-      selectedBatchId: null
+      selectedBatchId: null,
+      onlyDownloadInStock: true // 默认只下载在库数量大于0的批次
     }
   },
   computed: {
@@ -1090,12 +1122,97 @@ export default {
       this.filterModels = {
         bound_department: null
       }
+      this.downloadUrl = 'bound/batch/filelist/'
       _this.getSearchList()
     },
 
     updateProxy () {
       var _this = this
       _this.proxyDate = _this.date
+    },
+    // 更新下载URL
+    updateDownloadUrl () {
+      var _this = this
+      let downloadUrl = 'bound/batch/filelist/'
+      const params = []
+      
+      if (_this.date_range) {
+        params.push('create_time__range=' + _this.date_range)
+      }
+      
+      if (params.length > 0) {
+        downloadUrl += '?' + params.join('&')
+      }
+      
+      _this.downloadUrl = downloadUrl
+    },
+    // 下载批次剩余量数据
+    downloadBatchData () {
+      var _this = this
+      // 构建下载URL,从基础路径开始
+      let downloadUrl = 'bound/batch/filelist/'
+      
+      // 构建查询参数
+      const params = []
+      
+      // 如果选择了只下载在库批次,添加过滤条件
+      if (_this.onlyDownloadInStock) {
+        params.push('goods_in_location_qty__gt=0')
+      }
+      
+      // 添加时间范围过滤(如果有选择)
+      if (_this.date_range) {
+        params.push('create_time__range=' + _this.date_range)
+      }
+      
+      // 添加其他搜索条件
+      if (_this.filter) {
+        params.push('bound_number__icontains=' + encodeURIComponent(_this.filter))
+      }
+      
+      // 添加过滤条件
+      for (const [key, value] of Object.entries(_this.filterModels)) {
+        if (value !== null && value !== '') {
+          params.push(key + '=' + value)
+        }
+      }
+      
+      // 构建完整的下载URL
+      if (params.length > 0) {
+        downloadUrl += '?' + params.join('&')
+      }
+      
+      // 调用下载接口
+      getfile(downloadUrl)
+        .then(res => {
+          var timeStamp = Date.now()
+          var formattedString = date.formatDate(timeStamp, 'YYYYMMDDHHmmssSSS')
+          const status = exportFile(
+            'batch_剩余量_' + formattedString + '.csv',
+            '\uFEFF' + res.data,
+            'text/csv'
+          )
+          if (status !== true) {
+            _this.$q.notify({
+              message: '浏览器拒绝了文件下载...',
+              color: 'negative',
+              icon: 'warning'
+            })
+          } else {
+            _this.$q.notify({
+              message: '批次剩余量下载成功',
+              color: 'positive',
+              icon: 'check'
+            })
+          }
+        })
+        .catch(err => {
+          _this.$q.notify({
+            message: '下载失败: ' + (err.message || '未知错误'),
+            color: 'negative',
+            icon: 'close'
+          })
+        })
     }
   },
   created () {
@@ -1166,10 +1283,16 @@ export default {
 
         this.getSearchList()
         this.$refs.qDateProxy.hide()
+        
+        // 更新下载URL
+        this.updateDownloadUrl()
       } else {
         this.createDate2 = ''
         this.date_range = ''
         this.getSearchList()
+        
+        // 更新下载URL
+        this.updateDownloadUrl()
       }
     }
   }

+ 441 - 89
templates/src/pages/permission/PermissionManage.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="q-pa-md">
+  <div class="q-pa-md fit column">
     <q-tabs
       v-model="currentTab"
       dense
@@ -15,19 +15,22 @@
 
     <q-separator />
 
-    <q-tab-panels v-model="currentTab" animated>
+    <q-tab-panels v-model="currentTab" animated class="col scroll">
       <!-- 角色管理面板 -->
-      <q-tab-panel name="roles">
-        <q-card class="shadow-24">
-          <q-bar class="bg-light-blue-10 text-white ">
+      <q-tab-panel name="roles" class="fit column">
+        <q-card class="shadow-24 col column">
+          <q-bar class="bg-light-blue-10 text-white">
             <div>角色权限管理</div>
             <q-space />
+            <q-btn dense flat icon="restore" @click="resetDefaultPermissions">
+              <q-tooltip>恢复默认权限</q-tooltip>
+            </q-btn>
             <q-btn dense flat icon="add" @click="openRoleDialog">
               <q-tooltip>添加新角色</q-tooltip>
             </q-btn>
           </q-bar>
 
-          <q-card-section>
+          <q-card-section class="col">
             <div class="row q-mb-md">
               <div class="col-4">
                 <q-select
@@ -52,7 +55,18 @@
               dense
               flat
               bordered
+              :table-style="{ height: height }"
+              :table-class="'table-scroll'"
+              virtual-scroll
             >
+              <template v-slot:header-cell="props">
+                <q-th
+                  :props="props"
+                  @dblclick="handleHeaderDblClick('roles', props.col)"
+                >
+                  {{ props.col.label }}
+                </q-th>
+              </template>
               <template v-slot:body="props">
                 <q-tr :props="props">
                   <q-td key="page" :props="props">
@@ -74,7 +88,9 @@
                       >
                         <q-item-section>
                           <div class="row items-center">
-                            <div class="col">{{ comp.name }}</div>
+                            <div class="col">
+                              {{ comp.name || comp.component }}
+                            </div>
                             <div class="col-auto">
                               <q-toggle v-model="comp.enabled" />
                             </div>
@@ -82,6 +98,26 @@
                         </q-item-section>
                       </q-item>
                     </q-list>
+                    <div class="row q-mt-sm items-center">
+                      <div class="col">
+                        <q-input
+                          v-model="props.row._newComponent"
+                          dense
+                          outlined
+                          placeholder="组件标识(如: add_btn)"
+                        />
+                      </div>
+                      <div class="col-auto">
+                        <q-btn
+                          dense
+                          flat
+                          icon="add"
+                          @click="addComponentPermission(props.row)"
+                        >
+                          <q-tooltip>添加组件权限</q-tooltip>
+                        </q-btn>
+                      </div>
+                    </div>
                   </q-td>
 
                   <q-td key="actions" :props="props">
@@ -126,7 +162,19 @@
               bordered
               :filter="staffFilter"
               :pagination="staffPagination"
+              :loading="staffLoading"
+              :rows-per-page-options="[]"
+              @request="onStaffRequest"
+              ref="staffTable"
             >
+              <template v-slot:header-cell="props">
+                <q-th
+                  :props="props"
+                  @dblclick="handleHeaderDblClick('staff', props.col)"
+                >
+                  {{ props.col.label }}
+                </q-th>
+              </template>
               <template v-slot:top-right>
                 <q-input
                   v-model="staffFilter"
@@ -232,6 +280,14 @@
               bordered
               :filter="permissionFilter"
             >
+              <template v-slot:header-cell="props">
+                <q-th
+                  :props="props"
+                  @dblclick="handleHeaderDblClick('permissions', props.col)"
+                >
+                  {{ props.col.label }}
+                </q-th>
+              </template>
               <template v-slot:top-right>
                 <q-input
                   v-model="permissionFilter"
@@ -248,16 +304,22 @@
 
               <template v-slot:body="props">
                 <q-tr :props="props">
-                  <q-td key="page" :props="props">
-                    {{ props.row.page }}
-                  </q-td>
-                  <q-td key="name" :props="props">
-                    {{ props.row.name }}
-                  </q-td>
-
-                  <q-td key="description" :props="props">
-                    {{ props.row.description }}
+                  <q-td key="primary_page" :props="props">{{
+                    props.row.primary_page
+                  }}</q-td>
+                  <q-td key="page" :props="props">{{ props.row.page }}</q-td>
+                  <q-td key="component" :props="props">{{
+                    props.row.component
+                  }}</q-td>
+                  <q-td key="name" :props="props">{{ props.row.name }}</q-td>
+                  <q-td key="enabled" :props="props">
+                    <q-badge :color="props.row.enabled ? 'positive' : 'grey'">{{
+                      props.row.enabled ? "启用" : "禁用"
+                    }}</q-badge>
                   </q-td>
+                  <q-td key="description" :props="props">{{
+                    props.row.description
+                  }}</q-td>
 
                   <q-td key="actions" :props="props">
                     <q-btn
@@ -390,23 +452,41 @@
 
         <q-card-section>
           <q-input
-            v-model="permissionForm.name"
-            label="权限名称"
+            v-model="permissionForm.primary_page"
+            label="所属主页面(如:staff)"
             dense
             outlined
             autofocus
-            :rules="[(val) => !!val || '权限名称不能为空']"
+            :rules="[(val) => !!val || '主页面不能为空']"
           />
-
           <q-input
-            v-model="permissionForm.code"
-            label="权限代码"
+            v-model="permissionForm.page"
+            label="页面路径(如:/staff/stafflist)"
             dense
             outlined
             class="q-mt-md"
-            :rules="[(val) => !!val || '权限代码不能为空']"
+            :rules="[(val) => !!val || '页面路径不能为空']"
+          />
+          <q-input
+            v-model="permissionForm.component"
+            label="组件标识(可留空表示页面访问权限)"
+            dense
+            outlined
+            class="q-mt-md"
+          />
+          <q-input
+            v-model="permissionForm.name"
+            label="权限名称"
+            dense
+            outlined
+            class="q-mt-md"
+            :rules="[(val) => !!val || '权限名称不能为空']"
+          />
+          <q-toggle
+            v-model="permissionForm.enabled"
+            label="是否启用"
+            class="q-mt-md"
           />
-
           <q-input
             v-model="permissionForm.description"
             label="权限描述"
@@ -434,6 +514,7 @@ export default {
 
   data () {
     return {
+      height: '',
       currentTab: 'roles',
       selectedRole: null,
       roleOptions: [],
@@ -461,9 +542,12 @@ export default {
       editPermissionMode: false,
       permissionForm: {
         id: null,
+        primary_page: '',
+        page: '',
+        component: '',
         name: '',
-        code: '',
-        description: ''
+        description: '',
+        enabled: true
       },
 
       roleColumns: [
@@ -536,10 +620,17 @@ export default {
 
       permissionColumns: [
         {
-          name: 'page',
-          label: '界面',
+          name: 'primary_page',
+          label: '主页面',
+          align: 'left',
+          field: 'primary_page'
+        },
+        { name: 'page', label: '页面路径', align: 'left', field: 'page' },
+        {
+          name: 'component',
+          label: '组件标识',
           align: 'left',
-          field: 'code'
+          field: 'component'
         },
         {
           name: 'name',
@@ -549,25 +640,34 @@ export default {
           field: 'name',
           sortable: true
         },
-
+        { name: 'enabled', label: '启用', align: 'center', field: 'enabled' },
         {
           name: 'description',
           label: '权限描述',
           align: 'left',
           field: 'description'
         },
-        {
-          name: 'actions',
-          label: '操作',
-          align: 'center'
-        }
+        { name: 'actions', label: '操作', align: 'center' }
       ],
 
       staffPagination: {
-        rowsPerPage: 10
+        page: 1,
+        rowsPerPage: 11,
+        rowsNumber: 0
       },
 
-      permissionOptions: []
+      staffLoading: false,
+
+      permissionOptions: [],
+
+      // 双击表头搜索相关
+      activeSearchField: '',
+      activeSearchLabel: '',
+      roleFilterData: {}, // 角色管理表格过滤数据
+      staffFilterData: {}, // 成员管理表格过滤数据
+      permissionFilterData: {}, // 权限管理表格过滤数据
+      permissionsOriginal: [], // 角色管理表格原始数据
+      permissionListOriginal: [] // 权限管理表格原始数据
     }
   },
 
@@ -576,6 +676,20 @@ export default {
     this.loadStaffList()
     this.loadPermissions()
     this.loadPermissionOptions()
+    // compute table height for internal scroll
+    if (this.$q && this.$q.screen) {
+      if (
+        this.$q.platform &&
+        this.$q.platform.is &&
+        this.$q.platform.is.electron
+      ) {
+        this.height = String(this.$q.screen.height - 320) + 'px'
+      } else {
+        this.height = this.$q.screen.height - 320 + '' + 'px'
+      }
+    } else {
+      this.height = '480px'
+    }
   },
 
   methods: {
@@ -611,7 +725,6 @@ export default {
       }
     },
 
-    // 加载角色权限
     // 加载角色权限
     async loadRolePermissions () {
       if (!this.selectedRole) return
@@ -622,37 +735,68 @@ export default {
         )
 
         // 处理分组权限结构
-        this.permissions = response.permissions_group.map((group) => ({
+        // enabled 表示该角色是否拥有此权限(权限已关联到角色),而不是权限对象本身的 enabled 状态
+        const permissionsData = response.permissions_group.map((group) => ({
           page: group.page,
-          enabled: group.permissions.some(
-            (perm) => perm.component === null && perm.enabled
-          ),
+          enabled: group.permissions.some((perm) => perm.component === null), // 只要权限关联到角色,就认为角色拥有此权限
           components: group.permissions
-            .filter((perm) => perm.component !== null) // 过滤掉页面访问权限
+            .filter((perm) => perm.component !== null)
             .map((perm) => ({
               id: perm.id,
+              component: perm.component,
               name: perm.name,
               description: perm.description,
-              enabled: perm.enabled
-            }))
+              enabled: true // 权限已关联到角色,enabled 为 true
+            })),
+          _newComponent: ''
         }))
+        // 保存原始数据
+        this.permissionsOriginal = permissionsData
+        // 应用过滤
+        this.applyRoleFilter()
       } catch (error) {
         console.error('加载权限失败:', error)
         this.showNotification('加载权限失败: ' + error.message)
       }
     },
 
-    // 加载成员列表
-    async loadStaffList () {
+    // 加载成员列表(服务端分页)
+    async loadStaffList (
+      page = this.staffPagination.page,
+      rowsPerPage = this.staffPagination.rowsPerPage
+    ) {
+      this.staffLoading = true
       try {
-        const response = await getauth('staff/')
-        this.staffList = response.results.map((staff) => ({
+        const params = new URLSearchParams()
+        if (page) params.set('page', String(page))
+        if (rowsPerPage) params.set('page_size', String(rowsPerPage))
+        const response = await getauth(`staff/?${params.toString()}`)
+        this.staffList = (response.results || []).map((staff) => ({
           ...staff,
           staff_type: staff.staff_type || ''
         }))
+        this.staffPagination = {
+          ...this.staffPagination,
+          page,
+          rowsPerPage,
+          rowsNumber: Number(response.count || 0)
+        }
       } catch (error) {
         console.error('加载成员列表失败:', error)
         this.showNotification('加载成员列表失败: ' + error.message)
+      } finally {
+        this.staffLoading = false
+      }
+    },
+
+    // Quasar QTable 服务端请求回调
+    async onStaffRequest (props) {
+      const { page, rowsPerPage } = props.pagination || {}
+      // 如果有过滤条件,使用带过滤的方法
+      if (Object.keys(this.staffFilterData).length > 0) {
+        await this.loadStaffListWithFilter(page, rowsPerPage)
+      } else {
+        await this.loadStaffList(page, rowsPerPage)
       }
     },
 
@@ -660,9 +804,13 @@ export default {
     async loadPermissions () {
       try {
         const response = await getauth('staff/permission/')
-        this.permissionList = response.map((permission) => ({
+        const permissionData = response.map((permission) => ({
           ...permission
         }))
+        // 保存原始数据
+        this.permissionListOriginal = permissionData
+        // 应用过滤
+        this.applyPermissionFilter()
       } catch (error) {
         console.error('加载权限列表失败:', error)
         this.showNotification('加载权限列表失败: ' + error.message)
@@ -683,45 +831,50 @@ export default {
       }
     },
 
-    // 更新页面访问权限
+    // 更新页面访问权限(改为保存当前页所有权限)
     async updatePageAccess (pageData) {
-      if (!this.selectedRole) return
-
-      try {
-        await postauth('staff/permission/update/', {
-          role: this.selectedRole,
-          page: pageData.page,
-          access: pageData.enabled
-        })
-        this.showNotification('页面访问权限更新成功', 'positive')
-      } catch (error) {
-        console.error('更新权限失败:', error)
-        this.showNotification('更新失败: ' + error.message)
-        // 恢复原始状态
-        pageData.enabled = !pageData.enabled
-      }
+      await this.savePagePermissions(pageData)
     },
 
-    // 保存页面权限
+    // 保存页面权限(调用后端角色权限更新接口)
     async savePagePermissions (pageData) {
       if (!this.selectedRole) return
-
       try {
-        await postauth('staff/permission/component/', {
-          role: this.selectedRole,
-          page: pageData.page,
-          components: pageData.components.map((comp) => ({
-            name: comp.name,
-            enabled: comp.enabled
-          }))
-        })
-        this.showNotification('组件权限更新成功', 'positive')
+        const payload = {
+          permissions: [
+            {
+              page: pageData.page,
+              component: null,
+              enabled: !!pageData.enabled
+            },
+            ...pageData.components.map((comp) => ({
+              page: pageData.page,
+              component: comp.component || comp.name,
+              enabled: !!comp.enabled
+            }))
+          ]
+        }
+        await putauth(`staff/role-permissions/${this.selectedRole}/`, payload)
+        this.showNotification('页面权限已保存', 'positive')
       } catch (error) {
         console.error('保存权限失败:', error)
         this.showNotification('保存失败: ' + error.message)
       }
     },
 
+    addComponentPermission (pageData) {
+      const comp = (pageData._newComponent || '').trim()
+      if (!comp) return
+      if (!pageData.components.find((c) => (c.component || c.name) === comp)) {
+        pageData.components.push({
+          component: comp,
+          name: comp,
+          enabled: true
+        })
+      }
+      pageData._newComponent = ''
+    },
+
     // 打开添加角色对话框
     openRoleDialog () {
       this.newRoleName = ''
@@ -748,9 +901,12 @@ export default {
     openPermissionDialog () {
       this.permissionForm = {
         id: null,
+        primary_page: '',
+        page: '',
+        component: '',
         name: '',
-        code: '',
-        description: ''
+        description: '',
+        enabled: true
       }
       this.editPermissionMode = false
       this.permissionDialog = true
@@ -774,9 +930,12 @@ export default {
     editPermission (permission) {
       this.permissionForm = {
         id: permission.id,
-        name: permission.name,
-        code: permission.code,
-        description: permission.description
+        primary_page: permission.primary_page || '',
+        page: permission.page || '',
+        component: permission.component || '',
+        name: permission.name || '',
+        description: permission.description || '',
+        enabled: !!permission.enabled
       }
       this.editPermissionMode = true
       this.permissionDialog = true
@@ -787,7 +946,10 @@ export default {
       try {
         await deleteauth(`staff/${staff.id}/`)
         this.showNotification('成员删除成功', 'positive')
-        this.loadStaffList()
+        this.loadStaffList(
+          this.staffPagination.page,
+          this.staffPagination.rowsPerPage
+        )
       } catch (error) {
         console.error('删除成员失败:', error)
         this.showNotification('删除失败: ' + error.message)
@@ -817,7 +979,10 @@ export default {
           this.showNotification('成员添加成功', 'positive')
         }
         this.staffDialog = false
-        this.loadStaffList()
+        this.loadStaffList(
+          this.staffPagination.page,
+          this.staffPagination.rowsPerPage
+        )
       } catch (error) {
         console.error('保存成员失败:', error)
         this.showNotification('保存失败: ' + error.message)
@@ -827,19 +992,19 @@ export default {
     // 保存权限
     async savePermission () {
       try {
+        const payload = { ...this.permissionForm }
+        if (!payload.component) payload.component = null
         if (this.editPermissionMode) {
-          await putauth(
-            `staff/permission/${this.permissionForm.id}/`,
-            this.permissionForm
-          )
+          await putauth(`staff/permission/${payload.id}/`, payload)
           this.showNotification('权限更新成功', 'positive')
         } else {
-          await postauth('staff/permission/', this.permissionForm)
+          await postauth('staff/permission/', payload)
           this.showNotification('权限添加成功', 'positive')
         }
         this.permissionDialog = false
         this.loadPermissions()
         this.loadPermissionOptions()
+        this.loadRolePermissions()
       } catch (error) {
         console.error('保存权限失败:', error)
         this.showNotification('保存失败: ' + error.message)
@@ -892,6 +1057,193 @@ export default {
         console.error('创建角色失败:', error)
         this.showNotification('创建失败: ' + error.message)
       }
+    },
+
+    // 恢复默认权限
+    async resetDefaultPermissions () {
+      try {
+        const response = await postauth(
+          'staff/role-permissions/reset-default/',
+          {}
+        )
+        this.showNotification(
+          `默认权限已恢复成功!创建权限: ${response.created_permissions}, 更新角色: ${response.roles_updated}, 分配权限: ${response.permissions_assigned}`,
+          'positive'
+        )
+        // 重新加载角色和权限
+        this.loadRoles()
+        if (this.selectedRole) {
+          this.loadRolePermissions()
+        }
+      } catch (error) {
+        console.error('恢复默认权限失败:', error)
+        this.showNotification(
+          '恢复默认权限失败: ' +
+            (error.response?.data?.detail || error.message),
+          'negative'
+        )
+      }
+    },
+
+    // 双击表头处理
+    handleHeaderDblClick (tableType, column) {
+      // 排除不需要搜索的列
+      if (
+        [
+          'actions',
+          'components',
+          'enabled',
+          'is_lock',
+          'is_look',
+          'is_edit'
+        ].includes(column.name)
+      ) {
+        return
+      }
+
+      this.activeSearchField = column.field || column.name
+      this.activeSearchLabel = column.label
+
+      // 弹出搜索对话框
+      this.$q
+        .dialog({
+          title: `搜索${column.label}`,
+          message: `请输入${column.label}的搜索条件`,
+          prompt: {
+            model: '',
+            type: 'text'
+          },
+          cancel: true,
+          persistent: true
+        })
+        .onOk((data) => {
+          // 执行搜索
+          this.executeColumnSearch(tableType, column, data)
+        })
+        .onCancel(() => {
+          this.activeSearchField = ''
+          this.activeSearchLabel = ''
+        })
+    },
+
+    // 执行列搜索
+    executeColumnSearch (tableType, column, value) {
+      if (!value || value.trim() === '') {
+        // 清除搜索
+        if (tableType === 'roles') {
+          this.roleFilterData = {}
+          if (this.selectedRole) {
+            this.loadRolePermissions() // 重新加载数据
+          }
+        } else if (tableType === 'staff') {
+          this.staffFilterData = {}
+          // 重置到第一页并重新加载
+          this.staffPagination.page = 1
+          this.loadStaffList(1, this.staffPagination.rowsPerPage)
+        } else if (tableType === 'permissions') {
+          this.permissionFilterData = {}
+          this.loadPermissions() // 重新加载数据
+        }
+        this.$q.notify({
+          message: `已清除 ${this.activeSearchLabel} 的搜索条件`,
+          icon: 'clear',
+          color: 'info'
+        })
+      } else {
+        const field = column.field || column.name
+        // 保存搜索条件
+        if (tableType === 'roles') {
+          this.roleFilterData[field] = value.trim()
+          this.applyRoleFilter()
+        } else if (tableType === 'staff') {
+          this.staffFilterData[field] = value.trim()
+          // 重置到第一页并重新加载(带搜索条件)
+          this.staffPagination.page = 1
+          this.loadStaffListWithFilter(1, this.staffPagination.rowsPerPage)
+        } else if (tableType === 'permissions') {
+          this.permissionFilterData[field] = value.trim()
+          this.applyPermissionFilter()
+        }
+        this.$q.notify({
+          message: `已搜索 ${this.activeSearchLabel} 含有 "${value}" 的结果`,
+          icon: 'search',
+          color: 'positive'
+        })
+      }
+
+      // 重置激活的搜索字段
+      this.activeSearchField = ''
+      this.activeSearchLabel = ''
+    },
+
+    // 应用角色管理表格过滤
+    applyRoleFilter () {
+      if (Object.keys(this.roleFilterData).length === 0) {
+        // 如果没有过滤条件,使用原始数据
+        this.permissions = [...this.permissionsOriginal]
+        return
+      }
+
+      // 从原始数据过滤
+      this.permissions = this.permissionsOriginal.filter((item) => {
+        return Object.keys(this.roleFilterData).every((key) => {
+          const filterValue = this.roleFilterData[key].toLowerCase()
+          const itemValue = String(item[key] || '').toLowerCase()
+          return itemValue.includes(filterValue)
+        })
+      })
+    },
+
+    // 应用权限管理表格过滤
+    applyPermissionFilter () {
+      if (Object.keys(this.permissionFilterData).length === 0) {
+        // 如果没有过滤条件,使用原始数据
+        this.permissionList = [...this.permissionListOriginal]
+        return
+      }
+
+      // 从原始数据过滤
+      this.permissionList = this.permissionListOriginal.filter((item) => {
+        return Object.keys(this.permissionFilterData).every((key) => {
+          const filterValue = this.permissionFilterData[key].toLowerCase()
+          const itemValue = String(item[key] || '').toLowerCase()
+          return itemValue.includes(filterValue)
+        })
+      })
+    },
+
+    // 带过滤条件的加载成员列表
+    async loadStaffListWithFilter (page = 1, rowsPerPage = 11) {
+      this.staffLoading = true
+      try {
+        const params = new URLSearchParams()
+        if (page) params.set('page', String(page))
+        if (rowsPerPage) params.set('page_size', String(rowsPerPage))
+
+        // 添加搜索条件
+        Object.keys(this.staffFilterData).forEach((key) => {
+          if (this.staffFilterData[key]) {
+            params.set(`${key}__icontains`, String(this.staffFilterData[key]))
+          }
+        })
+
+        const response = await getauth(`staff/?${params.toString()}`)
+        this.staffList = (response.results || []).map((staff) => ({
+          ...staff,
+          staff_type: staff.staff_type || ''
+        }))
+        this.staffPagination = {
+          ...this.staffPagination,
+          page,
+          rowsPerPage,
+          rowsNumber: Number(response.count || 0)
+        }
+      } catch (error) {
+        console.error('加载成员列表失败:', error)
+        this.showNotification('加载成员列表失败: ' + error.message)
+      } finally {
+        this.staffLoading = false
+      }
     }
   }
 }

+ 2 - 2
templates/src/pages/staff/staff.vue

@@ -42,7 +42,7 @@
 
             <!-- 人员管理 -->
             <transition appear enter-active-class="animated zoomIn">
-              <q-route-tab
+          <q-route-tab
                 
                 name="permission"
                 :label="'人员管理'"
@@ -55,7 +55,7 @@
         </div>
       </div>
     </template>
-    <div :style="{ width: '100%', margin: '-10px 10px 0 10px' }">
+    <div :style="{ width: '100%', margin: '-10px 10px 0 10px' }" class="fit scroll">
       <router-view />
     </div>
   </q-page>

+ 66 - 1
templates/src/pages/staff/stafflist.vue

@@ -112,7 +112,7 @@
               props.row.update_time
             }}</q-td>
             <template v-if="!editMode">
-              <q-td key="action" :props="props" style="width: 175px">
+              <q-td key="action" :props="props" style="width: 240px">
                 <q-btn
                   round
                   flat
@@ -163,6 +163,20 @@
                     >{{ $t("delete") }}</q-tooltip
                   >
                 </q-btn>
+                <q-btn
+                  round
+                  flat
+                  push
+                  color="primary"
+                  icon="key"
+                  @click="openChangeCode(props.row)"
+                >
+                  <q-tooltip
+                    content-class="bg-amber text-black shadow-4"
+                    :offset="[10, 10]"
+                    content-style="font-size: 12px"
+                  >修改密码</q-tooltip>
+                </q-btn>
               </q-td>
             </template>
             <template v-else-if="editMode">
@@ -316,6 +330,31 @@
         </div>
       </q-card>
     </q-dialog>
+    <q-dialog v-model="changeCodeDialog">
+      <q-card class="shadow-24">
+        <q-bar class="bg-light-blue-10 text-white rounded-borders" style="height: 50px">
+          <div>修改密码</div>
+          <q-space />
+          <q-btn dense flat icon="close" v-close-popup />
+        </q-bar>
+        <q-card-section style="max-height: 240px; width: 360px" class="scroll">
+          <q-input
+            dense
+            outlined
+            square
+            v-model.number="newCheckCode"
+            label="请输入新的4位数字密码"
+            type="number"
+            :rules="[(val) => (val !== null && String(val).length <= 4) || '请输入不超过4位数字']"
+            @keyup.enter="submitChangeCode"
+          />
+        </q-card-section>
+        <div style="float: right; padding: 15px 15px 15px 0">
+          <q-btn color="white" text-color="black" style="margin-right: 25px" @click="changeCodeDialog=false">取消</q-btn>
+          <q-btn color="primary" @click="submitChangeCode">确定</q-btn>
+        </div>
+      </q-card>
+    </q-dialog>
   </div>
 </template>
 <router-view />
@@ -395,6 +434,9 @@ export default {
       max: 0,
       total: 0,
       paginationIpt: 1
+      , changeCodeDialog: false,
+      newCheckCode: null,
+      selectedRow: null
     }
   },
   methods: {
@@ -731,6 +773,29 @@ export default {
           icon: 'warning'
         })
       }
+    },
+    openChangeCode (row) {
+      this.selectedRow = row
+      this.newCheckCode = null
+      this.changeCodeDialog = true
+    },
+    submitChangeCode () {
+      if (this.selectedRow == null) return
+      const code = Number(this.newCheckCode)
+      if (isNaN(code) || code < 0 || code > 9999) {
+        this.$q.notify({ message: '请输入0-9999之间的数字', color: 'negative', icon: 'warning' })
+        return
+      }
+      postauth(this.pathname + 'change-check-code/' + this.selectedRow.id + '/', { new_check_code: code })
+        .then(() => {
+          this.$q.notify({ message: '密码已更新', color: 'green', icon: 'check' })
+          this.changeCodeDialog = false
+          this.getList()
+        })
+        .catch((err) => {
+          const msg = err?.detail || err?.message || '更新失败'
+          this.$q.notify({ message: msg, color: 'negative', icon: 'close' })
+        })
     }
   },
 

+ 70 - 3
templates/src/pages/staff/stafflist_check_code.vue

@@ -38,6 +38,13 @@
             <q-td key="update_time" :props="props">
               {{ props.row.update_time }}
             </q-td>
+            <q-td v-if="showActions" key="action" :props="props" style="width: 120px">
+              <q-btn round flat push color="primary" icon="key" @click="openChangeCode(props.row)">
+                <q-tooltip content-class="bg-amber text-black shadow-4" :offset="[10, 10]" content-style="font-size: 12px">
+                  修改密码
+                </q-tooltip>
+              </q-btn>
+            </q-td>
           </q-tr>
         </template>
       </q-table>
@@ -55,12 +62,37 @@
         <q-btn flat push color="dark" :label="$t('no_data')"></q-btn>
       </div>
     </template>
+    <q-dialog v-model="changeCodeDialog">
+      <q-card class="shadow-24">
+        <q-bar class="bg-light-blue-10 text-white rounded-borders" style="height: 50px">
+          <div>修改密码</div>
+          <q-space />
+          <q-btn dense flat icon="close" v-close-popup />
+        </q-bar>
+        <q-card-section style="max-height: 240px; width: 360px" class="scroll">
+          <q-input
+            dense
+            outlined
+            square
+            v-model.number="newCheckCode"
+            label="请输入新的4位数字密码"
+            type="number"
+            :rules="[(val) => (val !== null && String(val).length <= 4) || '请输入不超过4位数字']"
+            @keyup.enter="submitChangeCode"
+          />
+        </q-card-section>
+        <div style="float: right; padding: 15px 15px 15px 0">
+          <q-btn color="white" text-color="black" style="margin-right: 25px" @click="changeCodeDialog=false">取消</q-btn>
+          <q-btn color="primary" @click="submitChangeCode">确定</q-btn>
+        </div>
+      </q-card>
+    </q-dialog>
   </div>
 </template>
 <router-view />
 
 <script>
-import { getauth } from 'boot/axios_request'
+import { getauth, postauth } from 'boot/axios_request'
 import { LocalStorage } from 'quasar'
 
 export default {
@@ -84,7 +116,8 @@ export default {
         { name: 'staff_type', label: this.$t('staff.view_staff.staff_type'), field: 'staff_type', align: 'center' },
         { name: 'check_code', label: this.$t('staff.check_code'), field: 'check_code', align: 'center' },
         { name: 'create_time', label: this.$t('createtime'), field: 'create_time', align: 'center' },
-        { name: 'update_time', label: this.$t('updatetime'), field: 'update_time', align: 'center' }
+        { name: 'update_time', label: this.$t('updatetime'), field: 'update_time', align: 'center' },
+        { name: 'action', label: this.$t('action'), align: 'right' }
       ],
       filter: '',
       pagination: {
@@ -94,7 +127,10 @@ export default {
       current: 1,
       max: 0,
       total: 0,
-      paginationIpt: 1
+      paginationIpt: 1,
+      changeCodeDialog: false,
+      newCheckCode: null,
+      selectedRow: null
     }
   },
   methods: {
@@ -290,6 +326,37 @@ export default {
     reFresh() {
       var _this = this
       _this.getList()
+    },
+    openChangeCode (row) {
+      this.selectedRow = row
+      this.newCheckCode = null
+      this.changeCodeDialog = true
+    },
+    submitChangeCode () {
+      if (this.selectedRow == null) return
+      const code = Number(this.newCheckCode)
+      if (isNaN(code) || code < 0 || code > 9999) {
+        this.$q.notify({ message: '请输入0-9999之间的数字', color: 'negative', icon: 'warning' })
+        return
+      }
+      // 使用封装的 postauth
+      postauth('staff/change-check-code/' + this.selectedRow.id + '/', { new_check_code: code })
+        .then(() => {
+          this.$q.notify({ message: '密码已更新', color: 'green', icon: 'check' })
+          this.changeCodeDialog = false
+          this.getList()
+        })
+        .catch((err) => {
+          const msg = err?.detail || err?.message || '更新失败'
+          this.$q.notify({ message: msg, color: 'negative', icon: 'close' })
+        })
+    }
+  },
+  computed: {
+    showActions () {
+      // 普通用户只能改自己;管理员/主管等显示操作
+      const loginMode = LocalStorage.getItem('login_mode')
+      return !!loginMode
     }
   },
   created() {

+ 1 - 0
templates/src/pages/task/task.vue

@@ -132,6 +132,7 @@
                   content-style="font-size: 12px"
                   >{{ "WCS长时间未收到" }}</q-tooltip
                 >
+                
               </q-btn>
             </q-td>
             <q-td