feat(WMS): 新增dispatchTasksToWCS.vue批量下发弹窗组件 - 工具栏按钮触发弹窗 - 表格展示选中任务,可编辑地址和优先级 - 非可下发状态任务行标红且不可编辑 - 显示下发失败任务列表

feat: 新增任务管理功能及优化任务处理逻辑

1. 新增任务状态管理功能,支持根据任务ID修改状态
2. 新增移库任务完成功能,自动更新库存和货位状态
3. 新增机械手任务功能,支持组盘、换盘、拆盘操作
4. 新增自动出库任务功能,定时处理到期库存
5. 新增空托盘任务功能,支持空托盘出入库
6. 新增手动任务功能,支持手动创建和下发任务
7. 优化任务下发逻辑,增加重复任务检测
8. 前端任务下发界面增加托盘号编辑功能
9. 新增任务结果DTO,统一任务处理结果格式
10. 优化任务状态枚举和描述信息

fix(WMS): 合并gridBody组件解决弹窗无法打开问题 - 创建gridBodyExtension.vue组合手动创建任务和手动下发任务两个弹窗 - 删除独立的dispatchTasksToWCS.vue - task.js改为引用gridBodyExtension

fix(WMS): 修复DispatchTasksToWCSAsync编译错误 - 修正wcsResult?.Message为wcsResult?.Data?.Message - 修正Error方法参数数量(只接受1个参数)

feat(WMS): task.js添加工具栏'手动下发'按钮 - 引入dispatchTasksToWCS.vue组件 - 点击按钮获取选中任务并打开下发弹窗

fix(WMS): 修复dispatchTasksToWCS组件问题 - 修正API路由为/api/Task/DispatchTasksToWCS - 添加取消按钮 - 删除未使用的dispatchableStatuses

fix(WMS): ITaskService接口添加DispatchTasksToWCSAsync声明

feat(WMS): TaskController新增DispatchTasksToWCS接口 POST /api/TaskInfo/DispatchTasksToWCS

fix(WMS): 修复DispatchTasksToWCS数据一致性问题 - 先调用WCS再更新DB

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

feat(WMS): 新增手动下发任务Dto - DispatchTaskDto: 下发请求参数 - DispatchTaskResultDto: 单个任务下发结果 - DispatchResultDto: 批量下发结果

fix: 修复手动下发任务计划中的三个问题 1. 修正API路由为/api/TaskInfo/DispatchTasksToWCS 2. TaskId类型改为long与SPEC一致 3. 修正前端Vue组件中任务类型/状态枚举值

docs: 添加手动下发任务到WCS功能实施计划

