2 Commits 20851fabf2 ... 893abefd29

Autore SHA1 Messaggio Data
  flowerstonezl 893abefd29 界面更新 2 mesi fa
  flowerstonezl b5ef15e29f 定时任务 2 mesi fa
39 ha cambiato i file con 2003 aggiunte e 40 eliminazioni
  1. 1 2
      backup/urls.py
  2. 222 0
      backup/views.py
  3. 11 0
      bin/serializers.py
  4. 4 0
      bin/urls.py
  5. 83 1
      bin/views.py
  6. 3 0
      location_statistics/urls.py
  7. 330 0
      location_statistics/views.py
  8. 1 0
      templates/dist/spa/css/35.4ed880df.css
  9. 0 1
      templates/dist/spa/css/35.da66b954.css
  10. 0 1
      templates/dist/spa/css/39.2ac1dad1.css
  11. 1 0
      templates/dist/spa/css/39.fffe0096.css
  12. 1 0
      templates/dist/spa/css/7.058b37f5.css
  13. 0 1
      templates/dist/spa/css/7.b5042fa0.css
  14. 1 1
      templates/dist/spa/index.html
  15. 1 0
      templates/dist/spa/js/35.a938d86d.js
  16. BIN
      templates/dist/spa/js/35.a938d86d.js.gz
  17. 0 1
      templates/dist/spa/js/35.e9d60c9f.js
  18. BIN
      templates/dist/spa/js/35.e9d60c9f.js.gz
  19. 1 0
      templates/dist/spa/js/37.e70b8a57.js
  20. BIN
      templates/dist/spa/js/37.e70b8a57.js.gz
  21. 0 1
      templates/dist/spa/js/37.f9bec936.js
  22. BIN
      templates/dist/spa/js/37.f9bec936.js.gz
  23. 1 0
      templates/dist/spa/js/39.4e22ecc5.js
  24. BIN
      templates/dist/spa/js/39.4e22ecc5.js.gz
  25. 0 1
      templates/dist/spa/js/39.77c36f65.js
  26. BIN
      templates/dist/spa/js/39.77c36f65.js.gz
  27. 0 1
      templates/dist/spa/js/7.15a488eb.js
  28. BIN
      templates/dist/spa/js/7.15a488eb.js.gz
  29. 1 0
      templates/dist/spa/js/7.1773c9a2.js
  30. BIN
      templates/dist/spa/js/7.1773c9a2.js.gz
  31. 1 1
      templates/dist/spa/js/app.86855383.js
  32. BIN
      templates/dist/spa/js/app.3082c386.js.gz
  33. BIN
      templates/dist/spa/js/app.86855383.js.gz
  34. 2 2
      templates/dist/spa/js/vendor.7897d76a.js
  35. BIN
      templates/dist/spa/js/vendor.7897d76a.js.gz
  36. 451 1
      templates/src/layouts/MainLayout.vue
  37. 668 16
      templates/src/pages/container/containerlist.vue
  38. 129 3
      templates/src/pages/count/batch.vue
  39. 90 6
      templates/src/pages/task/task.vue

+ 1 - 2
backup/urls.py

@@ -4,7 +4,6 @@ from . import views
 urlpatterns = [
     path('trigger/', views.trigger_backup, name='trigger_backup'),
     path('list/', views.list_backups, name='list_backups'),
-
     path('point/', views.restore_to_point, name='restore_to_point'),
-
+    path('shutdown/', views.shutdown_system, name='shutdown_system'),
 ]

+ 222 - 0
backup/views.py

@@ -12,6 +12,9 @@ import math
 from operation_log.views import log_success_operation, log_failure_operation
 from operation_log.models import OperationLog
 from django.utils import timezone
+import threading
+import sys
+import signal
 
 logger = logging.getLogger(__name__)
 
@@ -64,6 +67,126 @@ def scheduled_backup():
         except Exception as log_error:
             logger.error(f"定时备份失败日志记录失败: {str(log_error)}")
 
+def scheduled_consistency_fix():
+    """定时修复分组状态和托盘数一致性任务"""
+    try:
+        from location_statistics.views import LocationConsistencyChecker
+        
+        logger.info("开始执行定时一致性修复任务")
+        
+        fixed_groups = 0
+        error_groups = 0
+        fixed_details = 0
+        error_details = 0
+        group_fix_success = False
+        detail_fix_success = False
+        
+        # 执行分组状态修复
+        try:
+            group_checker = LocationConsistencyChecker(
+                warehouse_code=None,  # None表示检查所有仓库
+                layer=None,  # None表示检查所有楼层
+                auto_fix=True,  # 启用自动修复
+                fix_scope=['groups']  # 只修复分组状态
+            )
+            group_checker.check_all()
+            group_report = group_checker.generate_report()
+            
+            fixed_groups = group_report['summary']['fixed']['groups']
+            error_groups = group_report['summary']['errors_found']['groups']
+            group_fix_success = True
+            
+            logger.info(f"分组状态修复完成: 发现{error_groups}个问题,修复{fixed_groups}个")
+        except Exception as group_error:
+            logger.error(f"分组状态修复失败: {str(group_error)}", exc_info=True)
+        
+        # 执行托盘明细状态修复
+        try:
+            detail_checker = LocationConsistencyChecker(
+                warehouse_code=None,  # None表示检查所有仓库
+                layer=None,  # None表示检查所有楼层
+                auto_fix=True,  # 启用自动修复
+                fix_scope=['details']  # 只修复托盘明细状态
+            )
+            detail_checker.check_all()
+            detail_report = detail_checker.generate_report()
+            
+            fixed_details = detail_report['summary']['fixed']['details']
+            error_details = detail_report['summary']['errors_found']['details']
+            detail_fix_success = True
+            
+            logger.info(f"托盘明细状态修复完成: 发现{error_details}个问题,修复{fixed_details}个")
+        except Exception as detail_error:
+            logger.error(f"托盘明细状态修复失败: {str(detail_error)}", exc_info=True)
+        
+        # 记录操作日志
+        if group_fix_success and detail_fix_success:
+            operation_result = "success"
+            operation_content = f"定时一致性修复完成: 分组状态-发现{error_groups}个问题,修复{fixed_groups}个;托盘明细-发现{error_details}个问题,修复{fixed_details}个"
+        elif group_fix_success or detail_fix_success:
+            operation_result = "partial"
+            operation_content = f"定时一致性修复部分完成: "
+            if group_fix_success:
+                operation_content += f"分组状态-发现{error_groups}个问题,修复{fixed_groups}个;"
+            else:
+                operation_content += "分组状态修复失败;"
+            if detail_fix_success:
+                operation_content += f"托盘明细-发现{error_details}个问题,修复{fixed_details}个"
+            else:
+                operation_content += "托盘明细修复失败"
+        else:
+            operation_result = "failure"
+            operation_content = "定时一致性修复失败: 分组状态和托盘明细修复均失败"
+        
+        try:
+            OperationLog.objects.create(
+                operator="系统定时任务",
+                operation_content=operation_content,
+                operation_level="other",
+                operation_result=operation_result,
+                ip_address=None,
+                user_agent="系统定时任务",
+                request_method="CRON",
+                request_path="/backup/consistency_fix",
+                module_name="数据一致性修复"
+            )
+        except Exception as log_error:
+            logger.error(f"定时一致性修复日志记录失败: {str(log_error)}")
+            
+    except ImportError as e:
+        logger.warning(f"一致性修复模块未找到,跳过修复: {str(e)}")
+        try:
+            OperationLog.objects.create(
+                operator="系统定时任务",
+                operation_content=f"定时一致性修复失败: 模块未找到 - {str(e)}",
+                operation_level="other",
+                operation_result="failure",
+                ip_address=None,
+                user_agent="系统定时任务",
+                request_method="CRON",
+                request_path="/backup/consistency_fix",
+                module_name="数据一致性修复"
+            )
+        except Exception as log_error:
+            logger.error(f"定时一致性修复失败日志记录失败: {str(log_error)}")
+    except Exception as e:
+        logger.error(f"定时一致性修复失败: {str(e)}", exc_info=True)
+        # 记录失败日志
+        try:
+            OperationLog.objects.create(
+                operator="系统定时任务",
+                operation_content=f"定时一致性修复失败: {str(e)}",
+                operation_level="other",
+                operation_result="failure",
+                ip_address=None,
+                user_agent="系统定时任务",
+                request_method="CRON",
+                request_path="/backup/consistency_fix",
+                module_name="数据一致性修复"
+            )
+        except Exception as log_error:
+            logger.error(f"定时一致性修复失败日志记录失败: {str(log_error)}")
+
 # 启动定时备份(每6小时执行一次)
 if not scheduler.running:
     scheduler.add_job(
@@ -73,8 +196,17 @@ if not scheduler.running:
         minute=0,    # 在0分钟时执行
         id='db_backup_job'
     )
+    # 启动定时一致性修复任务(每6小时执行一次)
+    scheduler.add_job(
+        scheduled_consistency_fix,
+        'cron',
+        hour='*/6',  # 每2小时执行一次
+        minute=10,   # 在10分钟时执行(避免与备份任务冲突)
+        id='consistency_fix_job'
+    )
     scheduler.start()
     logger.info("定时备份任务已启动")
+    logger.info("定时一致性修复任务已启动")
 
 def get_backup_files(page=1, page_size=5):
     """获取备份文件列表(带分页)"""
@@ -290,3 +422,93 @@ def restore_to_point(request):
             'status': 'error',
             'message': str(e)
         }, status=500)
