refactor: 插件 UI 从 WinForms 迁移到 CycleGUI,并修复代码质量问题
将 StandardScene 各插件的配置/监控窗体从 WinForms 迁移到 CycleGUI(删除 .Designer.cs/.resx,重写为 PanelBuilder 立即模式 UI,新增 CycleUiHelper 统一对话框)。 同时修复代码审核中的问题: - 后台文件写入加锁 + try/catch(ButtonBoxManager / DoorManager,对齐 LoopViewer.SaveTasks 模式) - CoderFieldsMetadata.cs 启用 #nullable enable,消除 CS8632 警告 - DummyCar 移除已废弃的 rightClickAction()/SetPosition() - CarRemoteHelper.OpenVehicleWebPage 的 Process.Start 加 try/catch - 重命名名不副实的 Mstsc()(现为打开网页) - 统一弃元命名为 _ - TrafficInterlockViewer 改用稳定 Id(GUID)做选择/编辑,替代行索引 - csproj 改用 $(CGUILibDir) 解析 CycleGUI,绝对路径收敛到 Directory.Build.props 构建:dotnet build StandardScene.sln → 0 错误,30 警告(均为历史遗留)。 注:static 单例状态重构(审核第 8 项)暂未处理,留待单独任务。
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CycleGUI;
|
||||
using StandardScene.Model;
|
||||
using SimpleLite;
|
||||
using SimpleCore;
|
||||
@@ -17,35 +14,162 @@ using static StandardScene.Chained.ChainedDeliveryMission;
|
||||
|
||||
namespace StandardScene.Chained
|
||||
{
|
||||
public partial class DeliveryViewer : Form
|
||||
/// <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 int DisplayColumnIndexOverdueFlag = 9;
|
||||
private const int OverdueMinutesThreshold = 10000; // 约 7 天视为超时
|
||||
private const string TableId = "delivery-task-list";
|
||||
|
||||
private static readonly HttpClient SharedHttpClient = new HttpClient();
|
||||
/// <summary>选中行的背景色</summary>
|
||||
private static readonly Color SelectedRowBackColor = Color.FromArgb(220, 230, 250);
|
||||
/// <summary>缓存选中行索引,避免在 RetrieveVirtualItem 中访问 SelectedIndices 引发递归</summary>
|
||||
private readonly HashSet<int> _selectedIndicesCache = new HashSet<int>();
|
||||
/// <summary>超时任务整行底色(深色主题下的暗红,醒目但不刺眼)。</summary>
|
||||
private static readonly Color OverdueRowColor = Color.FromArgb(255, 90, 36, 36);
|
||||
|
||||
private ListViewItem _item = null;
|
||||
private static Panel _panel;
|
||||
private static bool _showFinished = true; // 显示已完成任务
|
||||
private static bool _showAbolished = true; // 显示废止任务(Error / Canceled / Terminated)
|
||||
|
||||
public DeliveryViewer()
|
||||
// 渲染快照:由后台线程按 FlushInterval 刷新,渲染线程只读引用;锁/文件 IO 绝不放在渲染线程,避免界面卡死。
|
||||
private static volatile List<Delivery> _snapshot = new List<Delivery>();
|
||||
private static volatile bool _refreshing;
|
||||
private static DateTime _lastFlush = DateTime.MinValue;
|
||||
private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(1);
|
||||
private static volatile string _status = "";
|
||||
|
||||
/// <summary>打开(或置前)任务管理面板。兼容原 <c>new DeliveryViewer().Show()</c> 调用方式。</summary>
|
||||
public void Show() => Open();
|
||||
|
||||
/// <summary>打开(或置前)任务管理面板。</summary>
|
||||
public static void Open()
|
||||
{
|
||||
InitializeComponent();
|
||||
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 readonly ContextMenuStrip strip = new ContextMenuStrip();
|
||||
private static bool IsOverdue(Delivery dd) =>
|
||||
(DateTime.Now - dd.CreateTime).TotalMinutes > OverdueMinutesThreshold;
|
||||
|
||||
private void DeliveryViewer_Load(object sender, EventArgs e)
|
||||
/// <summary>
|
||||
/// 渲染线程调用:到达刷新间隔且无在途刷新时,<b>在后台线程</b>重新拉取任务快照(超时任务置顶)。
|
||||
/// 业务侧的锁与文件 IO 一律放到后台,渲染线程只读 <see cref="_snapshot"/> 引用,避免界面卡死。
|
||||
/// </summary>
|
||||
private static void EnsureSnapshotFresh()
|
||||
{
|
||||
strip.Items.Clear();
|
||||
strip.Items.Add("取消任务", null, CancelClick);
|
||||
strip.Items.Add("重发任务", null, ResendClick);
|
||||
strip.Items.Add("换车重发任务", null, ChangeCarResendClick);
|
||||
currentTaskList.ContextMenuStrip = strip;
|
||||
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 List<string[]> _listDeliveries = new List<string[]>();
|
||||
private static string SafeSiteName(int siteId)
|
||||
{
|
||||
try { return SimpleLib.GetSite(siteId)?.name ?? ""; }
|
||||
catch { return ""; }
|
||||
}
|
||||
|
||||
/// <summary>将任务标记为已取消(Canceled)。</summary>
|
||||
private static void MarkDeliveryCanceled(Delivery d)
|
||||
@@ -93,93 +217,8 @@ namespace StandardScene.Chained
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual string[] GetDisplayContent(Delivery dd)
|
||||
private static void ResendDelivery(string taskCode)
|
||||
{
|
||||
var srcName =SimpleLib.GetSite(dd.Src).name;
|
||||
var dstName =SimpleLib.GetSite(dd.Dst).name;
|
||||
var now = DateTime.Now;
|
||||
var usingCar = dd.UsingCar == null ? string.Empty : dd.UsingCar.name;
|
||||
return
|
||||
[
|
||||
$"{dd.Id}",
|
||||
$"{usingCar}",
|
||||
$"{dd.Src}-{srcName}",
|
||||
$"{dd.Dst}-{dstName}",
|
||||
$"{dd.GetStatus()}",
|
||||
$"{dd.CreateTime:yyyy-mm-dd HH:mm:ss:fff}",
|
||||
$"{dd.StartTime:yyyy-mm-dd HH:mm:ss:fff}",
|
||||
$"{dd.FinishTime:yyyy-mm-dd HH:mm:ss:fff}",
|
||||
|
||||
$"{dd.Priority}",
|
||||
$"{((now - dd.CreateTime).TotalMinutes > OverdueMinutesThreshold ? 1 : 0)}",
|
||||
$"{dd.Id}"
|
||||
];
|
||||
}
|
||||
|
||||
private void TaskFlush()
|
||||
{
|
||||
_listDeliveries.Clear();
|
||||
try
|
||||
{
|
||||
foreach (var cdm in SimpleProject.proj.Missions.OfType<ChainedDeliveryMission>())
|
||||
foreach (var dd in cdm.GetDeliveries(checkBox1.Checked, checkBox2.Checked,checkBox2.Checked,checkBox2.Checked))
|
||||
_listDeliveries.Add(GetDisplayContent(dd));
|
||||
|
||||
if (_listDeliveries.Count > 0)
|
||||
{
|
||||
var len = _listDeliveries[0].Length;
|
||||
if (len > 0) _listDeliveries = _listDeliveries.OrderByDescending(p => int.Parse(p[len - 2])).ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Post($"TaskFlush 异常: {ExceptionFormatter.FormatEx(ex)}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void timer1_Tick(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
TaskFlush();
|
||||
currentTaskList.VirtualListSize = _listDeliveries.Count;
|
||||
currentTaskList.Invalidate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Post($"timer1_Tick 异常: {ExceptionFormatter.FormatEx(ex)}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void currentTaskList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var n = e.ItemIndex;
|
||||
e.Item = new ListViewItem(_listDeliveries[n]);
|
||||
if (_listDeliveries[n].Length > DisplayColumnIndexOverdueFlag && _listDeliveries[n][DisplayColumnIndexOverdueFlag] == "1")
|
||||
e.Item.ForeColor = Color.Red;
|
||||
if (_selectedIndicesCache.Contains(n))
|
||||
e.Item.BackColor = SelectedRowBackColor;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
e.Item = new ListViewItem(["", "", "", "", "", "", "", "", ""]);
|
||||
}
|
||||
}
|
||||
|
||||
private void currentTaskList_MouseClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button != MouseButtons.Right) return;
|
||||
_item = currentTaskList.GetItemAt(e.X, e.Y);
|
||||
}
|
||||
|
||||
private void ResendClick(object sender, EventArgs e)
|
||||
{
|
||||
if (_item == null) return;
|
||||
string taskCode = _item.Text;
|
||||
try
|
||||
{
|
||||
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
|
||||
@@ -189,44 +228,40 @@ namespace StandardScene.Chained
|
||||
.FirstOrDefault(s => s.Id == taskCode);
|
||||
if (d == null)
|
||||
{
|
||||
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_status = $"重发失败:列表中不存在任务 {taskCode}";
|
||||
return;
|
||||
}
|
||||
var ms = MessageBox.Show($"是否重发任务--{taskCode}", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
|
||||
if (ms != System.Windows.Forms.DialogResult.OK) return;
|
||||
if (!MarkDeliveryWaiting(d, clearCarForChange: false))
|
||||
{
|
||||
MessageBox.Show("重发任务失败:当前状态不允许重发", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_status = "重发任务失败:当前状态不允许重发";
|
||||
return;
|
||||
}
|
||||
|
||||
// 状态已改为 Waiting,持久化
|
||||
cdm.PersistDelivery(d);
|
||||
_status = $"已重发任务 {taskCode}";
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_status = $"重发任务 {taskCode} 异常,详见日志";
|
||||
Diagnosis.Post($"重发任务 {taskCode} 异常: {ExceptionFormatter.FormatEx(ex)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelClick(object sender, EventArgs e)
|
||||
private static void CancelDelivery(string taskCode)
|
||||
{
|
||||
if (_item == null) return;
|
||||
string str = _item.Text;
|
||||
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 == str);
|
||||
.FirstOrDefault(s => s.Id == taskCode);
|
||||
if (d == null)
|
||||
{
|
||||
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_status = $"取消失败:列表中不存在任务 {taskCode}";
|
||||
return;
|
||||
}
|
||||
var ms = MessageBox.Show($"是否结束任务--{str}", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
|
||||
if (ms != System.Windows.Forms.DialogResult.OK || d.IsFinished()) return;
|
||||
if (d.IsFinished()) return;
|
||||
|
||||
// 1) 状态上将任务标记为已取消
|
||||
MarkDeliveryCanceled(d);
|
||||
@@ -242,17 +277,17 @@ namespace StandardScene.Chained
|
||||
|
||||
// 3) 持久化已取消状态
|
||||
cdm.PersistDelivery(d);
|
||||
_status = $"已结束任务 {taskCode}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Post($"结束任务 {str} 异常: {ExceptionFormatter.FormatEx(ex)}");
|
||||
_status = $"结束任务 {taskCode} 异常,详见日志";
|
||||
Diagnosis.Post($"结束任务 {taskCode} 异常: {ExceptionFormatter.FormatEx(ex)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeCarResendClick(object sender, EventArgs e)
|
||||
private static void ChangeCarResendDelivery(string taskCode)
|
||||
{
|
||||
if (_item == null) return;
|
||||
string taskCode = _item.Text;
|
||||
try
|
||||
{
|
||||
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
|
||||
@@ -262,55 +297,24 @@ namespace StandardScene.Chained
|
||||
.FirstOrDefault(s => s.Id == taskCode);
|
||||
if (d == null)
|
||||
{
|
||||
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_status = $"换车重发失败:列表中不存在任务 {taskCode}";
|
||||
return;
|
||||
}
|
||||
|
||||
var ms = MessageBox.Show($"是否换车重发任务--{taskCode}", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
|
||||
if (ms != System.Windows.Forms.DialogResult.OK) return;
|
||||
|
||||
if (!MarkDeliveryWaiting(d, clearCarForChange: true))
|
||||
{
|
||||
MessageBox.Show("换车重发失败:仅当任务状态为 Suspended 或 Waiting 且未处于放货阶段时才允许换车重发", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_status = "换车重发失败:仅当任务状态为 Suspended 或 Waiting 且未处于放货阶段时才允许换车重发";
|
||||
return;
|
||||
}
|
||||
|
||||
// 状态已改为 Waiting 且 UsingCar 已清空,持久化
|
||||
cdm.PersistDelivery(d);
|
||||
_status = $"已换车重发任务 {taskCode}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_status = $"换车重发任务 {taskCode} 异常,详见日志";
|
||||
Diagnosis.Post($"换车重发任务 {taskCode} 异常: {ExceptionFormatter.FormatEx(ex)}");
|
||||
MessageBox.Show("换车重发任务异常,请查看日志", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeliveryViewer_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (e.CloseReason == CloseReason.UserClosing)
|
||||
{
|
||||
e.Cancel = true;
|
||||
this.Visible = false;
|
||||
timer1.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SetVisibleCore(bool value)
|
||||
{
|
||||
if (!IsHandleCreated && value)
|
||||
CreateHandle();
|
||||
bool wasVisible = Visible;
|
||||
base.SetVisibleCore(value);
|
||||
if (value && !wasVisible)
|
||||
timer1.Start();
|
||||
}
|
||||
|
||||
private void currentTaskList_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
_selectedIndicesCache.Clear();
|
||||
foreach (int i in currentTaskList.SelectedIndices)
|
||||
_selectedIndicesCache.Add(i);
|
||||
this.BeginInvoke(() => currentTaskList.Invalidate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user