flower_bs vor 4 Monaten
Ursprung
Commit
aa195ad88b

+ 24 - 1
container/filter.py

@@ -1,5 +1,5 @@
 from django_filters import FilterSet, NumberFilter, CharFilter
-from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel,ContainerWCSModel,ContainerDetailLogModel,batchLogModel
+from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel,ContainerWCSModel,ContainerDetailLogModel,batchLogModel,WCSTaskLogModel
 
 from django_filters import rest_framework as filters
 
@@ -139,4 +139,27 @@ class TaskFilter(FilterSet):
             "container_detail__goods_code": ['exact', 'icontains'],
             "container_detail__goods_desc": ['exact', 'icontains'],  
             }
+
+class WCSTaskLogFilter(FilterSet):
+    taskNumber = filters.CharFilter(field_name='task_number', lookup_expr='icontains')
+    class Meta:
+        model = WCSTaskLogModel
+        fields = {
+            "id": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "task_number": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "container": ['exact', 'icontains'],
+            "current_location": ['exact', 'icontains'],
+            "target_location": ['exact', 'icontains'],
+            "location_group_id": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "access_priority": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "left_priority": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "right_priority": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "task_type": ['exact', 'icontains'],
+            "order_number": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "sequence": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "send_time": ['exact', 'gt', 'gte', 'lt', 'lte', 'range'],
+            "floor": ['exact', 'icontains'],
+            "log_type": ['exact', 'icontains'],
+            "is_completed": ['exact'],
+        }
         

+ 44 - 0
container/migrations/0035_dispatchconfig_and_more.py

@@ -0,0 +1,44 @@
+# Generated by Django 4.1.2 on 2025-11-17 10:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0034_materialchangehistory_count_time'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='DispatchConfig',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('cross_floor_concurrent_limit', models.PositiveIntegerField(default=2, verbose_name='跨楼层并发上限')),
+                ('intra_floor_order', models.CharField(choices=[('batch_then_sequence', '按批次优先,再按顺序')], default='batch_then_sequence', max_length=64, verbose_name='同层排序策略')),
+                ('enabled', models.BooleanField(default=True, verbose_name='是否启用')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+            ],
+            options={
+                'verbose_name': '任务下发配置',
+                'verbose_name_plural': '任务下发配置',
+                'db_table': 'dispatch_config',
+                'ordering': ['-id'],
+            },
+        ),
+        migrations.AlterField(
+            model_name='containerdetaillogmodel',
+            name='new_status',
+            field=models.IntegerField(blank=True, choices=[(0, '空盘'), (2, '在盘'), (3, '离库')], null=True, verbose_name='新状态'),
+        ),
+        migrations.AlterField(
+            model_name='containerdetaillogmodel',
+            name='old_status',
+            field=models.IntegerField(blank=True, choices=[(0, '空盘'), (2, '在盘'), (3, '离库')], null=True, verbose_name='原状态'),
+        ),
+        migrations.AlterField(
+            model_name='containerdetailmodel',
+            name='status',
+            field=models.IntegerField(choices=[(0, '空盘'), (2, '在盘'), (3, '离库')], default=0, verbose_name='状态'),
+        ),
+    ]

+ 51 - 0
container/migrations/0036_wcstasklogmodel.py

@@ -0,0 +1,51 @@
+# Generated manually for WCSTaskLogModel
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0035_dispatchconfig_and_more'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='WCSTaskLogModel',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('task_number', models.BigIntegerField(verbose_name='任务号')),
+                ('container', models.CharField(max_length=50, verbose_name='托盘号')),
+                ('current_location', models.CharField(max_length=100, verbose_name='起始位置')),
+                ('target_location', models.CharField(max_length=100, verbose_name='目标位置')),
+                ('location_group_id', models.IntegerField(blank=True, null=True, verbose_name='库位组ID')),
+                ('access_priority', models.IntegerField(blank=True, null=True, verbose_name='靠里程度优先级')),
+                ('left_priority', models.IntegerField(blank=True, null=True, verbose_name='左侧优先级')),
+                ('right_priority', models.IntegerField(blank=True, null=True, verbose_name='右侧优先级')),
+                ('task_type', models.CharField(blank=True, max_length=50, null=True, verbose_name='任务类型')),
+                ('order_number', models.IntegerField(blank=True, null=True, verbose_name='订单号')),
+                ('sequence', models.IntegerField(blank=True, null=True, verbose_name='序列号')),
+                ('send_time', models.DateTimeField(auto_now_add=True, verbose_name='发送时间')),
+                ('response_data', models.JSONField(blank=True, null=True, verbose_name='WCS返回结果')),
+            ],
+            options={
+                'verbose_name': 'WCS任务发送日志',
+                'verbose_name_plural': 'WCS任务发送日志',
+                'db_table': 'wcs_task_log',
+                'ordering': ['-send_time'],
+            },
+        ),
+        migrations.AddIndex(
+            model_name='wcstasklogmodel',
+            index=models.Index(fields=['task_number'], name='wcs_task_l_task_nu_idx'),
+        ),
+        migrations.AddIndex(
+            model_name='wcstasklogmodel',
+            index=models.Index(fields=['container'], name='wcs_task_l_contain_idx'),
+        ),
+        migrations.AddIndex(
+            model_name='wcstasklogmodel',
+            index=models.Index(fields=['send_time'], name='wcs_task_l_send_ti_idx'),
+        ),
+    ]
+

+ 28 - 0
container/migrations/0037_rename_wcs_task_l_task_nu_idx_wcs_task_lo_task_nu_968290_idx_and_more.py

@@ -0,0 +1,28 @@
+# Generated by Django 4.1.2 on 2025-11-17 10:22
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0036_wcstasklogmodel'),
+    ]
+
+    operations = [
+        migrations.RenameIndex(
+            model_name='wcstasklogmodel',
+            new_name='wcs_task_lo_task_nu_968290_idx',
+            old_name='wcs_task_l_task_nu_idx',
+        ),
+        migrations.RenameIndex(
+            model_name='wcstasklogmodel',
+            new_name='wcs_task_lo_contain_22390a_idx',
+            old_name='wcs_task_l_contain_idx',
+        ),
+        migrations.RenameIndex(
+            model_name='wcstasklogmodel',
+            new_name='wcs_task_lo_send_ti_fc233e_idx',
+            old_name='wcs_task_l_send_ti_idx',
+        ),
+    ]

+ 28 - 0
container/migrations/0038_wcstasklogmodel_floor_wcstasklogmodel_is_completed_and_more.py

@@ -0,0 +1,28 @@
+# Generated by Django 4.1.2 on 2025-11-17 16:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('container', '0037_rename_wcs_task_l_task_nu_idx_wcs_task_lo_task_nu_968290_idx_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='wcstasklogmodel',
+            name='floor',
+            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='楼层'),
+        ),
+        migrations.AddField(
+            model_name='wcstasklogmodel',
+            name='is_completed',
+            field=models.BooleanField(default=False, verbose_name='是否完成'),
+        ),
+        migrations.AddField(
+            model_name='wcstasklogmodel',
+            name='log_type',
+            field=models.CharField(default='outbound', max_length=32, verbose_name='日志类型'),
+        ),
+    ]