+
+def _shutdown_system(delay=5):
+    """延迟关闭系统"""
+    def shutdown():
+        logger.critical("系统正在关闭...")
+        try:
+            # 停止调度器
+            if scheduler.running:
+                scheduler.shutdown(wait=False)
+                logger.info("定时任务调度器已停止")
+        except Exception as e:
+            logger.error(f"停止调度器失败: {str(e)}")
+        
+        # 记录关闭日志
+        try:
+            OperationLog.objects.create(
+                operator="系统远程关闭",
+                operation_content="系统已通过远程接口关闭",
+                operation_level="critical",
+                operation_result="success",
+                ip_address=None,
+                user_agent="系统远程关闭接口",
+                request_method="SHUTDOWN",
+                request_path="/backup/shutdown",
+                module_name="系统管理"
+            )
+        except Exception as log_error:
+            logger.error(f"关闭日志记录失败: {str(log_error)}")
+        
+        # 强制退出
+        logger.critical("系统关闭完成,正在退出...")
+        os._exit(0)
+    
+    # 使用线程延迟执行关闭
+    timer = threading.Timer(delay, shutdown)
+    timer.daemon = True
+    timer.start()
+    logger.warning(f"系统将在 {delay} 秒后关闭")
+
+@csrf_exempt
+@require_POST
+def shutdown_system(request):
+    """远程关闭系统接口"""
+    try:
+        data = json.loads(request.body) if request.body else {}
+        delay = int(data.get('delay', 5))  # 默认5秒延迟
+        
+        # 验证延迟时间范围(1-60秒)
+        if delay < 1 or delay > 60:
+            delay = 5
+        
+        # 记录关闭请求日志
+        try:
+            operator_name = request.user.username if hasattr(request, 'user') and request.user.is_authenticated else "未知用户"
+            ip_address = request.META.get('REMOTE_ADDR', '未知IP')
+            log_success_operation(
+                request=request,
+                operation_content=f"远程关闭系统请求,延迟时间: {delay}秒",
+                operation_level="critical",
+                module_name="系统管理"
+            )
+        except Exception as log_error:
+            logger.error(f"关闭请求日志记录失败: {str(log_error)}")
+        
+        logger.critical(f"收到系统关闭请求,将在 {delay} 秒后关闭系统")
+        
+        # 启动延迟关闭
+        _shutdown_system(delay=delay)
+        
+        return JsonResponse({
+            'status': 'success',
+            'message': f'系统将在 {delay} 秒后关闭',
+            'delay': delay
+        })
+        
+    except Exception as e:
+        logger.error(f"处理系统关闭请求失败: {str(e)}", exc_info=True)
+        try:
+            log_failure_operation(
+                request=request,
+                operation_content=f"远程关闭系统请求失败: {str(e)}",
+                operation_level="critical",
+                module_name="系统管理"
+            )
+        except Exception as log_error:
+            logger.error(f"关闭失败日志记录失败: {str(log_error)}")
+        return JsonResponse({
+            'status': 'error',
+            'message': f'处理关闭请求失败: {str(e)}'
+        }, status=500)

+ 11 - 0
bin/serializers.py

@@ -50,6 +50,17 @@ class LocationGroupPostSerializer(serializers.ModelSerializer):
             'current_batch': {'required': False, 'allow_blank': True},
         }
 
+class LocationContainerLinkSerializer(serializers.ModelSerializer):
+    """库位-托盘关联序列化器"""
+    container = ContainerSimpleSerializer(read_only=True)
+    container_code = serializers.CharField(source='container.container_code', read_only=True)
+    location_code = serializers.CharField(source='location.location_code', read_only=True)
+    
+    class Meta:
+        model = LocationContainerLink
+        fields = ['id', 'location', 'location_code', 'container', 'container_code', 'is_active', 'put_time', 'operator']
+        read_only_fields = ['id', 'put_time']
+
 
 
 

+ 4 - 0
bin/urls.py

@@ -10,6 +10,10 @@ urlpatterns = [
     path (r'group/check-pallet-consistency/', views.locationGroupViewSet.as_view({"get": "check_pallet_consistency"}), name='location_group_check_pallet_consistency'),
     path (r'group/fix-pallet-consistency/', views.locationGroupViewSet.as_view({"post": "fix_pallet_consistency"}), name='location_group_fix_pallet_consistency'),
     re_path(r'^group/(?P<pk>\d+)/$',  views.locationGroupViewSet.as_view({"get": "retrieve", "put": "update"}), name='location_detail'),
+    
+    # 库位-托盘关联查询
+    path (r'link/', views.locationContainerLinkViewSet.as_view({"get": "list"}), name='location_container_link_list'),
+    re_path(r'^link/(?P<pk>\d+)/$', views.locationContainerLinkViewSet.as_view({"get": "retrieve"}), name='location_container_link_detail'),
     # path(r'management/', views.stockshelfViewSet.as_view({"get": "list", "post": "create"}), name="management"),
     # re_path(r'^management/(?P<pk>\d+)/$', views.stockshelfViewSet.as_view({'get': 'retrieve','put': 'update','patch': 'partial_update','delete': 'destroy'}), name="staff_1"),
 

+ 83 - 1
bin/views.py

@@ -18,7 +18,7 @@ from bound.models import BoundBatchModel,BoundDetailModel,BoundListModel
 
 from .filter import DeviceFilter,LocationFilter,LocationContainerLinkFilter,LocationChangeLogFilter,LocationGroupFilter
 from .serializers import LocationListSerializer,LocationPostSerializer
-from .serializers import LocationGroupListSerializer,LocationGroupPostSerializer
+from .serializers import LocationGroupListSerializer,LocationGroupPostSerializer,LocationContainerLinkSerializer
 # 以后添加模块时,只需要在这里添加即可
 from rest_framework.permissions import AllowAny
 from container.models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel
@@ -329,7 +329,89 @@ class locationViewSet(viewsets.ModelViewSet):
         )
         
         return Response(data, status=200)
+
+class locationContainerLinkViewSet(viewsets.ReadOnlyModelViewSet):
+    """
+    库位-托盘关联查询 ViewSet
+    retrieve:
+        获取单个关联详情(get)
+    list:
+        获取关联列表(all)
+    """
+    pagination_class = MyPageNumberPagination
+    filter_backends = [DjangoFilterBackend, OrderingFilter]
+    ordering_fields = ['id', 'put_time', 'create_time', 'update_time']
+    filter_class = LocationContainerLinkFilter
+    serializer_class = LocationContainerLinkSerializer
+
+    def get_queryset(self):
+        # 检查认证,使用与其他 ViewSet 相同的方式
+        if not self.request.auth:
+            logger.warning("未认证用户尝试访问库位-托盘关联列表")
+            return LocationContainerLink.objects.none()
+        
+        queryset = LocationContainerLink.objects.select_related(
+            'location', 'container'
+        ).all()
+        
+        # 获取查询参数
+        location_code = self.request.query_params.get('location_code', None)
+        container_code = self.request.query_params.get('container_code', None)
+
+        is_active_param = self.request.query_params.get('is_active', 'true')
+
+        # 支持通过 location_code 查询
+        if location_code:  
+            if len(location_code.split('-')) == 4:
+                location_row = location_code.split('-')[1]
+                location_col = location_code.split('-')[2]
+                location_layer = location_code.split('-')[3]
+                queryset = queryset.filter(location__row=location_row, location__col=location_col, location__layer=location_layer)
+            else:
+                queryset = queryset.filter(location__location_code=location_code)
+            logger.debug(f"按 location_code 过滤: {location_code}")
+        
+        # 支持通过 container_code 查询
+        if container_code:
+            queryset = queryset.filter(container__container_code=container_code)
+            logger.debug(f"按 container_code 过滤: {container_code}")
+        
+        # 处理 is_active 参数
+        if is_active_param:
+            is_active_str = str(is_active_param).lower().strip()
+            if is_active_str in ('true', '1', 'yes'):
+                queryset = queryset.filter(is_active=True)
+            elif is_active_str in ('false', '0', 'no'):
+                queryset = queryset.filter(is_active=False)
+            # 如果参数值不是预期的,默认返回激活的
+            else:
+                queryset = queryset.filter(is_active=True)
+        else:
+            # 如果没有指定 is_active 参数,默认只返回激活的
+            queryset = queryset.filter(is_active=True)
+        
+        # 添加调试日志
+        count = queryset.count()
+        logger.info(f"查询库位-托盘关联: location_code={location_code}, container_code={container_code}, is_active={is_active_param}, 结果数量={count}")
+        
+        # 如果结果为空,记录更详细的信息用于调试
+        if count == 0:
+            total_count = LocationContainerLink.objects.count()
+            active_count = LocationContainerLink.objects.filter(is_active=True).count()
+            logger.debug(f"查询结果为空。数据库总记录数: {total_count}, 激活记录数: {active_count}")
+            if location_code:
+                location_exists = LocationModel.objects.filter(location_code=location_code).exists()
+                logger.debug(f"库位 {location_code} 是否存在: {location_exists}")
+        
+        log_operation(
+            request=self.request,
+            operation_content=f"查看库位-托盘关联列表 (location_code={location_code}, container_code={container_code}, 结果数={count})",
+            operation_level="view",
+            operator=self.request.auth.name if self.request.auth else None,
+            module_name="库位关联"
+        )
         
