feat: 环线批量操作、任务筛选与模拟下发搬运任务

为 LoopMission 增加一键上线/结束任务(含二次确认),DeliveryViewer 增加起点/终点/车辆/状态筛选并修复 CycleGUI 控件 id 碰撞,TransportMission 增加 Shelf 站点间随机模拟下发能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-06-30 22:06:16 +08:00
co-authored by Cursor
parent 54cda958db
commit 1724ea57f6
4 changed files with 182 additions and 3 deletions
+1
View File
@@ -364,3 +364,4 @@ FodyWeavers.xsd
build/
/build/SimpleComposer.exe
/build/plugins/StandardScene.dll
/.cursor
@@ -9,6 +9,7 @@ using SimpleCore.PropType;
using StandardScene.CarTypes;
using StandardScene.Chained.Loop;
using StandardScene.Model;
using StandardScene.Utils;
using System;
using System.Collections.Generic;
using System.IO;
@@ -2161,5 +2162,51 @@ namespace StandardScene.Chained
StopAll();
}
[MethodMember(Name = "一键上线所有AGV", Description = "将所有已初始化的正常 AGV 标记为在线")]
public void OnlineAllCars()
{
CycleUiHelper.ConfirmThen("确认将所有已初始化的正常 AGV 一键上线?", () => Task.Run(OnlineAllCarsCore));
}
private void OnlineAllCarsCore()
{
int count = 0;
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
{
if (car.GetLastSite() == -1 && Commons.GetVehicleStatus(car) == VehicleStatus.Normal)
{
Commons.AddOrUpdateTag(car.tags, "Online", "true");
car.Reset();
count++;
}
}
status.status = $"已上线 {count} 台AGV";
Diagnosis.Post($"[AbstractLoopMission] 一键上线所有AGV:已上线 {count} 台", "Loop-OnlineAll", true);
}
[MethodMember(Name = "一键结束所有AGV任务", Description = "停止环线调度并清除所有 AGV 的环线任务分配")]
public void StopAllCarTasks()
{
CycleUiHelper.ConfirmThen("确认停止环线调度并结束所有 AGV 的环线任务?", () => Task.Run(StopAllCarTasksCore));
}
private void StopAllCarTasksCore()
{
int count = 0;
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
{
Commons.DeleteTag(car.tags, "goalSite");
Commons.DeleteTag(car.tags, "loopAssigned");
Commons.DeleteTag(car.tags, "occupied");
car.ForceStop();
count++;
}
status.status = $"已结束 {count} 台AGV任务";
Diagnosis.Post($"[AbstractLoopMission] 一键结束所有AGV任务:已停止环线并清理 {count} 台 AGV 的任务标签", "Loop-StopAllTasks", true);
}
}
}
+51 -3
View File
@@ -43,6 +43,14 @@ namespace StandardScene.Chained
private readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(1);
private volatile string _status = "";
// 显示筛选(仅作用于已拉取的快照,渲染线程内即时过滤,不触发重新拉取)
private string _filterSrc = "";
private string _filterDst = "";
private string _filterCar = "";
private int _filterStatusIdx;
private readonly string[] _statusFilterNames =
new[] { "全部" }.Concat(Enum.GetNames(typeof(DeliveryStatus))).ToArray();
/// <summary>打开(或置前)任务管理面板。兼容原 <c>new DeliveryViewer().Show()</c> 调用方式。</summary>
public void Show() => Open();
@@ -91,13 +99,27 @@ namespace StandardScene.Chained
EnsureSnapshotFresh();
var items = _snapshot;
pb.Label($"共 {items.Count} 个任务(超时任务高亮置顶)");
// 筛选栏:起点 / 终点 / 车辆 / 任务状态(仅过滤当前快照,不触发重新拉取)
// ###id:控件 id 经 ASCII 哈希,同字数纯中文标签(起点筛选/终点筛选)会撞 id,用 ### 后缀给唯一 ASCII id(显示不变)。
var (fs, _) = pb.TextInput("起点筛选(站点ID或名称)###flt-src", _filterSrc, alwaysReturnString: true); _filterSrc = fs;
var (fd, _) = pb.TextInput("终点筛选(站点ID或名称)###flt-dst", _filterDst, alwaysReturnString: true); _filterDst = fd;
var (fc, _) = pb.TextInput("车辆筛选(车辆名)###flt-car", _filterCar, alwaysReturnString: true); _filterCar = fc;
pb.DropdownBox("状态筛选###flt-status", _statusFilterNames, ref _filterStatusIdx);
if (pb.Button("清空筛选", distinct: "delivery-clear-filter"))
{
_filterSrc = _filterDst = _filterCar = "";
_filterStatusIdx = 0;
}
var filtered = ApplyFilters(items);
pb.Label($"共 {filtered.Count}/{items.Count} 个任务(超时任务高亮置顶)");
pb.Table(TableId,
new[] { "任务号", "小车", "取货点", "放货点", "任务状态", "下发时间", "执行时间", "结束时间", "优先级", "操作" },
items.Count, (row, i) =>
filtered.Count, (row, i) =>
{
var dd = items[i];
var dd = filtered[i];
if (IsOverdue(dd)) row.SetColor(OverdueRowColor);
row.Label($"{dd.Id}");
@@ -134,6 +156,32 @@ namespace StandardScene.Chained
private bool IsOverdue(Delivery dd) =>
(DateTime.Now - dd.CreateTime).TotalMinutes > OverdueMinutesThreshold;
/// <summary>按 起点 / 终点 / 车辆 / 任务状态 对当前快照即时过滤(空条件不限制;起点终点匹配“ID-名称”,大小写不敏感)。</summary>
private List<Delivery> ApplyFilters(List<Delivery> items)
{
IEnumerable<Delivery> q = items;
var fSrc = (_filterSrc ?? "").Trim();
if (fSrc.Length > 0)
q = q.Where(d => $"{d.Src}-{SafeSiteName(d.Src)}".Contains(fSrc, StringComparison.OrdinalIgnoreCase));
var fDst = (_filterDst ?? "").Trim();
if (fDst.Length > 0)
q = q.Where(d => $"{d.Dst}-{SafeSiteName(d.Dst)}".Contains(fDst, StringComparison.OrdinalIgnoreCase));
var fCar = (_filterCar ?? "").Trim();
if (fCar.Length > 0)
q = q.Where(d => (d.UsingCar?.name ?? "").Contains(fCar, StringComparison.OrdinalIgnoreCase));
if (_filterStatusIdx > 0 && _filterStatusIdx < _statusFilterNames.Length)
{
var st = _statusFilterNames[_filterStatusIdx];
q = q.Where(d => d.GetStatus().ToString() == st);
}
return q.ToList();
}
/// <summary>
/// 渲染线程调用:到达刷新间隔且无在途刷新时,<b>在后台线程</b>重新拉取任务快照(超时任务置顶)。
/// 业务侧的锁与文件 IO 一律放到后台,渲染线程只读 <see cref="_snapshot"/> 引用,避免界面卡死。
@@ -354,6 +354,89 @@ namespace StandardScene.Chained
}
}
#region /
/// <summary>模拟下发维持的在途任务数上限</summary>
private const int SimMaxTasks = 30;
/// <summary>模拟下发任务的 TaskId 前缀,用于识别与统计模拟任务</summary>
private const string SimTaskPrefix = "Sim-";
[JsonIgnore] private CancellationTokenSource _simCts;
[JsonIgnore] private Task _simTask;
[JsonIgnore] private volatile bool _simRunning;
[MethodMember(Name = "模拟下发搬运任务", Description = "随机在标记 Shelf 的站点间下发搬运任务,维持最多30个在途任务,完成后自动补发")]
public void StartSimulateTransport()
{
if (_simRunning)
{
CycleUiHelper.Alert("提示", "模拟下发搬运任务已在运行中。");
return;
}
_simRunning = true;
_simCts = new CancellationTokenSource();
_simTask = Task.Run(() => SimulateTransportLoop(_simCts.Token));
G.pushStatus($"已开始模拟下发搬运任务(目标在途 {SimMaxTasks} 个)");
}
[MethodMember(Name = "结束模拟下发搬运任务", Description = "停止模拟下发搬运任务")]
public void StopSimulateTransport()
{
_simRunning = false;
try { _simCts?.Cancel(); } catch { /* ignore */ }
G.pushStatus("已结束模拟下发搬运任务");
}
private async Task SimulateTransportLoop(CancellationToken token)
{
var rnd = new Random();
while (!token.IsCancellationRequested)
{
try
{
var shelfSiteIds = SimpleLib.GetAllSites()
.Where(s => s.fields.ContainsKey("shelf"))
.Select(s => s.id)
.ToList();
if (shelfSiteIds.Count < 2)
{
Diagnosis.Post("[TransportMission] 模拟下发:标记 Shelf 字段的站点不足 2 个,暂不下发", "SimTransport", true);
}
else
{
int activeSim = GetDeliveries()
.Count(d => !string.IsNullOrEmpty(d.TaskId) && d.TaskId.StartsWith(SimTaskPrefix));
for (int i = activeSim; i < SimMaxTasks && !token.IsCancellationRequested; i++)
{
int src = shelfSiteIds[rnd.Next(shelfSiteIds.Count)];
int dst;
do { dst = shelfSiteIds[rnd.Next(shelfSiteIds.Count)]; } while (dst == src);
Enqueue(new TransportDelivery
{
CarType = "Car",
Src = src,
Dst = dst,
TaskId = $"{SimTaskPrefix}{Guid.NewGuid():N}"
});
}
}
}
catch (Exception ex)
{
Diagnosis.Post($"[TransportMission] 模拟下发异常: {ExceptionFormatter.FormatEx(ex)}", "SimTransport", true);
}
try { await Task.Delay(1000, token); } catch { /* cancelled */ }
}
}
#endregion
/// <summary>
/// 查看任务列表界面
/// 打开任务查看器窗口,显示所有任务的状态