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 { /// /// 搬运任务管理界面(CycleGUI 版,替代原 WinForms DeliveryViewer 窗体)。 /// /// 单实例:再次打开则把已有面板置前。 /// 约每 1s 节流刷新任务快照(在渲染线程内节流,避免并发),面板 500ms 准实时重绘。 /// 每行提供「取消 / 重发 / 换车重发」按钮(带二次确认),超时任务整行高亮。 /// /// 保留可实例化 + 以兼容既有调用 new DeliveryViewer().Show()。 /// public class DeliveryViewer { private const int OverdueMinutesThreshold = 10000; // 约 7 天视为超时 private const string TableId = "delivery-task-list"; private static readonly HttpClient SharedHttpClient = new HttpClient(); /// 超时任务整行底色(深色主题下的暗红,醒目但不刺眼)。 private static readonly Color OverdueRowColor = Color.FromArgb(255, 90, 36, 36); private static Panel _panel; private static bool _showFinished = true; // 显示已完成任务 private static bool _showAbolished = true; // 显示废止任务(Error / Canceled / Terminated) // 渲染快照:由后台线程按 FlushInterval 刷新,渲染线程只读引用;锁/文件 IO 绝不放在渲染线程,避免界面卡死。 private static volatile List _snapshot = new List(); private static volatile bool _refreshing; private static DateTime _lastFlush = DateTime.MinValue; private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(1); private static volatile string _status = ""; /// 打开(或置前)任务管理面板。兼容原 new DeliveryViewer().Show() 调用方式。 public void Show() => Open(); /// 打开(或置前)任务管理面板。 public static void Open() { 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; pb.Label($"共 {items.Count} 个任务(超时任务高亮置顶)"); pb.Table(TableId, new[] { "任务号", "小车", "取货点", "放货点", "任务状态", "下发时间", "执行时间", "结束时间", "优先级", "操作" }, items.Count, (row, i) => { var dd = items[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 static bool IsOverdue(Delivery dd) => (DateTime.Now - dd.CreateTime).TotalMinutes > OverdueMinutesThreshold; /// /// 渲染线程调用:到达刷新间隔且无在途刷新时,在后台线程重新拉取任务快照(超时任务置顶)。 /// 业务侧的锁与文件 IO 一律放到后台,渲染线程只读 引用,避免界面卡死。 /// private static 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(); foreach (var cdm in SimpleProject.proj.Missions.OfType()) 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 static string SafeSiteName(int siteId) { try { return SimpleLib.GetSite(siteId)?.name ?? ""; } catch { return ""; } } /// 将任务标记为已取消(Canceled)。 private static void MarkDeliveryCanceled(Delivery d) { if (d == null) return; lock (d.SyncStatus) { d.Canceled = true; d.Active = false; } } /// /// 将任务状态重置为 Waiting。 /// 当 clearCarForChange=true 时,仅当状态为 Suspended 或 Waiting 且未处于放货阶段时, /// 才会清空 UsingCar 并返回 true;否则返回 false。 /// private static 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 static void ResendDelivery(string taskCode) { try { var cdm = SimpleProject.proj.Missions.OfType().FirstOrDefault(); if (cdm == null) return; var d = cdm.GetDeliveries(true, true, true, true) .OfType() .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 static void CancelDelivery(string taskCode) { try { var cdm = SimpleProject.proj.Missions.OfType().FirstOrDefault(); if (cdm == null) return; var d = cdm.GetDeliveries(true, true, true, true) .OfType() .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 static void ChangeCarResendDelivery(string taskCode) { try { var cdm = SimpleProject.proj.Missions.OfType().FirstOrDefault(); if (cdm == null) return; var d = cdm.GetDeliveries(true, true, true, true) .OfType() .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)}"); } } } }