+        return queryset
 
 class locationGroupViewSet(viewsets.ModelViewSet):
     """

+ 3 - 0
location_statistics/urls.py

@@ -7,4 +7,7 @@ urlpatterns = [
     path('group-statistics/', views.LocationGroupStatisticsView.as_view(), name='location-group-statistics'),
     path('refresh-statistics/', views.LocationStatisticsView.as_view(), name='refresh-location-statistics'),
     path('CheckView/', views.LocationConsistencyCheckView.as_view(), name='refresh-location-group-statistics'),
+    path('bind-container/', views.BindContainerView.as_view(), name='bind-container'),
+    path('update-location-status/', views.UpdateLocationStatusView.as_view(), name='update-location-status'),
+    path('unbind-container/', views.UnbindContainerView.as_view(), name='unbind-container'),
 ]

+ 330 - 0
location_statistics/views.py

@@ -756,3 +756,333 @@ def get_consistency_report(warehouse_code=None, layer=None, auto_fix=False):
     checker.check_all()
     return checker.generate_report()
 
+
+class BindContainerView(APIView):
+    """重新绑定托盘到库位"""
+    
+    def post(self, request):
+        location_code = request.data.get('location_code')
+        container_code = request.data.get('container_code')
+        
+        if not location_code:
+            return Response({
+                'success': False,
+                'message': '缺少库位编码参数'
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        if not container_code:
+            return Response({
+                'success': False,
+                'message': '缺少托盘编码参数'
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        try:
+            from bin.models import LocationModel, LocationContainerLink
+            from container.models import ContainerListModel
+            from bin.views import LocationAllocation
+            if len(location_code.split('-')) == 4:
+                location_row = location_code.split('-')[1]
+                location_col = location_code.split('-')[2]
+                location_layer = location_code.split('-')[3]
+                location = LocationModel.objects.filter(
+                    row=location_row,
+                    col=location_col,
+                    layer=location_layer,
+                    is_active=True).first()
+            else:
+                location = LocationModel.objects.filter(
+                    location_code=location_code,
+                    is_active=True
+                ).first()
+
+        
+            if not location:
+                return Response({
+                    'success': False,
+                    'message': f'库位 {location_code} 不存在或已禁用'
+                }, status=status.HTTP_404_NOT_FOUND)
+            
+            # 验证托盘是否存在
+            container = ContainerListModel.objects.filter(
+                container_code=container_code
+            ).first()
+            
+            if not container:
+                return Response({
+                    'success': False,
+                    'message': f'托盘 {container_code} 不存在'
+                }, status=status.HTTP_404_NOT_FOUND)
+            
+            # 使用 LocationAllocation 的方法来更新关联
+            allocator = LocationAllocation()
+            
+            # 先解除该库位的所有现有关联
+            LocationContainerLink.objects.filter(
+                location=location,
+                is_active=True
+            ).update(is_active=False)
+            
+            # 创建新的关联
+            with transaction.atomic():
+                # 检查是否已存在关联(即使是非活跃的)
+                existing_link = LocationContainerLink.objects.filter(
+                    location=location,
+                    container=container
+                ).first()
+                
+                if existing_link:
+                    # 如果存在,重新激活
+                    existing_link.is_active = True
+                    existing_link.save()
+                else:
+                    # 创建新关联
+                    LocationContainerLink.objects.create(
+                        location=location,
+                        container=container,
+                        is_active=True,
+                        operator=request.auth.name if request.auth else None,
+         
+                    )
+                
+                # 更新库位状态为占用
+                location.status = 'occupied'
+                location.save()
+                
+                # 更新库位组状态
+                allocator.update_location_group_status(location_code)
+            
+            logger.info(f"成功重新绑定托盘 {container_code} 到库位 {location_code}")
+            
+            return Response({
+                'success': True,
+                'code': '200',
+                'message': '重新绑定托盘成功',
+                'data': {
+                    'location_code': location_code,
+                    'container_code': container_code,
+                    'status': 'occupied'
+                }
+            }, status=status.HTTP_200_OK)
+            
+        except Exception as e:
+            logger.error(f"重新绑定托盘失败: {str(e)}", exc_info=True)
+            return Response({
+                'success': False,
+                'message': f'重新绑定托盘失败: {str(e)}'
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class UpdateLocationStatusView(APIView):
+    """更新库位状态"""
+    
+    def post(self, request):
+        location_code = request.data.get('location_code')
+        location_status = request.data.get('status')  # 重命名变量避免与 status 模块冲突
+        
+        if not location_code:
+            return Response({
+                'success': False,
+                'message': '缺少库位编码参数'
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        if not location_status:
+            return Response({
+                'success': False,
+                'message': '缺少状态参数'
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        # 验证状态值
+        valid_statuses = ['available', 'occupied', 'disabled', 'reserved', 'maintenance']
+        if location_status not in valid_statuses:
+            return Response({
+                'success': False,
+                'message': f'无效的状态值,允许的值: {", ".join(valid_statuses)}'
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        try:
+            from bin.models import LocationModel
+            from bin.views import LocationAllocation
+            
+            # 验证库位是否存在
+            location = LocationModel.objects.filter(
+                location_code=location_code,
+                is_active=True
+            ).first()
+            
+            if not location:
+                return Response({
+                    'success': False,
+                    'message': f'库位 {location_code} 不存在或已禁用'
+                }, status=status.HTTP_404_NOT_FOUND)
+            
+            # 保存旧状态
+            old_status = location.status
+            
+            # 使用 LocationAllocation 的方法来更新状态
+            allocator = LocationAllocation()
+            result = allocator.update_location_status(location_code, location_status, request=request)
+            
+            if result:
+                logger.info(f"成功更新库位 {location_code} 状态为 {location_status}")
+                
+                return Response({
+                    'success': True,
+                    'code': '200',
+                    'message': '更新库位状态成功',
+                    'data': {
+                        'location_code': location_code,
+                        'old_status': old_status,
+                        'new_status': location_status
+                    }
+                }, status=status.HTTP_200_OK)
+            else:
+                return Response({
+                    'success': False,
+                    'message': '更新库位状态失败'
+                }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+                
+        except Exception as e:
+            logger.error(f"更新库位状态失败: {str(e)}", exc_info=True)
+            return Response({
+                'success': False,
+                'message': f'更新库位状态失败: {str(e)}'
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class UnbindContainerView(APIView):
+    """解除托盘与库位的绑定关系"""
+    
+    def post(self, request):
+        location_code = request.data.get('location_code')
+        container_code = request.data.get('container_code')  # 可选参数
+        
+        if not location_code:
+            return Response({
+                'success': False,
+                'message': '缺少库位编码参数'
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        try:
+            from bin.models import LocationModel, LocationContainerLink
+            from bin.views import LocationAllocation
+            
+            # 验证库位是否存在
+            location = LocationModel.objects.filter(
+                location_code=location_code,
+                is_active=True
+            ).first()
+            
+            if not location:
+                return Response({
+                    'success': False,
+                    'message': f'库位 {location_code} 不存在或已禁用'
+                }, status=status.HTTP_404_NOT_FOUND)
+            
+            # 根据是否提供 container_code 来决定解除范围
+            if container_code:
+                # 只解除指定托盘的绑定
+                from container.models import ContainerListModel
+                container = ContainerListModel.objects.filter(
+                    container_code=container_code,
+                    is_delete=False
+                ).first()
+                
+                if not container:
+                    return Response({
+                        'success': False,
+                        'message': f'托盘 {container_code} 不存在或已删除'
+                    }, status=status.HTTP_404_NOT_FOUND)
+                
+                # 获取该库位与该托盘的关联
+                active_links = LocationContainerLink.objects.filter(
+                    location=location,
+                    container=container,
+                    is_active=True
+                )
+                
+                if not active_links.exists():
+                    return Response({
+                        'success': False,
+                        'message': f'库位 {location_code} 与托盘 {container_code} 没有绑定关系'
+                    }, status=status.HTTP_400_BAD_REQUEST)
+                
+                unbound_count = active_links.count()
+                
+                with transaction.atomic():
+                    # 标记该关联为非活跃
+                    active_links.update(is_active=False)
+                    
+                    # 检查是否还有其他活跃关联
+                    remaining_links = LocationContainerLink.objects.filter(
+                        location=location,
+                        is_active=True
+                    )
+                    
+                    # 如果没有其他活跃关联,更新库位状态为可用
+                    if not remaining_links.exists():
+                        location.status = 'available'
+                        location.save()
+                    
+                    # 更新库位组状态
+                    allocator = LocationAllocation()
+                    allocator.update_location_group_status(location_code)
+                
+                logger.info(f"成功解除库位 {location_code} 与托盘 {container_code} 的绑定")
+                
+                return Response({
+                    'success': True,
+                    'code': '200',
+                    'message': f'解除托盘 {container_code} 绑定成功',
+                    'data': {
+                        'location_code': location_code,
+                        'container_code': container_code,
+                        'unbound_count': unbound_count,
+                        'remaining_links': remaining_links.count()
+                    }
+                }, status=status.HTTP_200_OK)
+            else:
+                # 解除所有关联
+                active_links = LocationContainerLink.objects.filter(
+                    location=location,
+                    is_active=True
+                )
+                
+                if not active_links.exists():
+                    return Response({
+                        'success': False,
+                        'message': f'库位 {location_code} 没有绑定的托盘'
+                    }, status=status.HTTP_400_BAD_REQUEST)
+                
+                unbound_count = active_links.count()
+                
+                with transaction.atomic():
+                    # 标记所有关联为非活跃
+                    active_links.update(is_active=False)
+                    
+                    # 更新库位状态为可用
+                    location.status = 'available'
+                    location.save()
+                    
+                    # 更新库位组状态
+                    allocator = LocationAllocation()
+                    allocator.update_location_group_status(location_code)
+                
+                logger.info(f"成功解除库位 {location_code} 的所有托盘绑定")
+                
+                return Response({
+                    'success': True,
+                    'code': '200',
+                    'message': '解除所有托盘绑定成功',
+                    'data': {
+                        'location_code': location_code,
+                        'unbound_count': unbound_count,
+                        'new_status': 'available'
+                    }
+                }, status=status.HTTP_200_OK)
+            
+        except Exception as e:
+            logger.error(f"解除托盘绑定失败: {str(e)}", exc_info=True)
+            return Response({
+                'success': False,
+                'message': f'解除托盘绑定失败: {str(e)}'
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/css/35.4ed880df.css


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/css/35.da66b954.css


+ 0 - 1
templates/dist/spa/css/39.2ac1dad1.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-3867045e]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-3867045e]{background-color:rgba(25,118,210,0.1)}

+ 1 - 0
templates/dist/spa/css/39.fffe0096.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-67432470]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-67432470]{background-color:rgba(25,118,210,0.1)}

File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/css/7.058b37f5.css


+ 0 - 1
templates/dist/spa/css/7.b5042fa0.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-6eb8828c]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-6eb8828c]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-6eb8828c]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-6eb8828c]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-6eb8828c]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-6eb8828c]{color:#485573}

File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/index.html


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/35.a938d86d.js


BIN
templates/dist/spa/js/35.a938d86d.js.gz


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/35.e9d60c9f.js


BIN
templates/dist/spa/js/35.e9d60c9f.js.gz


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/37.e70b8a57.js


BIN
templates/dist/spa/js/37.e70b8a57.js.gz


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/37.f9bec936.js


BIN
templates/dist/spa/js/37.f9bec936.js.gz


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/39.4e22ecc5.js


BIN
templates/dist/spa/js/39.4e22ecc5.js.gz


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/39.77c36f65.js


BIN
templates/dist/spa/js/39.77c36f65.js.gz


File diff suppressed because it is too large
+ 0 - 1
templates/dist/spa/js/7.15a488eb.js


BIN
templates/dist/spa/js/7.15a488eb.js.gz


File diff suppressed because it is too large
+ 1 - 0
templates/dist/spa/js/7.1773c9a2.js


BIN
templates/dist/spa/js/7.1773c9a2.js.gz


File diff suppressed because it is too large
+ 1 - 1
templates/dist/spa/js/app.86855383.js


BIN
templates/dist/spa/js/app.3082c386.js.gz


BIN
templates/dist/spa/js/app.86855383.js.gz


File diff suppressed because it is too large
+ 2 - 2
templates/dist/spa/js/vendor.7897d76a.js


BIN
templates/dist/spa/js/vendor.7897d76a.js.gz


+ 451 - 1
templates/src/layouts/MainLayout.vue

@@ -39,6 +39,29 @@
             >{{ "保存数据库" }}</q-tooltip
           ></q-btn
         >
+        <q-btn 
+          flat 
+          @click="showLocationErrorsDialog = true" 
+          round 
+          dense 
+          icon="mail"
+          style="position: relative"
+        >
+          <q-badge
+            v-if="locationErrorsCount > 0"
+            color="red"
+            floating
+            rounded
+            :label="locationErrorsCount > 99 ? '99+' : locationErrorsCount"
+          />
+          <q-tooltip
+            content-class="bg-amber text-black shadow-4"
+            :offset="[15, 15]"
+            content-style="font-size: 12px"
+          >
+            位置异常通知 ({{ locationErrorsCount }})
+          </q-tooltip>
+        </q-btn>
         <screenfull id="screenfull" class="right-menu-item hover-effect" />
         <transition appear enter-active-class="animated zoomIn">
           <q-btn
@@ -690,6 +713,174 @@
         </q-card-section>
       </q-card>
     </q-dialog>
+
+    <!-- 位置错误通知对话框 -->
+    <q-dialog v-model="showLocationErrorsDialog" maximized>
+      <q-card>
+        <q-card-section class="row items-center q-pb-none">
+          <div class="text-h6">位置异常通知</div>
+          <q-space />
+          <q-btn icon="close" flat round dense v-close-popup />
+        </q-card-section>
+
+        <q-card-section>
+          <q-list v-if="locationErrors.length > 0" separator>
+            <q-expansion-item
+              v-for="(error, index) in locationErrors"
+              :key="index"
+              :label="`${error.location_code} - ${error.error_type_display}`"
+              :caption="`检测时间: ${formatErrorDateTime(error.detected_at)}`"
+              header-class="text-primary"
+            >
+              <q-card>
+                <q-card-section>
+                  <div class="row q-mb-md">
+                    <div class="col-6">
+                      <div class="text-caption text-grey">位置编码</div>
+                      <div class="text-body1">{{ error.location_code }}</div>
+                    </div>
+                    <div class="col-3">
+                      <div class="text-caption text-grey">层/行/列</div>
+                      <div class="text-body1">{{ error.layer }}/{{ error.row }}/{{ error.col }}</div>
+                    </div>
+                    <div class="col-3">
+                      <div class="text-caption text-grey">当前状态</div>
+                      <div class="text-body1">{{ error.current_status }}</div>
+                    </div>
+                  </div>
+
+                  <q-separator class="q-my-md" />
+
+                  <!-- 错误类型1: 库位占用但没有绑定托盘 -->
+                  <div v-if="error.error_type === 'occupied_no_container'">
+                    <div class="text-subtitle2 q-mb-sm">
+                      <q-icon name="warning" color="orange" class="q-mr-xs" />
+                      库位状态为占用,但未绑定托盘
+                    </div>
+                    <div class="q-mb-md">
+                      <q-radio
+                        v-model="error.userAction"
+                        val="has_container"
+                        label="确认该库位有托盘"
+                      />
+                      <q-radio
+                        v-model="error.userAction"
+                        val="no_container"
+                        label="确认该库位没有托盘"
+                      />
+                    </div>
+
+                    <div v-if="error.userAction === 'has_container'" class="q-mb-md">
+                      <q-input
+                        v-model="error.container_code"
+                        label="请输入托盘码"
+                        outlined
+                        dense
+                        class="q-mb-sm"
+                      />
+                      <q-btn
+                        color="primary"
+                        label="重新绑定托盘"
+                        @click="rebindContainer(error)"
+                        :loading="error.processing"
+                      />
+                    </div>
+
+                    <div v-if="error.userAction === 'no_container'" class="q-mb-md">
+                      <q-btn
+                        color="primary"
+                        label="修改库位状态为可用"
+                        @click="updateLocationStatus(error, 'available')"
+                        :loading="error.processing"
+                      />
+                    </div>
+                  </div>
+
+                  <!-- 错误类型2: 库位可用但有托盘绑定 -->
+                  <div v-if="error.error_type === 'available_with_container'">
+                    <div class="text-subtitle2 q-mb-sm">
+                      <q-icon name="warning" color="orange" class="q-mr-xs" />
+                      库位状态为可用,但有托盘绑定
+                    </div>
+                    <div class="q-mb-md">
+                      <q-radio
+                        v-model="error.userAction"
+                        val="has_container"
+                        label="确认该库位有托盘"
+                      />
+                      <q-radio
+                        v-model="error.userAction"
+                        val="no_container"
+                        label="确认该库位没有托盘"
+                      />
+                    </div>
+
+                    <div v-if="error.userAction === 'has_container'" class="q-mb-md">
+                      <q-btn
+                        color="primary"
+                        label="修改库位状态为占用"
+                        @click="updateLocationStatus(error, 'occupied')"
+                        :loading="error.processing"
+                      />
+                    </div>
+
+                    <div v-if="error.userAction === 'no_container'" class="q-mb-md">
+                      <q-btn
+                        color="primary"
+                        label="解除托盘绑定关系"
+                        @click="unbindContainer(error)"
+                        :loading="error.processing"
+                      />
+                    </div>
+                  </div>
+
+                  <!-- 辅助查询信息 -->
+                  <q-separator class="q-my-md" />
+                  <div class="text-subtitle2 q-mb-sm">
+                    <q-icon name="search" color="primary" class="q-mr-xs" />
+                    辅助查询 - 该位置的托盘信息
+                  </div>
+                  <q-btn
+                    color="secondary"
+                    label="查询该位置的托盘"
+                    @click="queryContainersByLocation(error)"
+                    :loading="error.querying"
+                    class="q-mb-sm"
+                  />
+                  <q-table
+                    v-if="error.containers && error.containers.length > 0"
+                    :data="error.containers"
+                    :columns="containerColumns"
+                    row-key="id"
+                    flat
+                    bordered
+                    dense
+                    class="q-mt-sm"
+                  />
+                  <div v-else-if="error.containers && error.containers.length === 0" class="text-caption text-grey q-mt-sm">
+                    未找到该位置的托盘
+                  </div>
+                </q-card-section>
+              </q-card>
+            </q-expansion-item>
+          </q-list>
+          <div v-else class="text-center q-pa-lg text-grey">
+            暂无位置异常
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right">
+          <q-btn flat label="关闭" v-close-popup />
+          <q-btn
+            flat
+            label="刷新"
+            color="primary"
+            @click="refreshLocationErrors"
+            :loading="loadingLocationErrors"
+          />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
   </q-layout>
 </template>
 <script>
@@ -750,7 +941,32 @@ export default {
       pollInterval: null,
       timer: null,
       selectedRole: '',
-      permissions: [] // 存储权限数据
+      permissions: [], // 存储权限数据
+      showLocationErrorsDialog: false,
+      locationErrors: [],
+      locationErrorsCount: 0,
+      loadingLocationErrors: false,
+      containerColumns: [
+        {
+          name: 'container_code',
+          label: '托盘码',
+          field: 'container_code',
+          align: 'left'
+        },
+        {
+          name: 'current_location',
+          label: '当前位置',
+          field: 'current_location',
+          align: 'left'
+        },
+        {
+          name: 'target_location',
+          label: '目标位置',
+          field: 'target_location',
+          align: 'left'
+        }
+
+      ]
     }
   },
   methods: {
@@ -1029,6 +1245,238 @@ export default {
       getauth('/wms/outboundBills/?bound_status=0').then((res) => {
         this.ERPOutTasks = res.count
       })
+      // 检查位置错误
+      // this.checkLocationErrors()
+    },
+    // 检查位置错误
+    checkLocationErrors () {
+      // const layer = 1 // 默认检查第一层,可以根据需要调整
+      const url = `/location_statistics/CheckView/`
+      const payload = {}
+      if (localStorage.getItem('warehouse_code')) {
+        payload.warehouse_code = localStorage.getItem('warehouse_code')
+      }
+
+      postauth(url, payload)
+        .then((res) => {
+          if (res && res.data) {
+            if (res.data.success === false) {
+              this.locationErrors = []
+              this.locationErrorsCount = 0
+              return
+            }
+            const checkResult = res.data.data || res.data
+            if (checkResult && checkResult.details && checkResult.details.location_errors) {
+              this.locationErrors = checkResult.details.location_errors.map(err => ({
+                ...err,
+                userAction: null,
+                container_code: '',
+                processing: false,
+                querying: false,
+                containers: null,
+                error_type_display: this.getErrorTypeDisplay(err),
+                error_type: this.determineErrorType(err)
+              }))
+              this.locationErrorsCount = this.locationErrors.length
+            } else {
+              this.locationErrors = []
+              this.locationErrorsCount = 0
+            }
+          } else {
+            this.locationErrors = []
+            this.locationErrorsCount = 0
+          }
+        })
+        .catch((err) => {
+          console.error('检查位置错误失败', err)
+          this.locationErrors = []
+          this.locationErrorsCount = 0
+        })
+    },
+    // 确定错误类型
+    determineErrorType (error) {
+      // 根据当前状态和期望状态判断错误类型
+      // 类型1: 库位占用但没有绑定托盘
+      // current_status 为 occupied/reserved,但 expected_status 为 available(表示应该没有托盘)
+      if ((error.current_status === 'occupied' || error.current_status === 'reserved') && 
+          (error.expected_status === 'available' || !error.expected_status)) {
+        return 'occupied_no_container'
+      }
+      // 类型2: 库位可用但有托盘绑定
+      // current_status 为 available,但 expected_status 为 occupied(表示应该有托盘)
+      if (error.current_status === 'available' && 
+          (error.expected_status === 'occupied' || error.expected_status === 'reserved')) {
+        return 'available_with_container'
+      }
+      // 如果没有明确的状态,根据当前状态判断
+      if (error.current_status === 'occupied' || error.current_status === 'reserved') {
+        return 'occupied_no_container'
+      }
+      return 'available_with_container'
+    },
+    // 刷新位置错误
+    refreshLocationErrors () {
+      this.loadingLocationErrors = true
+      this.checkLocationErrors()
+      setTimeout(() => {
+        this.loadingLocationErrors = false
+      }, 1000)
+    },
+    // 获取错误类型显示文本
+    getErrorTypeDisplay (error) {
+      const errorType = this.determineErrorType(error)
+      if (errorType === 'occupied_no_container') {
+        return '库位占用但未绑定托盘'
+      } else if (errorType === 'available_with_container') {
+        return '库位可用但有托盘绑定'
+      }
+      return error.error_type || '未知错误'
+    },
+    // 格式化错误时间
+    formatErrorDateTime (dateStr) {
+      if (!dateStr) return '-'
+      if (typeof dateStr === 'string' && dateStr.includes('T')) {
+        return dateStr.replace('T', ' ').substring(0, 19)
+      }
+      return dateStr
+    },
+    // 重新绑定托盘
+    rebindContainer (error) {
+      if (!error.container_code || !error.container_code.trim()) {
+        this.$q.notify({
+          type: 'negative',
+          message: '请输入托盘码'
+        })
+        return
+      }
+
+      error.processing = true
+      const url = '/location_statistics/bind-container/'
+      const payload = {
+        location_code: error.location_code,
+        container_code: error.container_code.trim()
+      }
+
+      postauth(url, payload)
+        .then((res) => {
+          if (res && (res.code === '200' || res.success)) {
+            this.$q.notify({
+              type: 'positive',
+              message: '重新绑定托盘成功'
+            })
+            // 从列表中移除该错误并刷新
+            this.checkLocationErrors()
+          } else {
+            this.$q.notify({
+              type: 'negative',
+              message: res?.msg || res?.message || '重新绑定托盘失败'
+            })
+          }
+        })
+        .catch((err) => {
+          this.$q.notify({
+            type: 'negative',
+            message: '重新绑定托盘失败: ' + (err?.message || err?.detail || '未知错误')
+          })
+        })
+        .finally(() => {
+          error.processing = false
+        })
+    },
+    // 更新库位状态
+    updateLocationStatus (error, status) {
+      error.processing = true
+      const url = '/location_statistics/update-location-status/'
+      const payload = {
+        location_code: error.location_code,
+        status: status
+      }
+
+      postauth(url, payload)
+        .then((res) => {
+          if (res && (res.code === '200' || res.success)) {
+            this.$q.notify({
+              type: 'positive',
+              message: '更新库位状态成功'
+            })
+            // 刷新错误列表
+            this.checkLocationErrors()
+          } else {
+            this.$q.notify({
+              type: 'negative',
+              message: res?.msg || res?.message || '更新库位状态失败'
+            })
+          }
+        })
+        .catch((err) => {
+          this.$q.notify({
+            type: 'negative',
+            message: '更新库位状态失败: ' + (err?.message || err?.detail || '未知错误')
+          })
+        })
+        .finally(() => {
+          error.processing = false
+        })
+    },
+    // 解除托盘绑定
+    unbindContainer (error) {
+      error.processing = true
+      const url = '/location_statistics/unbind-container/'
+      const payload = {
+        location_code: error.location_code
+      }
+
+      postauth(url, payload)
+        .then((res) => {
+          if (res && (res.code === '200' || res.success)) {
+            this.$q.notify({
+              type: 'positive',
+              message: '解除托盘绑定成功'
+            })
+            // 刷新错误列表
+            this.checkLocationErrors()
+          } else {
+            this.$q.notify({
+              type: 'negative',
+              message: res?.msg || res?.message || '解除托盘绑定失败'
+            })
+          }
+        })
+        .catch((err) => {
+          this.$q.notify({
+            type: 'negative',
+            message: '解除托盘绑定失败: ' + (err?.message || err?.detail || '未知错误')
+          })
+        })
+        .finally(() => {
+          error.processing = false
+        })
+    },
+    // 查询该位置的托盘
+    queryContainersByLocation (error) {
+      error.querying = true
+      // 拼接当前位置的库位码 W01-row(两位数字)-col(两位数字)-layer(一位数字)
+      const current_location_code = `W01-${error.row.toString().padStart(2, '0')}-${error.col.toString().padStart(2, '0')}-${error.layer.toString().padStart(2, '0')}`
+      const url = `/container/list/?current_location__icontains=${current_location_code}`
+      getauth(url)
+        .then((res) => {
+          if (res && res.results) {
+            error.containers = res.results
+          } else {
+            error.containers = []
+          }
+        })
+        .catch((err) => {
+          console.error('查询托盘失败', err)
+          error.containers = []
+          this.$q.notify({
+            type: 'negative',
+            message: '查询托盘失败: ' + (err?.message || '未知错误')
+          })
+        })
+        .finally(() => {
+          error.querying = false
+        })
     }
   },
   created () {
@@ -1066,6 +1514,8 @@ export default {
       _this.isLoggedIn()
     })
     _this.loadRolePermissions()
+    // 初始检查位置错误
+    _this.checkLocationErrors()
 
     _this.timer = setInterval(() => {
       _this.handleTimer()

+ 668 - 16
templates/src/pages/container/containerlist.vue

@@ -143,9 +143,24 @@
               </q-td>
             </template>
             <template v-else-if="props.row.id !== editid">
-              <q-td key="current_location" :props="props">{{
-                props.row.current_location
-              }}</q-td>
+              <q-td key="current_location" :props="props">
+                <div class="row items-center no-wrap">
+                  <div class="col">{{ props.row.current_location }}</div>
+                  <q-btn
+                    v-if="props.row.current_location && props.row.current_location !== 'N/A' && String(props.row.current_location).startsWith('W')"
+                    flat
+                    dense
+                    round
+                    size="sm"
+                    icon="link"
+                    color="primary"
+                    @click="openLocationBindDialog(props.row)"
+                    class="q-ml-xs"
+                  >
+                    <q-tooltip>库位绑定</q-tooltip>
+                  </q-btn>
+                </div>
+              </q-td>
             </template>
 
             <template v-if="props.row.id === editid">
@@ -329,6 +344,219 @@
         </q-card-section>
       </q-card>
     </q-dialog>
+
+    <!-- 库位绑定对话框 -->
+    <q-dialog v-model="locationBindDialog" persistent>
+      <q-card style="min-width: 600px">
+        <q-card-section class="row items-center q-pb-none">
+          <div class="text-h6">库位绑定管理</div>
+          <q-space />
+          <q-btn icon="close" flat round dense v-close-popup />
+        </q-card-section>
+
+        <q-separator />
+
+        <q-card-section v-if="locationBindData">
+          <q-inner-loading :showing="locationBindData.loading">
+            <q-spinner color="primary" size="50px" />
+          </q-inner-loading>
+
+          <div v-if="!locationBindData.loading">
+            <div class="row q-mb-md">
+              <div class="col-6">
+                <div class="text-caption text-grey">库位编码</div>
+                <div class="text-body1">{{ locationBindData.location_code || '-' }}</div>
+              </div>
+              <div class="col-3">
+                <div class="text-caption text-grey">层/行/列</div>
+                <div class="text-body1">
+                  {{ locationBindData.layer || '-' }}/{{ locationBindData.row || '-' }}/{{ locationBindData.col || '-' }}
+                </div>
+              </div>
+              <div class="col-3">
+                <div class="text-caption text-grey">库位状态</div>
+                <div class="text-body1">{{ locationBindData.status || '-' }}</div>
+              </div>
+            </div>
+
+            <q-separator class="q-my-md" />
+
+            <div class="q-mb-md">
+              <div class="text-subtitle2 q-mb-sm">已绑定的托盘</div>
+              <div v-if="locationBindData.bound_containers && locationBindData.bound_containers.length > 0">
+                <q-list bordered>
+                  <q-item
+                    v-for="(container, index) in locationBindData.bound_containers"
+                    :key="index"
+                  >
+                    <q-item-section>
+                      <q-item-label>
+                        托盘码: {{ container.container_code }}
+                        <q-badge
+                          v-if="isSameContainerCode(container.container_code, locationBindData.current_container_code)"
+                          color="green"
+                          class="q-ml-sm"
+                        >
+                          当前托盘
+                        </q-badge>
+                        <q-badge
+                          v-else-if="locationBindData.current_container_code && !isSameContainerCode(container.container_code, locationBindData.current_container_code)"
+                          color="orange"
+                          class="q-ml-sm"
+                        >
+                          不一致
+                        </q-badge>
+                      </q-item-label>
+                      <q-item-label caption>绑定时间: {{ formatDateTime(container.put_time) }}</q-item-label>
+                    </q-item-section>
+                    <q-item-section side>
+                      <q-btn
+                        flat
+                        dense
+                        round
+                        icon="link_off"
+                        color="negative"
+                        @click="handleUnbindContainer(container.container_code)"
+                        :loading="container.unbinding"
+                        :disable="bindingContainer"
+                      >
+                        <q-tooltip>解除绑定</q-tooltip>
+                      </q-btn>
+                    </q-item-section>
+                  </q-item>
+                </q-list>
+              </div>
+              <div v-else class="text-grey text-caption">该库位未绑定托盘</div>
+            </div>
+
+            <div class="q-mb-md">
+              <div class="text-subtitle2 q-mb-sm">当前托盘信息(从托盘列表)</div>
+              <div class="text-body2">
+                托盘码: <strong>{{ locationBindData.current_container_code || '-' }}</strong>
+              </div>
+            </div>
+
+            <q-separator class="q-my-md" />
+
+            <div v-if="locationBindData.needs_bind" class="q-mb-md">
+              <q-banner
+                rounded
+                dense
+                class="bg-orange-1 text-orange-9"
+              >
+                <template v-slot:avatar>
+                  <q-icon name="warning" color="orange" />
+                </template>
+                <div class="text-weight-medium q-mb-xs">绑定关系不一致</div>
+                <div class="text-body2">
+                  {{ locationBindData.bind_reason || '库位绑定关系与托盘列表不一致,请确认是否需要重新绑定。' }}
+                </div>
+              </q-banner>
+            </div>
+
+            <div v-else class="q-mb-md">
+              <q-banner
+                rounded
+                dense
+                class="bg-green-1 text-green-9"
+              >
+                <template v-slot:avatar>
+                  <q-icon name="check_circle" color="green" />
+                </template>
+                <div class="text-body2">绑定关系一致</div>
+              </q-banner>
+            </div>
+
+            <!-- 绑定操作按钮区域 -->
+            <div class="q-mt-md">
+              <div class="row q-gutter-sm">
+                <!-- 始终显示绑定按钮(即使没有绑定关系) -->
+                <div
+                  v-if="locationBindData.current_container_code"
+                  class="col"
+                >
+                  <q-btn
+                    color="primary"
+                    :label="locationBindData.bound_containers && locationBindData.bound_containers.length > 0 && locationBindData.bound_containers.some(c => isSameContainerCode(c.container_code, locationBindData.current_container_code)) ? '重新绑定当前托盘' : '绑定当前托盘'"
+                    icon="link"
+                    @click="handleBindContainer"
+                    :loading="bindingContainer"
+                    class="full-width"
+                  />
+                </div>
+                <!-- 如果有已绑定的托盘,显示解除所有绑定按钮 -->
+                <div
+                  v-if="locationBindData.bound_containers && locationBindData.bound_containers.length > 0"
+                  class="col"
+                >
+                  <q-btn
+                    color="negative"
+                    label="解除所有绑定"
+                    icon="link_off"
+                    @click="handleUnbindAllContainers"
+                    :loading="unbindingAll"
+                    class="full-width"
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right">
+          <q-btn flat label="关闭" v-close-popup />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
+
+    <!-- 新增托盘对话框 -->
+    <q-dialog v-model="addContainerDialog" persistent>
+      <q-card style="min-width: 400px">
+        <q-card-section class="row items-center q-pb-none">
+          <div class="text-h6">新增托盘</div>
+          <q-space />
+          <q-btn icon="close" flat round dense v-close-popup />
+        </q-card-section>
+
+        <q-separator />
+
+        <q-card-section>
+          <q-input
+            v-model="newContainerCode"
+            label="托盘编码(5位)"
+            outlined
+            :rules="[
+              val => (val && val.length === 5) || '托盘编码必须为5位',
+              val => /^[A-Za-z0-9]+$/.test(val) || '托盘编码只能包含字母和数字'
+            ]"
+            maxlength="5"
+            @input="checkContainerCodeExists"
+            :loading="checkingContainerCode"
+          >
+            <template v-slot:hint>
+              <div v-if="containerCodeCheckResult === 'exists'" class="text-negative">
+                该托盘编码已存在
+              </div>
+              <div v-else-if="containerCodeCheckResult === 'available'" class="text-positive">
+                该托盘编码可以使用
+              </div>
+            </template>
+          </q-input>
+        </q-card-section>
+
+        <q-card-actions align="right">
+          <q-btn flat label="取消" v-close-popup @click="resetAddDialog" />
+          <q-btn
+            flat
+            label="确定"
+            color="primary"
+            @click="confirmAddContainer"
+            :disable="!newContainerCode || newContainerCode.length !== 5 || containerCodeCheckResult === 'exists' || checkingContainerCode"
+            :loading="addingContainer"
+          />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
   </div>
 </template>
 
@@ -461,12 +689,21 @@ export default {
       activeSearchField: '',
       activeSearchLabel: '',
       userComponentPermissions: [], // 用户权限
-      login_mode: LocalStorage.getItem('login_mode') // 登录模式
+      login_mode: LocalStorage.getItem('login_mode'), // 登录模式
+      locationBindDialog: false,
+      locationBindData: null,
+      bindingContainer: false,
+      // 新增托盘对话框相关
+      addContainerDialog: false,
+      newContainerCode: '',
+      containerCodeCheckResult: '', // 'exists' | 'available' | ''
+      checkingContainerCode: false,
+      addingContainer: false
     }
   },
 
   methods: {
-     // 检查用户是否有指定页面的组件访问权限
+    // 检查用户是否有指定页面的组件访问权限
     loadUserPermissions () {
       postauth('staff/role-comPermissions/' + this.login_mode + '/', {
         page: '/container/containerlist'
@@ -695,22 +932,100 @@ export default {
       }
     },
     add () {
-      postauth('/container/list/', {})
-        .then((res) => {
-          this.$q.notify({
-            message: '添加成功',
-            icon: 'done',
-            color: 'positive'
-          })
-          this.reFresh()
+      this.resetAddDialog()
+      this.addContainerDialog = true
+    },
+    // 重置新增对话框
+    resetAddDialog () {
+      this.newContainerCode = ''
+      this.containerCodeCheckResult = ''
+      this.checkingContainerCode = false
+    },
+    // 校验托盘码是否存在
+    async checkContainerCodeExists () {
+      // 清空之前的校验结果
+      this.containerCodeCheckResult = ''
+
+      // 如果托盘码不是5位,不进行校验
+      if (!this.newContainerCode || this.newContainerCode.length !== 5) {
+        return
+      }
+
+      this.checkingContainerCode = true
+      try {
+        // 查询是否已存在该托盘码
+        const url = `${this.pathname}?container_code=${encodeURIComponent(this.newContainerCode)}`
+        const response = await getauth(url)
+
+        if (response && response.results && response.results.length > 0) {
+          this.containerCodeCheckResult = 'exists'
+        } else {
+          this.containerCodeCheckResult = 'available'
+        }
+      } catch (error) {
+        console.error('校验托盘码失败:', error)
+        // 校验失败时不清除结果,让用户知道需要重试
+      } finally {
+        this.checkingContainerCode = false
+      }
+    },
+    // 确认新增托盘
+    async confirmAddContainer () {
+      // 最终校验
+      if (!this.newContainerCode || this.newContainerCode.length !== 5) {
+        this.$q.notify({
+          message: '托盘编码必须为5位',
+          icon: 'close',
+          color: 'negative'
         })
-        .catch((err) => {
+        return
+      }
+
+      // 如果校验结果不明确,再次校验
+      if (this.containerCodeCheckResult !== 'available') {
+        await this.checkContainerCodeExists()
+        if (this.containerCodeCheckResult === 'exists') {
           this.$q.notify({
-            message: err.detail,
+            message: '该托盘编码已存在,请使用其他编码',
             icon: 'close',
             color: 'negative'
           })
+          return
+        }
+        if (this.containerCodeCheckResult !== 'available') {
+          this.$q.notify({
+            message: '请稍候,正在校验托盘编码...',
+            icon: 'info',
+            color: 'info'
+          })
+          return
+        }
+      }
+
+      this.addingContainer = true
+      try {
+        await postauth('/container/list/', {
+          container_code: this.newContainerCode
+        })
+
+        this.$q.notify({
+          message: '添加成功',
+          icon: 'done',
+          color: 'positive'
+        })
+        this.addContainerDialog = false
+        this.resetAddDialog()
+        this.reFresh()
+      } catch (err) {
+        const errorMessage = err.detail || err.message || '添加失败'
+        this.$q.notify({
+          message: errorMessage,
+          icon: 'close',
+          color: 'negative'
         })
+      } finally {
+        this.addingContainer = false
+      }
     },
     check_container () {
       var _this = this
@@ -921,11 +1236,348 @@ export default {
             color: 'negative'
           })
         })
+    },
+    // 打开库位绑定对话框
+    async openLocationBindDialog (row) {
+      if (!row || !row.current_location || row.current_location === 'N/A') {
+        this.$q.notify({
+          type: 'negative',
+          message: '该托盘没有当前位置信息'
+        })
+        return
+      }
+
+      const locationCode = row.current_location
+      this.locationBindDialog = true
+      this.locationBindData = {
+        location_code: locationCode,
+        current_container_code: row.container_code,
+        loading: true
+      }
+
+      try {
+        // 查询库位信息
+        // await this.queryLocationInfo(locationCode)
+        // 查询库位绑定的托盘
+        await this.queryLocationContainers(locationCode)
+        // 检查是否需要绑定
+        this.checkBindingStatus()
+      } catch (error) {
+        console.error('查询库位信息失败:', error)
+        this.$q.notify({
+          type: 'negative',
+          message: '查询库位信息失败: ' + (error?.message || '未知错误')
+        })
+      } finally {
+        this.locationBindData.loading = false
+      }
+    },
+    // 查询库位信息
+    async queryLocationInfo (locationCode) {
+      try {
+        // 从库位列表查询库位信息
+        const url = `bin/?location_code=${encodeURIComponent(locationCode)}`
+        const response = await getauth(url)
+
+        if (response && response.results && response.results.length > 0) {
+          const location = response.results[0]
+
+          this.locationBindData = {
+            ...this.locationBindData,
+            location_code: location.location_code,
+            row: location.row,
+            col: location.col,
+            layer: location.layer,
+            status: location.status,
+            location_type: location.location_type
+          }
+        } else {
+          // 如果查询不到,使用基本信息
+          this.locationBindData = {
+            ...this.locationBindData,
+            location_code: locationCode
+          }
+        }
+      } catch (error) {
+        console.error('查询库位信息失败:', error)
+        // 即使查询失败,也保留基本信息
+        this.locationBindData = {
+          ...this.locationBindData,
+          location_code: locationCode
+        }
+      }
+    },
+    // 查询库位绑定的托盘(使用新的 link API)
+    async queryLocationContainers (locationCode) {
+      try {
+        // 使用新的 link API 查询库位关联的托盘
+        const url = `bin/link/?location_code=${encodeURIComponent(locationCode)}&is_active=true`
+        const response = await getauth(url)
+
+        const boundContainers = []
+        if (response && response.results && Array.isArray(response.results)) {
+          response.results.forEach(link => {
+            if (link.is_active && link.container_code) {
+              boundContainers.push({
+                container_code: link.container_code,
+                put_time: link.put_time || link.create_time
+              })
+            }
+          })
+        }
+
+        this.locationBindData.bound_containers = boundContainers
+      } catch (error) {
+        console.error('查询库位关联托盘失败:', error)
+        this.locationBindData.bound_containers = []
+      }
+    },
+    // 检查绑定状态
+    checkBindingStatus () {
+      const boundContainers = this.locationBindData.bound_containers || []
+      const currentContainerCode = this.locationBindData.current_container_code
+      const locationStatus = this.locationBindData.status
+
+      // 检查是否需要绑定
+      let needsBind = false
+      let bindReason = ''
+
+      // 如果没有绑定任何托盘,且当前托盘存在,需要绑定
+      if (boundContainers.length === 0 && currentContainerCode) {
+        needsBind = true
+        if (locationStatus === 'occupied' || locationStatus === 'reserved') {
+          bindReason = '库位状态为占用,但未绑定托盘'
+        } else {
+          bindReason = '库位未绑定托盘,需要绑定当前托盘'
+        }
+      } else if (locationStatus === 'occupied' || locationStatus === 'reserved') {
+        // 库位状态为占用,应该绑定托盘
+        if (boundContainers.length === 0) {
+          needsBind = true
+          bindReason = '库位状态为占用,但未绑定托盘'
+        } else if (currentContainerCode && !boundContainers.some(c => this.isSameContainerCode(c.container_code, currentContainerCode))) {
+          needsBind = true
+          bindReason = '库位绑定的托盘与当前托盘不一致'
+        }
+      } else if (locationStatus === 'available' && boundContainers.length > 0) {
+        // 库位状态为可用,但绑定了托盘
+        if (currentContainerCode && boundContainers.some(c => this.isSameContainerCode(c.container_code, currentContainerCode))) {
+          needsBind = true
+          bindReason = '库位状态为可用,但绑定了托盘'
+        }
+      }
+
+      this.locationBindData.needs_bind = needsBind
+      this.locationBindData.bind_reason = bindReason
+    },
+    // 绑定托盘
+    async handleBindContainer () {
+      if (!this.locationBindData || !this.locationBindData.location_code) {
+        return
+      }
+
+      const locationCode = this.locationBindData.location_code
+      const containerCode = this.locationBindData.current_container_code
+
+      if (!containerCode) {
+        this.$q.notify({
+          type: 'negative',
+          message: '无法获取托盘编码'
+        })
+        return
+      }
+
+      this.bindingContainer = true
+      try {
+        const url = '/location_statistics/bind-container/'
+        const payload = {
+          location_code: locationCode,
+          container_code: containerCode
+        }
+
+        const response = await postauth(url, payload)
+
+        if (response && (response.code === '200' || response.success)) {
+          this.$q.notify({
+            type: 'positive',
+            message: '绑定成功',
+            icon: 'check'
+          })
+          // 重新查询绑定关系和库位信息(确保状态已更新,参考任务结束后的逻辑)
+          await Promise.all([
+            this.queryLocationContainers(locationCode),
+            this.queryLocationInfo(locationCode)
+          ])
+          this.checkBindingStatus()
+          // 刷新列表
+          this.getSearchList()
+        } else {
+          this.$q.notify({
+            type: 'negative',
+            message: response?.msg || response?.message || '绑定失败'
+          })
+        }
+      } catch (error) {
+        console.error('绑定失败:', error)
+        this.$q.notify({
+          type: 'negative',
+          message: '绑定失败: ' + (error?.message || error?.detail || '未知错误')
+        })
+      } finally {
+        this.bindingContainer = false
+      }
+    },
+    // 解除单个托盘绑定
+    async handleUnbindContainer (containerCode) {
+      if (!this.locationBindData || !this.locationBindData.location_code) {
+        return
+      }
+
+      const locationCode = this.locationBindData.location_code
+
+      // 确认对话框
+      this.$q
+        .dialog({
+          title: '确认解除绑定',
+          message: `确定要解除库位 ${locationCode} 与托盘 ${containerCode} 的绑定关系吗?`,
+          cancel: true,
+          persistent: true
+        })
+        .onOk(async () => {
+          // 找到对应的容器对象,设置 loading 状态
+          const container = this.locationBindData.bound_containers.find(
+            c => this.isSameContainerCode(c.container_code, containerCode)
+          )
+          if (container) {
+            this.$set(container, 'unbinding', true)
+          }
+
+          try {
+            const url = '/location_statistics/unbind-container/'
+            const payload = {
+              location_code: locationCode,
+              container_code: containerCode
+            }
+
+            const response = await postauth(url, payload)
+
+            if (response && (response.code === '200' || response.success)) {
+              this.$q.notify({
+                type: 'positive',
+                message: '解除绑定成功',
+                icon: 'check'
+              })
+              // 重新查询绑定关系和库位信息(确保状态已更新)
+              await Promise.all([
+                this.queryLocationContainers(locationCode),
+                this.queryLocationInfo(locationCode)
+              ])
+              this.checkBindingStatus()
+              // 刷新列表
+              this.getSearchList()
+            } else {
+              this.$q.notify({
+                type: 'negative',
+                message: response?.msg || response?.message || '解除绑定失败'
+              })
+            }
+          } catch (error) {
+            console.error('解除绑定失败:', error)
+            this.$q.notify({
+              type: 'negative',
+              message: '解除绑定失败: ' + (error?.message || error?.detail || '未知错误')
+            })
+          } finally {
+            if (container) {
+              this.$set(container, 'unbinding', false)
+            }
+          }
+        })
+    },
+    // 解除所有绑定
+    async handleUnbindAllContainers () {
+      if (!this.locationBindData || !this.locationBindData.location_code) {
+        return
+      }
+
+      const locationCode = this.locationBindData.location_code
+      const boundContainers = this.locationBindData.bound_containers || []
+
+      if (boundContainers.length === 0) {
+        this.$q.notify({
+          type: 'info',
+          message: '该库位没有绑定的托盘'
+        })
+        return
+      }
+
+      // 确认对话框
+      this.$q
+        .dialog({
+          title: '确认解除所有绑定',
+          message: `确定要解除库位 ${locationCode} 的所有托盘绑定关系吗?共 ${boundContainers.length} 个托盘。`,
+          cancel: true,
+          persistent: true
+        })
+        .onOk(async () => {
+          this.unbindingAll = true
+
+          try {
+            const url = '/location_statistics/unbind-container/'
+            const payload = {
+              location_code: locationCode
+            }
+
+            const response = await postauth(url, payload)
+
+            if (response && (response.code === '200' || response.success)) {
+              this.$q.notify({
+                type: 'positive',
+                message: '解除所有绑定成功',
+                icon: 'check'
+              })
+              // 重新查询绑定关系和库位信息(确保状态已更新)
+              await Promise.all([
+                this.queryLocationContainers(locationCode),
+                this.queryLocationInfo(locationCode)
+              ])
+              this.checkBindingStatus()
+              // 刷新列表
+              this.getSearchList()
+            } else {
+              this.$q.notify({
+                type: 'negative',
+                message: response?.msg || response?.message || '解除绑定失败'
+              })
+            }
+          } catch (error) {
+            console.error('解除绑定失败:', error)
+            this.$q.notify({
+              type: 'negative',
+              message: '解除绑定失败: ' + (error?.message || error?.detail || '未知错误')
+            })
+          } finally {
+            this.unbindingAll = false
+          }
+        })
+    },
+    // 格式化时间
+    formatDateTime (dateStr) {
+      if (!dateStr) return '-'
+      if (typeof dateStr === 'string' && dateStr.includes('T')) {
+        return dateStr.replace('T', ' ').substring(0, 19)
+      }
+      return dateStr
+    },
+    // 比较容器码(处理类型不一致问题)
+    isSameContainerCode (code1, code2) {
+      if (!code1 || !code2) return false
+      return String(code1).trim() === String(code2).trim()
     }
   },
   created () {
     var _this = this
-    _this.loadUserPermissions();
+    _this.loadUserPermissions()
     if (LocalStorage.has('openid')) {
       _this.openid = LocalStorage.getItem('openid')
     } else {

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

@@ -186,10 +186,30 @@
               :key="col.name"
               :props="props"
             >
-              <span v-if="col.name === 'check_status'">
+              <!-- 管理批次列:显示批次号+打印按钮 -->
+              <template v-if="col.name === 'bound_number'">
+                <div class="row items-center no-wrap">
+                  <div class="col">{{ props.row.bound_number }}</div>
+                  <q-btn
+                    icon="print"
+                    flat
+                    dense
+                    round
+                    size="sm"
+                    v-print="getPrintConfig()"
+                    @click="setCurrentBatch(props.row)"
+                    class="q-ml-xs"
+                  >
+                    <q-tooltip>打印条码</q-tooltip>
+                  </q-btn>
+                </div>
+              </template>
+              <!-- 质检状态列:显示状态文本 -->
+              <span v-else-if="col.name === 'check_status'">
                 {{ checkStatusToText(props.row[col.field]) }}
               </span>
-              <span v-else-if="col.name !== 'check_status'">
+              <!-- 其他列:显示字段值 -->
+              <span v-else>
                 {{ col.field ? props.row[col.field] : props.row[col.name] }}
               </span>
             </q-td>
@@ -517,6 +537,21 @@
         </q-card-actions>
       </q-card>
     </q-dialog>
+    <div id="printBarcode" class="print-area">
+      <div class="q-pa-md text-center" style="flex: none">
+        <div class="row no-wrap">
+          <div class="col text-left">
+            <p style="font-weight: 500">{{ currentgoods.goods_desc || '' }}</p>
+          </div>
+          <div class="col text-right">
+            <p>
+              数量: {{ currentgoods.goods_qty || '' }}{{ currentgoods.goods_unit || '' }}
+            </p>
+          </div>
+        </div>
+        <svg ref="barcodeElement" style="width: 100%; height: auto"></svg>
+      </div>
+    </div>
   </div>
 </template>
 <router-view />
@@ -525,6 +560,7 @@
 import { getauth, postauth, putauth, deleteauth, getfile } from 'boot/axios_request'
 import { date, exportFile, LocalStorage } from 'quasar'
 import containercard from 'components/containercard.vue'
+import JsBarcode from 'jsbarcode'
 
 
 export default {
@@ -690,7 +726,10 @@ export default {
       userComponentPermissions: [], // 用户权限
       login_mode: LocalStorage.getItem('login_mode'), // 登录模式
       selectedBatchId: null,
-      onlyDownloadInStock: true // 默认只下载在库数量大于0的批次
+      onlyDownloadInStock: true, // 默认只下载在库数量大于0的批次
+      // 打印相关
+      currentBarcode: '',
+      currentgoods: {}
     }
   },
   computed: {
@@ -956,6 +995,37 @@ export default {
       _this.select_container_code = container.container_code
       _this.$refs.containercard.handleclick()
     },
+    // 打印相关方法
+    setCurrentBatch (row) {
+      this.currentBarcode = row.bound_number || ''
+      this.currentgoods = {
+        goods_desc: row.goods_desc || '',
+        goods_qty: row.goods_qty || '',
+        goods_unit: row.goods_unit || ''
+      }
+    },
+    getPrintConfig () {
+      this.generateBarcode()
+      return {
+        id: 'printBarcode'
+      }
+    },
+    generateBarcode () {
+      if (!this.$refs.barcodeElement) return
+      this.$refs.barcodeElement.innerHTML = ''
+
+      try {
+        JsBarcode(this.$refs.barcodeElement, this.currentBarcode, {
+          format: 'CODE128',
+          displayValue: true,
+          fontSize: 16,
+          height: 60,
+          margin: 10
+        })
+      } catch (error) {
+        console.error('条码生成失败:', error)
+      }
+    },
 
     class_to_name (class_id) {
       const class_map = {
@@ -1326,4 +1396,60 @@ export default {
 .custom-node .q-timeline__content {
   color: #485573; /* 文字颜色 */
 }
+
+/* 修改打印样式 */
+#printBarcode {
+  width: 80mm; /* 标准标签纸宽度 */
+  max-width: 50mm;
+  padding: 2mm;
+  box-sizing: border-box;
+}
+
+/* 打印时强制缩放 */
+@media print {
+  body {
+    margin: 0 !important;
+    visibility: hidden;
+  }
+
+  #printBarcode,
+  #printBarcode * {
+    visibility: visible;
+    width: 100% !important;
+    max-width: 100% !important;
+  }
+
+  /* 强制单页布局 */
+  .print-area {
+    page-break-inside: avoid;
+    break-inside: avoid;
+    display: flex;
+    flex-direction: column;
+    gap: 2mm;
+  }
+
+  /* 条码缩放 */
+  svg {
+    transform: scale(0.9);
+    transform-origin: center top;
+    max-height: 30mm !important;
+  }
+
+  /* 文本适配 */
+  p {
+    font-size: 9pt !important;
+    margin: 0;
+    line-height: 1.2;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  /* 网格布局优化 */
+  .row {
+    display: grid !important;
+    grid-template-columns: 1fr 1fr;
+    gap: 1mm;
+  }
+}
 </style>

+ 90 - 6
templates/src/pages/task/task.vue

@@ -20,7 +20,7 @@
         <template v-slot:header-cell="props">
           <q-th :props="props" @dblclick="handleHeaderDblClick(props.col)">
             <!-- 为特定列添加下拉选择器 -->
-            <template v-if="['bound_department'].includes(props.col.name)">
+            <template v-if="['bound_department', 'tasktype'].includes(props.col.name)">
               <q-select
                 dense
                 outlined
@@ -54,6 +54,23 @@
                 >{{ $t("refreshtip") }}</q-tooltip
               >
             </q-btn>
+            <q-toggle
+              v-model="enableWcsLocationQuery"
+              checked-icon="check"
+              unchecked-icon="close"
+              :label="enableWcsLocationQuery ? 'WCS状态查询:开启' : 'WCS状态查询:关闭'"
+              size="sm"
+              color="primary"
+              @update:model-value="handleWcsQueryToggle"
+            >
+              <q-tooltip
+                content-class="bg-amber text-black shadow-4"
+                :offset="[10, 10]"
+                content-style="font-size: 12px"
+              >
+                {{ enableWcsLocationQuery ? '已开启WCS库位状态查询' : '已关闭WCS库位状态查询' }}
+              </q-tooltip>
+            </q-toggle>
           </q-btn-group>
 
           <q-space />
@@ -567,6 +584,7 @@ export default {
         { label: '盘点状态', value: 'Inventory' }
       ],
       wcsLocationApiBase: 'https://mock.apipost.net/mock/406ff305c801000/wcs/',
+      enableWcsLocationQuery: false, // 默认关闭WCS库位状态查询
 
       table_list: [],
 
@@ -691,7 +709,8 @@ export default {
       total: 0,
       paginationIpt: 1,
       filterModels: {
-        bound_department: null
+        bound_department: null,
+        tasktype: null
       },
       filterdata: {},
       activeSearchField: '',
@@ -754,6 +773,9 @@ export default {
       if (col.name === 'priority') {
         return this.resolvePriorityValue(row)
       }
+      if (col.name === 'tasktype') {
+        return this.getTaskTypeLabel(baseValue)
+      }
       if (baseValue === null || baseValue === undefined || baseValue === '') {
         return '-'
       }
@@ -992,10 +1014,11 @@ export default {
       const normalized = this.normalizeTaskType(value)
       const mapping = {
         inbound: '入库',
-        putaway: '上架',
-        move: '移库',
+        // putaway: '上架',
+        // move: '移库',
         outbound: '出库',
-        out: '出库'
+        // out: '出库',
+        check: '抽检'
       }
       return mapping[normalized] || value || '-'
     },
@@ -1164,6 +1187,14 @@ export default {
           ]
         case 'bound_department':
           return this.bound_department_list
+        case 'tasktype':
+          return [
+            { label: '入库', value: 'inbound' },
+            { label: '出库', value: 'outbound' },
+            { label: '抽检', value: 'check' }
+            // { label: '上架', value: 'putaway' },
+            // { label: '移库', value: 'move' }
+          ]
         default:
           return []
       }
@@ -1400,7 +1431,8 @@ export default {
       var _this = this
       this.filterdata = {}
       this.filterModels = {
-        bound_department: null
+        bound_department: null,
+        tasktype: null
       }
       _this.getSearchList()
     },
@@ -1534,6 +1566,10 @@ export default {
       }
     },
     async loadLocationStatuses () {
+      // 如果未启用WCS查询,直接返回
+      if (!this.enableWcsLocationQuery) {
+        return
+      }
       if (!Array.isArray(this.table_list)) return
       for (const row of this.table_list) {
         // 查询起始位置状态
@@ -1560,6 +1596,47 @@ export default {
       // 加载完成后,自动校验工作状态为1的任务
       this.validateWorkingStatusTasks()
     },
+    handleWcsQueryToggle (value) {
+      // 保存状态到 LocalStorage
+      LocalStorage.set('enableWcsLocationQuery', value)
+      if (value) {
+        // 如果开启,立即加载一次库位状态
+        this.loadLocationStatuses()
+        this.$q.notify({
+          type: 'positive',
+          message: '已开启WCS库位状态查询',
+          icon: 'check',
+          timeout: 2000
+        })
+      } else {
+        // 如果关闭,清除已查询的状态
+        this.clearLocationStatuses()
+        this.$q.notify({
+          type: 'info',
+          message: '已关闭WCS库位状态查询',
+          icon: 'info',
+          timeout: 2000
+        })
+      }
+    },
+    clearLocationStatuses () {
+      // 清除所有已查询的库位状态
+      if (!Array.isArray(this.table_list)) return
+      for (const row of this.table_list) {
+        if (row.current_location_status !== undefined) {
+          this.$set(row, 'current_location_status', null)
+        }
+        if (row.current_location_data !== undefined) {
+          this.$set(row, 'current_location_data', null)
+        }
+        if (row.target_location_status !== undefined) {
+          this.$set(row, 'target_location_status', null)
+        }
+        if (row.target_location_data !== undefined) {
+          this.$set(row, 'target_location_data', null)
+        }
+      }
+    },
     validateWorkingStatusTasks () {
       // 只校验工作状态为1的任务
       if (!Array.isArray(this.table_list)) return
@@ -1861,6 +1938,13 @@ export default {
       _this.login_name = ''
       LocalStorage.set('login_name', '')
     }
+    // 从 LocalStorage 读取 WCS 查询开关状态,默认关闭
+    if (LocalStorage.has('enableWcsLocationQuery')) {
+      _this.enableWcsLocationQuery = LocalStorage.getItem('enableWcsLocationQuery') === true || LocalStorage.getItem('enableWcsLocationQuery') === 'true'
+    } else {
+      _this.enableWcsLocationQuery = false
+      LocalStorage.set('enableWcsLocationQuery', false)
+    }
     if (LocalStorage.has('auth')) {
       const timeStamp = Date.now()
       const formattedString = date.formatDate(timeStamp, 'YYYY/MM/DD')