+ 71 - 1
container/models.py

@@ -808,4 +808,74 @@ class out_batch_detail(models.Model):
         db_table = 'out_batch_detail'
         verbose_name = 'OutBatchDetail'
         verbose_name_plural = "OutBatchDetail"
-        ordering = ['container']
+        ordering = ['container']
+
+class DispatchConfig(models.Model):
+    """
+    任务下发调度配置
+    - cross_floor_concurrent_limit: 跨楼层并发下发上限(不同楼层同时下发的任务数)
+    - intra_floor_order: 同层排序策略(当前仅支持:batch_then_sequence)
+    - enabled: 是否启用
+    """
+    INTRA_FLOOR_ORDER_CHOICES = (
+        ('batch_then_sequence', '按批次优先,再按顺序'),
+    )
+    cross_floor_concurrent_limit = models.PositiveIntegerField(
+        default=2,
+        verbose_name='跨楼层并发上限'
+    )
+    intra_floor_order = models.CharField(
+        max_length=64,
+        choices=INTRA_FLOOR_ORDER_CHOICES,
+        default='batch_then_sequence',
+        verbose_name='同层排序策略'
+    )
+    enabled = models.BooleanField(default=True, verbose_name='是否启用')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
+
+    class Meta:
+        db_table = 'dispatch_config'
+        verbose_name = '任务下发配置'
+        verbose_name_plural = "任务下发配置"
+        ordering = ['-id']
+
+    @classmethod
+    def get_active_config(cls):
+        config = cls.objects.filter(enabled=True).first()
+        if not config:
+            config = cls.objects.create()
+        return config
+
+
+class WCSTaskLogModel(models.Model):
+    """
+    WCS 任务发送日志记录
+    每当系统发送任务到 WCS 时异步记录(不阻塞发送)
+    """
+    task_number = models.BigIntegerField(verbose_name='任务号')
+    container = models.CharField(max_length=50, verbose_name='托盘号')
+    current_location = models.CharField(max_length=100, verbose_name='起始位置')
+    target_location = models.CharField(max_length=100, verbose_name='目标位置')
+    location_group_id = models.IntegerField(null=True, blank=True, verbose_name='库位组ID')
+    access_priority = models.IntegerField(null=True, blank=True, verbose_name='靠里程度优先级')
+    left_priority = models.IntegerField(null=True, blank=True, verbose_name='左侧优先级')
+    right_priority = models.IntegerField(null=True, blank=True, verbose_name='右侧优先级')
+    task_type = models.CharField(max_length=50, null=True, blank=True, verbose_name='任务类型')
+    order_number = models.IntegerField(null=True, blank=True, verbose_name='订单号')
+    sequence = models.IntegerField(null=True, blank=True, verbose_name='序列号')
+    send_time = models.DateTimeField(auto_now_add=True, verbose_name='发送时间')
+    response_data = models.JSONField(null=True, blank=True, verbose_name='WCS返回结果')
+    floor = models.CharField(max_length=20, null=True, blank=True, verbose_name='楼层')
+    is_completed = models.BooleanField(default=False, verbose_name='是否完成')
+    log_type = models.CharField(max_length=32, default='outbound', verbose_name='日志类型')
+    
+    class Meta:
+        db_table = 'wcs_task_log'
+        verbose_name = 'WCS任务发送日志'
+        verbose_name_plural = "WCS任务发送日志"
+        ordering = ['-send_time']
+        indexes = [
+            models.Index(fields=['task_number']),
+            models.Index(fields=['container']),
+            models.Index(fields=['send_time']),
+        ]

+ 22 - 1
container/serializers.py

@@ -1,6 +1,6 @@
 from rest_framework import serializers
 
-from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel,ContainerWCSModel,out_batch_detail,ContainerDetailLogModel,batchLogModel
+from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel,ContainerWCSModel,out_batch_detail,ContainerDetailLogModel,batchLogModel,DispatchConfig,WCSTaskLogModel
 from bound.models import BoundBatchModel,BoundDetailModel
 
 from utils import datasolve
@@ -367,3 +367,24 @@ class OutBoundFullDetailSerializer(serializers.ModelSerializer):
         model = out_batch_detail
         fields = '__all__'
         read_only_fields = ['id', 'out_bound', 'container', 'container_detail', 'working']
+
+class DispatchConfigSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = DispatchConfig
+        fields = ['id', 'cross_floor_concurrent_limit', 'intra_floor_order', 'enabled', 'update_time']
+        read_only_fields = ['id', 'update_time']
+
+
+class WCSTaskLogSerializer(serializers.ModelSerializer):
+    time = serializers.DateTimeField(source='send_time', format='%Y-%m-%d %H:%M:%S', read_only=True)
+    taskNumber = serializers.IntegerField(source='task_number', read_only=True)
+    
+    class Meta:
+        model = WCSTaskLogModel
+        fields = [
+            'id', 'time', 'taskNumber', 'container', 'current_location', 
+            'target_location', 'location_group_id', 'access_priority', 
+            'left_priority', 'right_priority', 'task_type', 'order_number', 
+            'sequence', 'response_data', 'send_time', 'floor', 'is_completed', 'log_type'
+        ]
+        read_only_fields = ['id', 'send_time']

+ 5 - 0
container/urls.py

