对齐 FairyView 宿主命名。环线/充电/按钮/门进程的 Stop 改为 override,避免隐藏基类;Kiva.ForceStop 改为 override。 Co-authored-by: Cursor <cursoragent@cursor.com>
373 lines
16 KiB
C#
373 lines
16 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Drawing;
|
||
using System.Linq;
|
||
using System.Net.Http;
|
||
using System.Threading.Tasks;
|
||
using FairyView;
|
||
using StandardScene.Model;
|
||
using Simple3;
|
||
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("任务列表")
|
||
.ForceFloat()
|
||
.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) FairyUiHelper.ConfirmThen($"是否结束任务 {taskCode}?", () => Task.Run(() => CancelDelivery(taskCode)));
|
||
else if (op == 1) FairyUiHelper.ConfirmThen($"是否重发任务 {taskCode}?", () => Task.Run(() => ResendDelivery(taskCode)));
|
||
else if (op == 2) FairyUiHelper.ConfirmThen($"是否换车重发任务 {taskCode}?", () => Task.Run(() => ChangeCarResendDelivery(taskCode)));
|
||
}, height: FairyUiHelper.TableHeightPx(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)}");
|
||
}
|
||
}
|
||
}
|
||
}
|