编辑 | blame | 历史 | 原始文档

手动下发任务到 WCS 功能实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在 WMS 前端任务管理页面添加工具栏按钮,支持选中一个或多个任务后,编辑地址(起点/终点)和优先级后手动下发到 WCS。

Architecture:
- 前端:新增 dispatchTasksToWCS.vue 弹窗组件,复用 task.js 扩展机制添加工具栏按钮
- 后端:新增 DispatchTasksToWCSAsync 方法,复用 ReceiveManualTask 接口下发到 WCS

Tech Stack: Vue 3 + Element Plus(前端),ASP.NET Core + SqlSugar(后端)


文件结构

改动类型 文件路径
新增 WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/dispatchTasksToWCS.vue
修改 WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js
新增 WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/DispatchTaskDto.cs
修改 WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_WCS.cs
修改 WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs

Task 1: 后端 - 新增 DispatchTaskDto

文件:
- 创建: WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/DispatchTaskDto.cs

  • [ ] 创建 DispatchTaskDto.cs
using System.Text.Json.Serialization;

namespace WIDESEA_DTO.Task
{
    /// <summary>
    /// 手动下发任务Dto
    /// </summary>
    public class DispatchTaskDto
    {
        /// <summary>
        /// 任务ID
        /// </summary>
        [JsonPropertyName("taskId")]
        public long TaskId { get; set; }

        /// <summary>
        /// 起点地址
        /// </summary>
        [JsonPropertyName("sourceAddress")]
        public string SourceAddress { get; set; }

        /// <summary>
        /// 终点地址
        /// </summary>
        [JsonPropertyName("targetAddress")]
        public string TargetAddress { get; set; }

        /// <summary>
        /// 优先级
        /// </summary>
        [JsonPropertyName("grade")]
        public int Grade { get; set; }
    }

    /// <summary>
    /// 任务下发结果Dto
    /// </summary>
    public class DispatchTaskResultDto
    {
        /// <summary>
        /// 任务ID
        /// </summary>
        [JsonPropertyName("taskId")]
        public long TaskId { get; set; }

        /// <summary>
        /// 任务号
        /// </summary>
        [JsonPropertyName("taskNum")]
        public int TaskNum { get; set; }

        /// <summary>
        /// 是否成功
        /// </summary>
        [JsonPropertyName("success")]
        public bool Success { get; set; }

        /// <summary>
        /// 错误信息
        /// </summary>
        [JsonPropertyName("errorMessage")]
        public string ErrorMessage { get; set; }
    }

    /// <summary>
    /// 批量下发结果Dto
    /// </summary>
    public class DispatchResultDto
    {
        /// <summary>
        /// 成功数量
        /// </summary>
        [JsonPropertyName("successCount")]
        public int SuccessCount { get; set; }

        /// <summary>
        /// 失败数量
        /// </summary>
        [JsonPropertyName("failCount")]
        public int FailCount { get; set; }

        /// <summary>
        /// 失败任务列表
        /// </summary>
        [JsonPropertyName("failResults")]
        public List<DispatchTaskResultDto> FailResults { get; set; }
    }
}
  • [ ] Commit
git add WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/DispatchTaskDto.cs
git commit -m "feat(WMS): 新增手动下发任务Dto
- DispatchTaskDto: 下发请求参数
- DispatchTaskResultDto: 单个任务下发结果
- DispatchResultDto: 批量下发结果

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 2: 后端 - TaskService 新增 DispatchTasksToWCSAsync

文件:
- 修改: WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_WCS.cs

前置了解:
- 任务状态枚举:TaskInStatusEnum.InNew(入库新单)、TaskOutStatusEnum.OutNew(出库新单)、TaskRelocationStatusEnum.RelocationNew(移库新单)
- 已有 WCS 调用方式:_httpClientHelper.Post<WebResponseContent>("http://localhost:9292/api/Task/ReceiveManualTask", json)
- 已有事务处理:await _unitOfWorkManage.BeginTranAsync(async () => { ... })
- 已有 WMSTaskDTO 用于给 WCS 发送任务

TaskService_WCS.cs 末尾 #endregion WCS逻辑处理 之前添加:

