Files
StandardSence/StandardScene.Core/Chained/DeliveryViewer.cs
T
zhaowei.huang 54cda958db refactor: 插件窗体由 static 单例状态改为实例字段(对齐 TextViewer)
将 10 个 CycleGUI 窗体/管理器从进程级 static 单例状态改为实例字段 + 实例方法,
消除本地端/Web 端共享同一可变状态的隐患(对齐 TextViewer 正例)。

每个类保留一个 private static _instance 单实例持有者;静态入口 Open()/OpenViewer()
与实例 Show() 均转发到该实例,调用方零改动、单实例“置前”行为不变。

涉及:ChargeStationManagementForm、ChargeStrategyConfigForm、CommunicationMonitorForm、
AlarmConfigManagementForm、ButtonBoxManager、DoorManager、DoorMonitor、
LoopViewer、DeliveryViewer、TrafficInterlockViewer。

构建:dotnet build StandardScene.sln --no-incremental → 0 错误,30 警告(与基线一致,无新增)。
2026-06-26 15:31:02 +08:00

325 lines
14 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 = "";
/// <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;
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 bool IsOverdue(Delivery dd) =>
(DateTime.Now - dd.CreateTime).TotalMinutes > OverdueMinutesThreshold;
/// <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)}");
}
}
}
}