docs: 添加手动下发任务到WCS功能设计文档
已添加15个文件
已删除1个文件
已修改4个文件
3738 ■■■■ 文件已修改
Code/.omc/state/idle-notif-cooldown.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_DTO/TaskInfo/ReceiveTaskResultDto.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskService.cs 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/gridBodyExtension.vue 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/DispatchTaskDto.cs 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/ReceiveTaskResultDto.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_WCS.cs 960 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_AutoOutbound.cs 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Inbound.cs 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Manual.cs 328 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Outbound.cs 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Relocation.cs 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Robot.cs 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_TaskStatus.cs 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Tray.cs 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/plans/2026-04-12-manual-dispatch-tasks-to-wcs-plan.md 723 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/specs/2026-04-12-manual-dispatch-tasks-to-wcs-design.md 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/.omc/state/idle-notif-cooldown.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
{
  "lastSentAt": "2026-04-12T12:35:52.350Z"
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_DTO/TaskInfo/ReceiveTaskResultDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
namespace WIDESEAWCS_DTO.TaskInfo
{
    /// <summary>
    /// WCS接收WMS任务的结果Dto
    /// </summary>
    public class ReceiveTaskResultDto
    {
        /// <summary>
        /// æ˜¯å¦æˆåŠŸ
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// æ¶ˆæ¯
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// æ–°å»ºçš„任务数量
        /// </summary>
        public int CreatedCount { get; set; }
        /// <summary>
        /// é‡å¤/已存在的任务列表(只包含任务号和托盘号)
        /// </summary>
        public List<DuplicateTaskDto> DuplicateTasks { get; set; }
    }
    /// <summary>
    /// é‡å¤ä»»åŠ¡ä¿¡æ¯Dto
    /// </summary>
    public class DuplicateTaskDto
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        public int TaskNum { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å·
        /// </summary>
        public string PalletCode { get; set; }
        /// <summary>
        /// ä»»åŠ¡ç±»åž‹
        /// </summary>
        public int TaskType { get; set; }
        /// <summary>
        /// å½“前状态
        /// </summary>
        public int TaskStatus { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskService.cs
@@ -655,8 +655,42 @@
            WebResponseContent content = new WebResponseContent();
            try
            {
                // æ”¶é›†é‡å¤ä»»åŠ¡çš„ä¿¡æ¯
                var duplicateTasks = new List<DuplicateTaskDto>();
                // å€’序遍历,安全删除并收集被移除的项
                for (int i = taskDTOs.Count - 1; i >= 0; i--)
                {
                    var item = taskDTOs[i];
                    var exists = BaseDal.QueryFirst(x => x.TaskNum == item.TaskNum || x.PalletCode == item.PalletCode);
                    if (exists != null)
                    {
                        duplicateTasks.Add(new DuplicateTaskDto
                        {
                            TaskNum = exists.TaskNum,
                            PalletCode = exists.PalletCode,
                            TaskType = exists.TaskType,
                            TaskStatus = exists.TaskStatus
                        });
                        taskDTOs.RemoveAt(i);
                    }
                }
                // è°ƒç”¨ ReceiveWMSTask åˆ›å»º WCS ä»»åŠ¡
                content = ReceiveWMSTask(taskDTOs);
                // å¦‚果有重复任务,修改返回结果
                if (duplicateTasks.Count > 0 && content.Status)
                {
                    var result = new ReceiveTaskResultDto
                    {
                        Success = true,
                        Message = content.Message + $",其中{duplicateTasks.Count}个任务在WCS中已存在",
                        CreatedCount = taskDTOs.Count,
                        DuplicateTasks = duplicateTasks
                    };
                    content.Data = result;
                }
                return content;
            }
            catch (Exception ex)
Code/WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/gridBodyExtension.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,329 @@
<template>
  <div>
    <!-- æ‰‹åŠ¨åˆ›å»ºä»»åŠ¡å¼¹çª— -->
    <vol-box v-model="showManualCreate" :lazy="true" width="500px" :padding="15" title="手动创建任务">
      <el-form :model="manualFormData" ref="form" label-width="100px">
        <el-form-item label="任务类型" prop="taskType" required>
          <el-select v-model="manualFormData.taskType" placeholder="请选择任务类型">
            <el-option label="入库" value="入库"></el-option>
            <el-option label="出库" value="出库"></el-option>
            <el-option label="移库" value="移库"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="起点地址" prop="sourceAddress" required>
          <el-input v-model="manualFormData.sourceAddress" placeholder="请输入起点地址"></el-input>
        </el-form-item>
        <el-form-item label="终点地址" prop="targetAddress" required>
          <el-input v-model="manualFormData.targetAddress" placeholder="请输入终点地址"></el-input>
        </el-form-item>
        <el-form-item label="条码" prop="barcode" required>
          <el-input v-model="manualFormData.barcode" placeholder="请输入条码"></el-input>
        </el-form-item>
        <el-form-item label="仓库ID" prop="warehouseId" required>
          <el-input v-model="manualFormData.warehouseId" placeholder="请输入仓库ID"></el-input>
        </el-form-item>
        <el-form-item label="优先级">
          <el-input v-model="manualFormData.grade" readonly></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" size="small" @click="submitManualCreate">确定</el-button>
        <el-button type="danger" size="small" @click="showManualCreate = false">关闭</el-button>
      </template>
    </vol-box>
    <!-- æ‰‹åŠ¨ä¸‹å‘ä»»åŠ¡å¼¹çª— -->
    <vol-box v-model="showDispatch" :lazy="true" width="900px" :padding="15" title="手动下发任务到 WCS">
      <div v-if="dispatchRows.length > 0" class="dispatch-info">
        å·²é€‰ä»»åŠ¡æ•°: <span class="count">{{ dispatchRows.length }}</span> ä¸ª
      </div>
      <el-table :data="dispatchTableData" 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="palletCode" label="托盘号" width="140">
          <template v-slot="{ row }">
            <el-input v-if="row && row.editable" v-model="row.palletCode" size="small" placeholder="请输入"></el-input>
            <span v-else>{{ row ? row.palletCode : '' }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="sourceAddress" label="起点地址" width="160">
          <template v-slot="{ row }">
            <el-input v-if="row && row.editable" v-model="row.sourceAddress" size="small" placeholder="请输入"></el-input>
            <span v-else>{{ row ? row.sourceAddress : '' }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="targetAddress" label="终点地址" width="160">
          <template v-slot="{ row }">
            <el-input v-if="row && row.editable" v-model="row.targetAddress" size="small" placeholder="请输入"></el-input>
            <span v-else>{{ row ? row.targetAddress : '' }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="grade" label="优先级" width="100">
          <template v-slot="{ row }">
            <el-input-number v-if="row && row.editable" v-model="row.grade" :min="1" :max="99" size="small" style="width: 80px"></el-input-number>
            <span v-else>{{ row ? row.grade : '' }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="statusName" label="状态" width="120">
          <template v-slot="{ row }">
            <span :class="{ 'status-error': row && !row.editable }">{{ row ? row.statusName : '' }}</span>
          </template>
        </el-table-column>
      </el-table>
      <div v-if="dispatchFailResults && dispatchFailResults.length > 0" class="fail-results">
        <div class="fail-title">下发失败任务:</div>
        <el-table :data="dispatchFailResults" 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="submitDispatch" :loading="dispatchLoading">确认下发</el-button>
        <el-button type="danger" size="small" @click="showDispatch = false">取消</el-button>
      </template>
    </vol-box>
  </div>
</template>
<script>
import VolBox from "@/components/basic/VolBox.vue";
export default {
  components: { VolBox },
  emits: ["parentCall"],
  data() {
    return {
      // æ‰‹åŠ¨åˆ›å»ºä»»åŠ¡
      showManualCreate: false,
      manualFormData: {
        taskType: "",
        sourceAddress: "",
        targetAddress: "",
        barcode: "",
        warehouseId: "",
        grade: 1,
      },
      // æ‰‹åŠ¨ä¸‹å‘ä»»åŠ¡
      showDispatch: false,
      dispatchLoading: false,
      dispatchRows: [],
      dispatchTableData: [],
      dispatchFailResults: [],
      dispatchOriginalData: [],
    };
  },
  methods: {
    // æ‰‹åŠ¨åˆ›å»ºä»»åŠ¡ - æ‰“å¼€
    open() {
      this.showManualCreate = true;
      this.resetManualForm();
    },
    resetManualForm() {
      this.manualFormData = {
        taskType: "",
        sourceAddress: "",
        targetAddress: "",
        barcode: "",
        warehouseId: "",
        grade: 1,
      };
    },
    submitManualCreate() {
      if (!this.manualFormData.taskType) return this.$message.error("请选择任务类型");
      if (!this.manualFormData.sourceAddress) return this.$message.error("请输入起点地址");
      if (!this.manualFormData.targetAddress) return this.$message.error("请输入终点地址");
      if (!this.manualFormData.barcode) return this.$message.error("请输入条码");
      if (!this.manualFormData.warehouseId) return this.$message.error("请输入仓库ID");
      this.http
        .post("/api/Task/CreateManualTask", this.manualFormData, "数据处理中...")
        .then((res) => {
          if (!res.status) return this.$message.error(res.message);
          this.$message.success("任务创建成功");
          this.showManualCreate = false;
          this.$emit("parentCall", ($vue) => { $vue.refresh(); });
        });
    },
    // æ‰‹åŠ¨ä¸‹å‘ä»»åŠ¡ - æ‰“å¼€
    openDispatch(rows) {
      console.log('openDispatch called with rows:', rows);
      console.log('rows type:', typeof rows, Array.isArray(rows));
      // ç¡®ä¿æ˜¯æ•°ç»„
      if (!rows) {
        this.dispatchRows = [];
      } else if (Array.isArray(rows)) {
        this.dispatchRows = rows;
      } else {
        // å¦‚果是单个对象,包装成数组
        this.dispatchRows = [rows];
      }
      console.log('dispatchRows length:', this.dispatchRows.length);
      this.dispatchFailResults = [];
      // å…ˆåˆå§‹åŒ–数据,再显示弹窗
      this.initDispatchTableData();
      this.showDispatch = true;
    },
    initDispatchTableData() {
      console.log('initDispatchTableData dispatchRows:', this.dispatchRows);
      console.log('dispatchRows isArray:', Array.isArray(this.dispatchRows), 'length:', this.dispatchRows?.length);
      // ç¡®ä¿ dispatchRows æ˜¯æ•°ç»„
      if (!this.dispatchRows || !Array.isArray(this.dispatchRows)) {
        console.warn('dispatchRows ä¸æ˜¯æœ‰æ•ˆæ•°ç»„,重置为空数组');
        this.dispatchTableData = [];
        return;
      }
      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': '堆垛机移库执行中'
      };
      const result = [];
      const original = [];
      let filteredCount = 0; // è®°å½•被过滤的任务数量
      for (const row of this.dispatchRows) {
        console.log('Processing row:', row);
        // Handle possible field name differences (camelCase vs PascalCase)
        const taskId = row.taskId ?? row.Id ?? row.id ?? 0;
        const taskType = row.taskType ?? row.TaskType ?? row.task_type ?? 0;
        const taskStatus = row.taskStatus ?? row.TaskStatus ?? row.task_status ?? 0;
        console.log('taskId:', taskId, 'taskType:', taskType, 'taskStatus:', taskStatus);
        const statusKey = this.getStatusKey(taskType, taskStatus);
        console.log('statusKey:', statusKey);
        const statusName = statusNames[statusKey] || `状态${taskStatus}`;
        const editable = this.isEditable(taskType, taskStatus);
        console.log('editable:', editable, '根据 taskType:', taskType, 'taskStatus:', taskStatus);
        // éžæ–°å»ºçŠ¶æ€çš„ä»»åŠ¡ä¸æ˜¾ç¤ºåœ¨å¼¹çª—ä¸­
        if (!editable) {
          console.log(`任务${taskId}状态[${statusName}]不是新建,已过滤`);
          filteredCount++;
          continue;
        }
        const item = {
          taskId: taskId,
          taskNum: row.taskNum ?? row.TaskNum ?? 0,
          sourceAddress: row.sourceAddress || row.SourceAddress || '',
          targetAddress: row.targetAddress || row.TargetAddress || '',
          grade: row.grade ?? row.Grade ?? 1,
          statusName: statusName,
          palletCode: row.palletCode || row.PalletCode || '',
          editable: editable,
          taskType: taskType,
          taskStatus: taskStatus
        };
        result.push(item);
        // ä¿å­˜åŽŸå§‹æ•°æ®çš„å‰¯æœ¬
        original.push({ ...item });
      }
      this.dispatchTableData = result;
      this.dispatchOriginalData = original;
      console.log('dispatchTableData result:', this.dispatchTableData);
      console.log('dispatchOriginalData:', this.dispatchOriginalData);
      // å¦‚果有被过滤的任务,显示提示
      if (filteredCount > 0) {
        this.$message.warning(`选中的${this.dispatchRows.length}个任务中有${filteredCount}个非新建状态任务已过滤`);
      }
    },
    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;
    },
    submitDispatch() {
      if (this.dispatchTableData.length === 0) return this.$message.error("请先选择任务");
      const dispatchData = this.dispatchTableData.map(row => ({
        taskId: row.taskId,
        palletCode: row.palletCode,
        sourceAddress: row.sourceAddress,
        targetAddress: row.targetAddress,
        grade: row.grade
      }));
      console.log('Dispatching data:', dispatchData);
      this.dispatchLoading = true;
      this.http
        .post("/api/Task/DispatchTasksToWCS", dispatchData, "数据处理中...")
        .then((res) => {
          this.dispatchLoading = false;
          if (!res.status) {
            this.$message.error(res.message);
            if (res.data && res.data.failResults) {
              this.dispatchFailResults = res.data.failResults;
            }
            return;
          }
          if (res.data && res.data.failCount === 0) {
            this.$message.success(res.message);
            this.showDispatch = false;
            this.$emit("parentCall", ($vue) => { $vue.refresh(); });
            return;
          }
          if (res.data && res.data.failResults) {
            this.dispatchFailResults = 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.dispatchLoading = 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>
Code/WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js
@@ -1,17 +1,17 @@
//此js文件是用来自定义扩展业务代码,可以扩展一些自定义页面或者重新配置生成的代码
import addManualTask from './extend/addManualTask.vue'
import gridBodyExtension from './extend/gridBodyExtension.vue'
let extension = {
    components: {
      //查询界面扩展组件
      gridHeader: '',
      gridBody: addManualTask,
      gridBody: gridBodyExtension,
      gridFooter: '',
      //新建、编辑弹出框扩展组件
      modelHeader: '',
      modelBody: '',
      modelFooter: ''
      modelFooter: '',
    },
    tableAction: '', //指定某张表的权限(这里填写表名,默认不用填写)
    buttons: { view: [], box: [], detail: [] }, //扩展的按钮
@@ -28,6 +28,18 @@
            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.gridBody.openDispatch(rows);
          }
        });
        let TaskHandCancelBtn = this.buttons.find(x => x.value == 'TaskHandCancel');
      if (TaskHandCancelBtn) {
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/DispatchTaskDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
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("palletCode")]
        public string PalletCode { 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; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/ReceiveTaskResultDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
namespace WIDESEA_DTO.Task
{
    /// <summary>
    /// WCS接收WMS任务的结果Dto
    /// </summary>
    public class ReceiveTaskResultDto
    {
        /// <summary>
        /// æ˜¯å¦æˆåŠŸ
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// æ¶ˆæ¯
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// æ–°å»ºçš„任务数量
        /// </summary>
        public int CreatedCount { get; set; }
        /// <summary>
        /// é‡å¤/已存在的任务列表
        /// </summary>
        public List<DuplicateTaskDto> DuplicateTasks { get; set; }
    }
    /// <summary>
    /// é‡å¤ä»»åŠ¡ä¿¡æ¯Dto
    /// </summary>
    public class DuplicateTaskDto
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        public int TaskNum { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å·
        /// </summary>
        public string PalletCode { get; set; }
        /// <summary>
        /// ä»»åŠ¡ç±»åž‹
        /// </summary>
        public int TaskType { get; set; }
        /// <summary>
        /// å½“前状态
        /// </summary>
        public int TaskStatus { get; set; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs
@@ -167,6 +167,13 @@
        /// <returns></returns>
        Task<WebResponseContent> CreateManualTaskAsync(CreateManualTaskDto dto);
        /// <summary>
        /// æ‰‹åŠ¨ä¸‹å‘ä»»åŠ¡åˆ°WCS
        /// </summary>
        /// <param name="dtos">下发任务参数列表</param>
        /// <returns>批量下发结果</returns>
        Task<WebResponseContent> DispatchTasksToWCSAsync(List<DispatchTaskDto> dtos);
        #region æžå·åº“任务模块
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_WCS.cs
ÎļþÒÑɾ³ý
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_AutoOutbound.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,175 @@
using Microsoft.Extensions.Configuration;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.Task;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region è‡ªåŠ¨å‡ºåº“ä»»åŠ¡
        /// <summary>
        /// è‡ªåŠ¨åˆ›å»ºå‡ºåº“ä»»åŠ¡ - æŸ¥è¯¢åˆ°æœŸåº“存并创建任务
        /// </summary>
        public async Task<WebResponseContent> CreateAutoOutboundTasksAsync()
        {
            try
            {
                // 1. æŸ¥è¯¢åˆ°æœŸåº“å­˜
                var expiredStocks = await _stockInfoService.Repository
                    .QueryDataNavAsync(s => s.OutboundDate <= DateTime.Now
                        && s.StockStatus == StockStatusEmun.入库完成.GetHashCode());
                if (expiredStocks == null || !expiredStocks.Any())
                {
                    return WebResponseContent.Instance.OK("无到期库存需要处理");
                }
                // è¿‡æ»¤æœ‰ä½ç½®ä¸”位置有库存的记录
                expiredStocks = expiredStocks
                    .Where(s => s.LocationDetails != null
                        && s.LocationDetails.LocationStatus == LocationStatusEnum.InStock.GetHashCode())
                    .ToList();
                if (!expiredStocks.Any())
                {
                    return WebResponseContent.Instance.OK("无符合条件的到期库存");
                }
                // 2. æ£€æŸ¥å·²å­˜åœ¨çš„任务
                var palletCodes = expiredStocks.Select(s => s.PalletCode).ToList();
                var existingTasks = await Repository.QueryDataAsync(t =>
                    palletCodes.Contains(t.PalletCode)
                    && (t.TaskStatus == TaskStatusEnum.New.GetHashCode()
                        || t.TaskStatus == TaskStatusEnum.SC_Executing.GetHashCode()
                        || t.TaskStatus == TaskInStatusEnum.InNew.GetHashCode()));
                var processedPallets = existingTasks.Select(t => t.PalletCode).ToHashSet();
                // 3. ç­›é€‰éœ€è¦å¤„理的库存
                var stocksToProcess = expiredStocks
                    .Where(s => !processedPallets.Contains(s.PalletCode))
                    .ToList();
                if (!stocksToProcess.Any())
                {
                    return WebResponseContent.Instance.OK("所有到期库存已存在任务");
                }
                // 4. èŽ·å–é…ç½®çš„ç›®æ ‡åœ°å€æ˜ å°„
                var targetAddressMap = _configuration.GetSection("AutoOutboundTask:TargetAddresses")
                    .Get<Dictionary<string, List<string>>>()
                    ?? new Dictionary<string, List<string>>();
                // 5. æ‰¹é‡åˆ›å»ºä»»åŠ¡
                var taskList = new List<Dt_Task>();
                foreach (var stock in stocksToProcess)
                {
                    // æ ¹æ®å··é“确定目标地址(优先根据 Remark ç¡®å®šï¼ŒRemark ä¸ºç©ºåˆ™æ ¹æ®å··é“配置)
                    var targetAddress = DetermineTargetAddressByRemark(
                        stock.Remark ?? "",
                        stock.LocationDetails?.RoadwayNo ?? "",
                        targetAddressMap);
                    var task = new Dt_Task
                    {
                        WarehouseId = stock.WarehouseId,
                        PalletCode = stock.PalletCode,
                        PalletType = stock.PalletType,
                        SourceAddress = stock.LocationCode,
                        CurrentAddress = stock.LocationCode,
                        NextAddress = targetAddress,
                        TargetAddress = targetAddress,
                        Roadway = stock.LocationDetails?.RoadwayNo ?? "",
                        TaskType = TaskTypeEnum.Outbound.GetHashCode(),
                        TaskStatus = TaskStatusEnum.New.GetHashCode(),
                        Grade = 1,
                        TaskNum = await BaseDal.GetTaskNo(),
                        Creater = "system_auto"
                    };
                    taskList.Add(task);
                }
                var transactionResult = await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    var addResult = await BaseDal.AddDataAsync(taskList) > 0;
                    if (!addResult)
                    {
                        return WebResponseContent.Instance.Error($"批量创建任务失败,共 {taskList.Count} ä¸ªä»»åŠ¡");
                    }
                    // ä»»åŠ¡åˆ›å»ºæˆåŠŸåŽï¼ŒåŒæ­¥é”å®šåº“å­˜å’Œè´§ä½çŠ¶æ€ï¼Œé¿å…é‡å¤åˆ†é…
                    var stocksToUpdate = stocksToProcess
                        .Select(s =>
                        {
                            s.StockStatus = StockStatusEmun.出库锁定.GetHashCode();
                            return s;
                        })
                        .ToList();
                    var updateStockResult = await _stockInfoService.Repository.UpdateDataAsync(stocksToUpdate);
                    if (!updateStockResult)
                    {
                        return WebResponseContent.Instance.Error($"任务创建成功,但库存状态更新失败,共 {stocksToUpdate.Count} æ¡");
                    }
                    var locationsToUpdate = stocksToProcess
                        .Where(s => s.LocationDetails != null)
                        .GroupBy(s => s.LocationDetails.Id)
                        .Select(g =>
                        {
                            var location = g.First().LocationDetails;
                            location.LocationStatus = LocationStatusEnum.InStockLock.GetHashCode();
                            return location;
                        })
                        .ToList();
                    if (locationsToUpdate.Any())
                    {
                        var updateLocationResult = await _locationInfoService.Repository.UpdateDataAsync(locationsToUpdate);
                        if (!updateLocationResult)
                        {
                            return WebResponseContent.Instance.Error($"任务创建成功,但货位状态更新失败,共 {locationsToUpdate.Count} æ¡");
                        }
                    }
                    return WebResponseContent.Instance.OK();
                });
                if (!transactionResult.Status)
                {
                    return transactionResult;
                }
                // 6. é€šçŸ¥ WCS(异步,不影响主流程)
                _ = Task.Run(() =>
                {
                    try
                    {
                        var wmstaskDtos = _mapper.Map<List<WMSTaskDTO>>(taskList);
                        _httpClientHelper.Post<WebResponseContent>(
                            "http://localhost:9292/api/Task/ReceiveTask",
                            wmstaskDtos.ToJson());
                    }
                    catch (Exception ex)
                    {
                        // WCS é€šçŸ¥å¤±è´¥ä¸å½±å“ä»»åŠ¡åˆ›å»ºï¼Œè®°å½•æ—¥å¿—å³å¯
                        Console.WriteLine($"WCS æ‰¹é‡é€šçŸ¥å¤±è´¥ï¼Œä»»åŠ¡æ•°é‡: {taskList.Count}, é”™è¯¯: {ex.Message}");
                    }
                });
                return WebResponseContent.Instance.OK($"成功创建 {taskList.Count} ä¸ªå‡ºåº“任务", taskList.Count);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"自动创建出库任务失败: {ex.Message}");
            }
        }
        #endregion è‡ªåŠ¨å‡ºåº“ä»»åŠ¡
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Inbound.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,210 @@
using WIDESEA_Common.Constants;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Task;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region å…¥åº“任务
        /// <summary>
        /// åˆ›å»ºä»»åŠ¡ï¼ˆç»„ç›˜å…¥åº“ä»»åŠ¡ã€ç©ºæ‰˜ç›˜å›žåº“ä»»åŠ¡ï¼‰
        /// </summary>
        public async Task<WebResponseContent> CreateTaskInboundAsync(CreateTaskDto taskDto)
        {
            try
            {
                WebResponseContent content = await GetTaskByPalletCodeAsync(taskDto.PalletCode);
                if (content.Status)
                {
                    return content;
                }
                if (string.IsNullOrWhiteSpace(taskDto.PalletCode) ||
                    string.IsNullOrWhiteSpace(taskDto.Roadway))
                {
                    return WebResponseContent.Instance.Error("无效的任务详情");
                }
                if (taskDto.TaskType != TaskTypeEnum.Inbound && taskDto.TaskType != TaskTypeEnum.InEmpty)
                {
                    return WebResponseContent.Instance.Error("无效的任务详情");
                }
                // ä½¿ç”¨ switch è¡¨è¾¾å¼æ˜ å°„任务类型
                int taskInboundType = taskDto.TaskType switch
                {
                    TaskTypeEnum.Inbound => TaskInboundTypeEnum.Inbound.GetHashCode(),
                    TaskTypeEnum.InEmpty => TaskInboundTypeEnum.InEmpty.GetHashCode(),
                    _ => 0 // ç†è®ºä¸Šä¸ä¼šèµ°åˆ°è¿™é‡Œï¼Œå› ä¸ºå·²ç»éªŒè¯è¿‡äº†
                };
                var task = new Dt_Task
                {
                    TaskNum = await BaseDal.GetTaskNo(),
                    PalletCode = taskDto.PalletCode,
                    PalletType = taskDto.PalletType,
                    Roadway = taskDto.Roadway,
                    TaskType = taskInboundType,
                    TaskStatus = TaskInStatusEnum.InNew.GetHashCode(),
                    SourceAddress = taskDto.SourceAddress,
                    TargetAddress = taskDto.TargetAddress,
                    CurrentAddress = taskDto.SourceAddress,
                    NextAddress = taskDto.TargetAddress,
                    WarehouseId = taskDto.WarehouseId,
                    Grade = 1,
                    Creater = "system"
                };
                var result = await Repository.AddDataAsync(task) > 0;
                if (!result) return WebResponseContent.Instance.Error("任务创建失败");
                var wmstaskDto = _mapper.Map<WMSTaskDTO>(task);
                return WebResponseContent.Instance.OK("任务创建成功", wmstaskDto);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"任务创建失败: {ex.Message}");
            }
        }
        /// <summary>
        /// èŽ·å–å¯å…¥åº“è´§ä½
        /// </summary>
        public async Task<WebResponseContent> GetTasksLocationAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
                var locationInfo = await _locationInfoService.GetLocationInfo(task.Roadway);
                if (locationInfo == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    locationInfo.LocationStatus = LocationStatusEnum.FreeLock.GetHashCode();
                    task.CurrentAddress = task.SourceAddress;
                    task.NextAddress = locationInfo.LocationCode;
                    task.TargetAddress = locationInfo.LocationCode;
                    task.TaskStatus = TaskInStatusEnum.Line_InFinish.GetHashCode();
                    var updateTaskResult = await BaseDal.UpdateDataAsync(task);
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
                    if (!updateTaskResult || !updateLocationResult)
                    {
                        return WebResponseContent.Instance.Error("任务更新失败");
                    }
                    return WebResponseContent.Instance.OK("任务更新成功", locationInfo.LocationCode);
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"获取任务失败: {ex.Message}");
            }
        }
        /// <summary>
        /// å…¥åº“任务完成:添加库存,修改货位状态,删除任务数据,添加历史任务数据
        /// </summary>
        public async Task<WebResponseContent> InboundFinishTaskAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
                var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.TargetAddress);
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                // åˆ¤æ–­æ˜¯ä¸æ˜¯æžå·åº“任务
                if (taskDto.WarehouseId == (int)WarehouseEnum.FJ1 || taskDto.WarehouseId == (int)WarehouseEnum.ZJ1)
                {
                    return await CompleteAgvInboundTaskAsync(taskDto);
                }
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    WebResponseContent content = new WebResponseContent();
                    stockInfo.LocationCode = location.LocationCode;
                    stockInfo.LocationId = location.Id;
                    SetOutboundDateByRoadway(task, stockInfo);
                    stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
                    location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    // è°ƒç”¨MES托盘进站
                    //var inboundRequest = new InboundInContainerRequest
                    //{
                    //    EquipmentCode = "STK-GROUP-001",
                    //    ResourceCode = "STK-GROUP-001",
                    //    LocalTime = DateTime.Now,
                    //    ContainerCode = taskDto.PalletCode
                    //};
                    //var inboundResult = _mesService.InboundInContainer(inboundRequest);
                    //if (inboundResult == null || inboundResult.Data == null || !inboundResult.Data.IsSuccess)
                    //{
                    //    return content.Error($"任务完成失败:MES进站失败: {inboundResult?.Data?.Msg ?? inboundResult?.ErrorMessage ?? "未知错误"}");
                    //}
                    return await CompleteTaskAsync(task, "入库完成");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ ¹æ®å··é“类型设置库存的出库时间和备注
        /// </summary>
        /// <param name="task">任务信息</param>
        /// <param name="stockInfo">库存信息</param>
        private void SetOutboundDateByRoadway(Dt_Task task, Dt_StockInfo stockInfo)
        {
            var now = DateTime.Now;
            if (task.Roadway.Contains("GW"))
            {
                stockInfo.OutboundDate = string.IsNullOrEmpty(stockInfo.Remark)
                    ? now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_GW1_FIRST)
                    : stockInfo.Remark == StockRemarkConstants.GW1
                        ? now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_GW1_SECOND)
                        : now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_GW1_FIRST);
                stockInfo.Remark = string.IsNullOrEmpty(stockInfo.Remark)
                    ? StockRemarkConstants.GW1
                    : stockInfo.Remark == StockRemarkConstants.GW1
                        ? StockRemarkConstants.GW2
                        : stockInfo.Remark;
            }
            else if (task.Roadway.Contains("CW"))
            {
                stockInfo.OutboundDate = now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_CW1);
                if (stockInfo.Remark == StockRemarkConstants.GW2)
                    stockInfo.Remark = StockRemarkConstants.CW1;
            }
            else
            {
                stockInfo.OutboundDate = now;
            }
        }
        #endregion å…¥åº“任务
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Manual.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,328 @@
using System.ComponentModel;
using System.Reflection;
using Newtonsoft.Json;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.Task;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region æ‰‹åŠ¨ä»»åŠ¡
        /// <summary>
        /// æ‰‹åŠ¨åˆ›å»ºä»»åŠ¡
        /// </summary>
        /// <param name="dto">手动创建任务参数</param>
        /// <returns></returns>
        public async Task<WebResponseContent> CreateManualTaskAsync(CreateManualTaskDto dto)
        {
            try
            {
                int taskType;
                int taskStatus;
                switch (dto.TaskType)
                {
                    case "入库":
                        taskType = TaskInboundTypeEnum.Inbound.GetHashCode();
                        taskStatus = TaskInStatusEnum.InNew.GetHashCode();
                        break;
                    case "出库":
                        taskType = TaskOutboundTypeEnum.Outbound.GetHashCode();
                        taskStatus = TaskOutStatusEnum.OutNew.GetHashCode();
                        break;
                    case "移库":
                        taskType = TaskRelocationTypeEnum.Relocation.GetHashCode();
                        taskStatus = TaskRelocationStatusEnum.RelocationNew.GetHashCode();
                        break;
                    default:
                        return WebResponseContent.Instance.Error($"不支持的任务类型: {dto.TaskType}");
                }
                int taskNum = await BaseDal.GetTaskNo();
                var task = new Dt_Task
                {
                    TaskNum = taskNum,
                    PalletCode = dto.Barcode,
                    SourceAddress = dto.SourceAddress,
                    TargetAddress = dto.TargetAddress,
                    TaskType = taskType,
                    TaskStatus = taskStatus,
                    Grade = dto.Grade,
                    Roadway = dto.TargetAddress,
                    WarehouseId = dto.WarehouseId,
                    CurrentAddress = dto.SourceAddress,
                    NextAddress = dto.TargetAddress,
                    Creater = "manual",
                    CreateDate = DateTime.Now,
                    ModifyDate = DateTime.Now
                };
                var wmsTaskDtos = new List<WMSTaskDTO>()
                {
                    new()
                    {
                        TaskNum = task.TaskNum,
                        PalletCode = task.PalletCode,
                        SourceAddress = task.SourceAddress,
                        TargetAddress = task.TargetAddress,
                        TaskType = task.TaskType,
                        Roadway = task.Roadway,
                        TaskStatus = task.TaskStatus,
                        WarehouseId = task.WarehouseId
                    }
                };
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    // ä¿å­˜åˆ°æ•°æ®åº“并同步发送给WCS
                    var result = await BaseDal.AddDataAsync(task) > 0;
                    if (!result)
                        return WebResponseContent.Instance.Error("创建任务失败");
                    var wcsResult = _httpClientHelper.Post<WebResponseContent>(
                        "http://localhost:9292/api/Task/ReceiveManualTask",
                        wmsTaskDtos.ToJson());
                    if (!wcsResult.IsSuccess || !wcsResult.Data.Status)
                        return WebResponseContent.Instance.Error($"任务已创建但发送给WCS失败: {wcsResult.Data?.Message}");
                    return WebResponseContent.Instance.OK($"手动创建任务成功,任务号: {taskNum}");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"手动创建任务异常: {ex.Message}");
            }
        }
        /// <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>()
                };
                // ç¬¬ä¸€æ­¥ï¼šæŸ¥è¯¢å¹¶æ ¡éªŒæ‰€æœ‰ä»»åŠ¡çŠ¶æ€ï¼Œæ”¶é›†æœ‰æ•ˆä»»åŠ¡
                var validTasks = new List<(DispatchTaskDto Dto, Dt_Task Task)>();
                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;
                    }
                    validTasks.Add((dto, task));
                }
                // å¦‚果全部校验失败,直接返回
                if (validTasks.Count == 0)
                    return WebResponseContent.Instance.Error($"下发失败,共{resultDto.FailCount}个任务");
                // ç¬¬äºŒæ­¥ï¼šæž„造所有WMSTaskDTO,一次性调用WCS
                var wmsTaskDtos = validTasks.Select(vt => new WMSTaskDTO
                {
                    Id = vt.Task.TaskId,
                    TaskNum = vt.Task.TaskNum,
                    PalletCode = vt.Dto.PalletCode,
                    SourceAddress = vt.Dto.SourceAddress,
                    TargetAddress = vt.Dto.TargetAddress,
                    CurrentAddress = vt.Dto.SourceAddress,
                    NextAddress = vt.Dto.TargetAddress,
                    TaskType = vt.Task.TaskType,
                    TaskStatus = vt.Task.TaskStatus,
                    Roadway = vt.Task.Roadway,
                    Grade = vt.Dto.Grade,
                    WarehouseId = vt.Task.WarehouseId,
                    PalletType = vt.Task.PalletType
                }).ToList();
                // ä¸€æ¬¡æ€§è°ƒç”¨WCS批量接口
                var wcsResult = _httpClientHelper.Post<WebResponseContent>(
                    "http://localhost:9292/api/Task/ReceiveManualTask",
                    wmsTaskDtos.ToJson());
                if (wcsResult == null || !wcsResult.IsSuccess)
                {
                    // WCS调用失败,所有任务都算失败
                    // å°è¯•从WCS响应中解析错误详情
                    string errorDetail = "";
                    if (wcsResult?.Data != null)
                    {
                        // å°è¯•å°†Data转换为错误信息
                        try
                        {
                            var errorData = wcsResult.Data.ToString();
                            errorDetail = $"WCS错误: {errorData}";
                        }
                        catch
                        {
                            errorDetail = wcsResult?.ErrorMessage ?? "WCS响应异常";
                        }
                    }
                    else
                    {
                        errorDetail = wcsResult?.ErrorMessage ?? "WCS响应异常";
                    }
                    foreach (var vt in validTasks)
                    {
                        resultDto.FailResults.Add(new DispatchTaskResultDto
                        {
                            TaskId = vt.Dto.TaskId,
                            TaskNum = vt.Task.TaskNum,
                            Success = false,
                            ErrorMessage = $"{errorDetail} (任务号:{vt.Task.TaskNum}, æ‰˜ç›˜:{vt.Dto.PalletCode})"
                        });
                        resultDto.FailCount++;
                    }
                    resultDto.SuccessCount = 0;
                    return WebResponseContent.Instance.Error($"WCS批量下发失败,共{resultDto.FailCount}个任务");
                }
                // WCS调用成功,解析返回的结构化数据
                ReceiveTaskResultDto wcsResultData = null;
                try
                {
                    if (wcsResult.Data?.Data != null)
                    {
                        var jsonStr = wcsResult.Data.Data.ToString();
                        if (!string.IsNullOrEmpty(jsonStr) && jsonStr.Contains("duplicateTasks"))
                        {
                            wcsResultData = JsonConvert.DeserializeObject<ReceiveTaskResultDto>(jsonStr);
                        }
                    }
                }
                catch (Exception ex)
                {
                    // è§£æžWCS返回数据失败,记录日志但继续处理
                    Console.WriteLine($"解析WCS返回数据异常: {ex.Message}");
                }
                // å¦‚果有重复任务,记录到失败结果中
                if (wcsResultData?.DuplicateTasks != null && wcsResultData.DuplicateTasks.Count > 0)
                {
                    foreach (var dup in wcsResultData.DuplicateTasks)
                    {
                        var statusName = GetTaskStatusName(dup.TaskType, dup.TaskStatus);
                        resultDto.FailResults.Add(new DispatchTaskResultDto
                        {
                            TaskId = 0, // é‡å¤ä»»åŠ¡å¯èƒ½æ²¡æœ‰WMS的TaskId
                            TaskNum = dup.TaskNum,
                            Success = false,
                            ErrorMessage = $"WCS中已存在该任务(托盘:{dup.PalletCode}, çŠ¶æ€:{statusName})"
                        });
                        resultDto.FailCount++;
                    }
                }
                // ç¬¬ä¸‰æ­¥ï¼šWCS调用成功后,批量更新DB
                foreach (var vt in validTasks)
                {
                    vt.Task.PalletCode = vt.Dto.PalletCode;
                    vt.Task.SourceAddress = vt.Dto.SourceAddress;
                    vt.Task.TargetAddress = vt.Dto.TargetAddress;
                    vt.Task.CurrentAddress = vt.Dto.SourceAddress;
                    vt.Task.NextAddress = vt.Dto.TargetAddress;
                    vt.Task.Grade = vt.Dto.Grade;
                    vt.Task.Dispatchertime = DateTime.Now;
                }
                // æ‰¹é‡æ›´æ–°DB
                var tasksToUpdate = validTasks.Select(vt => vt.Task).ToList();
                await BaseDal.UpdateDataAsync(tasksToUpdate);
                resultDto.SuccessCount = validTasks.Count - (wcsResultData?.DuplicateTasks?.Count ?? 0);
                if (resultDto.FailCount == 0)
                    return WebResponseContent.Instance.OK($"成功下发{resultDto.SuccessCount}个任务", resultDto);
                var errorResponse = WebResponseContent.Instance.Error($"部分下发成功{resultDto.SuccessCount}个,失败{resultDto.FailCount}个");
                errorResponse.Data = resultDto;
                return errorResponse;
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"下发任务异常: {ex.Message}");
            }
        }
        /// <summary>
        /// èŽ·å–ä»»åŠ¡çŠ¶æ€åç§°
        /// </summary>
        /// <param name="taskType">任务类型</param>
        /// <param name="taskStatus">任务状态</param>
        /// <returns>状态的中文描述</returns>
        private string GetTaskStatusName(int taskType, int taskStatus)
        {
            FieldInfo? fieldInfo = taskType switch
            {
                _ when taskType == TaskInboundTypeEnum.Inbound.GetHashCode() => typeof(TaskInStatusEnum).GetField(((TaskInStatusEnum)taskStatus).ToString()),
                _ when taskType == TaskOutboundTypeEnum.Outbound.GetHashCode() => typeof(TaskOutStatusEnum).GetField(((TaskOutStatusEnum)taskStatus).ToString()),
                _ when taskType == TaskRelocationTypeEnum.Relocation.GetHashCode() => typeof(TaskRelocationStatusEnum).GetField(((TaskRelocationStatusEnum)taskStatus).ToString()),
                _ => null
            };
            var descAttr = fieldInfo?.GetCustomAttributes(typeof(DescriptionAttribute), false)
                .FirstOrDefault() as DescriptionAttribute;
            return descAttr?.Description ?? taskStatus.ToString();
        }
        #endregion æ‰‹åŠ¨ä»»åŠ¡
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Outbound.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
using WIDESEA_Common.Constants;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Task;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region å‡ºåº“任务
        /// <summary>
        /// æ ¹æ®æŒ‡å®šçš„任务详情异步创建新的出库任务
        /// </summary>
        public async Task<WebResponseContent> CreateTaskOutboundAsync(CreateTaskDto taskDto)
        {
            try
            {
                var stockResult = await _stockInfoService.GetStockInfoAsync(taskDto.WarehouseId);
                if (stockResult == null || !stockResult.Any())
                    return WebResponseContent.Instance.Error("未找到库存信息");
                var taskList = stockResult.Select(item => new Dt_Task
                {
                    WarehouseId = item.WarehouseId,
                    PalletCode = item.PalletCode,
                    PalletType = item.PalletType,
                    SourceAddress = item.LocationCode,
                    TargetAddress = taskDto.TargetAddress,
                    Roadway = item.LocationDetails.RoadwayNo,
                    TaskType = TaskTypeEnum.Outbound.GetHashCode(),
                    TaskStatus = TaskStatusEnum.New.GetHashCode(),
                    Grade = 1,
                    TaskNum = 0,
                    CurrentAddress = item.LocationCode,
                    NextAddress = taskDto.TargetAddress,
                    Creater = "system",
                }).ToList();
                var result = await BaseDal.AddDataAsync(taskList) > 0;
                var wmstaskDto = result ? _mapper.Map<WMSTaskDTO>(taskList) : null;
                return WebResponseContent.Instance.OK(result ? "任务创建成功" : "任务创建失败", wmstaskDto ?? new object());
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"任务创建失败: {ex.Message}");
            }
        }
        /// <summary>
        /// å‡ºåº“任务完成 ï¼šä¿®æ”¹åº“存,修改货位状态,删除任务数据,添加历史任务数据
        /// </summary>
        public async Task<WebResponseContent> OutboundFinishTaskAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
                var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.SourceAddress);
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                // åˆ¤æ–­æ˜¯ä¸æ˜¯æžå·åº“任务
                if (taskDto.WarehouseId == (int)WarehouseEnum.FJ1 || taskDto.WarehouseId == (int)WarehouseEnum.ZJ1)
                {
                    OutTaskCompleteDto outTaskCompleteDto = new OutTaskCompleteDto()
                    {
                        TaskId = task.OrderNo ?? string.Empty,
                        DevId = task.TargetAddress ?? string.Empty,
                        ReqTime = DateTime.Now.ToString()
                    };
                    return await OutTaskComplete(outTaskCompleteDto);
                }
                WebResponseContent content = new WebResponseContent();
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    stockInfo.LocationId = 0;
                    stockInfo.LocationCode = string.Empty;
                    stockInfo.OutboundDate = DateTime.Now;
                    location.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    // é«˜æ¸©2号出库到CWSC1时,自动创建入库任务到常温1号巷道
                    WMSTaskDTO? inboundTaskDto = null;
                    if (task.TargetAddress == TaskAddressConstants.GW2_ADDRESS)
                    {
                        var inboundTask = new Dt_Task
                        {
                            TaskNum = await BaseDal.GetTaskNo(),
                            PalletCode = task.PalletCode,
                            PalletType = task.PalletType,
                            Roadway = "CW1",
                            TaskType = TaskInboundTypeEnum.Inbound.GetHashCode(),
                            TaskStatus = TaskInStatusEnum.InNew.GetHashCode(),
                            SourceAddress = task.TargetAddress,
                            TargetAddress = task.TargetAddress,
                            CurrentAddress = task.TargetAddress,
                            NextAddress = task.TargetAddress,
                            WarehouseId = (int)WarehouseEnum.CW1,
                            Grade = 1,
                            Creater = "system_auto"
                        };
                        await Repository.AddDataAsync(inboundTask);
                        inboundTaskDto = _mapper.Map<WMSTaskDTO>(inboundTask);
                    }
                    var completeResult = await CompleteTaskAsync(task, "出库完成");
                    if (!completeResult.Status)
                        return completeResult;
                    // è¿”回入库任务信息(如果有)
                    if (inboundTaskDto != null)
                    {
                        return content.OK("出库完成,已创建入库任务", inboundTaskDto);
                    }
                    return content.OK("出库完成");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
            }
        }
        #endregion å‡ºåº“任务
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Relocation.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Task;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region ç§»åº“任务
        /// <summary>
        /// ç§»åº“任务完成:修改库存位置与状态,修改源/目标货位状态,删除任务数据
        /// </summary>
        public async Task<WebResponseContent> RelocationFinishTaskAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s =>
                    s.PalletCode == taskDto.PalletCode &&
                    s.TaskType == TaskRelocationTypeEnum.Relocation.GetHashCode());
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的移库任务");
                var sourceLocation = await _locationInfoService.GetLocationInfo(task.Roadway, task.SourceAddress);
                if (sourceLocation == null) return WebResponseContent.Instance.Error("未找到移库源货位");
                var targetLocation = await _locationInfoService.GetLocationInfo(task.Roadway, task.TargetAddress);
                if (targetLocation == null) return WebResponseContent.Instance.Error("未找到移库目标货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    stockInfo.LocationCode = targetLocation.LocationCode;
                    stockInfo.LocationId = targetLocation.Id;
                    stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
                    sourceLocation.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                    targetLocation.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                    var updateSourceResult = await _locationInfoService.UpdateLocationInfoAsync(sourceLocation);
                    var updateTargetResult = await _locationInfoService.UpdateLocationInfoAsync(targetLocation);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateSourceResult || !updateTargetResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("移库任务完成失败");
                    return await CompleteTaskAsync(task, "移库完成");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
            }
        }
        #endregion ç§»åº“任务
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Robot.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Stock;
using WIDESEA_DTO.Task;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region æœºæ¢°æ‰‹ä»»åŠ¡
        /// <summary>
        /// åˆ›å»ºæœºæ¢°æ‰‹ç»„盘任务
        /// </summary>
        public async Task<WebResponseContent> CreateRobotGroupPalletTaskAsync(StockDTO stock)
        {
            return await CreateRobotPalletTaskAsync(
                stock,
                "组盘",
                RobotTaskTypeEnum.GroupPallet,
                s => string.IsNullOrWhiteSpace(s.TargetPalletNo) ? s.SourcePalletNo : s.TargetPalletNo,
                requireStockWithoutLocation: false);
        }
        /// <summary>
        /// åˆ›å»ºæœºæ¢°æ‰‹æ¢ç›˜ä»»åŠ¡
        /// </summary>
        public async Task<WebResponseContent> CreateRobotChangePalletTaskAsync(StockDTO stock)
        {
            return await CreateRobotPalletTaskAsync(
                stock,
                "换盘",
                RobotTaskTypeEnum.ChangePallet,
                s => s.SourcePalletNo,
                requireStockWithoutLocation: true,
                stockPalletCodeSelector: s => s.SourcePalletNo);
        }
        /// <summary>
        /// åˆ›å»ºæœºæ¢°æ‰‹æ‹†ç›˜ä»»åŠ¡
        /// </summary>
        public async Task<WebResponseContent> CreateRobotSplitPalletTaskAsync(StockDTO stock)
        {
            return await CreateRobotPalletTaskAsync(
                stock,
                "拆盘",
                RobotTaskTypeEnum.SplitPallet,
                s => s.SourcePalletNo,
                requireStockWithoutLocation: true,
                stockPalletCodeSelector: s => s.SourcePalletNo);
        }
        /// <summary>
        /// åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡çš„é€šç”¨æ–¹æ³•
        /// </summary>
        /// <param name="stock">库存信息</param>
        /// <param name="taskName">任务名称(组盘/换盘/拆盘)</param>
        /// <param name="taskType">机械手任务类型</param>
        /// <param name="palletCodeSelector">托盘号选择器</param>
        /// <param name="requireStockWithoutLocation">是否要求库存未绑定货位</param>
        /// <param name="stockPalletCodeSelector">库存查询用的托盘号选择器</param>
        /// <returns>创建结果</returns>
        private async Task<WebResponseContent> CreateRobotPalletTaskAsync(
            StockDTO stock,
            string taskName,
            RobotTaskTypeEnum taskType,
            Func<StockDTO, string?> palletCodeSelector,
            bool requireStockWithoutLocation,
            Func<StockDTO, string?>? stockPalletCodeSelector = null)
        {
            try
            {
                if (stock == null)
                    return WebResponseContent.Instance.Error("任务参数不能为空");
                var palletCode = palletCodeSelector(stock)?.Trim();
                if (string.IsNullOrWhiteSpace(palletCode))
                    return WebResponseContent.Instance.Error("托盘号不能为空");
                var sourceLineNo = stock.SourceLineNo?.Trim();
                var targetLineNo = stock.TargetLineNo?.Trim();
                if (string.IsNullOrWhiteSpace(sourceLineNo) || string.IsNullOrWhiteSpace(targetLineNo))
                    return WebResponseContent.Instance.Error("来源线体编号和目标线体编号不能为空");
                var existingTask = await BaseDal.QueryFirstAsync(t =>
                    t.PalletCode == palletCode &&
                    (t.TaskStatus == TaskRobotStatusEnum.RobotNew.GetHashCode()
                     || t.TaskStatus == TaskRobotStatusEnum.RobotExecuting.GetHashCode()
                     || t.TaskStatus == TaskRobotStatusEnum.RobotPickFinish.GetHashCode()
                     || t.TaskStatus == TaskRobotStatusEnum.RobotPutFinish.GetHashCode()
                     || t.TaskStatus == TaskRobotStatusEnum.RobotPending.GetHashCode()));
                if (existingTask != null)
                    return WebResponseContent.Instance.Error($"托盘[{palletCode}]已存在未完成任务");
                Dt_StockInfo? stockInfo = null;
                if (requireStockWithoutLocation)
                {
                    var stockPalletCode = (stockPalletCodeSelector ?? palletCodeSelector).Invoke(stock)?.Trim();
                    if (string.IsNullOrWhiteSpace(stockPalletCode))
                        return WebResponseContent.Instance.Error("源托盘号不能为空");
                    stockInfo = await _stockInfoService.GetStockInfoAsync(stockPalletCode);
                    if (stockInfo == null)
                        return WebResponseContent.Instance.Error($"托盘[{stockPalletCode}]库存不存在");
                    if (stockInfo.LocationId > 0 || !string.IsNullOrWhiteSpace(stockInfo.LocationCode))
                        return WebResponseContent.Instance.Error($"托盘[{stockPalletCode}]库存已绑定货位,不能创建机械手{taskName}任务");
                }
                var section = App.Configuration.GetSection("RobotTaskAddressRules").GetSection(targetLineNo).GetChildren().Select(c => c.Value).ToArray();
                if (section.Length < 2)
                    return WebResponseContent.Instance.Error($"未找到线体[{targetLineNo}]的地址配置");
                var task = new Dt_Task
                {
                    TaskNum = await BaseDal.GetTaskNo(),
                    PalletCode = palletCode,
                    PalletType = stockInfo?.PalletType ?? 0,
                    Roadway = stock.Roadway ?? string.Empty,
                    TaskType = taskType.GetHashCode(),
                    TaskStatus = TaskRobotStatusEnum.RobotNew.GetHashCode(),
                    SourceAddress = section[0]!,
                    TargetAddress = section[1]!,
                    CurrentAddress = targetLineNo,
                    NextAddress = targetLineNo,
                    WarehouseId = stockInfo?.WarehouseId ?? 1,
                    Grade = 1,
                    Remark = $"机械手{taskName}",
                    Creater = "system"
                };
                var result = await Repository.AddDataAsync(task) > 0;
                if (!result)
                    return WebResponseContent.Instance.Error($"机械手{taskName}任务创建失败");
                var wmstaskDto = _mapper.Map<WMSTaskDTO>(task) ?? new WMSTaskDTO();
                return WebResponseContent.Instance.OK($"机械手{taskName}任务创建成功", wmstaskDto);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"机械手{taskName}任务创建失败: {ex.Message}");
            }
        }
        #endregion æœºæ¢°æ‰‹ä»»åŠ¡
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_TaskStatus.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Task;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region ä»»åŠ¡çŠ¶æ€ç®¡ç†
        /// <summary>
        /// ä¿®æ”¹ä»»åŠ¡çŠ¶æ€ï¼ˆæ ¹æ®ä»»åŠ¡ID修改为指定状态)
        /// </summary>
        /// <param name="taskDto">任务状态更新参数</param>
        /// <returns>修改结果</returns>
        public async Task<WebResponseContent> UpdateTaskByStatusAsync(UpdateTaskDto taskDto)
        {
            try
            {
                var taskInfo = await BaseDal.QueryFirstAsync(s => s.TaskNum == taskDto.Id);
                if (taskInfo == null)
                    return WebResponseContent.Instance.Error("未找到对应的任务");
                taskInfo.TaskStatus = taskDto.NewStatus;
                taskInfo.NextAddress = taskDto.NextAddress;
                taskInfo.CurrentAddress = taskDto.CurrentAddress;
                await BaseDal.UpdateDataAsync(taskInfo);
                return WebResponseContent.Instance.OK("修改成功", taskInfo);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"修改失败: {ex.Message}");
            }
        }
        #endregion ä»»åŠ¡çŠ¶æ€ç®¡ç†
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Tray.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,197 @@
using Mapster;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Task;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        #region ç©ºæ‰˜ç›˜ä»»åŠ¡
        /// <summary>
        /// åˆ›å»ºç©ºæ‰˜ç›˜å…¥åº“任务
        /// </summary>
        /// <param name="taskDto"></param>
        /// <returns></returns>
        public async Task<WebResponseContent> CreateTaskInboundTrayAsync(CreateTaskDto taskDto)
        {
            try
            {
                WebResponseContent content = await GetTaskByPalletCodeAsync(taskDto.PalletCode);
                if (content.Status)
                {
                    return content;
                }
                return WebResponseContent.Instance.OK("查询成功");
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"查询任务失败: {ex.Message}");
            }
        }
        /// <summary>
        /// ç©ºæ‰˜ç›˜å…¥åº“完成
        /// </summary>
        public async Task<WebResponseContent> InboundFinishTaskTrayAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
                var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.TargetAddress);
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    stockInfo.LocationCode = location.LocationCode;
                    stockInfo.LocationId = location.Id;
                    stockInfo.StockStatus = StockStatusEmun.空托盘库存.GetHashCode();
                    location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    var saveTaskHistoryResult = await SaveTaskHistoryAsync(task, "空托盘入库完成");
                    if (!saveTaskHistoryResult.Status)
                        return saveTaskHistoryResult;
                    var saveStockHistoryResult = await SaveStockHistoryAsync(stockInfo, "空托盘入库完成");
                    if (!saveStockHistoryResult.Status)
                        return saveStockHistoryResult;
                    var deleteResult = await BaseDal.DeleteDataAsync(task);
                    if (!deleteResult) return WebResponseContent.Instance.Error("任务完成失败");
                    return WebResponseContent.Instance.OK("任务完成");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
            }
        }
        /// <summary>
        /// åˆ›å»ºç©ºæ‰˜ç›˜å‡ºåº“任务
        /// </summary>
        /// <param name="taskDto"></param>
        /// <returns></returns>
        public async Task<WebResponseContent> GetOutBoundTrayTaskAsync(CreateTaskDto taskDto)
        {
            try
            {
                var dt_Task = await BaseDal.QueryFirstAsync(x => x.TargetAddress == taskDto.TargetAddress);
                if (dt_Task != null)
                {
                    var taskDTO = dt_Task.Adapt<WMSTaskDTO>();
                    return WebResponseContent.Instance.OK("任务创建成功", taskDTO);
                }
                var stockInfo = await _stockInfoService.Repository.QueryDataNavFirstAsync(x => x.LocationDetails.WarehouseId == taskDto.WarehouseId && x.LocationDetails.LocationStatus == LocationStatusEnum.InStock.GetHashCode() && x.StockStatus == StockStatusEmun.空托盘库存.GetHashCode());
                if (stockInfo == null)
                    return WebResponseContent.Instance.Error("未找到对应的库存信息");
                var task = new Dt_Task()
                {
                    WarehouseId = stockInfo.LocationDetails.WarehouseId,
                    PalletCode = stockInfo.PalletCode,
                    PalletType = stockInfo.PalletType,
                    SourceAddress = stockInfo.LocationCode,
                    CurrentAddress = stockInfo.LocationCode,
                    NextAddress = taskDto.TargetAddress,
                    TargetAddress = taskDto.TargetAddress,
                    Roadway = stockInfo.LocationDetails.RoadwayNo,
                    TaskType = TaskOutboundTypeEnum.OutEmpty.GetHashCode(),
                    TaskStatus = TaskStatusEnum.New.GetHashCode(),
                    Grade = 1,
                    TaskNum = await BaseDal.GetTaskNo(),
                    Creater = "system",
                };
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationId);
                    locationInfo.LocationStatus = LocationStatusEnum.InStockLock.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
                    if (!updateLocationResult)
                        return WebResponseContent.Instance.Error("任务创建失败");
                    var taskDtos = task.Adapt<WMSTaskDTO>();
                    var addResult = await BaseDal.AddDataAsync(task) > 0;
                    if (!addResult)
                        return WebResponseContent.Instance.Error("任务创建失败");
                    return WebResponseContent.Instance.OK("任务创建成功", taskDtos);
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"查询任务失败: {ex.Message}");
            }
        }
        /// <summary>
        /// ç©ºæ‰˜ç›˜å‡ºåº“完成
        /// </summary>
        public async Task<WebResponseContent> OutboundFinishTaskTrayAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
                var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.SourceAddress);
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await _unitOfWorkManage.BeginTranAsync(async () =>
                {
                    stockInfo.LocationId = 0;
                    stockInfo.LocationCode = string.Empty;
                    stockInfo.StockStatus = StockStatusEmun.出库完成.GetHashCode();
                    location.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    var saveTaskHistoryResult = await SaveTaskHistoryAsync(task, "空托盘出库完成");
                    if (!saveTaskHistoryResult.Status)
                        return saveTaskHistoryResult;
                    var saveStockHistoryResult = await SaveStockHistoryAsync(stockInfo, "空托盘出库完成");
                    if (!saveStockHistoryResult.Status)
                        return saveStockHistoryResult;
                    var deleteResult = await BaseDal.DeleteDataAsync(task);
                    if (!deleteResult) return WebResponseContent.Instance.Error("任务完成失败");
                    return WebResponseContent.Instance.OK("任务完成");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
            }
        }
        #endregion ç©ºæ‰˜ç›˜ä»»åŠ¡
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
@@ -58,6 +58,17 @@
        }
        /// <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);
        }
        /// <summary>
        /// èŽ·å–å¯å…¥åº“è´§ä½
        /// </summary>
        /// <param name="taskDto"></param>