/// <summary>
/// 手动下发任务到WCS
/// </summary>
/// <param name="dtos">下发任务参数列表</param>
/// <returns>批量下发结果</returns>
public async Task<WebResponseContent> DispatchTasksToWCSAsync(List<DispatchTaskDto> dtos)
{
    try
    {
        if (dtos == null || !dtos.Any())
            return WebResponseContent.Instance.Error("请选择要下发的任务");

        var resultDto = new DispatchResultDto
        {
            SuccessCount = 0,
            FailCount = 0,
            FailResults = new List<DispatchTaskResultDto>()
        };

        foreach (var dto in dtos)
        {
            var task = await BaseDal.QueryFirstAsync(t => t.TaskId == dto.TaskId);
            if (task == null)
            {
                resultDto.FailResults.Add(new DispatchTaskResultDto
                {
                    TaskId = dto.TaskId,
                    TaskNum = 0,
                    Success = false,
                    ErrorMessage = "任务不存在"
                });
                resultDto.FailCount++;
                continue;
            }

            // 校验任务状态:仅入库新单/出库新单/移库新单可下发
            bool canDispatch = false;
            if (task.TaskType == TaskInboundTypeEnum.Inbound.GetHashCode()
                && task.TaskStatus == TaskInStatusEnum.InNew.GetHashCode())
                canDispatch = true;
            else if (task.TaskType == TaskOutboundTypeEnum.Outbound.GetHashCode()
                && task.TaskStatus == TaskOutStatusEnum.OutNew.GetHashCode())
                canDispatch = true;
            else if (task.TaskType == TaskRelocationTypeEnum.Relocation.GetHashCode()
                && task.TaskStatus == TaskRelocationStatusEnum.RelocationNew.GetHashCode())
                canDispatch = true;

            if (!canDispatch)
            {
                var statusName = GetTaskStatusName(task.TaskType, task.TaskStatus);
                resultDto.FailResults.Add(new DispatchTaskResultDto
                {
                    TaskId = dto.TaskId,
                    TaskNum = task.TaskNum,
                    Success = false,
                    ErrorMessage = $"任务状态[{statusName}]不允许下发"
                });
                resultDto.FailCount++;
                continue;
            }

            // 更新任务的地址和优先级
            task.SourceAddress = dto.SourceAddress;
            task.TargetAddress = dto.TargetAddress;
            task.CurrentAddress = dto.SourceAddress;
            task.NextAddress = dto.TargetAddress;
            task.Grade = dto.Grade;
            task.Dispatchertime = DateTime.Now;

            await BaseDal.UpdateDataAsync(task);

            // 构造WMSTaskDTO发送给WCS
            var wmsTaskDto = new WMSTaskDTO
            {
                Id = task.TaskId,
                TaskNum = task.TaskNum,
                PalletCode = task.PalletCode,
                SourceAddress = task.SourceAddress,
                TargetAddress = task.TargetAddress,
                CurrentAddress = task.CurrentAddress,
                NextAddress = task.NextAddress,
                TaskType = task.TaskType,
                TaskStatus = task.TaskStatus,
                Roadway = task.Roadway,
                Grade = task.Grade,
                WarehouseId = task.WarehouseId,
                PalletType = task.PalletType
            };

            var wcsResult = _httpClientHelper.Post<WebResponseContent>(
                "http://localhost:9292/api/Task/ReceiveManualTask",
                new List<WMSTaskDTO> { wmsTaskDto }.ToJson());

            if (wcsResult != null && wcsResult.IsSuccess)
            {
                resultDto.SuccessCount++;
            }
            else
            {
                resultDto.FailResults.Add(new DispatchTaskResultDto
                {
                    TaskId = dto.TaskId,
                    TaskNum = task.TaskNum,
                    Success = false,
                    ErrorMessage = wcsResult?.Message ?? "WCS响应异常"
                });
                resultDto.FailCount++;
            }
        }

        if (resultDto.FailCount == 0)
            return WebResponseContent.Instance.OK($"成功下发{resultDto.SuccessCount}个任务", resultDto);

        if (resultDto.SuccessCount == 0)
            return WebResponseContent.Instance.Error($"下发失败,共{resultDto.FailCount}个任务", resultDto);

        return WebResponseContent.Instance.Error($"部分下发成功{resultDto.SuccessCount}个,失败{resultDto.FailCount}个", resultDto);
    }
    catch (Exception ex)
    {
        return WebResponseContent.Instance.Error($"下发任务异常: {ex.Message}");
    }
}

