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 |
文件:
- 创建: WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/DispatchTaskDto.cs
using System.Text.Json.Serialization;
namespace WIDESEA_DTO.Task
{
/// <summary>
/// 手动下发任务Dto
/// </summary>
public class DispatchTaskDto
{
/// <summary>
/// 任务ID
/// </summary>
[JsonPropertyName("taskId")]
public long TaskId { get; set; }
/// <summary>
/// 起点地址
/// </summary>
[JsonPropertyName("sourceAddress")]
public string SourceAddress { get; set; }
/// <summary>
/// 终点地址
/// </summary>
[JsonPropertyName("targetAddress")]
public string TargetAddress { get; set; }
/// <summary>
/// 优先级
/// </summary>
[JsonPropertyName("grade")]
public int Grade { get; set; }
}
/// <summary>
/// 任务下发结果Dto
/// </summary>
public class DispatchTaskResultDto
{
/// <summary>
/// 任务ID
/// </summary>
[JsonPropertyName("taskId")]
public long TaskId { get; set; }
/// <summary>
/// 任务号
/// </summary>
[JsonPropertyName("taskNum")]
public int TaskNum { get; set; }
/// <summary>
/// 是否成功
/// </summary>
[JsonPropertyName("success")]
public bool Success { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
}
/// <summary>
/// 批量下发结果Dto
/// </summary>
public class DispatchResultDto
{
/// <summary>
/// 成功数量
/// </summary>
[JsonPropertyName("successCount")]
public int SuccessCount { get; set; }
/// <summary>
/// 失败数量
/// </summary>
[JsonPropertyName("failCount")]
public int FailCount { get; set; }
/// <summary>
/// 失败任务列表
/// </summary>
[JsonPropertyName("failResults")]
public List<DispatchTaskResultDto> FailResults { get; set; }
}
}
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>"
文件:
- 修改: WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_WCS.cs
前置了解:
- 任务状态枚举:TaskInStatusEnum.InNew(入库新单)、TaskOutStatusEnum.OutNew(出库新单)、TaskRelocationStatusEnum.RelocationNew(移库新单)
- 已有 WCS 调用方式:_httpClientHelper.Post<WebResponseContent>("http://localhost:9292/api/Task/ReceiveManualTask", json)
- 已有事务处理:await _unitOfWorkManage.BeginTranAsync(async () => { ... })
- 已有 WMSTaskDTO 用于给 WCS 发送任务
在 TaskService_WCS.cs 末尾 #endregion WCS逻辑处理 之前添加:
/// <summary>
/// 手动下发任务到WCS
/// </summary>
/// <param name="dtos">下发任务参数列表</param>
/// <returns>批量下发结果</returns>
public async Task<WebResponseContent> DispatchTasksToWCSAsync(List<DispatchTaskDto> dtos)
{
try
{
if (dtos == null || !dtos.Any())
return WebResponseContent.Instance.Error("请选择要下发的任务");
var resultDto = new DispatchResultDto
{
SuccessCount = 0,
FailCount = 0,
FailResults = new List<DispatchTaskResultDto>()
};
foreach (var dto in dtos)
{
var task = await BaseDal.QueryFirstAsync(t => t.TaskId == dto.TaskId);
if (task == null)
{
resultDto.FailResults.Add(new DispatchTaskResultDto
{
TaskId = dto.TaskId,
TaskNum = 0,
Success = false,
ErrorMessage = "任务不存在"
});
resultDto.FailCount++;
continue;
}
// 校验任务状态:仅入库新单/出库新单/移库新单可下发
bool canDispatch = false;
if (task.TaskType == TaskInboundTypeEnum.Inbound.GetHashCode()
&& task.TaskStatus == TaskInStatusEnum.InNew.GetHashCode())
canDispatch = true;
else if (task.TaskType == TaskOutboundTypeEnum.Outbound.GetHashCode()
&& task.TaskStatus == TaskOutStatusEnum.OutNew.GetHashCode())
canDispatch = true;
else if (task.TaskType == TaskRelocationTypeEnum.Relocation.GetHashCode()
&& task.TaskStatus == TaskRelocationStatusEnum.RelocationNew.GetHashCode())
canDispatch = true;
if (!canDispatch)
{
var statusName = GetTaskStatusName(task.TaskType, task.TaskStatus);
resultDto.FailResults.Add(new DispatchTaskResultDto
{
TaskId = dto.TaskId,
TaskNum = task.TaskNum,
Success = false,
ErrorMessage = $"任务状态[{statusName}]不允许下发"
});
resultDto.FailCount++;
continue;
}
// 更新任务的地址和优先级
task.SourceAddress = dto.SourceAddress;
task.TargetAddress = dto.TargetAddress;
task.CurrentAddress = dto.SourceAddress;
task.NextAddress = dto.TargetAddress;
task.Grade = dto.Grade;
task.Dispatchertime = DateTime.Now;
await BaseDal.UpdateDataAsync(task);
// 构造WMSTaskDTO发送给WCS
var wmsTaskDto = new WMSTaskDTO
{
Id = task.TaskId,
TaskNum = task.TaskNum,
PalletCode = task.PalletCode,
SourceAddress = task.SourceAddress,
TargetAddress = task.TargetAddress,
CurrentAddress = task.CurrentAddress,
NextAddress = task.NextAddress,
TaskType = task.TaskType,
TaskStatus = task.TaskStatus,
Roadway = task.Roadway,
Grade = task.Grade,
WarehouseId = task.WarehouseId,
PalletType = task.PalletType
};
var wcsResult = _httpClientHelper.Post<WebResponseContent>(
"http://localhost:9292/api/Task/ReceiveManualTask",
new List<WMSTaskDTO> { wmsTaskDto }.ToJson());
if (wcsResult != null && wcsResult.IsSuccess)
{
resultDto.SuccessCount++;
}
else
{
resultDto.FailResults.Add(new DispatchTaskResultDto
{
TaskId = dto.TaskId,
TaskNum = task.TaskNum,
Success = false,
ErrorMessage = wcsResult?.Message ?? "WCS响应异常"
});
resultDto.FailCount++;
}
}
if (resultDto.FailCount == 0)
return WebResponseContent.Instance.OK($"成功下发{resultDto.SuccessCount}个任务", resultDto);
if (resultDto.SuccessCount == 0)
return WebResponseContent.Instance.Error($"下发失败,共{resultDto.FailCount}个任务", resultDto);
return WebResponseContent.Instance.Error($"部分下发成功{resultDto.SuccessCount}个,失败{resultDto.FailCount}个", resultDto);
}
catch (Exception ex)
{
return WebResponseContent.Instance.Error($"下发任务异常: {ex.Message}");
}
}
/// <summary>
/// 获取任务状态名称
/// </summary>
private string GetTaskStatusName(int taskType, int taskStatus)
{
if (taskType == TaskInboundTypeEnum.Inbound.GetHashCode())
return ((TaskInStatusEnum)taskStatus).ToString();
if (taskType == TaskOutboundTypeEnum.Outbound.GetHashCode())
return ((TaskOutStatusEnum)taskStatus).ToString();
if (taskType == TaskRelocationTypeEnum.Relocation.GetHashCode())
return ((TaskRelocationStatusEnum)taskStatus).ToString();
return taskStatus.ToString();
}
注意: 需要在文件顶部确认已有以下 using:
```csharp
using WIDESEA_DTO.Task;
状态同步说明:WMS 下发成功后,WMS 端任务状态保持"New"不变,不切换到执行中状态。这是现有
CreateManualTaskAsync的设计模式(因为 WCS 侧有独立的任务生命周期),不跟进此 PR 改变。如需 WMS 端也同步状态,需另行评估 WCS→WMS 的状态回调机制。
using WIDESEA_Model.Models;
```
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>"
文件:
- 修改: WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
在 Controller 中添加以下方法(放在其他方法附近,建议在 CreateManualTaskAsync 之后):
/// <summary>
/// 手动下发任务到WCS
/// </summary>
/// <param name="dtos">下发任务参数列表</param>
/// <returns>批量下发结果</returns>
[HttpGet, HttpPost, Route("DispatchTasksToWCS"), AllowAnonymous]
public async Task<WebResponseContent?> DispatchTasksToWCSAsync([FromBody] List<DispatchTaskDto> dtos)
{
return await Service.DispatchTasksToWCSAsync(dtos);
}
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>"
文件:
- 创建: WMS/WIDESEA_WMSClient/src/extension/taskinfo/extend/dispatchTasksToWCS.vue
参考现有 addManualTask.vue 的模式,使用 el-table 实现批量编辑:
<template>
<div>
<vol-box v-model="showBox" :lazy="true" width="900px" :padding="15" title="手动下发任务到 WCS">
<div v-if="selectedRows.length > 0" class="dispatch-info">
已选任务数: <span class="count">{{ selectedRows.length }}</span> 个
</div>
<el-table :data="tableData" border style="width: 100%; margin-top: 10px" max-height="400">
<el-table-column prop="taskNum" label="任务号" width="120"></el-table-column>
<el-table-column prop="sourceAddress" label="起点地址" width="160">
<template slot-scope="scope">
<el-input
v-if="scope.row.editable"
v-model="scope.row.sourceAddress"
size="small"
placeholder="请输入"
></el-input>
<span v-else>{{ scope.row.sourceAddress }}</span>
</template>
</el-table-column>
<el-table-column prop="targetAddress" label="终点地址" width="160">
<template slot-scope="scope">
<el-input
v-if="scope.row.editable"
v-model="scope.row.targetAddress"
size="small"
placeholder="请输入"
></el-input>
<span v-else>{{ scope.row.targetAddress }}</span>
</template>
</el-table-column>
<el-table-column prop="grade" label="优先级" width="100">
<template slot-scope="scope">
<el-input-number
v-if="scope.row.editable"
v-model="scope.row.grade"
:min="1"
:max="99"
size="small"
style="width: 80px"
></el-input-number>
<span v-else>{{ scope.row.grade }}</span>
</template>
</el-table-column>
<el-table-column prop="statusName" label="状态" width="120">
<template slot-scope="scope">
<span :class="{ 'status-error': !scope.row.editable }">{{ scope.row.statusName }}</span>
</template>
</el-table-column>
<el-table-column prop="palletCode" label="托盘号"></el-table-column>
</el-table>
<!-- 失败结果 -->
<div v-if="failResults.length > 0" class="fail-results">
<div class="fail-title">下发失败任务:</div>
<el-table :data="failResults" border style="width: 100%; margin-top: 10px" max-height="200">
<el-table-column prop="taskNum" label="任务号" width="120"></el-table-column>
<el-table-column prop="errorMessage" label="失败原因"></el-table-column>
</el-table>
</div>
<template #footer>
<el-button type="primary" size="small" @click="submit" :loading="loading">确认下发</el-button>
<el-button type="danger" size="small" @click="showBox = false">关闭</el-button>
</template>
</vol-box>
</div>
</template>
<script>
import VolBox from "@/components/basic/VolBox.vue";
export default {
components: { VolBox },
data() {
return {
showBox: false,
loading: false,
selectedRows: [],
tableData: [],
failResults: [],
// 可下发状态的任务类型和状态值(需与后端 TaskTypeEnum 枚举一致)
// Inbound=200, Outbound=100, Relocation=300
// 状态: InNew=200, OutNew=100, RelocationNew=300
dispatchableStatuses: {
inbound: { taskType: 200, status: 200 }, // 入库新单
outbound: { taskType: 100, status: 100 }, // 出库新单
relocation: { taskType: 300, status: 300 } // 移库新单
}
};
},
methods: {
open(rows) {
this.showBox = true;
this.selectedRows = rows || [];
this.failResults = [];
this.initTableData();
},
initTableData() {
// 任务状态名称映射(需与后端枚举一致)
// taskType: Inbound=200, Outbound=100, Relocation=300
// status: InNew=200, OutNew=100, RelocationNew=300, SC_OutExecuting=110, etc.
const statusNames = {
'inbound_200': '入库新单',
'outbound_100': '出库新单',
'relocation_300': '移库新单',
'outbound_110': '堆垛机出库执行中',
'outbound_115': '堆垛机出库完成',
'inbound_220': '输送线入库执行中',
'inbound_230': '堆垛机入库执行中',
'inbound_290': '入库任务完成',
'outbound_120': '输送线出库执行中',
'outbound_125': '输送线出库完成',
'outbound_200': '出库任务完成',
'relocation_310': '堆垛机移库执行中'
};
this.tableData = this.selectedRows.map(row => {
const taskType = row.taskType || 0;
const taskStatus = row.taskStatus || 0;
const statusKey = this.getStatusKey(taskType, taskStatus);
const statusName = statusNames[statusKey] || `状态${taskStatus}`;
// 判断是否可编辑:仅入库新单/出库新单/移库新单可编辑
const editable = this.isEditable(taskType, taskStatus);
return {
taskId: row.taskId,
taskNum: row.taskNum,
sourceAddress: row.sourceAddress || '',
targetAddress: row.targetAddress || '',
grade: row.grade || 1,
statusName: statusName,
palletCode: row.palletCode || '',
editable: editable,
taskType: taskType,
taskStatus: taskStatus
};
});
},
getStatusKey(taskType, taskStatus) {
if (taskType === 200) return `inbound_${taskStatus}`;
if (taskType === 100) return `outbound_${taskStatus}`;
if (taskType === 300) return `relocation_${taskStatus}`;
return `other_${taskStatus}`;
},
isEditable(taskType, taskStatus) {
if (taskType === 200 && taskStatus === 200) return true; // 入库新单
if (taskType === 100 && taskStatus === 100) return true; // 出库新单
if (taskType === 300 && taskStatus === 300) return true; // 移库新单
return false;
},
submit() {
if (this.tableData.length === 0) return this.$message.error("请先选择任务");
const dispatchData = this.tableData.map(row => ({
taskId: row.taskId,
sourceAddress: row.sourceAddress,
targetAddress: row.targetAddress,
grade: row.grade
}));
this.loading = true;
this.http
.post("/api/TaskInfo/DispatchTasksToWCS", dispatchData, "数据处理中...")
.then((res) => {
this.loading = false;
if (!res.status) {
this.$message.error(res.message);
// 显示失败列表
if (res.data && res.data.failResults) {
this.failResults = res.data.failResults;
}
return;
}
// 全部成功
if (res.data && res.data.failCount === 0) {
this.$message.success(res.message);
this.showBox = false;
this.$emit("parentCall", ($vue) => {
$vue.refresh();
});
return;
}
// 部分成功或全部失败
if (res.data && res.data.failResults) {
this.failResults = res.data.failResults;
}
if (res.data && res.data.failCount > 0 && res.data.successCount > 0) {
this.$message.warning(res.message);
} else {
this.$message.error(res.message);
}
})
.catch(() => {
this.loading = false;
});
}
}
};
</script>
<style scoped>
.dispatch-info {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.dispatch-info .count {
color: #409eff;
font-weight: bold;
}
.status-error {
color: #f56c6c;
}
.fail-results {
margin-top: 15px;
padding: 10px;
background: #fef0f0;
border-radius: 4px;
}
.fail-title {
font-size: 14px;
color: #f56c6c;
margin-bottom: 5px;
}
</style>
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>"
文件:
- 修改: WMS/WIDESEA_WMSClient/src/extension/taskinfo/task.js
需要修改两处:
在 components 部分引入弹窗组件:javascript import addManualTask from './extend/addManualTask.vue' import dispatchTasksToWCS from './extend/dispatchTasksToWCS.vue' // 新增
在 components 对象中注册:javascript components: { gridBody: addManualTask, dispatchBody: dispatchTasksToWCS, // 新增 },
在 onInit() 方法中添加手动下发按钮(在手动创建任务按钮之后):
// 添加"手动下发"按钮
this.buttons.push({
name: '手动下发',
icon: 'el-icon-s-promotion',
type: 'primary',
value: 'DispatchTasksToWCS',
onClick: () => {
let rows = this.$refs.table.getSelected();
if (rows.length == 0) return this.$error("请先选择任务");
this.$refs.dispatchBody.open(rows);
}
});
完整修改后的 task.js 关键部分:
import addManualTask from './extend/addManualTask.vue'
import dispatchTasksToWCS from './extend/dispatchTasksToWCS.vue'
let extension = {
components: {
gridBody: addManualTask,
dispatchBody: dispatchTasksToWCS,
},
buttons: { view: [], box: [], detail: [] },
methods: {
onInit() {
//添加"手动创建任务"按钮
this.buttons.push({
name: '手动创建任务',
icon: 'el-icon-plus',
type: 'primary',
value: 'ManualCreateTask',
onClick: () => {
this.$refs.gridBody.open();
}
});
// 添加"手动下发"按钮
this.buttons.push({
name: '手动下发',
icon: 'el-icon-s-promotion',
type: 'primary',
value: 'DispatchTasksToWCS',
onClick: () => {
let rows = this.$refs.table.getSelected();
if (rows.length == 0) return this.$error("请先选择任务");
this.$refs.dispatchBody.open(rows);
}
});
// ... 其余现有按钮逻辑保持不变
},
// ... 其余方法保持不变
}
};
export default extension;
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>"
cd D:/Git/ShanMeiXinNengYuan/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer
dotnet build
cd D:/Git/ShanMeiXinNengYuan/Code/WMS/WIDESEA_WMSClient
npm run serve