对齐 FairyView 宿主命名。环线/充电/按钮/门进程的 Stop 改为 override,避免隐藏基类;Kiva.ForceStop 改为 override。 Co-authored-by: Cursor <cursoragent@cursor.com>
366 lines
13 KiB
C#
366 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using FairyView;
|
|
using Newtonsoft.Json;
|
|
using StandardScene.InterLock;
|
|
using StandardScene.Utils;
|
|
|
|
namespace LoopViewerApp
|
|
{
|
|
/// <summary>
|
|
/// 交通联锁区域管理界面(CycleGUI 版,替代原 WinForms <c>TrafficInterlockViewer</c> 窗体)。
|
|
/// <list type="bullet">
|
|
/// <item>维护 <see cref="TrafficInterlockMission.TrafficAreaList"/> 的增 / 改 / 删,并写入 <c>Config/traffic.json</c>。</item>
|
|
/// <item>单实例:再次打开则把已有面板置前。</item>
|
|
/// <item>勾选多行后「删除选中」可批量删除;每行「编辑」按钮加载下方编辑区。</item>
|
|
/// <item>文件写入放后台线程,绝不阻塞渲染线程。</item>
|
|
/// </list>
|
|
/// 沿用 <c>LoopViewer</c> / <c>DeliveryViewer</c> 同套模式(单实例面板、<c>pb.Table</c>、<c>FairyUiHelper.ConfirmThen</c>)。
|
|
/// 保留可实例化 + <see cref="Show"/> 以兼容既有调用 <c>new TrafficInterlockViewer().Show()</c>。
|
|
/// </summary>
|
|
public class TrafficInterlockViewer
|
|
{
|
|
private const string TableId = "traffic-area-list";
|
|
|
|
private string JsonPath =>
|
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config", "traffic.json");
|
|
|
|
private readonly object SaveLock = new object();
|
|
|
|
private Panel _panel;
|
|
private string _editingAreaId = "";
|
|
private readonly HashSet<string> _selected = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
private string _areaName = "";
|
|
private string _stationIds = "";
|
|
private string _controlRight = "";
|
|
private bool _isOccupied;
|
|
private bool _isEnabled = true;
|
|
private string _editingHint = "新增区域";
|
|
private string _editErr = "";
|
|
private volatile string _status = "";
|
|
|
|
/// <summary>打开(或置前)区域管理面板。兼容原 <c>new TrafficInterlockViewer().Show()</c> 调用方式。</summary>
|
|
public void Show() => Open();
|
|
|
|
/// <summary>打开(或置前)区域管理面板。</summary>
|
|
public static void Open() => (_instance ??= new TrafficInterlockViewer()).OpenCore();
|
|
|
|
private static TrafficInterlockViewer _instance;
|
|
|
|
private void OpenCore()
|
|
{
|
|
if (_panel != null)
|
|
{
|
|
try
|
|
{
|
|
_panel.BringToFront();
|
|
return;
|
|
}
|
|
catch
|
|
{
|
|
_panel = null;
|
|
}
|
|
}
|
|
|
|
ClearPanelInputs();
|
|
|
|
var panel = GUI.DeclarePanel()
|
|
.ShowTitle("交通联锁区域管理")
|
|
.ForceFloat()
|
|
.InitSize(800, 680)
|
|
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
|
|
_panel = panel;
|
|
panel.IfTerminalQuit(() => { if (_panel == panel) _panel = null; });
|
|
|
|
panel.Define(pb =>
|
|
{
|
|
if (pb.Closing())
|
|
{
|
|
panel.Exit();
|
|
_panel = null;
|
|
return;
|
|
}
|
|
|
|
List<TrafficArea> areas;
|
|
lock (TrafficInterlockMission.TrafficAreaList)
|
|
{
|
|
EnsureAreaIdsLocked();
|
|
areas = TrafficInterlockMission.TrafficAreaList.ToList();
|
|
}
|
|
_selected.RemoveWhere(id => areas.All(a => a.Id != id));
|
|
if (!string.IsNullOrEmpty(_editingAreaId) && areas.All(a => a.Id != _editingAreaId))
|
|
ClearPanelInputs();
|
|
|
|
if (pb.Button("删除选中", distinct: "traffic-del-selected"))
|
|
ConfirmDeleteSelected();
|
|
pb.SameLine(12);
|
|
if (pb.Button("刷新", distinct: "traffic-refresh"))
|
|
{
|
|
_status = "";
|
|
_editErr = "";
|
|
}
|
|
pb.SameLine(12);
|
|
pb.Label($"共 {areas.Count} 个区域,已选 {_selected.Count} 个");
|
|
|
|
pb.Table(TableId,
|
|
new[] { "选择", "区域名称", "区域站点集合", "控制权", "是否被占用", "是否启用", "操作" },
|
|
areas.Count, (row, i) =>
|
|
{
|
|
var a = areas[i];
|
|
var areaId = EnsureAreaId(a);
|
|
|
|
var sel = _selected.Contains(areaId);
|
|
if (row.Checkbox(ref sel, "勾选以批量删除"))
|
|
{
|
|
if (sel) _selected.Add(areaId);
|
|
else _selected.Remove(areaId);
|
|
}
|
|
|
|
row.Label(a.AreaName ?? "");
|
|
row.Label(a.SiteList != null && a.SiteList.Count > 0
|
|
? string.Join(", ", a.SiteList)
|
|
: "");
|
|
row.Label(a.ControllerName ?? "");
|
|
row.Label(a.IsOccupy ? "是" : "否");
|
|
row.Label(a.IsEnable ? "是" : "否");
|
|
|
|
if (row.ButtonGroup(new[] { "编辑" }, new[] { "编辑该区域" }) == 0)
|
|
{
|
|
_editingAreaId = areaId;
|
|
LoadAreaToFields(a);
|
|
}
|
|
}, height: FairyUiHelper.TableHeightPx(14), enableSearch: true);
|
|
|
|
pb.Separator();
|
|
pb.Label("数据新增/编辑(点击表格行「编辑」可在此查看并修改该行数据)");
|
|
pb.Label(_editingHint);
|
|
|
|
var (name, _) = pb.TextInput("1. 区域名称", _areaName, alwaysReturnString: true);
|
|
_areaName = name;
|
|
var (sites, _) = pb.TextInput("2. 站点集合 (逗号/分号/空格分隔)", _stationIds, alwaysReturnString: true);
|
|
_stationIds = sites;
|
|
var (ctrl, _) = pb.TextInput("3. 控制权", _controlRight, alwaysReturnString: true);
|
|
_controlRight = ctrl;
|
|
pb.CheckBox("4. 是否被占用", ref _isOccupied);
|
|
pb.SameLine(16);
|
|
pb.CheckBox("5. 是否启用", ref _isEnabled);
|
|
|
|
if (!string.IsNullOrEmpty(_editErr))
|
|
{
|
|
pb.Separator();
|
|
pb.Label(_editErr);
|
|
}
|
|
|
|
pb.Separator();
|
|
if (pb.Button("保存", distinct: "traffic-save"))
|
|
SaveArea();
|
|
pb.SameLine(12);
|
|
if (pb.Button("新增", distinct: "traffic-new"))
|
|
{
|
|
_selected.Clear();
|
|
ClearPanelInputs();
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(_status))
|
|
{
|
|
pb.Separator();
|
|
pb.Label(_status);
|
|
}
|
|
|
|
pb.Panel.Repaint(repaintTimeMs: 500);
|
|
});
|
|
}
|
|
|
|
private void ConfirmDeleteSelected()
|
|
{
|
|
if (_selected.Count == 0)
|
|
{
|
|
_status = "请先在上方列表中选择要删除的区域。";
|
|
_panel?.Repaint();
|
|
return;
|
|
}
|
|
|
|
string prompt = _selected.Count == 1
|
|
? "确定要删除选中的区域吗?"
|
|
: $"确定要删除所选 {_selected.Count} 个区域吗?";
|
|
|
|
FairyUiHelper.ConfirmThen(prompt, DeleteSelected);
|
|
}
|
|
|
|
private void DeleteSelected()
|
|
{
|
|
var selectedIds = _selected.ToHashSet(StringComparer.Ordinal);
|
|
var removed = 0;
|
|
lock (TrafficInterlockMission.TrafficAreaList)
|
|
{
|
|
removed = TrafficInterlockMission.TrafficAreaList.RemoveAll(a => selectedIds.Contains(a.Id));
|
|
}
|
|
|
|
_selected.Clear();
|
|
SaveToConfig();
|
|
ClearPanelInputs();
|
|
_status = $"已删除 {removed} 个区域";
|
|
_panel?.Repaint();
|
|
}
|
|
|
|
private void SaveArea()
|
|
{
|
|
try
|
|
{
|
|
string areaName = (_areaName ?? "").Trim();
|
|
if (string.IsNullOrEmpty(areaName))
|
|
{
|
|
_editErr = "请输入区域名称。";
|
|
_panel?.Repaint();
|
|
return;
|
|
}
|
|
|
|
var stationIds = ParseStationIds(_stationIds ?? "");
|
|
string controlRight = (_controlRight ?? "").Trim();
|
|
bool isOccupied = _isOccupied;
|
|
bool isEnabled = _isEnabled;
|
|
|
|
if (!string.IsNullOrEmpty(_editingAreaId))
|
|
{
|
|
lock (TrafficInterlockMission.TrafficAreaList)
|
|
{
|
|
var existing = TrafficInterlockMission.TrafficAreaList
|
|
.FirstOrDefault(a => string.Equals(a.Id, _editingAreaId, StringComparison.Ordinal));
|
|
if (existing == null)
|
|
{
|
|
_editErr = "正在编辑的区域已不存在,请刷新后重试。";
|
|
_panel?.Repaint();
|
|
return;
|
|
}
|
|
|
|
existing.AreaName = areaName;
|
|
existing.SiteList = stationIds;
|
|
existing.ControllerName = controlRight;
|
|
existing.IsOccupy = isOccupied;
|
|
existing.IsEnable = isEnabled;
|
|
}
|
|
_status = $"已保存区域:{areaName}";
|
|
}
|
|
else
|
|
{
|
|
lock (TrafficInterlockMission.TrafficAreaList)
|
|
{
|
|
TrafficInterlockMission.TrafficAreaList.Add(new TrafficArea
|
|
{
|
|
Id = NewAreaId(),
|
|
AreaName = areaName,
|
|
SiteList = stationIds,
|
|
ControllerName = controlRight,
|
|
IsOccupy = isOccupied,
|
|
IsEnable = isEnabled
|
|
});
|
|
}
|
|
_status = "已新增区域";
|
|
}
|
|
|
|
SaveToConfig();
|
|
ClearPanelInputs();
|
|
_editErr = "";
|
|
_panel?.Repaint();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_editErr = "操作失败:" + ex.Message;
|
|
_panel?.Repaint();
|
|
}
|
|
}
|
|
|
|
private void LoadAreaToFields(TrafficArea a)
|
|
{
|
|
if (a == null) return;
|
|
_editingHint = $"编辑:{a.AreaName}";
|
|
_areaName = a.AreaName ?? "";
|
|
_stationIds = a.SiteList != null && a.SiteList.Count > 0
|
|
? string.Join(", ", a.SiteList)
|
|
: "";
|
|
_controlRight = a.ControllerName ?? "";
|
|
_isOccupied = a.IsOccupy;
|
|
_isEnabled = a.IsEnable;
|
|
_editErr = "";
|
|
}
|
|
|
|
private void ClearPanelInputs()
|
|
{
|
|
_editingAreaId = "";
|
|
_editingHint = "新增区域";
|
|
_areaName = "";
|
|
_stationIds = "";
|
|
_controlRight = "";
|
|
_isOccupied = false;
|
|
_isEnabled = true;
|
|
_editErr = "";
|
|
}
|
|
|
|
private void SaveToConfig()
|
|
{
|
|
string json;
|
|
try
|
|
{
|
|
lock (TrafficInterlockMission.TrafficAreaList)
|
|
json = JsonConvert.SerializeObject(TrafficInterlockMission.TrafficAreaList, Formatting.Indented);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_status = "保存失败:" + ex.Message;
|
|
_panel?.Repaint();
|
|
return;
|
|
}
|
|
|
|
var path = JsonPath;
|
|
var dir = Path.GetDirectoryName(path);
|
|
Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
|
Directory.CreateDirectory(dir);
|
|
lock (SaveLock)
|
|
File.WriteAllText(path, json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_status = "保存失败:" + ex.Message;
|
|
_panel?.Repaint();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>解析站点集合字符串,如 "1,2,3" → <see cref="List{T}"/> of int。</summary>
|
|
private List<int> ParseStationIds(string text)
|
|
{
|
|
var list = new List<int>();
|
|
if (string.IsNullOrWhiteSpace(text)) return list;
|
|
foreach (var part in text.Split(new[] { ',', ';', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
if (int.TryParse(part.Trim(), out int id))
|
|
list.Add(id);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private void EnsureAreaIdsLocked()
|
|
{
|
|
foreach (var area in TrafficInterlockMission.TrafficAreaList)
|
|
EnsureAreaId(area);
|
|
}
|
|
|
|
private string EnsureAreaId(TrafficArea area)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(area.Id))
|
|
area.Id = NewAreaId();
|
|
return area.Id;
|
|
}
|
|
|
|
private string NewAreaId() => Guid.NewGuid().ToString("N");
|
|
}
|
|
}
|