Code/docs/superpowers/plans/2026-04-12-manual-dispatch-tasks-to-wcs-plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,723 @@
# æ‰‹åŠ¨ä¸‹å‘ä»»åŠ¡åˆ° 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**
```csharp
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**
```bash
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逻辑处理` ä¹‹å‰æ·»åŠ ï¼š**
```csharp
/// <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**
```bash
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 ä¹‹åŽï¼‰ï¼š**
```csharp
/// <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**
```bash
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 å®žçŽ°æ‰¹é‡ç¼–è¾‘ï¼š**
```vue
<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**
```bash
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()` æ–¹æ³•中添加手动下发按钮(在手动创建任务按钮之后):
```javascript
// æ·»åŠ "手动下发"按钮
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 å…³é”®éƒ¨åˆ†ï¼š**
```javascript
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**
```bash
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 åŽç«¯æž„建**
```bash
cd D:/Git/ShanMeiXinNengYuan/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer
dotnet build
```
- [ ] **WMS å‰ç«¯æž„建**
```bash
cd D:/Git/ShanMeiXinNengYuan/Code/WMS/WIDESEA_WMSClient
npm run serve
```
---
## éªŒæ”¶æ ‡å‡†
- [ ] å·¥å…·æ æ·»åŠ "手动下发"按钮,未选中任务时点击提示"请先选择任务"
- [ ] å¼¹çª—正确显示所有选中任务
- [ ] ä»…入库新单/出库新单/移库新单状态的任务可编辑地址和优先级
- [ ] å…¶ä»–状态任务行标红,地址/优先级不可编辑
- [ ] ç¡®è®¤ä¸‹å‘后调用后端接口
- [ ] æˆåŠŸæ—¶å¼¹çª—å…³é—­å¹¶åˆ·æ–°åˆ—è¡¨ï¼›å¤±è´¥æ—¶æ˜¾ç¤ºå¤±è´¥ä»»åŠ¡åŠåŽŸå› 
- [ ] æ‰¹é‡ä¸‹å‘时正确统计成功/失败数量
Code/docs/superpowers/specs/2026-04-12-manual-dispatch-tasks-to-wcs-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,154 @@
# æ‰‹åŠ¨ä¸‹å‘ä»»åŠ¡åˆ° WCS åŠŸèƒ½è®¾è®¡
**日期**: 2026-04-12
**状态**: å·²æ‰¹å‡†
---
## 1. åŠŸèƒ½æ¦‚è¿°
在 WMS å‰ç«¯ä»»åŠ¡ç®¡ç†é¡µé¢æ·»åŠ å·¥å…·æ æŒ‰é’®ï¼Œæ”¯æŒä»Žä»»åŠ¡åˆ—è¡¨ä¸­é€‰æ‹©ä¸€ä¸ªæˆ–å¤šä¸ªä»»åŠ¡ï¼Œæ‰‹åŠ¨ç¼–è¾‘åœ°å€ï¼ˆèµ·ç‚¹/终点)和优先级后,下发到 WCS ç³»ç»Ÿæ‰§è¡Œã€‚
**使用场景**:当任务因设备故障、网络问题等原因未能正常下发时,操作员可手动重新下发。
---
## 2. å‰ç«¯æ”¹åЍ
### 2.1 æ–°å¢žæ–‡ä»¶
| æ–‡ä»¶ | ç”¨é€” |
|------|------|
| `WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/dispatchTasksToWCS.vue` | æ‰¹é‡ä¸‹å‘编辑弹窗组件 |
### 2.2 ä¿®æ”¹æ–‡ä»¶
| æ–‡ä»¶ | æ”¹åЍ |
|------|------|
| `WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js` | æ·»åŠ å·¥å…·æ "手动下发"按钮 |
### 2.3 å¼¹çª—交互
**触发方式**:工具栏点击"手动下发"按钮
**前置条件**:至少选中一项任务
**弹窗内容**:
- é¡¶éƒ¨æ˜¾ç¤º"已选任务数: N ä¸ª"
- è¡¨æ ¼å±•示所有选中任务,列:任务号、起点地址、终点地址、优先级、状态
- åœ°å€å­—段:`el-input` æˆ– `el-select`(取决于是否有标准地址字典)
- ä¼˜å…ˆçº§ï¼š`el-input-number`,范围 1-99
- çŠ¶æ€åˆ—ï¼šä»…å±•ç¤ºï¼Œä¸å¯ç¼–è¾‘
**按钮**:取消、确认下发
**下发后**:
- æˆåŠŸï¼šå¼¹çª—å…³é—­ï¼Œåˆ·æ–°ä»»åŠ¡åˆ—è¡¨
- å¤±è´¥ï¼šå¼¹çª—内显示失败任务列表(任务号 + å¤±è´¥åŽŸå› ï¼‰
---
## 3. åŽç«¯æ”¹åЍ
### 3.1 æ–°å¢ž DTO
**文件**: `WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/DispatchTaskDto.cs`
```csharp
public class DispatchTaskDto
{
    public long TaskId { get; set; }
    public string SourceAddress { get; set; }
    public string TargetAddress { get; set; }
    public int Priority { get; set; }
}
```
### 3.2 æ–°å¢žæŽ¥å£
**Controller**: `WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs`
```
POST /api/TaskInfo/DispatchTasksToWCS
```
**Service**: `WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_WCS.cs`
新增方法 `DispatchTasksToWCSAsync(List<DispatchTaskDto> dtos)`
### 3.3 ä¸šåŠ¡é€»è¾‘
```
1. æ ¡éªŒé€‰ä¸­ä»»åŠ¡æ•°é‡ > 0
2. æ ¡éªŒä»»åŠ¡çŠ¶æ€ï¼Œä»…å…è®¸ä»¥ä¸‹çŠ¶æ€çš„ä»»åŠ¡ä¸‹å‘ï¼š
   - å…¥åº“:InNew(入库新单)
   - å‡ºåº“:OutNew(出库新单)
   - ç§»åº“:RelocationNew(移库新单)
