init commit

This commit is contained in:
zhaowei.huang
2026-06-14 11:19:15 +08:00
parent e79a3815a5
commit c8e540d272
174 changed files with 60830 additions and 39 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
using System;
using System.Linq;
using SimpleCore.Library;
namespace StandardScene.Chained
{
/// <summary>
/// 负责根据 Delivery 上记录的回调 key 列表,通过回调注册表统一挂载所有事件回调。
/// </summary>
public static class DeliveryCallbackAttacher
{
public static void AttachAll(ChainedDeliveryMission.Delivery d)
{
if (d == null) return;
// OnStart
foreach (var key in d.OnStartCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveOnStart(key);
if (cb != null) d.OnStart += cb;
else Diagnosis.Log($"Unknown OnStart callback key: {key}", "DeliveryCallbackAttacher");
}
// DoneFetch
foreach (var key in d.DoneFetchCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveDoneFetch(key);
if (cb != null) d.DoneFetch += cb;
else Diagnosis.Log($"Unknown DoneFetch callback key: {key}", "DeliveryCallbackAttacher");
}
// DonePut
foreach (var key in d.DonePutCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveDonePut(key);
if (cb != null) d.DonePut += cb;
else Diagnosis.Log($"Unknown DonePut callback key: {key}", "DeliveryCallbackAttacher");
}
// DoneMission
foreach (var key in d.DoneMissionCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveDoneMission(key);
if (cb != null) d.DoneMission += cb;
else Diagnosis.Log($"Unknown DoneMission callback key: {key}", "DeliveryCallbackAttacher");
}
// Failed
foreach (var key in d.FailedCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveFailed(key);
if (cb != null) d.Failed += cb;
else Diagnosis.Log($"Unknown Failed callback key: {key}", "DeliveryCallbackAttacher");
}
// OnTerminated
foreach (var key in d.OnTerminatedCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveOnTerminated(key);
if (cb != null) d.OnTerminated += cb;
else Diagnosis.Log($"Unknown OnTerminated callback key: {key}", "DeliveryCallbackAttacher");
}
}
}
}
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StandardScene.Chained
{
/// <summary>
/// 可用于区分 Delivery 各生命周期事件的枚举。
/// </summary>
public enum DeliveryEventType
{
OnStart,
DoneFetch,
DonePut,
DoneMission,
Failed,
OnTerminated
}
/// <summary>
/// 全局任务回调注册表:通过 (事件类型, key) 注册/解析各阶段回调,便于持久化和恢复。
/// </summary>
public static class DeliveryCallbackRegistry
{
private static readonly Dictionary<(DeliveryEventType, string), Delegate> _callbacks = new();
private static void RegisterInternal(DeliveryEventType type, string key, Delegate callback)
{
if (string.IsNullOrWhiteSpace(key) || callback == null) return;
_callbacks[(type, key)] = callback;
}
private static T ResolveInternal<T>(DeliveryEventType type, string key) where T : class
{
if (key == null) return null;
return _callbacks.TryGetValue((type, key), out var d) ? d as T : null;
}
public static void RegisterOnStart(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.OnStart, key, callback);
public static void RegisterDoneFetch(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.DoneFetch, key, callback);
public static void RegisterDonePut(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.DonePut, key, callback);
public static void RegisterDoneMission(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.DoneMission, key, callback);
public static void RegisterFailed(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.Failed, key, callback);
public static void RegisterOnTerminated(string key, Func<ChainedDeliveryMission.Delivery, string, Task<int>> callback) =>
RegisterInternal(DeliveryEventType.OnTerminated, key, callback);
public static Action<ChainedDeliveryMission.Delivery> ResolveOnStart(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.OnStart, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveDoneFetch(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.DoneFetch, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveDonePut(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.DonePut, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveDoneMission(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.DoneMission, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveFailed(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.Failed, key);
public static Func<ChainedDeliveryMission.Delivery, string, Task<int>> ResolveOnTerminated(string key) =>
ResolveInternal<Func<ChainedDeliveryMission.Delivery, string, Task<int>>>(DeliveryEventType.OnTerminated, key);
}
}
@@ -0,0 +1,316 @@
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 StandardScene.Model;
using SimpleLite;
using SimpleCore;
using SimpleCore.Library;
using StandardScene.Utils;
using static StandardScene.Chained.ChainedDeliveryMission;
namespace StandardScene.Chained
{
public partial class DeliveryViewer : Form
{
private const int OverdueMinutesThreshold = 10000; // 约7天视为超时
private const int DisplayColumnIndexOverdueFlag = 9;
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>();
private ListViewItem _item = null;
public DeliveryViewer()
{
InitializeComponent();
}
private readonly ContextMenuStrip strip = new ContextMenuStrip();
private void DeliveryViewer_Load(object sender, EventArgs e)
{
strip.Items.Clear();
strip.Items.Add("取消任务", null, CancelClick);
strip.Items.Add("重发任务", null, ResendClick);
strip.Items.Add("换车重发任务", null, ChangeCarResendClick);
currentTaskList.ContextMenuStrip = strip;
}
private List<string[]> _listDeliveries = new List<string[]>();
/// <summary>将任务标记为已取消(Canceled)。</summary>
private static 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 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;
}
}
protected virtual string[] GetDisplayContent(Delivery dd)
{
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();
if (cdm == null) return;
var d = cdm.GetDeliveries(true, true, true, true)
.OfType<TransportDelivery>()
.FirstOrDefault(s => s.Id == taskCode);
if (d == null)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
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);
return;
}
// 状态已改为 Waiting,持久化
cdm.PersistDelivery(d);
}
catch (Exception)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
private void CancelClick(object sender, EventArgs e)
{
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);
if (d == null)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var ms = MessageBox.Show($"是否结束任务--{str}", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
if (ms != System.Windows.Forms.DialogResult.OK || 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);
}
catch (Exception ex)
{
Diagnosis.Post($"结束任务 {str} 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
private void ChangeCarResendClick(object sender, EventArgs e)
{
if (_item == null) return;
string taskCode = _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 == taskCode);
if (d == null)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
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);
return;
}
// 状态已改为 Waiting 且 UsingCar 已清空,持久化
cdm.PersistDelivery(d);
}
catch (Exception ex)
{
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());
}
}
}
+206
View File
@@ -0,0 +1,206 @@
using System.Windows.Forms;
namespace StandardScene.Chained
{
partial class DeliveryViewer
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
if (disposing)
{
strip?.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.currentTaskList = new System.Windows.Forms.ListView();
this.columnHeader8 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader4 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader5 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader9 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader6 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader2 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader7 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader3 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.label2 = new System.Windows.Forms.Label();
this.timer1 = new System.Windows.Forms.Timer(this.components);
this.checkBox1 = new System.Windows.Forms.CheckBox();
this.checkBox2 = new System.Windows.Forms.CheckBox();
this.SuspendLayout();
//
// currentTaskList
//
this.currentTaskList.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.currentTaskList.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader8,
this.columnHeader1,
this.columnHeader4,
this.columnHeader5,
this.columnHeader9,
this.columnHeader6,
this.columnHeader2,
this.columnHeader7,
this.columnHeader3});
this.currentTaskList.Font = new System.Drawing.Font("微软雅黑", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.currentTaskList.FullRowSelect = true;
this.currentTaskList.GridLines = true;
this.currentTaskList.HideSelection = false;
this.currentTaskList.Location = new System.Drawing.Point(38, 62);
this.currentTaskList.Name = "currentTaskList";
this.currentTaskList.Size = new System.Drawing.Size(1146, 429);
this.currentTaskList.TabIndex = 2;
this.currentTaskList.UseCompatibleStateImageBehavior = false;
this.currentTaskList.View = System.Windows.Forms.View.Details;
this.currentTaskList.VirtualMode = true;
this.currentTaskList.RetrieveVirtualItem += new System.Windows.Forms.RetrieveVirtualItemEventHandler(this.currentTaskList_RetrieveVirtualItem);
this.currentTaskList.SelectedIndexChanged += new System.EventHandler(this.currentTaskList_SelectedIndexChanged);
this.currentTaskList.MouseClick += new System.Windows.Forms.MouseEventHandler(this.currentTaskList_MouseClick);
//
// columnHeader8
//
this.columnHeader8.Text = "任务号";
this.columnHeader8.Width = 130;
//
// columnHeader1
//
this.columnHeader1.Text = "小车";
this.columnHeader1.Width = 100;
//
// columnHeader4
//
this.columnHeader4.Text = "取货点";
this.columnHeader4.Width = 130;
//
// columnHeader5
//
this.columnHeader5.Text = "放货点";
this.columnHeader5.Width = 130;
//
// columnHeader9
//
this.columnHeader9.Text = "任务状态";
this.columnHeader9.Width = 100;
//
// columnHeader6
//
this.columnHeader6.Text = "下发时间";
this.columnHeader6.Width = 130;
//
// columnHeader2
//
this.columnHeader2.Text = "执行时间";
this.columnHeader2.Width = 130;
//
// columnHeader7
//
this.columnHeader7.Text = "结束时间";
this.columnHeader7.Width = 130;
//
// columnHeader3
//
this.columnHeader3.Text = "优先级";
this.columnHeader3.Width = 83;
//
// label2
//
this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("微软雅黑", 14.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.label2.Location = new System.Drawing.Point(33, 7);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(88, 26);
this.label2.TabIndex = 3;
this.label2.Text = "任务列表";
//
// timer1
//
this.timer1.Enabled = true;
this.timer1.Interval = 1000;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
//
// checkBox1
//
this.checkBox1.AutoSize = true;
this.checkBox1.Checked = true;
this.checkBox1.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkBox1.Location = new System.Drawing.Point(127, 15);
this.checkBox1.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
this.checkBox1.Name = "checkBox1";
this.checkBox1.Size = new System.Drawing.Size(108, 16);
this.checkBox1.TabIndex = 4;
this.checkBox1.Text = "显示已完成任务";
this.checkBox1.UseVisualStyleBackColor = true;
//
// checkBox2
//
this.checkBox2.AutoSize = true;
this.checkBox2.Checked = true;
this.checkBox2.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkBox2.Location = new System.Drawing.Point(239, 14);
this.checkBox2.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
this.checkBox2.Name = "checkBox2";
this.checkBox2.Size = new System.Drawing.Size(318, 16);
this.checkBox2.TabIndex = 5;
this.checkBox2.Text = "显示废止的任务(包括Error、Canceled、Terminated";
this.checkBox2.UseVisualStyleBackColor = true;
//
// DeliveryViewer
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1199, 551);
this.Controls.Add(this.checkBox2);
this.Controls.Add(this.checkBox1);
this.Controls.Add(this.label2);
this.Controls.Add(this.currentTaskList);
this.Name = "DeliveryViewer";
this.Text = "DeliveryViewer";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DeliveryViewer_FormClosing);
this.Load += new System.EventHandler(this.DeliveryViewer_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ColumnHeader columnHeader4;
private System.Windows.Forms.ColumnHeader columnHeader5;
private System.Windows.Forms.ColumnHeader columnHeader6;
private System.Windows.Forms.ColumnHeader columnHeader7;
private System.Windows.Forms.ColumnHeader columnHeader8;
private System.Windows.Forms.Timer timer1;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
private System.Windows.Forms.ColumnHeader columnHeader9;
private System.Windows.Forms.CheckBox checkBox1;
private System.Windows.Forms.CheckBox checkBox2;
public System.Windows.Forms.ListView currentTaskList;
private System.Windows.Forms.ColumnHeader columnHeader3;
}
}
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="timer1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>
@@ -0,0 +1,58 @@
using StandardScene.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static StandardScene.Chained.AbstractLoopMission;
namespace StandardScene.Chained.Loop
{
/// <summary>
/// 进入点规则:返回是否允许进入。
/// </summary>
public interface IEnterRule
{
bool CanEnter(LoopPoint point, LoopTask task, object context = null);
}
/// <summary>
/// 离开点规则:返回是否允许离开。
/// </summary>
public interface IExitRule
{
bool CanExit(LoopPoint point, LoopTask task, object context = null);
}
/// <summary>
/// 合流规则:从候选任务中选择一个
/// </summary>
public interface IJoinRule
{
LoopTask SelectJoin(IEnumerable<LoopTask> candidates, LoopPoint point);
}
/// <summary>
/// 分流规则:为任务选取下一分支标识
/// </summary>
public interface IBranchRule
{
string SelectBranch(LoopPoint point, LoopTask task, object context = null);
}
/// <summary>
/// 任务策略接口:提供任务数据来源与基础策略决策(由 LoopViewer 或其它组件实现/替换)。
/// </summary>
public interface ITaskStrategy
{
/// <summary>
/// 返回当前策略下的任务集合(策略负责数据来源,例如读取 LoopViewer 的 JSON)。
/// </summary>
IEnumerable<LoopTask> GetTasks();
/// <summary>
/// 刷新策略数据(例如重新加载 JSON)。
/// </summary>
void Refresh();
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Chained.Loop
{
/// <summary>
/// 触发器适配器统一接口:外部设备(PLC、按钮盒、API 网关等)实现此接口并触发事件。
/// </summary>
public interface ITriggerAdapter : IDisposable
{
/// <summary>
/// 外部触发事件:Source 用于区分来源("PLC","ButtonBox","API"等),Key/Value 为业务自定义负载。
/// </summary>
event EventHandler<TriggerEventArgs> TriggerRaised;
/// <summary>
/// 可选:启动适配器(开启轮询、建立连接等)。
/// </summary>
void Start();
/// <summary>
/// 可选:停止适配器。
/// </summary>
void Stop();
}
public class TriggerEventArgs : EventArgs
{
public string Source { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty;
public object Value { get; set; }
}
}
+163
View File
@@ -0,0 +1,163 @@
using IoTClient.Clients.PLC;
using IoTClient.Common.Enums;
using LessokajiWeaverUtilities.Utilities;
using Newtonsoft.Json;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore.Library;
using StandardScene.Chained;
using StandardScene.Model;
using System.Collections.Concurrent;
using System.Threading;
using static StandardScene.Chained.AbstractLoopMission;
namespace StandardScene
{
public class LoopMissionStatus : AbstractLoopMissionStatus
{
}
[MissionType(Name = "环线进程", editor = typeof(LoopMission))]
[I18N.DocumentTranslation(Name = "Loop Mission", locale = "en")]
public class LoopMission : AbstractLoopMission
{
[JsonIgnore] public override MissionStatus status { get; set; } = new LoopMissionStatus();
[JsonIgnore] public Thread myThread;
[JsonIgnore] public bool started = false;
[JsonIgnore] private readonly ConcurrentDictionary<int, byte> _buttonTriggeredSiteIds = new ConcurrentDictionary<int, byte>();
public float ChargesocStandard = 30;
public float NoChargesocStandard = 80;
/// <summary>
/// API 触发:检查站点物料状态
/// </summary>
protected override ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car)
{
/* var site = SimpleLib.GetSite(currentSiteId);
if (site != null && site.fields.TryGetValue("hasMaterial", out var val) && val == "true")
{
// 物料存在,使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
return ExternalTriggerResult.Fail();*/
if (car.status.enums.TryGetValue("Soc", out var soc))
{
if (float.Parse(soc) <= ChargesocStandard)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
}
/* var plcTarget = 17;
if (plcTarget > 0)
{
// PLC 指定了目标站点
return ExternalTriggerResult.UseTarget(plcTarget);
}*/
return ExternalTriggerResult.Fail();
}
/// <summary>
/// PLC 触发:根据 PLC 信号决定目标
/// </summary>
protected override ExternalTriggerResult OnPlcTrigger(int currentSiteId, LoopTask task, Car car)
{
//var plcTarget = ReadPlcTargetSite(currentSiteId);
SiemensClient client = new SiemensClient(SiemensVersion.S7_200Smart, "127.0.0.1", 103);
var M100 = client.ReadBoolean("M100").Value;
if (M100)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
/* var plcTarget = 17;
if (plcTarget > 0)
{
// PLC 指定了目标站点
return ExternalTriggerResult.UseTarget(plcTarget);
}*/
return ExternalTriggerResult.Fail();
}
protected override ExternalTriggerResult OnChargeTrigger(int currentSiteId, LoopTask task, Car car)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
if (car.status.enums.TryGetValue("Soc", out var soc))
{
if (float.Parse(soc) >= NoChargesocStandard)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
}
}
/// <summary>
/// 供 ButtonMission 反射调用:登记一个需要按钮放行的站点。
/// 如果站点已经存在于字典中则忽略,避免重复放行信号堆积。
/// </summary>
/// <param name="siteId">需要放行的站点 ID,通常对应 LoopTask.CurrentStationId</param>
/// <returns>始终返回 true,表示本次按钮触发已被接受</returns>
public bool EnqueueButtonTriggerSite(int siteId)
{
if (siteId <= 0)
{
Diagnosis.Log($"按钮放行登记失败:无效站点 {siteId}", "LoopMission", true);
return false;
}
var car = FindCarArrivedAtSite(siteId);
if (car == null)
{
Diagnosis.Post($"按钮放行登记忽略:站点 {siteId} 当前没有到站车辆", "LoopMission", false);
return false;
}
if (_buttonTriggeredSiteIds.TryAdd(siteId, 0))
{
Diagnosis.Post($"按钮放行登记成功:站点 {siteId},车辆 {car.name}", "LoopMission", false);
}
else
{
Diagnosis.Post($"按钮放行已存在,忽略重复登记:站点 {siteId}", "LoopMission", false);
}
return true;
}
/// <summary>
/// 按钮盒触发:只有当前站点已被按钮登记过,才允许使用配置目标放行。
/// 命中后立即消费并删除,保证一次按钮只放行一次。
/// </summary>
protected override ExternalTriggerResult OnButtonTrigger(int currentSiteId, LoopTask task, Car car)
{
if (_buttonTriggeredSiteIds.TryRemove(currentSiteId, out _))
{
Diagnosis.Post($"按钮放行消费成功:站点 {currentSiteId},车辆 {car?.name}", "LoopMission", false);
return ExternalTriggerResult.UseConfigTarget();
}
return ExternalTriggerResult.Fail();
}
/* private int ReadPlcTargetSite(int siteId)
{
// 从 PLC 读取目标站点的业务逻辑
// 返回 0 表示未获取到有效目标
return 0;
}
*/
}
}
+570
View File
@@ -0,0 +1,570 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LoopViewerApp
{
partial class LoopViewer
{
private System.ComponentModel.IContainer components = null;
private ComboBox cmbTaskKind;
private NumericUpDown numCurrent;
private NumericUpDown numTarget;
private NumericUpDown numTraffic;
private CheckBox chkViaPoint;
private ComboBox cmbStartType;
private NumericUpDown numPriority;
private Button btnEdit; // 保留字段以供代码逻辑/样式使用(在界面上隐藏)
private Button btnDelete; // 保留字段以供代码逻辑/样式使用(在界面上隐藏)
private Button btnSave;
private Button btnCancel;
private ListView lstTasks;
private GroupBox grpEdit;
// 布局控件
private SplitContainer splitContainer;
private TableLayoutPanel tlpEdit;
private FlowLayoutPanel flpButtons;
// 中间竖向按钮(列表与编辑区之间)
private Panel pnlMiddle;
private FlowLayoutPanel flpMiddle;
private Button btnMiddleEdit;
private Button btnMiddleDelete;
// 列头
private ColumnHeader colId;
private ColumnHeader colTaskType;
private ColumnHeader colCurrent;
private ColumnHeader colTarget;
private ColumnHeader colTraffic;
private ColumnHeader colPriority;
private ColumnHeader colViaPoint;
private ColumnHeader colStartType;
// 标签字段(编辑区)
private Label lblKind;
private Label lblCurrent;
private Label lblTarget;
private Label lblTraffic;
private Label lblPriority;
private Label lblVia;
private Label lblStartType;
private Label lblEditingId; // 显示当前编辑的任务ID
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.splitContainer = new System.Windows.Forms.SplitContainer();
this.pnlMiddle = new System.Windows.Forms.Panel();
this.flpMiddle = new System.Windows.Forms.FlowLayoutPanel();
this.btnMiddleEdit = new System.Windows.Forms.Button();
this.btnMiddleDelete = new System.Windows.Forms.Button();
this.lstTasks = new System.Windows.Forms.ListView();
this.colId = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colTaskType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colCurrent = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colTarget = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colTraffic = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colPriority = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colViaPoint = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colStartType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.grpEdit = new System.Windows.Forms.GroupBox();
this.tlpEdit = new System.Windows.Forms.TableLayoutPanel();
this.lblEditingId = new System.Windows.Forms.Label();
this.lblKind = new System.Windows.Forms.Label();
this.cmbTaskKind = new System.Windows.Forms.ComboBox();
this.lblCurrent = new System.Windows.Forms.Label();
this.numCurrent = new System.Windows.Forms.NumericUpDown();
this.lblTarget = new System.Windows.Forms.Label();
this.numTarget = new System.Windows.Forms.NumericUpDown();
this.lblTraffic = new System.Windows.Forms.Label();
this.numTraffic = new System.Windows.Forms.NumericUpDown();
this.lblPriority = new System.Windows.Forms.Label();
this.numPriority = new System.Windows.Forms.NumericUpDown();
this.lblVia = new System.Windows.Forms.Label();
this.chkViaPoint = new System.Windows.Forms.CheckBox();
this.lblStartType = new System.Windows.Forms.Label();
this.cmbStartType = new System.Windows.Forms.ComboBox();
this.flpButtons = new System.Windows.Forms.FlowLayoutPanel();
this.btnSave = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.btnEdit = new System.Windows.Forms.Button();
this.btnDelete = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
this.splitContainer.Panel1.SuspendLayout();
this.splitContainer.Panel2.SuspendLayout();
this.splitContainer.SuspendLayout();
this.pnlMiddle.SuspendLayout();
this.flpMiddle.SuspendLayout();
this.grpEdit.SuspendLayout();
this.tlpEdit.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.numCurrent)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numTarget)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numTraffic)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numPriority)).BeginInit();
this.flpButtons.SuspendLayout();
this.SuspendLayout();
//
// splitContainer
//
this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer.Location = new System.Drawing.Point(0, 0);
this.splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
this.splitContainer.Panel1.Controls.Add(this.pnlMiddle);
this.splitContainer.Panel1.Controls.Add(this.lstTasks);
//
// splitContainer.Panel2
//
this.splitContainer.Panel2.Controls.Add(this.grpEdit);
this.splitContainer.Size = new System.Drawing.Size(1200, 600);
this.splitContainer.SplitterDistance = 680;
this.splitContainer.SplitterWidth = 6;
this.splitContainer.TabIndex = 0;
//
// pnlMiddle
//
this.pnlMiddle.Controls.Add(this.flpMiddle);
this.pnlMiddle.Dock = System.Windows.Forms.DockStyle.Right;
this.pnlMiddle.Location = new System.Drawing.Point(614, 0);
this.pnlMiddle.Name = "pnlMiddle";
this.pnlMiddle.Padding = new System.Windows.Forms.Padding(6);
this.pnlMiddle.Size = new System.Drawing.Size(66, 600);
this.pnlMiddle.TabIndex = 0;
//
// flpMiddle
//
this.flpMiddle.Anchor = System.Windows.Forms.AnchorStyles.None;
this.flpMiddle.Controls.Add(this.btnMiddleEdit);
this.flpMiddle.Controls.Add(this.btnMiddleDelete);
this.flpMiddle.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
this.flpMiddle.Location = new System.Drawing.Point(0, 220);
this.flpMiddle.Name = "flpMiddle";
this.flpMiddle.Padding = new System.Windows.Forms.Padding(2);
this.flpMiddle.Size = new System.Drawing.Size(63, 160);
this.flpMiddle.TabIndex = 0;
this.flpMiddle.WrapContents = false;
//
// btnMiddleEdit
//
this.btnMiddleEdit.BackColor = System.Drawing.SystemColors.Control;
this.btnMiddleEdit.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnMiddleEdit.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnMiddleEdit.Location = new System.Drawing.Point(6, 12);
this.btnMiddleEdit.Margin = new System.Windows.Forms.Padding(4, 10, 4, 4);
this.btnMiddleEdit.Name = "btnMiddleEdit";
this.btnMiddleEdit.Size = new System.Drawing.Size(50, 40);
this.btnMiddleEdit.TabIndex = 0;
this.btnMiddleEdit.Text = "编辑";
this.btnMiddleEdit.UseVisualStyleBackColor = false;
this.btnMiddleEdit.Click += new System.EventHandler(this.btnEdit_Click);
//
// btnMiddleDelete
//
this.btnMiddleDelete.BackColor = System.Drawing.Color.LightCoral;
this.btnMiddleDelete.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnMiddleDelete.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnMiddleDelete.Location = new System.Drawing.Point(6, 62);
this.btnMiddleDelete.Margin = new System.Windows.Forms.Padding(4, 6, 4, 4);
this.btnMiddleDelete.Name = "btnMiddleDelete";
this.btnMiddleDelete.Size = new System.Drawing.Size(50, 40);
this.btnMiddleDelete.TabIndex = 1;
this.btnMiddleDelete.Text = "删除";
this.btnMiddleDelete.UseVisualStyleBackColor = false;
this.btnMiddleDelete.Click += new System.EventHandler(this.btnDelete_Click);
//
// lstTasks
//
this.lstTasks.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.colId,
this.colTaskType,
this.colCurrent,
this.colTarget,
this.colTraffic,
this.colPriority,
this.colViaPoint,
this.colStartType});
this.lstTasks.Dock = System.Windows.Forms.DockStyle.Fill;
this.lstTasks.FullRowSelect = true;
this.lstTasks.HideSelection = false;
this.lstTasks.Location = new System.Drawing.Point(0, 0);
this.lstTasks.Name = "lstTasks";
this.lstTasks.OwnerDraw = true;
this.lstTasks.Size = new System.Drawing.Size(680, 600);
this.lstTasks.TabIndex = 0;
this.lstTasks.UseCompatibleStateImageBehavior = false;
this.lstTasks.View = System.Windows.Forms.View.Details;
this.lstTasks.DrawColumnHeader += new System.Windows.Forms.DrawListViewColumnHeaderEventHandler(this.lstTasks_DrawColumnHeader);
this.lstTasks.DrawItem += new System.Windows.Forms.DrawListViewItemEventHandler(this.lstTasks_DrawItem);
this.lstTasks.DrawSubItem += new System.Windows.Forms.DrawListViewSubItemEventHandler(this.lstTasks_DrawSubItem);
this.lstTasks.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.lstTasks_MouseDoubleClick);
//
// colId
//
this.colId.Text = "ID";
this.colId.Width = 40;
//
// colTaskType
//
this.colTaskType.Text = "任务类别";
this.colTaskType.Width = 110;
//
// colCurrent
//
this.colCurrent.Text = "当前站点";
this.colCurrent.Width = 90;
//
// colTarget
//
this.colTarget.Text = "目标站点";
this.colTarget.Width = 90;
//
// colTraffic
//
this.colTraffic.Text = "流量控制";
this.colTraffic.Width = 90;
//
// colPriority
//
this.colPriority.Text = "优先级";
this.colPriority.Width = 80;
//
// colViaPoint
//
this.colViaPoint.Text = "途径点";
this.colViaPoint.Width = 70;
//
// colStartType
//
this.colStartType.Text = "启动类型";
this.colStartType.Width = 100;
//
// grpEdit
//
this.grpEdit.Controls.Add(this.tlpEdit);
this.grpEdit.Dock = System.Windows.Forms.DockStyle.Fill;
this.grpEdit.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.grpEdit.Location = new System.Drawing.Point(0, 0);
this.grpEdit.Name = "grpEdit";
this.grpEdit.Size = new System.Drawing.Size(514, 600);
this.grpEdit.TabIndex = 1;
this.grpEdit.TabStop = false;
this.grpEdit.Text = "任务信息(选中列表项后可编辑)";
//
// tlpEdit
//
this.tlpEdit.ColumnCount = 2;
this.tlpEdit.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 120F));
this.tlpEdit.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tlpEdit.Controls.Add(this.lblEditingId, 0, 0);
this.tlpEdit.Controls.Add(this.lblKind, 0, 1);
this.tlpEdit.Controls.Add(this.cmbTaskKind, 1, 1);
this.tlpEdit.Controls.Add(this.lblCurrent, 0, 2);
this.tlpEdit.Controls.Add(this.numCurrent, 1, 2);
this.tlpEdit.Controls.Add(this.lblTarget, 0, 3);
this.tlpEdit.Controls.Add(this.numTarget, 1, 3);
this.tlpEdit.Controls.Add(this.lblTraffic, 0, 4);
this.tlpEdit.Controls.Add(this.numTraffic, 1, 4);
this.tlpEdit.Controls.Add(this.lblPriority, 0, 5);
this.tlpEdit.Controls.Add(this.numPriority, 1, 5);
this.tlpEdit.Controls.Add(this.lblVia, 0, 6);
this.tlpEdit.Controls.Add(this.chkViaPoint, 1, 6);
this.tlpEdit.Controls.Add(this.lblStartType, 0, 7);
this.tlpEdit.Controls.Add(this.cmbStartType, 1, 7);
this.tlpEdit.Controls.Add(this.flpButtons, 1, 8);
this.tlpEdit.Dock = System.Windows.Forms.DockStyle.Fill;
this.tlpEdit.Location = new System.Drawing.Point(3, 25);
this.tlpEdit.Name = "tlpEdit";
this.tlpEdit.Padding = new System.Windows.Forms.Padding(8);
this.tlpEdit.RowCount = 9;
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tlpEdit.Size = new System.Drawing.Size(508, 572);
this.tlpEdit.TabIndex = 0;
//
// lblEditingId
//
this.lblEditingId.Anchor = System.Windows.Forms.AnchorStyles.Left;
this.lblEditingId.AutoSize = true;
this.tlpEdit.SetColumnSpan(this.lblEditingId, 2);
this.lblEditingId.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.lblEditingId.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(120)))), ((int)(((byte)(215)))));
this.lblEditingId.Location = new System.Drawing.Point(11, 14);
this.lblEditingId.Name = "lblEditingId";
this.lblEditingId.Size = new System.Drawing.Size(78, 24);
this.lblEditingId.TabIndex = 0;
this.lblEditingId.Text = "新增任务";
//
// lblKind
//
this.lblKind.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblKind.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblKind.Location = new System.Drawing.Point(11, 44);
this.lblKind.Name = "lblKind";
this.lblKind.Size = new System.Drawing.Size(114, 36);
this.lblKind.TabIndex = 1;
this.lblKind.Text = "任务类别:";
this.lblKind.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// cmbTaskKind
//
this.cmbTaskKind.Dock = System.Windows.Forms.DockStyle.Fill;
this.cmbTaskKind.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbTaskKind.Font = new System.Drawing.Font("微软雅黑", 10F);
this.cmbTaskKind.Items.AddRange(new object[] {
"Loop",
"BranchPoint",
"JoinPoint"});
this.cmbTaskKind.Location = new System.Drawing.Point(131, 47);
this.cmbTaskKind.Name = "cmbTaskKind";
this.cmbTaskKind.Size = new System.Drawing.Size(366, 31);
this.cmbTaskKind.TabIndex = 2;
//
// lblCurrent
//
this.lblCurrent.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblCurrent.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblCurrent.Location = new System.Drawing.Point(11, 80);
this.lblCurrent.Name = "lblCurrent";
this.lblCurrent.Size = new System.Drawing.Size(114, 36);
this.lblCurrent.TabIndex = 3;
this.lblCurrent.Text = "当前站点:";
this.lblCurrent.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numCurrent
//
this.numCurrent.Dock = System.Windows.Forms.DockStyle.Left;
this.numCurrent.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numCurrent.Location = new System.Drawing.Point(131, 83);
this.numCurrent.Maximum = new decimal(new int[] {
1000000,
0,
0,
0});
this.numCurrent.Name = "numCurrent";
this.numCurrent.Size = new System.Drawing.Size(120, 29);
this.numCurrent.TabIndex = 4;
//
// lblTarget
//
this.lblTarget.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblTarget.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblTarget.Location = new System.Drawing.Point(11, 116);
this.lblTarget.Name = "lblTarget";
this.lblTarget.Size = new System.Drawing.Size(114, 36);
this.lblTarget.TabIndex = 5;
this.lblTarget.Text = "目标站点:";
this.lblTarget.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numTarget
//
this.numTarget.Dock = System.Windows.Forms.DockStyle.Left;
this.numTarget.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numTarget.Location = new System.Drawing.Point(131, 119);
this.numTarget.Maximum = new decimal(new int[] {
1000000,
0,
0,
0});
this.numTarget.Name = "numTarget";
this.numTarget.Size = new System.Drawing.Size(120, 29);
this.numTarget.TabIndex = 6;
//
// lblTraffic
//
this.lblTraffic.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblTraffic.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblTraffic.Location = new System.Drawing.Point(11, 152);
this.lblTraffic.Name = "lblTraffic";
this.lblTraffic.Size = new System.Drawing.Size(114, 36);
this.lblTraffic.TabIndex = 7;
this.lblTraffic.Text = "流量控制:";
this.lblTraffic.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numTraffic
//
this.numTraffic.Dock = System.Windows.Forms.DockStyle.Left;
this.numTraffic.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numTraffic.Location = new System.Drawing.Point(131, 155);
this.numTraffic.Maximum = new decimal(new int[] {
1000,
0,
0,
0});
this.numTraffic.Name = "numTraffic";
this.numTraffic.Size = new System.Drawing.Size(120, 29);
this.numTraffic.TabIndex = 8;
//
// lblPriority
//
this.lblPriority.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblPriority.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblPriority.Location = new System.Drawing.Point(11, 188);
this.lblPriority.Name = "lblPriority";
this.lblPriority.Size = new System.Drawing.Size(114, 36);
this.lblPriority.TabIndex = 9;
this.lblPriority.Text = "优先级:";
this.lblPriority.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numPriority
//
this.numPriority.Dock = System.Windows.Forms.DockStyle.Left;
this.numPriority.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numPriority.Location = new System.Drawing.Point(131, 191);
this.numPriority.Name = "numPriority";
this.numPriority.Size = new System.Drawing.Size(120, 29);
this.numPriority.TabIndex = 10;
this.numPriority.Value = new decimal(new int[] {
1,
0,
0,
0});
//
// lblVia
//
this.lblVia.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblVia.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblVia.Location = new System.Drawing.Point(11, 224);
this.lblVia.Name = "lblVia";
this.lblVia.Size = new System.Drawing.Size(114, 36);
this.lblVia.TabIndex = 11;
this.lblVia.Text = "途径点:";
this.lblVia.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// chkViaPoint
//
this.chkViaPoint.Dock = System.Windows.Forms.DockStyle.Left;
this.chkViaPoint.Font = new System.Drawing.Font("微软雅黑", 10F);
this.chkViaPoint.Location = new System.Drawing.Point(131, 227);
this.chkViaPoint.Name = "chkViaPoint";
this.chkViaPoint.Size = new System.Drawing.Size(104, 30);
this.chkViaPoint.TabIndex = 12;
this.chkViaPoint.Text = "是";
//
// lblStartType
//
this.lblStartType.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblStartType.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblStartType.Location = new System.Drawing.Point(11, 260);
this.lblStartType.Name = "lblStartType";
this.lblStartType.Size = new System.Drawing.Size(114, 36);
this.lblStartType.TabIndex = 13;
this.lblStartType.Text = "启动类型:";
this.lblStartType.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// cmbStartType
//
this.cmbStartType.Dock = System.Windows.Forms.DockStyle.Fill;
this.cmbStartType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbStartType.Font = new System.Drawing.Font("微软雅黑", 10F);
this.cmbStartType.Items.AddRange(new object[] {
"Api",
"Plc",
"ButtonBox",
"AutoLoop",
"Charge"});
this.cmbStartType.Location = new System.Drawing.Point(131, 263);
this.cmbStartType.Name = "cmbStartType";
this.cmbStartType.Size = new System.Drawing.Size(366, 31);
this.cmbStartType.TabIndex = 14;
//
// flpButtons
//
this.flpButtons.AutoSize = true;
this.flpButtons.Controls.Add(this.btnSave);
this.flpButtons.Controls.Add(this.btnCancel);
this.flpButtons.Dock = System.Windows.Forms.DockStyle.Left;
this.flpButtons.Location = new System.Drawing.Point(131, 299);
this.flpButtons.Name = "flpButtons";
this.flpButtons.Size = new System.Drawing.Size(292, 262);
this.flpButtons.TabIndex = 15;
//
// btnSave
//
this.btnSave.BackColor = System.Drawing.Color.LightBlue;
this.btnSave.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSave.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Bold);
this.btnSave.Location = new System.Drawing.Point(3, 3);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(140, 40);
this.btnSave.TabIndex = 0;
this.btnSave.Text = "保存";
this.btnSave.UseVisualStyleBackColor = false;
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// btnCancel
//
this.btnCancel.BackColor = System.Drawing.SystemColors.Control;
this.btnCancel.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnCancel.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnCancel.Location = new System.Drawing.Point(149, 3);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(140, 40);
this.btnCancel.TabIndex = 1;
this.btnCancel.Text = "取消";
this.btnCancel.UseVisualStyleBackColor = false;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// btnEdit
//
this.btnEdit.Location = new System.Drawing.Point(0, 0);
this.btnEdit.Name = "btnEdit";
this.btnEdit.Size = new System.Drawing.Size(75, 23);
this.btnEdit.TabIndex = 0;
this.btnEdit.Visible = false;
//
// btnDelete
//
this.btnDelete.Location = new System.Drawing.Point(0, 0);
this.btnDelete.Name = "btnDelete";
this.btnDelete.Size = new System.Drawing.Size(75, 23);
this.btnDelete.TabIndex = 0;
this.btnDelete.Visible = false;
//
// LoopViewer
//
this.ClientSize = new System.Drawing.Size(1200, 600);
this.Controls.Add(this.splitContainer);
this.Font = new System.Drawing.Font("微软雅黑", 9F);
this.MinimumSize = new System.Drawing.Size(1000, 420);
this.Name = "LoopViewer";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "任务列表管理器";
this.splitContainer.Panel1.ResumeLayout(false);
this.splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
this.splitContainer.ResumeLayout(false);
this.pnlMiddle.ResumeLayout(false);
this.flpMiddle.ResumeLayout(false);
this.grpEdit.ResumeLayout(false);
this.tlpEdit.ResumeLayout(false);
this.tlpEdit.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.numCurrent)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numTarget)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numTraffic)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numPriority)).EndInit();
this.flpButtons.ResumeLayout(false);
this.ResumeLayout(false);
}
}
}
+595
View File
@@ -0,0 +1,595 @@
using Newtonsoft.Json;
using StandardScene.Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace LoopViewerApp
{
public partial class LoopViewer : Form
{
private readonly string jsonPath =
Path.Combine(Application.StartupPath, "tasklist.json");
private List<LoopTask> tasks = new List<LoopTask>();
// -1 表示新增模式;>=0 表示正在编辑对应索引
private int editingIndex = -1;
public LoopViewer()
{
InitializeComponent();
if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
return;
// 应用 ChargeStationManagementForm 风格的运行时样式调整
ApplyChargeStyle();
EnsureComboItems();
// 启用多选并绑定右键菜单与 Delete 键删除功能
try
{
if (lstTasks != null)
{
lstTasks.MultiSelect = true;
// 右键菜单:删除
var ctx = new ContextMenuStrip();
ctx.Items.Add("删除", null, (s, e) => OnDeleteSelectedTasks());
lstTasks.ContextMenuStrip = ctx;
// 键盘删除键绑定
lstTasks.KeyDown += lstTasks_KeyDown;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoopViewer context menu init error: {ex}");
}
try
{
InitOrLoadJson();
RenderListView();
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoopViewer initialization error: {ex}");
}
}
private void lstTasks_KeyDown(object sender, KeyEventArgs e)
{
try
{
if (e.KeyCode == Keys.Delete)
{
OnDeleteSelectedTasks();
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"lstTasks_KeyDown error: {ex}");
}
}
/// <summary>
/// 删除 ListView 中选中的任务(支持多选)
/// </summary>
private void OnDeleteSelectedTasks()
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0)
return;
// 收集被选中的索引并按降序删除,避免索引移动问题
var selectedIndices = lstTasks.SelectedIndices.Cast<int>().OrderByDescending(i => i).ToList();
// 构造确认提示
string prompt;
if (selectedIndices.Count == 1)
{
int idx = selectedIndices[0];
if (idx >= 0 && idx < tasks.Count)
prompt = $"确认删除任务 ID={tasks[idx].Id}";
else
prompt = "确认删除选中任务?";
}
else
{
prompt = $"确认删除所选 {selectedIndices.Count} 个任务?";
}
if (MessageBox.Show(prompt, "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
// 删除任务
foreach (var idx in selectedIndices)
{
if (idx >= 0 && idx < tasks.Count)
{
tasks.RemoveAt(idx);
}
}
// 如果被删除项包含当前正在编辑的项,退出编辑状态
if (editingIndex >= 0)
{
if (editingIndex >= tasks.Count || selectedIndices.Any(i => i == editingIndex))
{
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
else
{
// 重新计算编辑索引在删除后的新位置
int removedBefore = selectedIndices.Count(i => i < editingIndex);
editingIndex -= removedBefore;
}
}
// 持久化并刷新列表视图
Save();
RenderListView();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"OnDeleteSelectedTasks error: {ex}");
MessageBox.Show("删除失败:" + ex.Message);
}
}
/// <summary>
/// 将 LoopViewer 的运行时样式调整为与 ChargeStationManagementForm 接近的视觉风格:
/// - 全局字体设为微软雅黑
/// - 表头暖色替换为蓝色沉稳风格(和充电界面一致)
/// - 按钮字号、背景色与充电界面保持一致(保存/删除/取消)
/// - 列表视图设置为整行选择、无边框、交替背景等
/// 注意:不修改 Designer 文件,仅在运行时统一控件表现,避免破坏设计器生成代码。
/// </summary>
private void ApplyChargeStyle()
{
try
{
// 窗体级设置
this.StartPosition = FormStartPosition.CenterScreen;
this.MinimumSize = new System.Drawing.Size(1327, 738);
this.Font = new Font("微软雅黑", 9F, FontStyle.Regular);
// 调整 ListView(如果存在)
if (lstTasks != null)
{
lstTasks.View = View.Details;
lstTasks.FullRowSelect = true;
lstTasks.GridLines = false;
lstTasks.HeaderStyle = ColumnHeaderStyle.Nonclickable;
lstTasks.OwnerDraw = true; // 已有自定义绘制
lstTasks.BackColor = Color.White;
lstTasks.ForeColor = Color.FromArgb(33, 33, 33);
// 多选由初始化时控制(这里不强制)
}
// 下拉框统一字体
if (cmbTaskKind != null) cmbTaskKind.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (cmbStartType != null) cmbStartType.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
// 数值输入框统一字体
if (numCurrent != null) numCurrent.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numTarget != null) numTarget.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numTraffic != null) numTraffic.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numPriority != null) numPriority.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
// 标签字体统一
if (lblEditingId != null) lblEditingId.Font = new Font("微软雅黑", 10F, FontStyle.Bold);
// 按钮风格:与 ChargeStationManagementForm 保持一致的视觉优先级
if (btnSave != null)
{
btnSave.BackColor = Color.LightBlue;
btnSave.ForeColor = Color.Black;
btnSave.Font = new Font("微软雅黑", 11F, FontStyle.Bold);
btnSave.FlatStyle = FlatStyle.Flat;
}
if (btnDelete != null)
{
btnDelete.BackColor = Color.LightCoral;
btnDelete.ForeColor = Color.Black;
btnDelete.Font = new Font("微软雅黑", 11F, FontStyle.Bold);
btnDelete.FlatStyle = FlatStyle.Flat;
}
if (btnCancel != null)
{
btnCancel.BackColor = SystemColors.Control;
btnCancel.ForeColor = Color.Black;
btnCancel.Font = new Font("微软雅黑", 11F, FontStyle.Regular);
btnCancel.FlatStyle = FlatStyle.Flat;
}
// 如果存在额外的操作按钮(例如在面板上),尝试统一风格(容错)
foreach (Control ctrl in this.Controls)
{
if (ctrl is Panel pnl)
{
pnl.Padding = new Padding(12);
}
else if (ctrl is Button btn)
{
// 已设置主要按钮,其他按钮使用中性风格
if (btn == btnSave || btn == btnDelete || btn == btnCancel) continue;
btn.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ApplyChargeStyle error: {ex}");
}
}
private void EnsureComboItems()
{
try
{
if (cmbTaskKind != null && cmbTaskKind.Items.Count == 0)
{
cmbTaskKind.Items.AddRange(new object[] { "Loop", "BranchPoint", "JoinPoint" });
cmbTaskKind.SelectedIndex = 0;
}
if (cmbStartType != null && cmbStartType.Items.Count == 0)
{
cmbStartType.Items.AddRange(new object[] { "Api", "Plc", "ButtonBox", "AutoLoop" });
cmbStartType.SelectedIndex = 3;
}
// 确保下拉框字体一致(防止 Designer 未设置)
if (cmbTaskKind != null) cmbTaskKind.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (cmbStartType != null) cmbStartType.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
}
catch { }
}
#region /
private void InitOrLoadJson()
{
try
{
if (!File.Exists(jsonPath))
File.WriteAllText(jsonPath, "[]");
var text = File.ReadAllText(jsonPath);
tasks = JsonConvert.DeserializeObject<List<LoopTask>>(text) ?? new List<LoopTask>();
}
catch (Exception ex)
{
tasks = new List<LoopTask>();
System.Diagnostics.Debug.WriteLine($"Load tasks failed: {ex}");
}
}
#endregion
#region ID
/// <summary>
/// 获取下一个可用的任务ID(当前最大ID + 1)
/// </summary>
/// <returns>新的任务ID</returns>
private int GetNextTaskId()
{
if (tasks == null || tasks.Count == 0)
return 1;
int maxId = tasks.Max(t => t.Id);
return maxId + 1;
}
#endregion
#region OwnerDraw +
private void lstTasks_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
try
{
// 与 ChargeStationManagementForm 表头保持一致的深蓝背景与白色加粗字体
using (var backBrush = new SolidBrush(Color.FromArgb(63, 81, 181))) // 深蓝(与 Charge 界面一致)
using (var textBrush = new SolidBrush(Color.White)) // 白色文字
using (var font = new Font("微软雅黑", 9, FontStyle.Bold))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
var sf = new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Near };
var rect = e.Bounds;
rect.Inflate(-8, 0);
e.Graphics.DrawString(e.Header.Text, font, textBrush, rect, sf);
// 分隔线
using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
{
e.Graphics.DrawLine(pen, e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
}
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
private void lstTasks_DrawItem(object sender, DrawListViewItemEventArgs e)
{
// 由 DrawSubItem 绘制全部内容以保证每列对齐
}
private void lstTasks_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
try
{
var item = e.Item;
bool selected = item.Selected;
Rectangle bounds = e.Bounds;
// 选中行颜色:与 ChargeStationManagementForm 保持一致的蓝色强调
Color selectedBack = Color.FromArgb(0, 120, 215);
Color selectedFore = Color.White;
// 非选中行交替背景
Color evenBack = Color.White;
Color oddBack = Color.FromArgb(250, 251, 253);
Color normalFore = Color.FromArgb(33, 33, 33);
// 填充背景
if (selected)
{
using (var selBrush = new SolidBrush(selectedBack))
{
e.Graphics.FillRectangle(selBrush, bounds);
}
}
else
{
using (var back = new SolidBrush(e.ItemIndex % 2 == 0 ? evenBack : oddBack))
{
e.Graphics.FillRectangle(back, bounds);
}
}
// 绘制文本(加一点内边距)
string text = e.SubItem.Text ?? string.Empty;
Color fore = selected ? selectedFore : normalFore;
TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;
Rectangle textRect = bounds;
textRect.Inflate(-6, 0);
using (var font = new Font("微软雅黑", 9))
{
TextRenderer.DrawText(e.Graphics, text, font, textRect, fore, flags);
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
#endregion
#region /
private void RenderListView()
{
try
{
if (lstTasks == null) return;
lstTasks.BeginUpdate();
lstTasks.Items.Clear();
foreach (var t in tasks)
{
var lvi = new ListViewItem(new[]
{
t.Id.ToString(), // ID 列
t.Kind.ToString(),
t.CurrentStationId.ToString(),
t.TargetStationId.ToString(),
t.TrafficControl.ToString(),
t.Priority.ToString(),
t.IsViaPoint ? "是" : "否",
t.StartType.ToString()
});
lstTasks.Items.Add(lvi);
}
lstTasks.EndUpdate();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"RenderListView failed: {ex}");
}
}
private void Save()
{
try
{
File.WriteAllText(jsonPath, JsonConvert.SerializeObject(tasks, Formatting.Indented));
}
catch (Exception ex)
{
MessageBox.Show("保存失败:" + ex.Message);
}
}
#endregion
#region /
private void UpdateSaveButtonText()
{
if (btnSave != null)
{
// 文案固定为"保存"
btnSave.Text = "保存";
}
}
private void btnSave_Click(object sender, EventArgs e)
{
try
{
// 从面板读取值,直接在界面内编辑/新增
Enum.TryParse<TaskKind>(cmbTaskKind?.SelectedItem?.ToString() ?? "Loop", out var kind);
Enum.TryParse<TaskStartType>(cmbStartType?.SelectedItem?.ToString() ?? "AutoLoop", out var st);
if (editingIndex >= 0 && editingIndex < tasks.Count)
{
// 更新模式:保留原有ID
var existingTask = tasks[editingIndex];
existingTask.Kind = kind;
existingTask.CurrentStationId = (int)(numCurrent?.Value ?? 0);
existingTask.TargetStationId = (int)(numTarget?.Value ?? 0);
existingTask.TrafficControl = (int)(numTraffic?.Value ?? 0);
existingTask.Priority = (int)(numPriority?.Value ?? 1);
existingTask.IsViaPoint = chkViaPoint?.Checked ?? false;
existingTask.StartType = st;
}
else
{
// 新增模式:自动分配新ID
var t = new LoopTask
{
Id = GetNextTaskId(), // 自增ID
Kind = kind,
CurrentStationId = (int)(numCurrent?.Value ?? 0),
TargetStationId = (int)(numTarget?.Value ?? 0),
TrafficControl = (int)(numTraffic?.Value ?? 0),
Priority = (int)(numPriority?.Value ?? 1),
IsViaPoint = chkViaPoint?.Checked ?? false,
StartType = st
};
tasks.Add(t);
}
Save();
RenderListView();
// 恢复新增状态
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnSave_Click error: {ex}");
MessageBox.Show("操作失败:" + ex.Message);
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
// 取消编辑,清空面板并回到"添加"模式
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
private void btnEdit_Click(object sender, EventArgs e)
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0) return;
int idx = lstTasks.SelectedIndices[0];
if (idx < 0 || idx >= tasks.Count) return;
editingIndex = idx;
LoadTaskToPanel(tasks[idx]);
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnEdit_Click error: {ex}");
}
}
private void btnDelete_Click(object sender, EventArgs e)
{
// 兼容旧的删除按钮:复用统一删除逻辑
OnDeleteSelectedTasks();
}
#endregion
#region
private void lstTasks_MouseDoubleClick(object sender, MouseEventArgs e)
{
try
{
if (lstTasks == null) return;
var item = lstTasks.GetItemAt(e.X, e.Y);
if (item == null) return;
int idx = item.Index;
if (idx < 0 || idx >= tasks.Count) return;
editingIndex = idx;
LoadTaskToPanel(tasks[idx]);
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"lstTasks_MouseDoubleClick error: {ex}");
}
}
#endregion
#region
private void LoadTaskToPanel(LoopTask t)
{
if (t == null) return;
try
{
// 显示当前编辑的任务ID(只读显示)
if (lblEditingId != null) lblEditingId.Text = $"编辑任务 ID: {t.Id}";
if (cmbTaskKind != null) cmbTaskKind.SelectedItem = t.Kind.ToString();
if (numCurrent != null) numCurrent.Value = Math.Max(numCurrent.Minimum, Math.Min(numCurrent.Maximum, t.CurrentStationId));
if (numTarget != null) numTarget.Value = Math.Max(numTarget.Minimum, Math.Min(numTarget.Maximum, t.TargetStationId));
if (numTraffic != null) numTraffic.Value = Math.Max(numTraffic.Minimum, Math.Min(numTraffic.Maximum, t.TrafficControl));
if (numPriority != null) numPriority.Value = Math.Max(numPriority.Minimum, Math.Min(numPriority.Maximum, t.Priority));
if (chkViaPoint != null) chkViaPoint.Checked = t.IsViaPoint;
if (cmbStartType != null) cmbStartType.SelectedItem = t.StartType.ToString();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoadTaskToPanel error: {ex}");
}
}
private void ClearPanelInputs()
{
try
{
// 清除编辑ID显示
if (lblEditingId != null) lblEditingId.Text = "新增任务";
if (cmbTaskKind != null) cmbTaskKind.SelectedIndex = 0;
if (numCurrent != null) numCurrent.Value = 0;
if (numTarget != null) numTarget.Value = 0;
if (numTraffic != null) numTraffic.Value = 0;
if (numPriority != null) numPriority.Value = 1;
if (chkViaPoint != null) chkViaPoint.Checked = false;
if (cmbStartType != null) cmbStartType.SelectedIndex = 3;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ClearPanelInputs error: {ex}");
}
}
#endregion
}
}
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,206 @@
using SimpleCore;
using SimpleCore.Library;
using StandardScene.Utils;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using StandardScene.Model;
namespace StandardScene.Chained
{
/// <summary>
/// 统一管理 TransportDelivery 相关的任务回调方法,并通过回调注册表提供可恢复性。
/// </summary>
public static class TransportDeliveryCallbacks
{
public const string KeyOnStarted = "Transport.OnMissionStarted";
public const string KeyOnFetched = "Transport.OnFetched";
public const string KeyOnPut = "Transport.OnPut";
public const string KeyOnFinished = "Transport.OnMissionFinished";
public const string KeyOnFailed = "Transport.OnMissionFailed";
public const string KeyOnTerminated = "Transport.OnMissionTerminated";
private static bool _initialized;
private static readonly HttpClient _httpClient = new HttpClient();
private static string _callbackUrl = "http://127.0.0.1:20101/api/v1/MDCS/State";
static TransportDeliveryCallbacks()
{
if (_initialized) return;
_initialized = true;
DeliveryCallbackRegistry.RegisterOnStart (KeyOnStarted, OnMissionStarted);
DeliveryCallbackRegistry.RegisterDoneFetch (KeyOnFetched, OnFetched);
DeliveryCallbackRegistry.RegisterDonePut (KeyOnPut, OnPut);
DeliveryCallbackRegistry.RegisterDoneMission(KeyOnFinished, OnMissionFinished);
DeliveryCallbackRegistry.RegisterFailed (KeyOnFailed, OnMissionFailed);
DeliveryCallbackRegistry.RegisterOnTerminated(KeyOnTerminated, OnMissionTerminated);
}
/// <summary>
/// 显式调用以确保静态构造函数已执行(注册所有默认回调)。
/// </summary>
public static void EnsureInitialized()
{
// 访问本类,确保静态构造已执行
if (!_initialized)
{
// 触发 static ctorCLR 保证线程安全)
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(TransportDeliveryCallbacks).TypeHandle);
}
}
/// <summary>
/// 配置任务状态回调的完整 URL(例如 http://ip:port/api/v1/MDCS/State)。
/// 建议在 TransportMission 启动或构造时调用一次。
/// </summary>
public static void ConfigureCallbackUrl(string url)
{
if (!string.IsNullOrWhiteSpace(url))
{
_callbackUrl = url;
}
}
private static void DispatchMissionState(TransportDelivery d, MissionState.MissionStateEnum state)
{
var ms = new MissionState
{
MissionId = d.TaskId,
CarCode = d.UsingCar?.id.ToString() ?? "0",
TriggerTime = DateTime.Now,
State = state
};
MissionStatePost(ms);
}
private static async void MissionStatePost(MissionState ms)
{
var retryCount = 10;
var retryDelay = TimeSpan.FromSeconds(1);
var content = new StringContent(ms.ToJson());
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
for (int i = 0; i < retryCount; i++)
{
try
{
var resp = await _httpClient.PostAsync(_callbackUrl, content);
Diagnosis.Post($"{ms.State}回调结果 => {resp.Content.ReadAsStringAsync().Result}");
var body = resp.Content.ReadAsStringAsync().Result.JsonTo<BaseRespose>();
var result = body.Code == 200 ? "成功" : "失败";
Diagnosis.Post($"车辆编号:{ms.CarCode} 任务id{ms.MissionId} 状态回调执行{result}");
break;
}
catch (Exception ex)
{
Diagnosis.Log($"{ms.State}状态回调失败 => ex:{ex}");
if (i == retryCount - 1)
{
Diagnosis.Log($"StringContent:{content}", "apiError");
}
await Task.Delay(retryDelay);
}
}
}
public static async void OnMissionStarted(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
Diagnosis.Log($"task(#{d.TaskId}) started");
if (!string.IsNullOrWhiteSpace(d.TaskIdsString))
{
Commons.AddOrUpdateTag(d.UsingCar.tags, "taskIdsString", d.TaskIdsString);
Diagnosis.Log(
$"holdCarTagAddOrUpdate--TaskId:{d.TaskId},task:[{d.ToJson()}].taskIdsString:{d.TaskIdsString}",
"holdCarTag",
true);
}
Commons.DeleteTag(d.UsingCar.tags, "holdCar");
if (!string.IsNullOrEmpty(d.HoldCarSite))
{
Commons.AddOrUpdateTag(d.UsingCar.tags, "holdCar", d.HoldCarSite);
Diagnosis.Log(
$"holdCarTagAddOrUpdate--TaskId:{d.TaskId},task:[{d.ToJson()}].holdCar:{d.HoldCarSite}",
"holdCarTag",
true);
}
DispatchMissionState(d, MissionState.MissionStateEnum.Started);
}
public static async void OnFetched(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
if (d.Src == d.Dst) return;
Diagnosis.Log($"task(#{d.TaskId}) fetched");
DispatchMissionState(d, MissionState.MissionStateEnum.Fetched);
}
public static async void OnPut(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
if (d.Src == d.Dst) return;
Diagnosis.Log($"task(#{d.TaskId}) put");
DispatchMissionState(d, MissionState.MissionStateEnum.Put);
}
public static async void OnMissionFailed(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
Diagnosis.Log($"task(#{d.TaskId}) failed");
DispatchMissionState(d, MissionState.MissionStateEnum.Failed);
}
public static async void OnMissionFinished(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
Diagnosis.Log($"task(#{d.TaskId}) finished");
if (!string.IsNullOrWhiteSpace(d.MGTaskCode!) && d.MGTaskCode.Contains("-"))
{
var phase = d.MGTaskCode.Split('-')[1];
var last = int.Parse(phase) - 1;
if (last > 0 && string.IsNullOrWhiteSpace(d.TaskIdsString))
{
var holdCar = SimpleLib.GetAllCars()
.FirstOrDefault(e => e.tags.TryGetValue("taskIdsString", out var v) && v.Contains(d.TaskId));
if (holdCar != null)
{
Commons.DeleteTag(holdCar.tags, "taskIdsString");
Diagnosis.Log(
$"holdCarTagDelete-2OnMissionFinished--TaskId:{d.TaskId},task:[{d.ToJson()}].taskIdsString:{d.TaskIdsString}",
"holdCarTag",
true);
}
}
}
DispatchMissionState(d, MissionState.MissionStateEnum.Finished);
}
public static async Task<int> OnMissionTerminated(ChainedDeliveryMission.Delivery delivery, string ISRelease)
{
try
{
using var hc = new HttpClient();
var task = await hc.GetAsync($"http://{delivery.UsingCar.address}:8008/car/startOrPause?ISRelease={ISRelease}");
var resp = task.Content.ReadAsStringAsync().Result.JsonTo<BaseRespose>();
var result = resp.Code == 200 ? "成功" : "失败";
return 1;
}
catch (Exception ex)
{
Diagnosis.Log($"暂停恢复失败 => ex:{ExceptionFormatter.FormatEx(ex)}");
return 0;
}
}
}
}
@@ -0,0 +1,599 @@
using Newtonsoft.Json;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleLite.RCS.Signal;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleCore;
using SimpleCore.Compiler;
using SimpleCore.Library;
using SimpleCore.PropType;
using StandardScene.Model;
using StandardScene;
using StandardScene.Utils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32.SafeHandles;
namespace StandardScene.Chained
{
/// <summary>
/// 任务类型枚举
/// </summary>
public enum TaskType
{
= 1, // 移动任务(当前未使用)
= 2, // 搬运任务
= 3, // 装车任务
= 4, // 卸车任务
}
/// <summary>
/// 运输任务类
/// 继承自AbstractDelivery,实现具体的运输任务功能
/// </summary>
/// public class TransportDelivery : AbstractChainedDeliveryMission.AbstractDelivery
public class TransportDelivery : ChainedDeliveryMission.Delivery
{
/// <summary>任务类型(枚举值)</summary>
public TaskType Type;
/// <summary>任务类型(字符串形式)</summary>
public string TaskType;
/// <summary>使用的小车名称</summary>
public string UsingCarName = "/";
/// <summary>物料信息</summary>
public string Material = "";
/// <summary>占车站点</summary>
public string HoldCarSite = "";
/// <summary>阶段任务代码(用于标记阶段任务)</summary>
public string MGTaskCode = "";
/// <summary>任务ID字符串(用于标记阶段任务,可包含多个任务ID,用逗号分隔)</summary>
public string TaskIdsString = "";
/// <summary>
/// 构造函数
/// 注册任务生命周期回调函数
/// </summary>
public TransportDelivery()
{
// 注册任务开始回调
OnStart += d => { TransportOnStart((TransportDelivery)d); };
// 注册取货完成回调
DoneFetch += d => { TransportDoneFetch((TransportDelivery)d); };
// 注册放货完成回调
DonePut += d => { TransportDonePut((TransportDelivery)d); };
// 注册任务完成回调
DoneMission += d => { TransportDoneMission((TransportDelivery)d); };
}
/// <summary>
/// 任务开始时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportOnStart(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-onStart");
}
/// <summary>
/// 取货完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDoneFetch(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-doneFetch");
}
/// <summary>
/// 放货完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDonePut(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-donePut");
}
/// <summary>
/// 任务完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDoneMission(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-doneMission");
}
}
/// <summary>
/// 搬运任务进程类
/// 继承自AbstractChainedDeliveryMission,实现具体的搬运任务调度
/// </summary>
[MissionType(Name = "搬运任务进程", editor = typeof(TransportMission))]
internal class TransportMission : ChainedDeliveryMission
{
/// <summary>是否启用就近任务策略("1"=启用,"0"=禁用)</summary>
private static readonly string NearestTask = "1";
/// <summary>任务查看器窗口对象</summary>
private DeliveryViewer dv;
public TransportMission()
{
// 确保回调静态类已完成注册
TransportDeliveryCallbacks.EnsureInitialized();
// 根据配置的 MissionCallbackURL 设置回调地址(若有)
var p = Param;
if (p != null && !string.IsNullOrWhiteSpace(p.MissionCallbackURL))
{
if (Commons.IsValidHttpUrl(p.MissionCallbackURL))
{
_lastCallbackUrl = p.MissionCallbackURL;
TransportDeliveryCallbacks.ConfigureCallbackUrl(_lastCallbackUrl);
}
else
{
Diagnosis.Log($"无效的 MissionCallbackURL 配置: {p.MissionCallbackURL}", "TransportMission");
}
}
}
public class TransportMissionParam
{
/// <summary>
/// 递送任务状态回调地址。
/// </summary>
public string MissionCallbackURL;
}
public TransportMissionParam Param => StringDictConvert<TransportMissionParam>.Convert(fields);
private readonly bool _onDisplay = true;
private string _lastCallbackUrl;
protected override Delivery CreateDeliveryFromSnapshot(DeliveryStateSnapshot snap)
{
var usingCar = SimpleLib.GetCar(snap.UsingCarId);
if (snap.UsingCarId != -1 && usingCar == null)
{
Diagnosis.Post($"CreateDeliveryFromSnapshot[{snap.Id}] failed:usingCar[{snap.UsingCarId}] not found");
return null;
}
if (usingCar != null && usingCar is not Car)
{
Diagnosis.Post($"CreateDeliveryFromSnapshot[{snap.Id}] skipped:usingCar[{snap.UsingCarId}] is not Car type");
return null;
}
var d = new TransportDelivery
{
Id = snap.Id,
TaskId = snap.TaskId ?? string.Empty,
Src = snap.Src,
Dst = snap.Dst,
SkipFetch = snap.SkipFetch,
SkipPut = snap.SkipPut,
Putting = snap.Putting,
CreateTime = snap.CreateTime,
StartTime = snap.StartTime,
FinishTime = snap.FinishTime,
CarType = snap.CarType,
UsingCar = (Car)usingCar
};
if (usingCar != null && string.Equals(snap.Status, "putting", StringComparison.OrdinalIgnoreCase) && !CheckCarLoaded(usingCar))
{
d.Error = true;
}
// 恢复回调配置与事件绑定
d.ReportOnStarted = snap.ReportOnStarted;
d.ReportOnFetched = snap.ReportOnFetched;
d.ReportOnPut = snap.ReportOnPut;
d.ReportOnFinished = snap.ReportOnFinished;
d.ReportOnFailed = snap.ReportOnFailed;
d.ReportOnTerminated = snap.ReportOnTerminated;
d.OnStartCallbackKeys = snap.OnStartCallbackKeys ?? new List<string>();
d.DoneFetchCallbackKeys = snap.DoneFetchCallbackKeys ?? new List<string>();
d.DonePutCallbackKeys = snap.DonePutCallbackKeys ?? new List<string>();
d.DoneMissionCallbackKeys = snap.DoneMissionCallbackKeys ?? new List<string>();
d.FailedCallbackKeys = snap.FailedCallbackKeys ?? new List<string>();
d.OnTerminatedCallbackKeys = snap.OnTerminatedCallbackKeys ?? new List<string>();
// 统一通过 Attacher 挂载所有回调
DeliveryCallbackAttacher.AttachAll(d);
return d;
}
protected override bool CheckCarLoaded(AbstractCar car)
{
if(car is DummyCar)
return true;
var carLoaded = Commons.GetCarStatus((Car)car, "Loaded");
if (!string.IsNullOrEmpty(carLoaded) && bool.TryParse(carLoaded, out var result))
return result;
return false;
}
/// <summary>
/// 执行方法(启动任务调度进程)
/// 启动基类的任务调度循环,并启动状态显示线程
/// </summary>
public override void Execute()
{
base.Execute(); // 调用基类Execute方法,启动任务调度循环
//HideSimpleConsole(); // 隐藏控制台窗口
// 启动状态显示线程,实时显示小车状态和任务执行情况
new Thread(() =>
{
while (true)
{
Thread.Sleep(100); // 每100ms更新一次显示
var painter = SimpleMonitor.getPainter("TransportMissionPainter");
painter.clear();
if (!_onDisplay) continue; // 如果未启用显示,跳过
// 检查并更新任务状态回调 URL(如果配置发生变化)
var p = Param;
var currentUrl = p?.MissionCallbackURL;
if (!string.IsNullOrWhiteSpace(currentUrl) && currentUrl != _lastCallbackUrl)
{
// 使用 Commons 中的统一 URL 校验逻辑
if (Commons.IsValidHttpUrl(currentUrl))
{
TransportDeliveryCallbacks.ConfigureCallbackUrl(currentUrl);
_lastCallbackUrl = currentUrl;
}
else
{
Diagnosis.Log($"无效的 MissionCallbackURL 配置: {currentUrl}", "TransportMission");
}
}
var onMissionCnt = 0; // 正在执行任务的小车数量
var carStr = $"{string.Join("\n", SimpleLib.GetAllCars().Select(cc =>
{
var status = "";
if (cc.tags.TryGetValue("occupied", out var occupiedStr))
{
onMissionCnt++;
status = occupiedStr;
if (cc.tags.TryGetValue("idle", out var _))
{
Diagnosis.Post($"strange idle tag, {cc.name}({cc.id})");
cc.tags.Remove("idle");
}
}
else if (cc.tags.TryGetValue("idle", out var idleStr)) status = DateTime.TryParse(idleStr, out var idleTime) ? $"idle:{idleTime}" : $"idle:{idleStr}";
return $"{cc.name}({cc.id})\t{status}";
}))}";
//var waitingMissions = GetDeliveries()
// .Where(dd => dd.GetStatus() == DeliveryStatus.Waiting).ToList();
painter.drawTextFixed(
$"小车:\t开动率:{onMissionCnt}/{SimpleLib.GetAllCars().Length}\n{carStr}\n\n",
new SolidBrush(Color.Black),
VirtualPainter.DrawPosition.LeftTop, Color.AliceBlue);
painter = null;
}
}).Start();
}
/// <summary>
/// 变更任务优先级(重写基类方法)
/// 根据配置决定是否执行就近任务策略
/// </summary>
public override void ChangePriority()
{
try
{
if (NearestTask == "1") // NearestTask 等于1 执行就近原则
{
// 就近任务执行(当前被注释,未启用)
// NearestTaskExecute();
}
else
{
// 如果未启用就近原则,移除所有小车的就近选车标记
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
{
Commons.DeleteTag(car.tags, "changePriority");
}
}
}
catch (Exception ex)
{
Console.WriteLine("同一工序就近执行就近任务执行:" + ex.ToString());
}
}
/// <summary>
/// 一键初始化所有小车
/// 将状态为"正常"或"上线"且未初始化的小车进行重置
/// </summary>
[MethodMember(Name = "一键初始化")]
public void ResetAll()
{
foreach (var car in SimpleLib.GetAllCars().OfType<Car>().Where(c => c.lstatus.Contains("正常") || c.lstatus.Contains("上线")))
{
// 如果小车未初始化(GetLastSite返回-1),执行重置
if (car.GetLastSite() == -1)
((Car)car).Reset();
}
}
/// <summary>
/// 一键上线所有小车
/// 将状态为"正常"或"上线"且已初始化的小车标记为在线状态
/// </summary>
[MethodMember(Name = "一键上线")]
public void OnlineAll()
{
foreach (var car in SimpleLib.GetAllCars().OfType<Car>().Where(c => c.lstatus.Contains("正常") || c.lstatus.Contains("上线")))
{
// 如果小车已初始化,标记为在线
if (car.GetLastSite() != -1)
{
Commons.AddOrUpdateTag(car.tags, "Online", "true");
}
}
}
/// <summary>
/// 查看任务列表界面
/// 打开任务查看器窗口,显示所有任务的状态
/// </summary>
[MethodMember(Name = "查看任务", Description = "显示界面")]
public void Print()
{
dv = new DeliveryViewer();
dv.Show();
}
/// <summary>
/// 手动创建单取货任务
/// 通过UI交互选择小车和取货点,创建仅取货任务(跳过取货步骤,小车从当前位置移动到取货点)
/// </summary>
[MethodMember(Name = "单取货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueueFetch()
{
try
{
G.pushStatus("选择小车");
var selected = SimpleMonitor.selected.ToArray();
if (selected.Length == 0) { MessageBox.Show("请先选择需要控制的小车!"); return; }
var obj = selected[0];
if (obj is Car car)
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择取货点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
// 创建运输任务:从小车当前位置到选择的取货点
var d = new TransportDelivery
{
CarType = "Car",
Src = car.GetLastSite(), // 起始点为小车当前位置
Dst = pt1.site, // 目标点为选择的取货点
SkipFetch = true, // 跳过取货步骤(因为小车已经在起始点)
UsingCar = car, // 指定使用的小车
PutPlanInfo = new() { { "action", "fetch" } }, // 路径动作设置为fetch
};
G.pushStatus($"排序了一个{car.name}搬运任务{d.Id}: {car.GetLastSite()} -> {d.Dst}");
Enqueue(d);
}
else
{
MessageBox.Show("请选择需要控制的小车!");
}
}
catch
{
G.pushStatus($"结束任务链");
}
}
//写一个冒泡排序的算法,按照距离排序
[MethodMember(Name = "切换后台显示")]
public void SwitchBackgroundDisplay()
{
ShowSimpleConsole();
}
/// <summary>
/// 手动创建单放货任务
/// 通过UI交互选择小车和放货点,创建仅放货任务(小车从当前位置移动到放货点并放货)
/// </summary>
[MethodMember(Name = "单放货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueuePut()
{
try
{
G.pushStatus("选择小车");
var selected = SimpleMonitor.selected.ToArray();
if (selected.Length == 0) { MessageBox.Show("请先选择需要控制的小车!"); return; }
var obj = selected[0];
if (obj is Car car)
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择放货点(注释写的是"取货位"但实际是放货点)
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
// 创建运输任务:从小车当前位置到选择的放货点
var d = new TransportDelivery
{
CarType = "Car",
Src = car.GetLastSite(), // 起始点为小车当前位置
Dst = pt1.site, // 目标点为选择的放货点
UsingCar = car, // 指定使用的小车
SkipFetch = true, // 跳过取货步骤
PutPlanInfo = new() { { "action", "put" } }, // 路径动作设置为put
};
G.pushStatus($"排序了一个{car.name}搬运任务{d.Id}: {srcSite.id} -> {d.Dst}");
Enqueue(d);
}
else
{
MessageBox.Show("请选择需要控制的小车!");
}
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 手动创建完整的取放货任务
/// 通过UI交互选择取货点和放货点,创建完整的搬运任务(取货->放货)
/// </summary>
[MethodMember(Name = "取放货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueue()
{
try
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择取货点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
G.pushStatus("选择放货位");
// 等待用户在UI中选择放货点
var pt2 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var dstSite = SimpleLib.GetSite(pt2.site);
// 创建完整的运输任务:从取货点到放货点
var d = new TransportDelivery
{
CarType = "Car",
Src = srcSite.id, // 取货点
Dst = dstSite.id, // 放货点
TaskId = $"ManualTask-{DateTime.Now}" // 生成任务ID
};
G.pushStatus($"排序了一个叉车搬运任务{d.Id}: {srcSite.id} -> {dstSite.id}");
Enqueue(d);
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 手动创建移动任务(去某地)
/// 通过UI交互选择目标站点,创建移动任务(起点和终点相同,只移动不放货)
/// </summary>
[MethodMember(Name = "去某地", Description = "去某地")]
public async void ManualEnqueueGo()
{
try
{
G.pushStatus("选择前往的站点");
// 等待用户在UI中选择目标站点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
// 创建移动任务:起点和终点相同(只移动到目标点,不执行取放货操作)
var d = new TransportDelivery
{
CarType = "Car",
Src = pt1.site, // 起点(实际不会取货)
Dst = pt1.site, // 终点(实际不会放货)
TaskId = $"ManualTask-{DateTime.Now}",
SrcFindLoop = false, // 起点不查找回路
DstFindLoop = false // 终点不查找回路
};
G.pushStatus($"排序了一个叉车搬运任务{d.Id}: {srcSite.id} -> {srcSite.id}");
Enqueue(d);
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 供按钮盒进程反射调用:ButtonMission.ExecuteButtonActionInternal 反射调用本方法后,
/// 由 HandleMethodResult 解析返回值,驱动 OnActionExecuted 成功/失败反馈。
/// 返回 true 表示已入队;false 表示站点无效。若 Enqueue 抛错,由按钮进程捕获后同样视为失败。
/// 按钮配置示例:TriggerMission=TransportMissionTriggerMethod=EnqueueTransportByButtonTriggerMethodParams=取货站点ID,放货站点ID
/// </summary>
/// <param name="srcSiteId">取货站点 ID</param>
/// <param name="dstSiteId">放货站点 ID</param>
public bool EnqueueTransportByButton(int srcSiteId, int dstSiteId)
{
if (SimpleLib.GetSite(srcSiteId) == null || SimpleLib.GetSite(dstSiteId) == null)
{
Diagnosis.Log($"按钮排队搬运失败:站点不存在 src={srcSiteId}, dst={dstSiteId}", "TransportMission", true);
return false;
}
var d = new TransportDelivery
{
CarType = "Car",
Src = srcSiteId,
Dst = dstSiteId,
TaskId = $"Button-{DateTime.Now:yyyyMMddHHmmssfff}"
};
Enqueue(d);
Diagnosis.Post($"按钮排队搬运 {d.Id}: {srcSiteId} -> {dstSiteId}");
return true;
}
// ========== 控制台窗口显示控制 ==========
/// <summary>Windows API:获取控制台窗口句柄</summary>
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
/// <summary>Windows API:显示/隐藏窗口</summary>
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
/// <summary>控制台显示状态标志</summary>
private static bool ShowConsole = false;
/// <summary>
/// 切换控制台窗口显示/隐藏
/// </summary>
public static void ShowSimpleConsole()
{
ShowConsole = !ShowConsole; // 切换显示状态
var handle = GetConsoleWindow();
int n = ShowConsole ? 0 : 5; // 0=显示,5=隐藏
Console.WriteLine(n);
ShowWindow(handle, n);
}
/// <summary>
/// 隐藏控制台窗口
/// </summary>
public static void HideSimpleConsole()
{
var handle = GetConsoleWindow();
ShowWindow(handle, 0); // 0表示隐藏窗口
}
}
}