Files
StandardSence/StandardScene.Core/Chained/DeliveryViewer.cs
T
zhaowei.huangandCursor 1724ea57f6 feat: 环线批量操作、任务筛选与模拟下发搬运任务
为 LoopMission 增加一键上线/结束任务(含二次确认),DeliveryViewer 增加起点/终点/车辆/状态筛选并修复 CycleGUI 控件 id 碰撞,TransportMission 增加 Shelf 站点间随机模拟下发能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 22:06:16 +08:00

373 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using CycleGUI;
using StandardScene.Model;
using SimpleLite;
using SimpleCore;
using SimpleCore.Library;
using StandardScene.Utils;
using static StandardScene.Chained.ChainedDeliveryMission;
namespace StandardScene.Chained
{
/// <summary>
/// 搬运任务管理界面(CycleGUI 版,替代原 WinForms <c>DeliveryViewer</c> 窗体)。
/// <list type="bullet">
/// <item>单实例:再次打开则把已有面板置前。</item>
/// <item>约每 1s 节流刷新任务快照(在渲染线程内节流,避免并发),面板 500ms 准实时重绘。</item>
/// <item>每行提供「取消 / 重发 / 换车重发」按钮(带二次确认),超时任务整行高亮。</item>
/// </list>
/// 保留可实例化 + <see cref="Show"/> 以兼容既有调用 <c>new DeliveryViewer().Show()</c>。
/// </summary>
public class DeliveryViewer
{
private const int OverdueMinutesThreshold = 10000; // 约 7 天视为超时
private const string TableId = "delivery-task-list";
private readonly HttpClient SharedHttpClient = new HttpClient();
/// <summary>超时任务整行底色(深色主题下的暗红,醒目但不刺眼)。</summary>
private readonly Color OverdueRowColor = Color.FromArgb(255, 90, 36, 36);
private Panel _panel;
private bool _showFinished = true; // 显示已完成任务
private bool _showAbolished = true; // 显示废止任务(Error / Canceled / Terminated
// 渲染快照:由后台线程按 FlushInterval 刷新,渲染线程只读引用;锁/文件 IO 绝不放在渲染线程,避免界面卡死。
private volatile List<Delivery> _snapshot = new List<Delivery>();
private volatile bool _refreshing;
private DateTime _lastFlush = DateTime.MinValue;
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();
/// <summary>打开(或置前)任务管理面板。</summary>
public static void Open() => (_instance ??= new DeliveryViewer()).OpenCore();
private static DeliveryViewer _instance;
private void OpenCore()
{
if (_panel != null)
{
try
{
_panel.BringToFront();
return;
}
catch
{
_panel = null;
}
}
var panel = GUI.DeclarePanel()
.ShowTitle("任务列表")
.SetDefaultDocking(Panel.Docking.None)
.InitSize(1500, 620) // 列宽按内容自适应(SizingFixedFit),给足初始宽度避免 10 列横向拥挤
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_panel = panel;
panel.IfTerminalQuit(() => _panel = null);
panel.Define(pb =>
{
if (pb.Closing())
{
panel.Exit();
_panel = null;
return;
}
// 过滤开关:改变时强制立即刷新一次(不必等节流窗口)。
if (pb.CheckBox("显示已完成任务", ref _showFinished)) _lastFlush = DateTime.MinValue;
pb.SameLine(16);
if (pb.CheckBox("显示废止的任务(Error / Canceled / Terminated", ref _showAbolished)) _lastFlush = DateTime.MinValue;
EnsureSnapshotFresh();
var items = _snapshot;
// 筛选栏:起点 / 终点 / 车辆 / 任务状态(仅过滤当前快照,不触发重新拉取)
// ###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[] { "任务号", "小车", "取货点", "放货点", "任务状态", "下发时间", "执行时间", "结束时间", "优先级", "操作" },
filtered.Count, (row, i) =>
{
var dd = filtered[i];
if (IsOverdue(dd)) row.SetColor(OverdueRowColor);
row.Label($"{dd.Id}");
row.Label(dd.UsingCar?.name ?? "");
row.Label($"{dd.Src}-{SafeSiteName(dd.Src)}");
row.Label($"{dd.Dst}-{SafeSiteName(dd.Dst)}");
row.Label($"{dd.GetStatus()}");
row.Label($"{dd.CreateTime:yyyy-MM-dd HH:mm:ss}");
row.Label($"{dd.StartTime:yyyy-MM-dd HH:mm:ss}");
row.Label($"{dd.FinishTime:yyyy-MM-dd HH:mm:ss}");
row.Label($"{dd.Priority}");
var op = row.ButtonGroup(
new[] { "取消", "重发", "换车" },
new[] { "取消任务", "重发任务", "换车重发任务" });
var taskCode = dd.Id;
// 业务操作含文件 IO 与锁竞争,统一用 Task.Run 放后台执行,绝不阻塞渲染线程(否则界面卡死)。
if (op == 0) CycleUiHelper.ConfirmThen($"是否结束任务 {taskCode}", () => Task.Run(() => CancelDelivery(taskCode)));
else if (op == 1) CycleUiHelper.ConfirmThen($"是否重发任务 {taskCode}", () => Task.Run(() => ResendDelivery(taskCode)));
else if (op == 2) CycleUiHelper.ConfirmThen($"是否换车重发任务 {taskCode}", () => Task.Run(() => ChangeCarResendDelivery(taskCode)));
}, height: 18, enableSearch: true);
if (!string.IsNullOrEmpty(_status))
{
pb.Separator();
pb.Label(_status);
}
// 节流重绘:任务监控无需高帧率,约 500ms 刷新一次即可保持准实时,显著降低 CPU。
pb.Panel.Repaint(repaintTimeMs: 500);
});
}
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"/> 引用,避免界面卡死。
/// </summary>
private void EnsureSnapshotFresh()
{
if (_refreshing) return;
if (DateTime.Now - _lastFlush < FlushInterval) return;
_lastFlush = DateTime.Now;
_refreshing = true;
// 捕获当前过滤条件,避免后台读取过程中被 UI 改动。
bool showFinished = _showFinished, showAbolished = _showAbolished;
Task.Run(() =>
{
try
{
var list = new List<Delivery>();
foreach (var cdm in SimpleProject.proj.Missions.OfType<ChainedDeliveryMission>())
list.AddRange(cdm.GetDeliveries(showFinished, showAbolished, showAbolished, showAbolished));
// 与原窗体一致:超时任务排在最前。
_snapshot = list.OrderByDescending(d => IsOverdue(d) ? 1 : 0).ToList();
}
catch (Exception ex)
{
Diagnosis.Post($"DeliveryViewer 刷新异常: {ExceptionFormatter.FormatEx(ex)}");
}
finally
{
_refreshing = false;
}
});
}
private string SafeSiteName(int siteId)
{
try { return SimpleLib.GetSite(siteId)?.name ?? ""; }
catch { return ""; }
}
/// <summary>将任务标记为已取消(Canceled)。</summary>
private void MarkDeliveryCanceled(Delivery d)
{
if (d == null) return;
lock (d.SyncStatus)
{
d.Canceled = true;
d.Active = false;
}
}
/// <summary>
/// 将任务状态重置为 Waiting。
/// 当 clearCarForChange=true 时,仅当状态为 Suspended 或 Waiting 且未处于放货阶段时,
/// 才会清空 UsingCar 并返回 true;否则返回 false。
/// </summary>
private bool MarkDeliveryWaiting(Delivery d, bool clearCarForChange)
{
if (d == null) return false;
lock (d.SyncStatus)
{
var status = d.GetStatus();
if (clearCarForChange)
{
if ((status is not DeliveryStatus.Suspended and not DeliveryStatus.Waiting) || d.Putting)
{
return false;
}
d.UsingCar = null;
}
else
{
d.SkipFetch = d.Putting;
}
d.Active = false;
d.Finished = false;
d.Error = false;
d.Canceled = false;
d.Terminated = false;
d.Suspended = false;
return true;
}
}
private void ResendDelivery(string taskCode)
{
try
{
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
if (cdm == null) return;
var d = cdm.GetDeliveries(true, true, true, true)
.OfType<TransportDelivery>()
.FirstOrDefault(s => s.Id == taskCode);
if (d == null)
{
_status = $"重发失败:列表中不存在任务 {taskCode}";
return;
}
if (!MarkDeliveryWaiting(d, clearCarForChange: false))
{
_status = "重发任务失败:当前状态不允许重发";
return;
}
cdm.PersistDelivery(d);
_status = $"已重发任务 {taskCode}";
}
catch (Exception ex)
{
_status = $"重发任务 {taskCode} 异常,详见日志";
Diagnosis.Post($"重发任务 {taskCode} 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
private void CancelDelivery(string taskCode)
{
try
{
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
if (cdm == null) return;
var d = cdm.GetDeliveries(true, true, true, true)
.OfType<TransportDelivery>()
.FirstOrDefault(s => s.Id == taskCode);
if (d == null)
{
_status = $"取消失败:列表中不存在任务 {taskCode}";
return;
}
if (d.IsFinished()) return;
// 1) 状态上将任务标记为已取消
MarkDeliveryCanceled(d);
// 2) 若小车当前正在执行该任务,则下发 reset 指令
bool excutingTask = d.UsingCar != null && d.UsingCar.tags.IsEqual("taskCode", d.TaskId);
if (excutingTask && d.UsingCar != null
&& (d.UsingCar.tags?.Contains("occupied") == true || (d.UsingCar.status?.pendingLocks?.Length ?? 0) != 0))
{
_ = SharedHttpClient.GetStringAsync($"http://{d.UsingCar.address}:8008/reset");
Diagnosis.Log($"手动结束任务;{d.TaskId}", "task", true);
}
// 3) 持久化已取消状态
cdm.PersistDelivery(d);
_status = $"已结束任务 {taskCode}";
}
catch (Exception ex)
{
_status = $"结束任务 {taskCode} 异常,详见日志";
Diagnosis.Post($"结束任务 {taskCode} 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
private void ChangeCarResendDelivery(string taskCode)
{
try
{
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
if (cdm == null) return;
var d = cdm.GetDeliveries(true, true, true, true)
.OfType<TransportDelivery>()
.FirstOrDefault(s => s.Id == taskCode);
if (d == null)
{
_status = $"换车重发失败:列表中不存在任务 {taskCode}";
return;
}
if (!MarkDeliveryWaiting(d, clearCarForChange: true))
{
_status = "换车重发失败:仅当任务状态为 Suspended 或 Waiting 且未处于放货阶段时才允许换车重发";
return;
}
cdm.PersistDelivery(d);
_status = $"已换车重发任务 {taskCode}";
}
catch (Exception ex)
{
_status = $"换车重发任务 {taskCode} 异常,详见日志";
Diagnosis.Post($"换车重发任务 {taskCode} 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
}
}