3. æ›´æ–°ä»»åŠ¡çš„åœ°å€ï¼ˆSourceAddress、TargetAddress)和优先级(Grade)
4. è°ƒç”¨ WCS æŽ¥å£ï¼šPOST http://localhost:9292/api/Task/ReceiveManualTask
5. æ±‡æ€»æ¯æ¡ä»»åŠ¡çš„ä¸‹å‘ç»“æžœï¼ˆæˆåŠŸ/失败及原因)
6. è¿”回给前端
```
### 3.4 WCS æŽ¥å£å¤ç”¨
**已有接口**: `POST /api/Task/ReceiveManualTask`(commit b032763)
WMS è°ƒç”¨è¯¥æŽ¥å£ï¼Œå°†ä»»åŠ¡æ•°æ®ä»¥ `WMSTaskDTO` æ ¼å¼å‘送。
---
## 4. é”™è¯¯å¤„理
| åœºæ™¯ | å‰ç«¯å¤„理 | åŽç«¯å¤„理 |
|------|----------|----------|
| æœªé€‰ä¸­ä»»åŠ¡ | æŒ‰é’®ç½®ç°ï¼›ç‚¹å‡»æç¤º"请先选择任务" | è¿”回错误提示 |
| ä»»åŠ¡çŠ¶æ€ä¸å¯ä¸‹å‘ | è¡Œå†…状态列标红,地址/优先级置灰不可编辑 | æ ¡éªŒå¹¶è¿”回不可下发任务列表 |
| WCS è°ƒç”¨è¶…æ—¶ | è¯¥ä»»åŠ¡æ ‡è®°"下发超时" | æ ‡è®°ä»»åŠ¡ï¼Œä¸‹å‘ç»“æžœè¿”å›žå‰ç«¯ |
| WCS è¿”回失败 | æ˜¾ç¤º WCS é”™è¯¯åŽŸå› ï¼ˆå¦‚"设备忙") | é€ä¼  WCS é”™è¯¯ä¿¡æ¯ |
| å…¨éƒ¨æˆåŠŸ | å¼¹çª—关闭,刷新列表 | è¿”回成功 |
---
## 5. æ¶‰åŠçš„代码文件
| å±‚级 | æ–‡ä»¶è·¯å¾„ | æ”¹åŠ¨ç±»åž‹ |
|------|----------|----------|
| å‰ç«¯ | `WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js` | ä¿®æ”¹ |
| å‰ç«¯ | `WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/dispatchTasksToWCS.vue` | æ–°å¢ž |
| å‰ç«¯ | `WMS/WIDESEA_WMSClient/src/api/` | æ–°å¢ž API æ–¹æ³• |
| åŽç«¯ | `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` | ä¿®æ”¹ |
| åŽç«¯ | `WMS/WIDESEA_WMSServer/WIDESEA_Core/Helper/HTTP/HttpClientHelper.cs` | å¤ç”¨ |
---
## 6. UI ç¤ºæ„å›¾
```
┌─────────────────────────────────────────────────────────────┐
│  æ‰‹åŠ¨ä¸‹å‘ä»»åŠ¡åˆ° WCS                                    [X]  â”‚
├─────────────────────────────────────────────────────────────┤
│  å·²é€‰ä»»åŠ¡æ•°: 5 ä¸ª                                          â”‚
│  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”   â”‚
│  â”‚ ä»»åŠ¡å· â”‚ èµ·ç‚¹åœ°å€     â”‚ ç»ˆç‚¹åœ°å€    â”‚ ä¼˜å…ˆçº§ â”‚ çŠ¶æ€  â”‚   â”‚
│  â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¤   â”‚
│  â”‚ T001   â”‚ [PLC-A01  ] â”‚ [Rack-05] â”‚ [ 5 ] â”‚ å…¥åº“新单│   â”‚
│  â”‚ T002   â”‚ [PLC-A02  ] â”‚ [Rack-06] â”‚ [ 3 ] â”‚ å…¥åº“新单│   â”‚
│  â””─────────────────────────────────────────────────────┘   â”‚
├─────────────────────────────────────────────────────────────┤
│                                    [取消]  [确认下发]       â”‚
└─────────────────────────────────────────────────────────────┘
```
---
## 7. éªŒæ”¶æ ‡å‡†
- [ ] å·¥å…·æ æ·»åŠ "手动下发"按钮,未选中任务时按钮可用但点击提示选择
- [ ] å¼¹çª—正确显示所有选中任务
- [ ] ä»…入库新单/出库新单/移库新单状态的任务可编辑地址和优先级
- [ ] ç¡®è®¤ä¸‹å‘后调用后端接口
- [ ] æˆåŠŸ/失败结果正确反馈给用户
- [ ] æˆåŠŸåŽåˆ·æ–°ä»»åŠ¡åˆ—è¡¨