/// <summary>
/// 获取任务状态名称
/// </summary>
private string GetTaskStatusName(int taskType, int taskStatus)
{
    if (taskType == TaskInboundTypeEnum.Inbound.GetHashCode())
        return ((TaskInStatusEnum)taskStatus).ToString();
    if (taskType == TaskOutboundTypeEnum.Outbound.GetHashCode())
        return ((TaskOutStatusEnum)taskStatus).ToString();
    if (taskType == TaskRelocationTypeEnum.Relocation.GetHashCode())
        return ((TaskRelocationStatusEnum)taskStatus).ToString();
    return taskStatus.ToString();
}

注意: 需要在文件顶部确认已有以下 using:
```csharp
using WIDESEA_DTO.Task;

状态同步说明:WMS 下发成功后,WMS 端任务状态保持"New"不变,不切换到执行中状态。这是现有 CreateManualTaskAsync 的设计模式(因为 WCS 侧有独立的任务生命周期),不跟进此 PR 改变。如需 WMS 端也同步状态,需另行评估 WCS→WMS 的状态回调机制。
using WIDESEA_Model.Models;
```

  • [ ] Commit
git add WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_WCS.cs
git commit -m "feat(WMS): TaskService新增DispatchTasksToWCSAsync方法
- 校验任务状态,仅入库新单/出库新单/移库新单可下发
- 更新任务地址和优先级后调用WCS ReceiveManualTask接口
- 返回批量下发结果(成功/失败列表及原因)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 3: 后端 - TaskController 新增下发接口

文件:
- 修改: WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs

在 Controller 中添加以下方法(放在其他方法附近,建议在 CreateManualTaskAsync 之后):

/// <summary>
/// 手动下发任务到WCS
/// </summary>
/// <param name="dtos">下发任务参数列表</param>
/// <returns>批量下发结果</returns>
[HttpGet, HttpPost, Route("DispatchTasksToWCS"), AllowAnonymous]
public async Task<WebResponseContent?> DispatchTasksToWCSAsync([FromBody] List<DispatchTaskDto> dtos)
{
    return await Service.DispatchTasksToWCSAsync(dtos);
}
  • [ ] Commit
git add WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
git commit -m "feat(WMS): TaskController新增DispatchTasksToWCS接口
POST /api/TaskInfo/DispatchTasksToWCS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 4: 前端 - 新增 dispatchTasksToWCS.vue 弹窗组件

文件:
- 创建: WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/dispatchTasksToWCS.vue

参考现有 addManualTask.vue 的模式,使用 el-table 实现批量编辑:

<template>
  <div>
    <vol-box v-model="showBox" :lazy="true" width="900px" :padding="15" title="手动下发任务到 WCS">
      <div v-if="selectedRows.length > 0" class="dispatch-info">
        已选任务数: <span class="count">{{ selectedRows.length }}</span> 个
      </div>

      <el-table :data="tableData" border style="width: 100%; margin-top: 10px" max-height="400">
        <el-table-column prop="taskNum" label="任务号" width="120"></el-table-column>
        <el-table-column prop="sourceAddress" label="起点地址" width="160">
          <template slot-scope="scope">
            <el-input
              v-if="scope.row.editable"
              v-model="scope.row.sourceAddress"
              size="small"
              placeholder="请输入"
            ></el-input>
            <span v-else>{{ scope.row.sourceAddress }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="targetAddress" label="终点地址" width="160">
          <template slot-scope="scope">
            <el-input
              v-if="scope.row.editable"
              v-model="scope.row.targetAddress"
              size="small"
              placeholder="请输入"
            ></el-input>
            <span v-else>{{ scope.row.targetAddress }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="grade" label="优先级" width="100">
          <template slot-scope="scope">
            <el-input-number
              v-if="scope.row.editable"
              v-model="scope.row.grade"
              :min="1"
              :max="99"
              size="small"
              style="width: 80px"
            ></el-input-number>
            <span v-else>{{ scope.row.grade }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="statusName" label="状态" width="120">
          <template slot-scope="scope">
            <span :class="{ 'status-error': !scope.row.editable }">{{ scope.row.statusName }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="palletCode" label="托盘号"></el-table-column>
      </el-table>

      <!-- 失败结果 -->
      <div v-if="failResults.length > 0" class="fail-results">
        <div class="fail-title">下发失败任务:</div>
        <el-table :data="failResults" border style="width: 100%; margin-top: 10px" max-height="200">
          <el-table-column prop="taskNum" label="任务号" width="120"></el-table-column>
          <el-table-column prop="errorMessage" label="失败原因"></el-table-column>
        </el-table>
      </div>

      <template #footer>
        <el-button type="primary" size="small" @click="submit" :loading="loading">确认下发</el-button>
        <el-button type="danger" size="small" @click="showBox = false">关闭</el-button>
      </template>
    </vol-box>
  </div>
</template>

<script>
import VolBox from "@/components/basic/VolBox.vue";

export default {
  components: { VolBox },
  data() {
    return {
      showBox: false,
      loading: false,
      selectedRows: [],
      tableData: [],
      failResults: [],
      // 可下发状态的任务类型和状态值(需与后端 TaskTypeEnum 枚举一致)
      // Inbound=200, Outbound=100, Relocation=300
      // 状态: InNew=200, OutNew=100, RelocationNew=300
      dispatchableStatuses: {
        inbound: { taskType: 200, status: 200 },    // 入库新单
        outbound: { taskType: 100, status: 100 },  // 出库新单
        relocation: { taskType: 300, status: 300 } // 移库新单
      }
    };
  },
  methods: {
    open(rows) {
      this.showBox = true;
      this.selectedRows = rows || [];
      this.failResults = [];
      this.initTableData();
    },
    initTableData() {
      // 任务状态名称映射(需与后端枚举一致)
      // taskType: Inbound=200, Outbound=100, Relocation=300
      // status: InNew=200, OutNew=100, RelocationNew=300, SC_OutExecuting=110, etc.
      const statusNames = {
        'inbound_200': '入库新单',
        'outbound_100': '出库新单',
        'relocation_300': '移库新单',
        'outbound_110': '堆垛机出库执行中',
        'outbound_115': '堆垛机出库完成',
        'inbound_220': '输送线入库执行中',
        'inbound_230': '堆垛机入库执行中',
        'inbound_290': '入库任务完成',
        'outbound_120': '输送线出库执行中',
        'outbound_125': '输送线出库完成',
        'outbound_200': '出库任务完成',
        'relocation_310': '堆垛机移库执行中'
      };

      this.tableData = this.selectedRows.map(row => {
        const taskType = row.taskType || 0;
        const taskStatus = row.taskStatus || 0;
        const statusKey = this.getStatusKey(taskType, taskStatus);
        const statusName = statusNames[statusKey] || `状态${taskStatus}`;

        // 判断是否可编辑:仅入库新单/出库新单/移库新单可编辑
        const editable = this.isEditable(taskType, taskStatus);

        return {
          taskId: row.taskId,
          taskNum: row.taskNum,
          sourceAddress: row.sourceAddress || '',
          targetAddress: row.targetAddress || '',
          grade: row.grade || 1,
          statusName: statusName,
          palletCode: row.palletCode || '',
          editable: editable,
          taskType: taskType,
          taskStatus: taskStatus
        };
      });
    },
    getStatusKey(taskType, taskStatus) {
      if (taskType === 200) return `inbound_${taskStatus}`;
      if (taskType === 100) return `outbound_${taskStatus}`;
      if (taskType === 300) return `relocation_${taskStatus}`;
      return `other_${taskStatus}`;
    },
    isEditable(taskType, taskStatus) {
      if (taskType === 200 && taskStatus === 200) return true;  // 入库新单
      if (taskType === 100 && taskStatus === 100) return true;  // 出库新单
      if (taskType === 300 && taskStatus === 300) return true;  // 移库新单
      return false;
    },
    submit() {
      if (this.tableData.length === 0) return this.$message.error("请先选择任务");

      const dispatchData = this.tableData.map(row => ({
        taskId: row.taskId,
        sourceAddress: row.sourceAddress,
        targetAddress: row.targetAddress,
        grade: row.grade
      }));

      this.loading = true;
      this.http
        .post("/api/TaskInfo/DispatchTasksToWCS", dispatchData, "数据处理中...")
        .then((res) => {
          this.loading = false;
          if (!res.status) {
            this.$message.error(res.message);
            // 显示失败列表
            if (res.data && res.data.failResults) {
              this.failResults = res.data.failResults;
            }
            return;
          }

          // 全部成功
          if (res.data && res.data.failCount === 0) {
            this.$message.success(res.message);
            this.showBox = false;
            this.$emit("parentCall", ($vue) => {
              $vue.refresh();
            });
            return;
          }

          // 部分成功或全部失败
          if (res.data && res.data.failResults) {
            this.failResults = res.data.failResults;
          }

          if (res.data && res.data.failCount > 0 && res.data.successCount > 0) {
            this.$message.warning(res.message);
          } else {
            this.$message.error(res.message);
          }
        })
        .catch(() => {
          this.loading = false;
        });
    }
  }
};
</script>

<style scoped>
.dispatch-info {
  font-size: 14px;
  color: #606266;
  margin-bottom: 10px;
}
.dispatch-info .count {
  color: #409eff;
  font-weight: bold;
}
.status-error {
  color: #f56c6c;
}
.fail-results {
  margin-top: 15px;
  padding: 10px;
  background: #fef0f0;
  border-radius: 4px;
}
.fail-title {
  font-size: 14px;
  color: #f56c6c;
  margin-bottom: 5px;
}
</style>
  • [ ] Commit
git add WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/dispatchTasksToWCS.vue
git commit -m "feat(WMS): 新增dispatchTasksToWCS.vue批量下发弹窗组件
- 工具栏按钮触发弹窗
- 表格展示选中任务,可编辑地址和优先级
- 非可下发状态任务行标红且不可编辑
- 显示下发失败任务列表

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 5: 前端 - task.js 添加工具栏按钮

文件:
- 修改: WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js

需要修改两处:

  1. components 部分引入弹窗组件:
    javascript import addManualTask from './extend/addManualTask.vue' import dispatchTasksToWCS from './extend/dispatchTasksToWCS.vue' // 新增

  2. components 对象中注册:
    javascript components: { gridBody: addManualTask, dispatchBody: dispatchTasksToWCS, // 新增 },

  3. onInit() 方法中添加手动下发按钮(在手动创建任务按钮之后):

// 添加"手动下发"按钮
this.buttons.push({
  name: '手动下发',
  icon: 'el-icon-s-promotion',
  type: 'primary',
  value: 'DispatchTasksToWCS',
  onClick: () => {
    let rows = this.$refs.table.getSelected();
    if (rows.length == 0) return this.$error("请先选择任务");
    this.$refs.dispatchBody.open(rows);
  }
});

完整修改后的 task.js 关键部分:

import addManualTask from './extend/addManualTask.vue'
import dispatchTasksToWCS from './extend/dispatchTasksToWCS.vue'

let extension = {
    components: {
      gridBody: addManualTask,
      dispatchBody: dispatchTasksToWCS,
    },
    buttons: { view: [], box: [], detail: [] },
    methods: {
      onInit() {
        //添加"手动创建任务"按钮
        this.buttons.push({
          name: '手动创建任务',
          icon: 'el-icon-plus',
          type: 'primary',
          value: 'ManualCreateTask',
          onClick: () => {
            this.$refs.gridBody.open();
          }
        });

        // 添加"手动下发"按钮
        this.buttons.push({
          name: '手动下发',
          icon: 'el-icon-s-promotion',
          type: 'primary',
          value: 'DispatchTasksToWCS',
          onClick: () => {
            let rows = this.$refs.table.getSelected();
            if (rows.length == 0) return this.$error("请先选择任务");
            this.$refs.dispatchBody.open(rows);
          }
        });
        // ... 其余现有按钮逻辑保持不变
      },
      // ... 其余方法保持不变
    }
};
export default extension;
  • [ ] Commit
git add WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js
git commit -m "feat(WMS): task.js添加工具栏'手动下发'按钮
- 引入dispatchTasksToWCS.vue组件
- 点击按钮获取选中任务并打开下发弹窗

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 6: 验证构建

  • [ ] WMS 后端构建
cd D:/Git/ShanMeiXinNengYuan/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer
dotnet build
  • [ ] WMS 前端构建
cd D:/Git/ShanMeiXinNengYuan/Code/WMS/WIDESEA_WMSClient
npm run serve

验收标准

  • [ ] 工具栏添加"手动下发"按钮,未选中任务时点击提示"请先选择任务"
  • [ ] 弹窗正确显示所有选中任务
  • [ ] 仅入库新单/出库新单/移库新单状态的任务可编辑地址和优先级
  • [ ] 其他状态任务行标红,地址/优先级不可编辑
  • [ ] 确认下发后调用后端接口
  • [ ] 成功时弹窗关闭并刷新列表;失败时显示失败任务及原因
  • [ ] 批量下发时正确统计成功/失败数量