@@ -38,6 +38,11 @@ re_path(r'^operate/(?P<pk>\d+)/$', views.ContainerOperateViewSet.as_view({
 
 path(r'wcs_task/', views.WCSTaskViewSet.as_view({"get": "list", "post": "create"}), name="Task"),
 path(r'send_again/', views.WCSTaskViewSet.as_view({"post": "send_task_to_wcs"}), name="Task"),
+path(r'dispatch_config/', views.DispatchConfigView.as_view(), name='dispatch_config'),
+path(r'wcs/logs/', views.WCSTaskLogViewSet.as_view({"get": "list"}), name='wcs_task_logs'),
+re_path(r'^wcs/logs/(?P<pk>\d+)/$', views.WCSTaskLogViewSet.as_view({
+    'get': 'retrieve',
+}), name="wcs_task_logs_1"),
 
 path(r'task/', views.TaskViewSet.as_view({"get": "list", "post": "create"}), name="Task"),
 re_path(r'^task/(?P<pk>\d+)/$', views.TaskViewSet.as_view({

+ 333 - 45
container/views.py

@@ -14,21 +14,26 @@ import requests
 from django.db import transaction
 import logging
 from rest_framework import status
-from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,ContainerWCSModel,TaskModel,out_batch_detail,ContainerDetailLogModel,batchLogModel
+from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,ContainerWCSModel,TaskModel,out_batch_detail,ContainerDetailLogModel,batchLogModel,WCSTaskLogModel
 from bound.models import BoundDetailModel,BoundListModel,OutBoundDetailModel
 from bin.views import LocationAllocation,base_location
 from bin.models import LocationModel,LocationContainerLink,LocationGroupModel
 from bound.models import BoundBatchModel,OutBatchModel,BatchOperateLogModel
+from django.conf import settings
+from rest_framework.views import APIView
+from rest_framework.response import Response as DRFResponse
+import os
+import re
 
 from .serializers import ContainerDetailGetSerializer,ContainerDetailPostSerializer,ContainerDetailSimpleGetSerializer,ContainerDetailPutSerializer
 from .serializers import ContainerListGetSerializer,ContainerListPostSerializer
 from .serializers import ContainerOperationGetSerializer,ContainerOperationPostSerializer
 from .serializers import TaskGetSerializer,TaskPostSerializer
-from .serializers import WCSTaskGetSerializer
+from .serializers import WCSTaskGetSerializer,WCSTaskLogSerializer
 from .serializers import OutBoundFullDetailSerializer,OutBoundDetailSerializer
 from .serializers import ContainerDetailLogSerializer
 from .serializers import batchLogModelSerializer
-from .filter import ContainerDetailFilter,ContainerListFilter,ContainerOperationFilter,TaskFilter,WCSTaskFilter,ContainerDetailLogFilter,batchLogFilter
+from .filter import ContainerDetailFilter,ContainerListFilter,ContainerOperationFilter,TaskFilter,WCSTaskFilter,ContainerDetailLogFilter,batchLogFilter,WCSTaskLogFilter
 
 from rest_framework.permissions import AllowAny
 import threading
@@ -40,6 +45,8 @@ from staff.models import ListModel as StaffListModel
 from operation_log.views import log_success_operation, log_failure_operation, log_operation
 logger = logging.getLogger(__name__)
 loggertask = logging.getLogger('wms.WCSTask')
+from .models import DispatchConfig
+from .serializers import DispatchConfigSerializer
 
 
 # 托盘分类视图
@@ -1315,14 +1322,30 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             if not success:
                 return Response({'code': '500', 'message': '出库状态更新失败', 'data': None},
                                 status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-            OutboundService.process_next_task()
+            # WCS完成一条任务后,只下发一条新任务,优先同楼层
+            # 从刚完成的任务中提取楼层信息
+            preferred_layer = None
+            try:
+                parts = str(task.current_location).split('-')
+                preferred_layer = parts[3] if len(parts) >= 4 else None
+            except Exception:
+                pass
+            OutboundService.process_next_task(single_task=True, preferred_layer=preferred_layer)
 
         if task and task.tasktype == 'check' and task.status == 300:
             success = self.handle_outbound_completion(container_obj, task)
             if not success:
                 return Response({'code': '500', 'message': '出库状态更新失败', 'data': None},
                                 status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-            OutboundService.process_next_task()
+            # WCS完成一条任务后,只下发一条新任务,优先同楼层
+            # 从刚完成的任务中提取楼层信息
+            preferred_layer = None
+            try:
+                parts = str(task.current_location).split('-')
+                preferred_layer = parts[3] if len(parts) >= 4 else None
+            except Exception:
+                pass
+            OutboundService.process_next_task(single_task=True, preferred_layer=preferred_layer)
         
         return Response({
             'code': '200',
@@ -1348,6 +1371,14 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             task.message = '任务已完成'
             task.working = 0
             task.save()
+            try:
+                original_task_number = data.get('taskNumber')
+                if original_task_number is not None:
+                    WCSTaskLogModel.objects.filter(
+                        task_number=original_task_number
+                    ).update(is_completed=True)
+            except Exception as log_error:
+                logger.warning(f"更新任务日志完成状态失败: {log_error}")
 
         return task
 
@@ -2140,6 +2171,24 @@ class OutboundService:
         }
         loggertask.info(f"任务号:{task.tasknumber-20000000000}任务发送请求:{task.container},起始位置:{task.current_location},目标位置:{task.target_location},返回结果:{task_data}")
         
+        # 异步记录日志到数据库(不阻塞发送)
+        log_thread = threading.Thread(
+            target=OutboundService._async_log_handler,
+            kwargs={
+                'task_number': task.tasknumber - 20000000000,
+                'container': str(task.container),
+                'current_location': task.current_location,
+                'target_location': task.target_location,
+                'task_type': task.tasktype,
+                'order_number': task.order_number,
+                'sequence': task.sequence,
+                'response_data': task_data,
+                'log_type': task.tasktype or 'outbound',
+            },
+            daemon=True
+        )
+        log_thread.start()
+        
         # 创建并启动线程
         thread = threading.Thread(
             target=OutboundService._async_send_handler,
@@ -2149,6 +2198,57 @@ class OutboundService:
         thread.start()
         return True  # 立即返回表示已开始处理
 
+    @staticmethod
+    def _async_log_handler(task_number, container, current_location, target_location, task_type, order_number, sequence, response_data, log_type='outbound'):
+        """异步记录 WCS 任务发送日志到数据库(不阻塞发送)"""
+        try:
+            close_old_connections()
+            
+            # 解析库位组与优先级
+            group_id = None
+            access_priority = None
+            left_priority = None
+            right_priority = None
+            floor = None
+            try:
+                parts = current_location.split('-')
+                if len(parts) >= 4:
+                    row = int(parts[1])
+                    col = int(parts[2])
+                    layer = int(parts[3])
+                    floor = parts[3]
+                    loc = LocationModel.objects.filter(row=row, col=col, layer=layer).first()
+                    if loc:
+                        group_code = loc.location_group
+                        group = LocationGroupModel.objects.filter(group_code=group_code).first()
+                        if group:
+                            group_id = group.id
+                            access_priority = loc.c_number
+                            left_priority = group.left_priority
+                            right_priority = group.right_priority
+            except Exception as e:
+                logger.error(f"解析库位组信息失败: {e}")
+            
+            # 创建日志记录
+            WCSTaskLogModel.objects.create(
+                task_number=task_number,
+                container=container,
+                current_location=current_location,
+                target_location=target_location,
+                location_group_id=group_id,
+                access_priority=access_priority,
+                left_priority=left_priority,
+                right_priority=right_priority,
+                task_type=task_type,
+                order_number=order_number,
+                sequence=sequence,
+                response_data=response_data,
+                floor=floor,
+                log_type=log_type or task_type or 'outbound',
+            )
+        except Exception as e:
+            logger.error(f"记录 WCS 任务日志失败: {e}", exc_info=True)
+
     @staticmethod
     def _async_send_handler(task_id, send_data):
         """异步处理的实际工作函数"""
@@ -2160,12 +2260,12 @@ class OutboundService:
             task = ContainerWCSModel.objects.get(pk=task_id)
             
             # 发送第一个请求(不处理结果)
-            requests.post(
-                "http://127.0.0.1:8008/container/batch/",
-                json=send_data,
-                timeout=10
-            )
-            
+            # requests.post(
+                # "http://127.0.0.1:8008/container/batch/",
+                # json=send_data,
+                # timeout=10
+            # )
+            # 
             # 发送关键请求
             response = requests.post(
                 "http://192.168.18.200:1616/wcs/WebApi/getOutTask",
@@ -2190,25 +2290,59 @@ class OutboundService:
 
     @staticmethod
     def create_initial_tasks(container_list,bound_list_id):
-        """生成初始任务队列"""
+        """生成初始任务队列,返回楼层信息用于初始化发送"""
         with transaction.atomic():
-            current_WCS = ContainerWCSModel.objects.filter(tasktype='outbound',bound_list_id = bound_list_id).first()
+            current_WCS = ContainerWCSModel.objects.filter(
+                tasktype='outbound',
+                bound_list_id=bound_list_id,
+                is_delete=False,
+                status__lt=300
+            ).first()
             if current_WCS:
                 logger.error(f"当前{bound_list_id}已有出库任务")
-                return False
+                return {
+                    "success": False,
+                    "msg": f"出库申请 {bound_list_id} 仍有待完成任务",
+                }
             tasks = []
+            task_layers = set()
             start_sequence = ContainerWCSModel.objects.filter(tasktype='outbound').count() + 1
             tasknumber = ContainerWCSModel.objects.filter().count() 
             tasknumber_index = 1
             for index, container in enumerate(container_list, start=start_sequence):
                 container_obj = ContainerListModel.objects.filter(id =container['container_number']).first()
+                if not container_obj:
+                    logger.error(f"托盘记录 {container['container_number']} 不存在")
+                    return {
+                        "success": False,
+                        "msg": "托盘信息缺失,无法创建任务",
+                    }
                 if container_obj.current_location != container_obj.target_location:
                     logger.error(f"托盘 {container_obj.container_code} 未到达目的地,不生成任务")
-                    return False
+                    return {
+                        "success": False,
+                        "msg": f"托盘 {container_obj.container_code} 未处于可出库状态",
+                    }
+                # 检查前序作业
+                existing_task = ContainerWCSModel.objects.filter(
+                    container=container_obj.container_code,
+                    working=1,
+                    status__lt=300,
+                    is_delete=False
+                ).exists()
+                if existing_task:
+                    logger.error(f"托盘 {container_obj.container_code} 仍有未完成任务")
+                    return {
+                        "success": False,
+                        "msg": f"托盘 {container_obj.container_code} 仍有未完成任务",
+                    }
                 OutBoundDetail_obj = OutBoundDetailModel.objects.filter(bound_list=bound_list_id,bound_batch_number_id=container['batch_id']).first()
                 if not OutBoundDetail_obj:
                     logger.error(f"批次 {container['batch_id']} 不存在")
-                    return False
+                    return {
+                        "success": False,
+                        "msg": f"批次 {container['batch_id']} 不存在",
+                    }
                 month = int(timezone.now().strftime("%Y%m"))
                 task = ContainerWCSModel(
                     taskid=OutboundService.generate_task_id(),
@@ -2227,6 +2361,15 @@ class OutboundService:
                     message="等待出库",
                     status=100,
                 )
+                layer = None
+                try:
+                    parts = str(task.current_location).split('-')
+                    if len(parts) >= 4:
+                        layer = parts[3]
+                except Exception:
+                    layer = None
+                if layer:
+                    task_layers.add(layer)
                 tasknumber_index += 1
                 tasks.append(task)
                 container_obj = ContainerListModel.objects.filter(container_code=task.container).first()
@@ -2234,6 +2377,11 @@ class OutboundService:
                 container_obj.save()
             ContainerWCSModel.objects.bulk_create(tasks)
             logger.info(f"已创建 {len(tasks)} 个初始任务")
+            return {
+                "success": True,
+                "layers": sorted(task_layers),
+                "task_count": len(tasks),
+            }
     
     @staticmethod
     def create_initial_check_tasks(container_list,batch_id):
@@ -2319,11 +2467,19 @@ class OutboundService:
             logger.info(f"已插入 {len(new_tasks)} 个新任务")
 
     @staticmethod
-    def process_next_task():
-        """处理下一个任务 - 优化:同一批次连续出,支持一次下发两条任务"""
+    def process_next_task(single_task=False, preferred_layer=None, initial_layers=None):
+        """处理下一个任务 - 支持前端可配置的跨楼层并发与同层排序
+        
+        Args:
+            single_task: 如果为True,只下发一条任务(用于WCS完成回调场景)
+                        如果为False,批量下发多条任务(用于初始下发场景)
+            preferred_layer: 优先选择的楼层(用于single_task=True时,优先同楼层任务)
+        """
         # 获取待处理任务,优先按批次排序(同一批次连续出),同一批次内按sequence排序
         # 使用Case处理batch_out为None的情况,确保有批次的任务优先
         from django.db.models import F, Case, When, IntegerField
+        from django.conf import settings
+        from .models import DispatchConfig
         
         def get_pending_tasks():
             """获取待处理任务查询集"""
@@ -2340,28 +2496,97 @@ class OutboundService:
                 )
             ).order_by('batch_out_id_for_sort', 'sequence')
         
+        def extract_layer(location):
+            """从位置字符串中提取楼层信息"""
+            try:
+                parts = str(location).split('-')
+                return parts[3] if len(parts) >= 4 else None
+            except Exception:
+                return None
+        
         pending_tasks = get_pending_tasks()
         
         if not pending_tasks.exists():
             logger.info("没有待处理任务")
             return
         
-        # 处理任务列表(最多处理2条,保证WCS有缓冲)
+        # 读取调度配置(默认2条跨楼层并发)
+        cfg = DispatchConfig.get_active_config()
+        cross_floor_limit = max(1, int(cfg.cross_floor_concurrent_limit or 2))
+        desired_layers = set(initial_layers or [])
+        # 处理任务列表
+        # 如果single_task=True,只下发1条;否则批量下发(最多cross_floor_limit条)
         processed_count = 0
-        max_tasks = 2
+        if desired_layers:
+            max_tasks = max(len(desired_layers), 1)
+        else:
+            max_tasks = 1 if single_task else cross_floor_limit
         skip_count = 0
-        max_skip = 5  # 最多跳过5个任务,避免无限循环
+        max_skip = max(20, len(desired_layers) * 5)
         dispatched_ids = set()
+        used_layers = set()
         
         while processed_count < max_tasks and skip_count < max_skip:
             # 重新获取待处理任务(因为可能有任务被跳过)
             pending_tasks = get_pending_tasks().exclude(pk__in=dispatched_ids)
             if not pending_tasks.exists():
                 break
+            if desired_layers and used_layers.issuperset(desired_layers):
+                logger.info("已完成初始多楼层任务的分发")
+                break
+            
+            # 如果single_task=True且提供了preferred_layer,优先选择同楼层的任务
+            next_task = None
+            if desired_layers:
+                remaining_layers = desired_layers - used_layers
+                target_layers = remaining_layers if remaining_layers else desired_layers
+                prioritized = []
+                fallback = []
+                for task in pending_tasks:
+                    task_layer = extract_layer(task.current_location)
+                    if task_layer in target_layers:
+                        prioritized.append(task)
+                    else:
+                        fallback.append(task)
+                if prioritized:
+                    next_task = prioritized[0]
+                elif remaining_layers:
+                    skip_count += 1
+                    continue
+                elif fallback:
+                    next_task = fallback[0]
+            elif single_task and preferred_layer:
+                # 先尝试找同楼层的任务(在同楼层任务中,仍然按批次和sequence排序)
+                same_layer_tasks = []
+                other_layer_tasks = []
+                for task in pending_tasks:
+                    task_layer = extract_layer(task.current_location)
+                    if task_layer == preferred_layer:
+                        same_layer_tasks.append(task)
+                    else:
+                        other_layer_tasks.append(task)
+                
+                # 优先从同楼层任务中选择
+                if same_layer_tasks:
+                    next_task = same_layer_tasks[0]
+                    logger.info(f"优先选择同楼层任务,楼层: {preferred_layer}, 任务: {next_task.taskid}")
+                # 如果没找到同楼层的任务,使用第一个任务(按批次和sequence排序)
+                elif other_layer_tasks:
+                    next_task = other_layer_tasks[0]
+                    logger.info(f"未找到同楼层任务,使用其他楼层任务,任务: {next_task.taskid}")
+                else:
+                    next_task = pending_tasks.first()
+            else:
+                # 根据同层排序策略,仍旧使用 batch_then_sequence(已在order_by体现)
+                next_task = pending_tasks.first()
             
-            next_task = pending_tasks.first()
+            if not next_task:
+                break
+                
             dispatched_ids.add(next_task.pk)
             location = next_task.current_location
+            # 解析楼层(假设格式 Wxx-row-col-layer)
+            task_layer = extract_layer(location)
             
             if location == '103' or location == '203':
                 logger.info(f"需要跳过该任务: {next_task.taskid}, 位置: {location}")
@@ -2372,9 +2597,14 @@ class OutboundService:
                 # 跳过这个任务后,继续处理下一个
                 continue
             
+            # 跨楼层并发控制:同一轮不允许重复楼层(仅批量下发时生效)
+            if not single_task and not desired_layers and task_layer and task_layer in used_layers:
+                skip_count += 1
+                continue
+            
             try:
                 allocator = LocationAllocation()
-                allocation_success = OutboundService.perform_initial_allocation(
+                allocation_success = perform_initial_allocation(
                     allocator,
                     next_task.current_location
                 )
@@ -2387,6 +2617,8 @@ class OutboundService:
                 next_task.status = 150
                 next_task.save(update_fields=['status'])
                 processed_count += 1
+                if task_layer:
+                    used_layers.add(task_layer)
                 logger.info(f"成功下发任务: {next_task.taskid}, 批次: {next_task.batch_out_id if next_task.batch_out else '无批次'}")
             except Exception as e:
                 logger.error(f"任务处理失败: {next_task.taskid}, 错误: {str(e)}")
@@ -2404,31 +2636,77 @@ class OutboundService:
         try:
             task = ContainerWCSModel.objects.get(taskid=task_id)
             allocator = LocationAllocation()
-            OutboundService.perform_initial_allocation(allocator, task.current_location)
+            perform_initial_allocation(allocator, task.current_location)
             OutboundService.send_task_to_wcs(task)
         except Exception as e:
             logger.error(f"任务处理失败: {str(e)}")
       
 
 
-    def perform_initial_allocation(allocator, location):
-        """执行初始库位分配操作"""
-        location_row = location.split('-')[1]
-        location_col = location.split('-')[2]
-        location_layer = location.split('-')[3]
-        location_code = LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first().location_code
-        if not location_code:
-            logger.error(f"未找到库位: {location}")
-        operations = [
-            (allocator.update_location_status,location_code, 'reserved'),
-            (allocator.update_location_group_status,location_code)
-        ]
-        
-        for func, *args in operations:
-            if not func(*args):
-                logger.error(f"分配操作失败: {func.__name__}")
-                return False
-        return True
+class DispatchConfigView(APIView):
+    """
+    获取/更新任务下发调度配置
+    GET: 返回当前启用的配置
+    PUT: 更新配置(cross_floor_concurrent_limit, intra_floor_order, enabled)
+    """
+
+    def get(self, request):
+        cfg = DispatchConfig.get_active_config()
+        return DRFResponse(DispatchConfigSerializer(cfg).data, status=200)
+
+    def put(self, request):
+        cfg = DispatchConfig.get_active_config()
+        serializer = DispatchConfigSerializer(cfg, data=request.data, partial=True)
+        serializer.is_valid(raise_exception=True)
+        serializer.save()
+        return DRFResponse(serializer.data, status=200)
+
+
+class WCSTaskLogViewSet(viewsets.ModelViewSet):
+    """
+        retrieve:
+            Response a data list(get)
+        list:
+            Response a data list(all)
+    """
+    pagination_class = MyPageNumberPagination   
+    filter_backends = [DjangoFilterBackend, OrderingFilter, ]
+    ordering_fields = ['-id', "-send_time", "send_time", ]
+    filter_class = WCSTaskLogFilter
+
+    def get_queryset(self):
+        if self.request.user:
+            return WCSTaskLogModel.objects.all()
+        else:
+            return WCSTaskLogModel.objects.none()
+
+    def get_serializer_class(self):
+        if self.action in ['list', 'retrieve']:
+            return WCSTaskLogSerializer
+        else:
+            return self.http_method_not_allowed(request=self.request)
+
+
+def perform_initial_allocation(allocator, location):
+    """执行初始库位分配操作"""
+    location_row = location.split('-')[1]
+    location_col = location.split('-')[2]
+    location_layer = location.split('-')[3]
+    location_obj = LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
+    if not location_obj:
+        logger.error(f"未找到库位: {location}")
+        return False
+    location_code = location_obj.location_code
+    operations = [
+        (allocator.update_location_status, location_code, 'reserved'),
+        (allocator.update_location_group_status, location_code)
+    ]
+    
+    for func, *args in operations:
+        if not func(*args):
+            logger.error(f"分配操作失败: {func.__name__}")
+            return False
+    return True
 
 
 # 出库任务下发
@@ -2555,10 +2833,20 @@ class OutTaskViewSet(ViewSet):
             container_list = generate_result['data']
 
             #  2. 生成初始任务
-            OutboundService.create_initial_tasks(container_list,bound_list_id)
+            creation_result = OutboundService.create_initial_tasks(container_list,bound_list_id)
+            if not creation_result.get("success"):
+                return Response(
+                    {"code": 400, "msg": creation_result.get("msg", "创建任务失败")},
+                    status=400
+                )
             
-            # 3. 立即发送第一个任务
-            OutboundService.process_next_task()
+            # 3. 根据楼层信息初始化下发
+            initial_layers = creation_result.get("layers", [])
+            if creation_result.get("task_count", 0) > 0:
+                if len(initial_layers) > 1:
+                    OutboundService.process_next_task(initial_layers=initial_layers)
+                else:
+                    OutboundService.process_next_task()
             
             # 记录成功日志
             try:

+ 89 - 0
templates/src/pages/task/dispatchconfig.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="q-pa-md">
+    <q-card flat bordered>
+      <q-card-section>
+        <div class="text-h6">任务下发策略配置</div>
+      </q-card-section>
+      <q-separator />
+      <q-card-section>
+        <div class="row q-col-gutter-md">
+          <q-input
+            class="col-12 col-sm-4"
+            type="number"
+            v-model.number="form.cross_floor_concurrent_limit"
+            label="跨楼层并发上限(默认2)"
+            dense
+            outlined
+            :min="1"
+          />
+          <q-select
+            class="col-12 col-sm-4"
+            v-model="form.intra_floor_order"
+            :options="orderOptions"
+            label="同层排序策略"
+            dense
+            outlined
+          />
+          <q-toggle
+            class="col-12 col-sm-4"
+            v-model="form.enabled"
+            label="启用配置"
+          />
+        </div>
+      </q-card-section>
+      <q-card-actions align="right">
+        <q-btn color="primary" label="保存" :loading="saving" @click="save" />
+        <q-btn flat label="刷新" class="q-ml-sm" @click="load" />
+      </q-card-actions>
+    </q-card>
+
+    <div class="q-mt-md text-grey">说明:不同楼层之间可同时下发的任务数由“跨楼层并发上限”控制;同楼层内按“同层排序策略”(默认按批次优先,再按顺序)。</div>
+  </div>
+</template>
+
+<script>
+import { getauth, putauth } from 'boot/axios_request'
+
+export default {
+  name: 'DispatchConfig',
+  data () {
+    return {
+      form: {
+        cross_floor_concurrent_limit: 2,
+        intra_floor_order: 'batch_then_sequence',
+        enabled: true
+      },
+      orderOptions: [
+        { label: '按批次优先,再按顺序', value: 'batch_then_sequence' }
+      ],
+      saving: false
+    }
+  },
+  methods: {
+    async load () {
+      const res = await getauth('/container/dispatch_config/')
+      // 映射为当前表单
+      this.form.cross_floor_concurrent_limit = res.cross_floor_concurrent_limit
+      this.form.intra_floor_order = res.intra_floor_order
+      this.form.enabled = res.enabled
+    },
+    async save () {
+      this.saving = true
+      try {
+        await putauth('/container/dispatch_config/', this.form)
+        this.$q.notify({ type: 'positive', message: '保存成功' })
+      } finally {
+        this.saving = false
+      }
+    }
+  },
+  mounted () {
+    this.load()
+  }
+}
+</script>
+
+<style scoped>
+</style>
+
+

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

@@ -113,6 +113,19 @@
               </template>
             </q-input>
           </div>
+          <div v-if="activeTasks.length" class="full-width q-mt-md">
+            <q-banner
+              rounded
+              dense
+              class="bg-blue-1 text-primary q-pa-sm"
+            >
+              <template v-slot:avatar>
+                <q-icon name="info" color="primary" />
+              </template>
+              当前正在进行的任务({{ activeTasks.length }}):
+              {{ activeTaskSummary }}
+            </q-banner>
+          </div>
         </template>
         <template v-slot:body="props">
           <q-tr :props="props">
@@ -433,8 +446,41 @@ export default {
         this.$t("download_center.end")
       );
     },
+    activeTasks() {
+      const activeStatuses = [101, 150, 200];
+      if (!Array.isArray(this.table_list)) return [];
+      return this.table_list.filter((item) =>
+        activeStatuses.includes(Number(item.status))
+      );
+    },
+    activeTaskSummary() {
+      if (!this.activeTasks.length) {
+        return "";
+      }
+      return this.activeTasks
+        .map((item) => {
+          const location = this.resolveTaskLocation(item);
+          const floor = this.extractLayer(location) || "-";
+          return `${item.container || "-"}(${floor})`;
+        })
+        .join(",");
+    },
   },
   methods: {
+    resolveTaskLocation(task) {
+      if (!task) return "";
+      const taskType = String(task.tasktype || "").toLowerCase();
+      const useTargetLocation = ["inbound", "move"].includes(taskType);
+      return useTargetLocation ? task.target_location : task.current_location;
+    },
+    extractLayer(location) {
+      try {
+        const parts = String(location || "").split("-");
+        return parts[3] || "";
+      } catch (error) {
+        return "";
+      }
+    },
     // 检查用户是否有指定页面的组件访问权限
     loadUserPermissions() {
       postauth("staff/role-comPermissions/" + this.login_mode + "/", {

+ 4 - 1
templates/src/pages/task/taskpage.vue

@@ -28,8 +28,11 @@
           <q-route-tab name="more" :label="$t('inbound.more')" icon="img:statics/inbound/more.png" :to="{ name: 'more' }" exact/>
         </transition>
         <transition appear enter-active-class="animated zoomIn">
-          <q-route-tab name="asnfinish" :label="$t('inbound.asnfinish')" icon="img:statics/inbound/asnfinish.png" :to="{ name: 'asnfinish' }" exact/>
         </transition> -->
+        <q-route-tab name="task " :label="'任务列表'" icon="img:statics/inbound/more.png" :to="{ name: 'task' }" exact/>
+
+        <q-route-tab name="wcslog" :label="'发送日志'" icon="img:statics/inbound/presortstock.png" :to="{ name: 'wcslog' }" exact/>
+        <q-route-tab name="dispatchconfig" :label="'下发配置'" icon="img:statics/inbound/asnfinish.png" :to="{ name: 'dispatchconfig' }" exact/>
       </q-tabs>
     </div>
   </div>

+ 569 - 0
templates/src/pages/task/wcslog.vue

@@ -0,0 +1,569 @@
+<template>
+  <div>
+    <transition appear enter-active-class="animated fadeIn">
+      <q-table
+        class="my-sticky-header-column-table shadow-24"
+        :data="table_list"
+        row-key="taskNumber"
+        :separator="separator"
+        :loading="loading"
+        :columns="columns"
+        hide-bottom
+        :pagination.sync="pagination"
+        no-data-label="No data"
+        no-results-label="No data you want"
+        :table-style="{ height: height }"
+        flat
+        bordered
+      >
+        <template v-slot:header-cell="props">
+          <q-th :props="props" @dblclick="handleHeaderDblClick(props.col)">
+            {{ props.col.label }}
+          </q-th>
+        </template>
+        <template v-slot:top>
+          <q-btn-group push>
+            <q-btn :label="$t('refresh')" icon="refresh" @click="reFresh()">
+              <q-tooltip
+                content-class="bg-amber text-black shadow-4"
+                :offset="[10, 10]"
+                content-style="font-size: 12px"
+                >{{ $t("refreshtip") }}</q-tooltip
+              >
+            </q-btn>
+          </q-btn-group>
+
+          <q-space />
+
+          <div class="flex items-center">
+            <div class="q-mr-md">{{ $t("download_center.createTime") }}</div>
+            <q-input
+              readonly
+              outlined
+              dense
+              v-model="createDate2"
+              :placeholder="interval"
+            >
+              <template v-slot:append>
+                <q-icon name="event" class="cursor-pointer">
+                  <q-popup-proxy
+                    ref="qDateProxy"
+                    transition-show="scale"
+                    transition-hide="scale"
+                  >
+                    <q-date v-model="createDate1" range>
+                      <div class="row items-center justify-end q-gutter-sm">
+                        <q-btn
+                          :label="$t('index.cancel')"
+                          color="primary"
+                          flat
+                          v-close-popup
+                        />
+                        <q-btn
+                          :label="$t('index.clear')"
+                          color="primary"
+                          @click="
+                            createDate2 = '';
+                            createDate1 = '';
+                          "
+                          v-close-popup
+                        />
+                      </div>
+                    </q-date>
+                  </q-popup-proxy>
+                </q-icon>
+              </template>
+            </q-input>
+            <q-btn-group push class="q-ml-md"> </q-btn-group>
+            <q-input
+              outlined
+              rounded
+              dense
+              debounce="300"
+              color="primary"
+              v-model="filter"
+              :placeholder="$t('search')"
+              @input="getSearchList()"
+              @keyup.enter="getSearchList()"
+            >
+              <template v-slot:append>
+                <q-icon name="search" @click="getSearchList()" />
+              </template>
+            </q-input>
+            <q-select
+              class="q-ml-md"
+              dense
+              outlined
+              clearable
+              emit-value
+              map-options
+              v-model="logTypeFilter"
+              :options="logTypeOptions"
+              :placeholder="$t('task.log_type') || '日志类型'"
+              @update:model-value="getSearchList()"
+            >
+              <template v-slot:prepend>
+                <q-icon name="category" />
+              </template>
+            </q-select>
+          </div>
+        </template>
+        <template v-slot:body="props">
+          <q-tr :props="props">
+            <q-td
+              v-for="col in columns"
+              :key="col.name"
+              :props="props"
+            >
+            {{ formatCellValue(col, props.row) }}
+            </q-td>
+          </q-tr>
+        </template>
+      </q-table>
+    </transition>
+    <template>
+      <div v-show="max !== 0" class="q-pa-lg flex flex-center">
+        <div>{{ total }}</div>
+        <q-pagination
+          v-model="current"
+          color="black"
+          :max="max"
+          :max-pages="6"
+          boundary-links
+          @click="
+            getSearchList(current);
+            paginationIpt = current;
+          "
+        />
+        <div>
+          <input
+            v-model="paginationIpt"
+            @blur="changePageEnter"
+            @keyup.enter="changePageEnter"
+            style="width: 60px; text-align: center"
+          />
+        </div>
+      </div>
+      <div v-show="max === 0" class="q-pa-lg flex flex-center">
+        <q-btn flat push color="dark" :label="$t('no_data')"></q-btn>
+      </div>
+    </template>
+  </div>
+</template>
+<router-view />
+
+<script>
+import { getauth } from "boot/axios_request";
+import { date, LocalStorage } from "quasar";
+
+export default {
+  name: "WCSLog",
+  data() {
+    return {
+      createDate1: "",
+      createDate2: "",
+      date_range: "",
+      proxyDate: "",
+      date: "",
+      openid: "",
+      login_name: "",
+      authin: "0",
+      searchUrl: "",
+      pathname: "container/wcs/logs/",
+      pathname_previous: "",
+      pathname_next: "",
+      separator: "cell",
+      loading: false,
+      height: "",
+      table_list: [],
+      columns: [
+        {
+          name: "time",
+          label: "时间",
+          field: "time",
+          align: "center",
+          sortable: true,
+        },
+        {
+          name: "taskNumber",
+          label: "任务号",
+          field: "taskNumber",
+          align: "center",
+          sortable: true,
+        },
+        {
+          name: "container",
+          label: "托盘号",
+          field: "container",
+          align: "center",
+          sortable: true,
+        },
+        {
+          name: "current_location",
+          label: "起始位置",
+          field: "current_location",
+          align: "center",
+        },
+        {
+          name: "target_location",
+          label: "目标位置",
+          field: "target_location",
+          align: "center",
+        },
+        {
+          name: "floor",
+          label: "楼层",
+          field: "floor",
+          align: "center",
+        },
+        {
+          name: "log_type",
+          label: "日志类型",
+          field: "log_type",
+          align: "center",
+        },
+        {
+          name: "is_completed",
+          label: "完成状态",
+          field: "is_completed",
+          align: "center",
+        },
+        {
+          name: "location_group_id",
+          label: "库位组ID",
+          field: "location_group_id",
+          align: "center",
+        },
+        {
+          name: "access_priority",
+          label: "优先级(靠里程度)",
+          field: "access_priority",
+          align: "center",
+        },
+      ],
+      filter: "",
+      logTypeFilter: null,
+      logTypeOptions: [
+        { label: "出库任务", value: "outbound" },
+        { label: "入库任务", value: "inbound" },
+        { label: "抽检任务", value: "check" },
+        { label: "其他", value: "other" },
+      ],
+      pagination: {
+        page: 1,
+        rowsPerPage: 11,
+      },
+      current: 1,
+      max: 0,
+      total: 0,
+      paginationIpt: 1,
+      filterdata: {},
+      activeSearchField: "",
+      activeSearchLabel: "",
+    };
+  },
+  computed: {
+    interval() {
+      return (
+        this.$t("download_center.start") +
+        " - " +
+        this.$t("download_center.end")
+      );
+    },
+  },
+  methods: {
+    formatCellValue(col, row) {
+      const value = col.field ? row[col.field] : row[col.name];
+      if (col.name === "is_completed") {
+        if (value === true) {
+          return "已完成";
+        }
+        if (value === false) {
+          return "进行中";
+        }
+      }
+      if (col.name === "log_type") {
+         return this.resolveLogTypeLabel(value);
+      }
+      if (!value && value !== 0) {
+        return "-";
+      }
+      return value;
+    },
+    resolveLogTypeLabel(value) {
+      const option = this.logTypeOptions.find((item) => item.value === value);
+      return option ? option.label : value || "-";
+    },
+    handleHeaderDblClick(column) {
+      // 排除不需要搜索的列
+      if (["detail", "action"].includes(column.name)) return;
+
+      this.activeSearchField = column.field;
+      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(column.field, data);
+        })
+        .onCancel(() => {
+          this.activeSearchField = "";
+          this.activeSearchLabel = "";
+        });
+    },
+    // 执行列搜索
+    executeColumnSearch(field, value) {
+      // 构建搜索参数
+      const searchParams = {
+        [field + "__icontains"]: value,
+      };
+      // 清除其他搜索条件
+      this.filter = "";
+      this.date_range = "";
+
+      // 执行搜索
+      this.getList({
+        ...searchParams,
+        page: 1,
+      });
+      this.filterdata = searchParams;
+      this.$q.notify({
+        message: `已搜索 ${this.activeSearchLabel} 含有 "${value}" 的结果`,
+        icon: "search",
+        color: "positive",
+      });
+
+      // 重置激活的搜索字段
+      this.activeSearchField = "";
+      this.activeSearchLabel = "";
+    },
+    getList(params = {}) {
+      var _this = this;
+      _this.loading = true;
+      // 合并基础参数
+      const baseParams = {
+        page: _this.current,
+        max_page: _this.pagination.rowsPerPage,
+      };
+
+      // 创建URLSearchParams处理参数
+      const queryParams = new URLSearchParams({
+        ...baseParams,
+        ...params,
+      });
+
+      // 过滤空值参数
+      Array.from(queryParams.entries()).forEach(([key, value]) => {
+        if (value === "" || value === null || value === undefined) {
+          queryParams.delete(key);
+        }
+      });
+
+      getauth(`${_this.pathname}?${queryParams}`)
+        .then((res) => {
+          _this.table_list = res.results || [];
+
+          _this.total = res.count || 0;
+          _this.max = Math.ceil((res.count || 0) / _this.pagination.rowsPerPage) || 0;
+          _this.pathname_previous = res.previous;
+          _this.pathname_next = res.next;
+        })
+        .catch((err) => {
+          _this.$q.notify({
+            message: err.detail || err.message || "获取数据失败",
+            icon: "close",
+            color: "negative",
+          });
+        })
+        .finally(() => {
+          _this.loading = false;
+        });
+    },
+    changePageEnter() {
+      if (Number(this.paginationIpt) < 1) {
+        this.current = 1;
+        this.paginationIpt = 1;
+      } else if (Number(this.paginationIpt) > this.max) {
+        this.current = this.max;
+        this.paginationIpt = this.max;
+      } else {
+        this.current = Number(this.paginationIpt);
+      }
+      this.getSearchList(this.current);
+    },
+
+    // 带搜索条件加载
+    getSearchList(page = 1) {
+      this.current = page;
+      this.paginationIpt = page;
+
+      // 构建搜索参数
+      const searchParams = {};
+      
+      // 通用搜索(优先搜索托盘号,支持模糊匹配)
+      if (this.filter) {
+        // 如果搜索内容看起来像任务号(纯数字),则搜索任务号
+        if (/^\d+$/.test(this.filter)) {
+          searchParams.taskNumber = this.filter;
+        } else {
+          // 否则搜索托盘号(支持模糊匹配)
+          searchParams.container__icontains = this.filter;
+        }
+      }
+
+      // 时间范围搜索
+      if (this.date_range) {
+        const dateParts = this.date_range.split(",");
+        if (dateParts.length === 2) {
+          searchParams.send_time__gte = dateParts[0].trim();
+          searchParams.send_time__lte = dateParts[1].trim();
+        }
+      }
+
+      if (this.logTypeFilter) {
+        searchParams.log_type = this.logTypeFilter;
+      }
+
+      this.getList({
+        ...searchParams,
+        ...this.filterdata, // 添加其他过滤条件
+      });
+    },
+
+    getListPrevious() {
+      var _this = this;
+      if (LocalStorage.has("auth")) {
+        getauth(_this.pathname_previous, {})
+          .then((res) => {
+            _this.table_list = res.results || [];
+            _this.pathname_previous = res.previous;
+            _this.pathname_next = res.next;
+          })
+          .catch((err) => {
+            _this.$q.notify({
+              message: err.detail || err.message || "获取数据失败",
+              icon: "close",
+              color: "negative",
+            });
+          });
+      }
+    },
+    getListNext() {
+      var _this = this;
+      if (LocalStorage.has("auth")) {
+        getauth(_this.pathname_next, {})
+          .then((res) => {
+            _this.table_list = res.results || [];
+            _this.pathname_previous = res.previous;
+            _this.pathname_next = res.next;
+          })
+          .catch((err) => {
+            _this.$q.notify({
+              message: err.detail || err.message || "获取数据失败",
+              icon: "close",
+              color: "negative",
+            });
+          });
+      }
+    },
+    reFresh() {
+      var _this = this;
+      this.filterdata = {};
+      this.filter = "";
+      this.createDate1 = "";
+      this.createDate2 = "";
+      this.date_range = "";
+      this.logTypeFilter = null;
+      _this.getSearchList();
+    },
+  },
+  created() {
+    var _this = this;
+    if (LocalStorage.has("openid")) {
+      _this.openid = LocalStorage.getItem("openid");
+    } else {
+      _this.openid = "";
+      LocalStorage.set("openid", "");
+    }
+    if (LocalStorage.has("login_name")) {
+      _this.login_name = LocalStorage.getItem("login_name");
+    } else {
+      _this.login_name = "";
+      LocalStorage.set("login_name", "");
+    }
+    if (LocalStorage.has("auth")) {
+      const timeStamp = Date.now();
+      const formattedString = date.formatDate(timeStamp, "YYYY/MM/DD");
+      _this.date = formattedString;
+      _this.authin = "1";
+      _this.getList();
+    } else {
+      _this.authin = "0";
+    }
+  },
+  mounted() {
+    var _this = this;
+    if (_this.$q.platform.is.electron) {
+      _this.height = String(_this.$q.screen.height - 290) + "px";
+    } else {
+      _this.height = _this.$q.screen.height - 290 + "" + "px";
+    }
+  },
+  updated() {},
+  destroyed() {},
+  watch: {
+    createDate1(val) {
+      if (val) {
+        if (val.to) {
+          this.createDate2 = `${val.from} - ${val.to}`;
+          this.date_range = `${val.from},${val.to} `;
+        } else {
+          this.createDate2 = `${val}`;
+          this.dateArray = val.split("/");
+          this.searchUrl =
+            this.pathname +
+            "?" +
+            "send_time__year=" +
+            this.dateArray[0] +
+            "&" +
+            "send_time__month=" +
+            this.dateArray[1] +
+            "&" +
+            "send_time__day=" +
+            this.dateArray[2];
+        }
+        this.date_range = this.date_range.replace(/\//g, "-");
+
+        this.getSearchList();
+        this.$refs.qDateProxy.hide();
+      } else {
+        this.createDate2 = "";
+        this.date_range = "";
+        this.getSearchList();
+      }
+    },
+  },
+};
+</script>
+<style scoped>
+/* 添加在 <style> 中 */
+.q-date__calendar-item--selected {
+  transition: all 0.3s ease;
+  background-color: #1976d2 !important;
+}
+
+.q-date__range {
+  background-color: rgba(25, 118, 210, 0.1);
+}
+</style>

+ 10 - 0
templates/src/router/routes.js

@@ -409,6 +409,16 @@ const routes = [
             path: 'task',
             name: 'task',
             component: () => import('pages/task/task.vue')
+          },
+          {
+            path: 'wcslog',
+            name: 'wcslog',
+            component: () => import('pages/task/wcslog.vue')
+          },
+          {
+            path: 'dispatchconfig',
+            name: 'dispatchconfig',
+            component: () => import('pages/task/dispatchconfig.vue')
           }
         ]
       },