将 StandardScene 各插件的配置/监控窗体从 WinForms 迁移到 CycleGUI(删除 .Designer.cs/.resx,重写为 PanelBuilder 立即模式 UI,新增 CycleUiHelper 统一对话框)。 同时修复代码审核中的问题: - 后台文件写入加锁 + try/catch(ButtonBoxManager / DoorManager,对齐 LoopViewer.SaveTasks 模式) - CoderFieldsMetadata.cs 启用 #nullable enable,消除 CS8632 警告 - DummyCar 移除已废弃的 rightClickAction()/SetPosition() - CarRemoteHelper.OpenVehicleWebPage 的 Process.Start 加 try/catch - 重命名名不副实的 Mstsc()(现为打开网页) - 统一弃元命名为 _ - TrafficInterlockViewer 改用稳定 Id(GUID)做选择/编辑,替代行索引 - csproj 改用 $(CGUILibDir) 解析 CycleGUI,绝对路径收敛到 Directory.Build.props 构建:dotnet build StandardScene.sln → 0 错误,30 警告(均为历史遗留)。 注:static 单例状态重构(审核第 8 项)暂未处理,留待单独任务。
456 lines
20 KiB
C#
456 lines
20 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Drawing;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net.NetworkInformation;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using CycleGUI;
|
||
using SimpleCore;
|
||
using StandardScene.Utils;
|
||
|
||
namespace StandardScene.Charge
|
||
{
|
||
/// <summary>
|
||
/// 充电桩管理界面(CycleGUI 版,替代原 WinForms 窗体)。
|
||
/// </summary>
|
||
public class ChargeStationManagementForm
|
||
{
|
||
private const string TableId = "charge-station-list";
|
||
private static readonly string[] StatusFilterNames = { "全部状态", "空闲", "充电中", "故障", "离线" };
|
||
private static readonly string[] TypeNames = { "FRLD高款充电桩", "FRLD矮款充电桩", "牧星充电桩" };
|
||
private static readonly string[] MethodNames = { "地充", "尾充", "侧充" };
|
||
private static readonly string[] CarTypeNames = { "FRLD充电", "牧星充电桩充电" };
|
||
|
||
private static readonly ChargeStationDataService DataService = ChargeStationDataService.Instance;
|
||
private static readonly Ping Ping = new Ping();
|
||
|
||
private static Panel _panel;
|
||
private static Panel _editDialog;
|
||
private static List<ChargeStation> _snapshot = new List<ChargeStation>();
|
||
private static volatile bool _refreshing;
|
||
private static DateTime _lastFlush = DateTime.MinValue;
|
||
private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(3);
|
||
|
||
private static int _statusFilterIdx;
|
||
private static string _searchText = "";
|
||
private static string _statsText = "";
|
||
private static string _status = "";
|
||
|
||
public void Show() => Open();
|
||
|
||
public static void Open()
|
||
{
|
||
if (_panel != null)
|
||
{
|
||
try { _panel.BringToFront(); return; }
|
||
catch { _panel = null; }
|
||
}
|
||
|
||
_statusFilterIdx = 0;
|
||
_searchText = "";
|
||
_lastFlush = DateTime.MinValue;
|
||
|
||
var panel = GUI.DeclarePanel()
|
||
.ShowTitle("充电桩管理系统")
|
||
.SetDefaultDocking(Panel.Docking.None)
|
||
.InitSize(1400, 760)
|
||
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
|
||
_panel = panel;
|
||
panel.IfTerminalQuit(() => _panel = null);
|
||
|
||
panel.Define(pb =>
|
||
{
|
||
if (pb.Closing())
|
||
{
|
||
panel.Exit();
|
||
_panel = null;
|
||
return;
|
||
}
|
||
|
||
if (pb.Button("新增", distinct: "cs-add")) OpenEditDialog(null);
|
||
pb.SameLine(8);
|
||
if (pb.Button("刷新", distinct: "cs-refresh"))
|
||
{
|
||
DataService.Reload();
|
||
_lastFlush = DateTime.MinValue;
|
||
_status = "数据已刷新";
|
||
}
|
||
pb.SameLine(8);
|
||
if (pb.Button("策略配置", distinct: "cs-strategy")) ChargeStrategyConfigForm.Open();
|
||
pb.SameLine(8);
|
||
if (pb.Button("通讯监控", distinct: "cs-comm")) CommunicationMonitorForm.Open();
|
||
pb.SameLine(8);
|
||
if (pb.Button("报警配置", distinct: "cs-alarm")) AlarmConfigManagementForm.Open();
|
||
pb.SameLine(8);
|
||
if (pb.Button("导出", distinct: "cs-export")) ExportData(pb);
|
||
|
||
pb.Separator();
|
||
if (pb.DropdownBox("状态筛选", StatusFilterNames, ref _statusFilterIdx))
|
||
_lastFlush = DateTime.MinValue;
|
||
pb.SameLine(12);
|
||
var (search, _) = pb.TextInput("搜索", _searchText, alwaysReturnString: true, hintText: "编号/名称/IP");
|
||
if (search != _searchText) { _searchText = search; _lastFlush = DateTime.MinValue; }
|
||
|
||
EnsureSnapshotFresh();
|
||
var items = FilterSnapshot(_snapshot);
|
||
UpdateStats(_snapshot);
|
||
|
||
pb.Label(_statsText);
|
||
pb.Label($"显示 {items.Count} / 共 {_snapshot.Count} 个充电桩");
|
||
|
||
pb.Table(TableId,
|
||
new[]
|
||
{
|
||
"编号", "名称", "类型", "充电方式", "站点", "通讯", "状态",
|
||
"车辆", "电量", "电压", "电流", "启用", "操作"
|
||
},
|
||
items.Count, (row, i) =>
|
||
{
|
||
var s = items[i];
|
||
ApplyRowColor(row, s);
|
||
row.Label(s.StationId);
|
||
row.Label(s.Name);
|
||
row.Label(GetTypeText(s.Type));
|
||
row.Label(GetMethodText(s.ChargeMethod));
|
||
row.Label(s.SiteId?.ToString() ?? "");
|
||
row.Label(FormatComm(s.CommStatus));
|
||
row.Label(GetStatusText(s.Status));
|
||
row.Label(string.IsNullOrWhiteSpace(s.CurrentVehicle) ? "-" : s.CurrentVehicle);
|
||
row.Label(s.BatteryLevel > 0 ? $"{s.BatteryLevel:F1}%" : "-");
|
||
row.Label($"{s.RealTimeVoltage:F1}");
|
||
row.Label($"{s.RealTimeCurrent:F1}");
|
||
row.Label(s.Enabled ? "是" : "否");
|
||
var op = row.ButtonGroup(new[] { "编辑", "删除" }, new[] { "编辑充电桩", "删除充电桩" });
|
||
if (op == 0) OpenEditDialog(s);
|
||
else if (op == 1) ConfirmDelete(s);
|
||
}, height: 16, enableSearch: true);
|
||
|
||
if (!string.IsNullOrEmpty(_status))
|
||
{
|
||
pb.Separator();
|
||
pb.Label(_status);
|
||
}
|
||
|
||
pb.Panel.Repaint(repaintTimeMs: 1000);
|
||
});
|
||
}
|
||
|
||
private static void EnsureSnapshotFresh()
|
||
{
|
||
if (_refreshing) return;
|
||
if (DateTime.Now - _lastFlush < FlushInterval) return;
|
||
_lastFlush = DateTime.Now;
|
||
_refreshing = true;
|
||
Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
var list = DataService.GetAllStations().ToList();
|
||
foreach (var s in list)
|
||
{
|
||
try
|
||
{
|
||
s.CommStatus = Ping.Send(s.IpAddress, 1000).Status == IPStatus.Success
|
||
? CommunicationStatus.Normal : CommunicationStatus.Error;
|
||
}
|
||
catch { s.CommStatus = CommunicationStatus.Error; }
|
||
}
|
||
_snapshot = list;
|
||
}
|
||
catch (Exception ex) { _status = $"加载失败: {ex.Message}"; }
|
||
finally { _refreshing = false; _panel?.Repaint(); }
|
||
});
|
||
}
|
||
|
||
private static List<ChargeStation> FilterSnapshot(List<ChargeStation> src)
|
||
{
|
||
IEnumerable<ChargeStation> q = src;
|
||
if (_statusFilterIdx > 0)
|
||
{
|
||
var st = GetStatusFromFilterIndex(_statusFilterIdx);
|
||
q = q.Where(s => s.Status == st);
|
||
}
|
||
var text = (_searchText ?? "").Trim().ToLower();
|
||
if (!string.IsNullOrEmpty(text))
|
||
{
|
||
q = q.Where(s => s.StationId.ToLower().Contains(text)
|
||
|| s.Name.ToLower().Contains(text)
|
||
|| (s.IpAddress ?? "").Contains(text)
|
||
|| GetTypeText(s.Type).Contains(text));
|
||
}
|
||
return q.ToList();
|
||
}
|
||
|
||
private static void UpdateStats(List<ChargeStation> all)
|
||
{
|
||
_statsText = $"总数: {all.Count} | 空闲: {all.Count(s => s.Status == ChargeStationStatus.Idle)} | " +
|
||
$"充电中: {all.Count(s => s.Status == ChargeStationStatus.Charging)} | " +
|
||
$"故障: {all.Count(s => s.Status == ChargeStationStatus.Fault)} | " +
|
||
$"AGV电池已接入: {all.Count(s => s.Status == ChargeStationStatus.Battery)}";
|
||
}
|
||
|
||
private static void OpenEditDialog(ChargeStation existing)
|
||
{
|
||
if (_editDialog != null)
|
||
{
|
||
try { _editDialog.BringToFront(); return; }
|
||
catch { _editDialog = null; }
|
||
}
|
||
|
||
bool isAdd = existing == null;
|
||
string stationId = isAdd ? "" : existing.StationId;
|
||
string name = isAdd ? "" : existing.Name;
|
||
int typeIdx = isAdd ? 1 : (int)existing.Type;
|
||
int methodIdx = isAdd ? 0 : (int)existing.ChargeMethod;
|
||
int carTypeIdx = isAdd ? 0 : (int)existing.GroupCarType;
|
||
string ip = isAdd ? "192.168." : existing.IpAddress;
|
||
string port = isAdd ? "2000" : existing.Port.ToString();
|
||
string voltage = isAdd ? "48" : existing.SetVoltage.ToString("F1");
|
||
string current = isAdd ? "32" : existing.SetElectricCurrent.ToString("F1");
|
||
string siteId = isAdd ? "0" : (existing.SiteId?.ToString() ?? "0");
|
||
string remarks = isAdd ? "" : (existing.Remarks ?? "");
|
||
bool enabled = isAdd || existing.Enabled;
|
||
bool shield = !isAdd && existing.ShieldSiteMechanismStatus;
|
||
string err = "";
|
||
|
||
var dlg = GUI.DeclarePanel()
|
||
.ShowTitle(isAdd ? "新增充电桩" : $"编辑充电桩 [{existing.StationId}]")
|
||
.SetDefaultDocking(Panel.Docking.None)
|
||
.InitSize(480, 520)
|
||
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
|
||
_editDialog = dlg;
|
||
dlg.IfTerminalQuit(() => _editDialog = null);
|
||
|
||
dlg.Define(pb =>
|
||
{
|
||
if (pb.Closing()) { dlg.Exit(); _editDialog = null; return; }
|
||
|
||
var (sid, _) = pb.TextInput("1. 编号 (1-99)", stationId, alwaysReturnString: true);
|
||
stationId = sid;
|
||
var (nm, _) = pb.TextInput("2. 名称", name, alwaysReturnString: true);
|
||
name = nm;
|
||
pb.DropdownBox("3. 类型", TypeNames, ref typeIdx);
|
||
pb.DropdownBox("4. 充电方式", MethodNames, ref methodIdx);
|
||
pb.DropdownBox("5. 停靠车型", CarTypeNames, ref carTypeIdx);
|
||
var (ipT, _) = pb.TextInput("6. IP", ip, alwaysReturnString: true);
|
||
ip = ipT;
|
||
var (pt, _) = pb.TextInput("7. 端口", port, alwaysReturnString: true);
|
||
port = pt;
|
||
var (vt, _) = pb.TextInput("8. 电压", voltage, alwaysReturnString: true);
|
||
voltage = vt;
|
||
var (ct, _) = pb.TextInput("9. 电流", current, alwaysReturnString: true);
|
||
current = ct;
|
||
var (site, _) = pb.TextInput("10. 站点ID", siteId, alwaysReturnString: true);
|
||
siteId = site;
|
||
var (rm, _) = pb.TextInput("11. 备注", remarks, alwaysReturnString: true);
|
||
remarks = rm;
|
||
pb.CheckBox("12. 启用", ref enabled);
|
||
if (methodIdx == (int)ChargeMethodType.Side)
|
||
pb.CheckBox("13. 屏蔽机构状态交互", ref shield);
|
||
|
||
if (!isAdd)
|
||
{
|
||
pb.SeparatorText("实时状态(只读)");
|
||
pb.Label($"车辆: {(string.IsNullOrWhiteSpace(existing.CurrentVehicle) ? "-" : existing.CurrentVehicle)}");
|
||
pb.Label($"电量: {(existing.BatteryLevel > 0 ? $"{existing.BatteryLevel:F1}%" : "-")}");
|
||
pb.Label($"通讯: {FormatComm(existing.CommStatus)} | 机构: {FormatMech(existing.MechanismStatus)}");
|
||
pb.Label($"报警: {FormatAlarm(existing)}");
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(err)) { pb.Separator(); pb.Label(err); }
|
||
|
||
pb.Separator();
|
||
if (pb.Button("保存", distinct: "cs-edit-save"))
|
||
{
|
||
if (!TryBuildStation(isAdd, existing, stationId, name, typeIdx, methodIdx, carTypeIdx,
|
||
ip, port, voltage, current, siteId, remarks, enabled, shield, out var station, out err))
|
||
return;
|
||
|
||
string errorMessage;
|
||
bool ok = isAdd
|
||
? DataService.AddStation(station, out errorMessage)
|
||
: DataService.UpdateStation(station, out errorMessage, true);
|
||
|
||
if (!ok) { err = errorMessage; return; }
|
||
|
||
var mapSite = SimpleLib.GetSite(station.SiteId ?? 0);
|
||
if (mapSite != null)
|
||
{
|
||
mapSite.name = station.Name;
|
||
mapSite.fields["setVoltage"] = station.SetVoltage.ToString();
|
||
mapSite.fields["setElectricCurrent"] = station.SetElectricCurrent.ToString();
|
||
mapSite.fields["group"] = station.Enabled ? station.GroupCarType.ToString() : "禁用";
|
||
}
|
||
|
||
_lastFlush = DateTime.MinValue;
|
||
_status = "保存成功";
|
||
CycleUiHelper.Alert("提示", "保存成功");
|
||
dlg.Exit();
|
||
_editDialog = null;
|
||
_panel?.Repaint();
|
||
}
|
||
pb.SameLine(8);
|
||
if (pb.Button("取消", distinct: "cs-edit-cancel")) { dlg.Exit(); _editDialog = null; }
|
||
});
|
||
}
|
||
|
||
private static bool TryBuildStation(bool isAdd, ChargeStation existing, string stationId, string name,
|
||
int typeIdx, int methodIdx, int carTypeIdx, string ip, string port, string voltage, string current,
|
||
string siteIdText, string remarks, bool enabled, bool shield,
|
||
out ChargeStation station, out string err)
|
||
{
|
||
station = new ChargeStation();
|
||
err = "";
|
||
stationId = (stationId ?? "").Trim();
|
||
if (string.IsNullOrEmpty(stationId) || !int.TryParse(stationId, out var num) || num < 1 || num > 99)
|
||
{ err = "充电桩编号必须是 1-99 之间的数字"; return false; }
|
||
|
||
if (isAdd && DataService.GetAllStations().Any(s => s.StationId == stationId))
|
||
{ err = $"充电桩编号 {stationId} 已存在"; return false; }
|
||
|
||
if (!int.TryParse((siteIdText ?? "").Trim(), out var siteId))
|
||
{ err = "站点ID必须是数字"; return false; }
|
||
var site = SimpleLib.GetSite(siteId);
|
||
if (site == null) { err = $"站点ID {siteId} 未在调度系统上"; return false; }
|
||
|
||
if (!int.TryParse((port ?? "").Trim(), out var portNum)) { err = "端口必须是数字"; return false; }
|
||
if (!double.TryParse((voltage ?? "").Trim(), out var volt)) { err = "电压格式无效"; return false; }
|
||
if (!double.TryParse((current ?? "").Trim(), out var amp)) { err = "电流格式无效"; return false; }
|
||
|
||
station.StationId = stationId;
|
||
station.Name = (name ?? "").Trim();
|
||
station.Type = (ChargeStationType)typeIdx;
|
||
station.ChargeMethod = (ChargeMethodType)methodIdx;
|
||
station.GroupCarType = (ChargeStationCarType)carTypeIdx;
|
||
station.IpAddress = (ip ?? "").Trim();
|
||
station.Port = portNum;
|
||
station.SetVoltage = volt;
|
||
station.SetElectricCurrent = amp;
|
||
station.Enabled = enabled;
|
||
station.ShieldSiteMechanismStatus = shield;
|
||
station.SiteId = siteId > 0 ? siteId : (int?)null;
|
||
station.Remarks = remarks ?? "";
|
||
return true;
|
||
}
|
||
|
||
private static void ConfirmDelete(ChargeStation station)
|
||
{
|
||
CycleUiHelper.ConfirmThen($"确定删除充电桩 [{station.StationId}] {station.Name}?", () =>
|
||
{
|
||
if (!DataService.DeleteStation(station.StationId, out var err))
|
||
{
|
||
CycleUiHelper.Alert("错误", $"删除失败: {err}");
|
||
return;
|
||
}
|
||
var site = SimpleLib.GetSite(station.SiteId ?? 0);
|
||
if (site != null)
|
||
{
|
||
site.name = "NoName";
|
||
site.fields.Remove("setVoltage");
|
||
site.fields.Remove("setElectricCurrent");
|
||
site.fields.Remove("Charge");
|
||
site.fields.Remove("group");
|
||
}
|
||
_lastFlush = DateTime.MinValue;
|
||
_status = "删除成功";
|
||
_panel?.Repaint();
|
||
});
|
||
}
|
||
|
||
private static void ExportData(PanelBuilder pb)
|
||
{
|
||
if (!pb.SaveFile("导出充电桩", "*.json;*.csv", out var path) || string.IsNullOrEmpty(path)) return;
|
||
try
|
||
{
|
||
var stations = DataService.GetAllStations();
|
||
if (path.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var csv = new StringBuilder("编号,名称,类型,IP,端口,电压,电流,状态,启用,车型,站点ID,备注\n");
|
||
foreach (var s in stations)
|
||
{
|
||
csv.Append($"{s.StationId},{s.Name},{GetTypeText(s.Type)},{s.IpAddress},{s.Port}," +
|
||
$"{s.SetVoltage},{s.SetElectricCurrent},{GetStatusText(s.Status)}," +
|
||
$"{(s.Enabled ? "是" : "否")},{s.GroupCarType},{s.SiteId},{s.Remarks}\n");
|
||
}
|
||
File.WriteAllText(path, csv.ToString(), Encoding.UTF8);
|
||
}
|
||
else
|
||
{
|
||
File.WriteAllText(path, Newtonsoft.Json.JsonConvert.SerializeObject(stations, Newtonsoft.Json.Formatting.Indented));
|
||
}
|
||
CycleUiHelper.Alert("提示", "导出成功");
|
||
}
|
||
catch (Exception ex) { CycleUiHelper.Alert("错误", $"导出失败: {ex.Message}"); }
|
||
}
|
||
|
||
private static void ApplyRowColor(PanelBuilder.Row row, ChargeStation s)
|
||
{
|
||
if (s.HasAlarm) row.SetColor(Color.FromArgb(255, 205, 210));
|
||
else if (s.Status == ChargeStationStatus.Idle) row.SetColor(Color.FromArgb(232, 245, 233));
|
||
else if (s.Status == ChargeStationStatus.Charging) row.SetColor(Color.FromArgb(200, 230, 201));
|
||
else if (s.Status == ChargeStationStatus.Fault) row.SetColor(Color.FromArgb(255, 205, 210));
|
||
else if (s.Status == ChargeStationStatus.Battery) row.SetColor(Color.FromArgb(238, 238, 238));
|
||
}
|
||
|
||
private static ChargeStationStatus GetStatusFromFilterIndex(int idx) => idx switch
|
||
{
|
||
1 => ChargeStationStatus.Idle,
|
||
2 => ChargeStationStatus.Charging,
|
||
3 => ChargeStationStatus.Fault,
|
||
4 => ChargeStationStatus.Battery,
|
||
_ => ChargeStationStatus.Idle
|
||
};
|
||
|
||
private static string GetStatusText(ChargeStationStatus status) => status switch
|
||
{
|
||
ChargeStationStatus.Idle => "空闲",
|
||
ChargeStationStatus.Charging => "充电中",
|
||
ChargeStationStatus.Fault => "故障",
|
||
ChargeStationStatus.Battery => "AGV电池已接入",
|
||
_ => "未知"
|
||
};
|
||
|
||
private static string GetTypeText(ChargeStationType type) => type switch
|
||
{
|
||
ChargeStationType.FRLDTall => "FRLD高款充电桩",
|
||
ChargeStationType.FRLDShort => "FRLD矮款充电桩",
|
||
ChargeStationType.MuXing => "牧星充电桩",
|
||
_ => "未知"
|
||
};
|
||
|
||
private static string GetMethodText(ChargeMethodType m) => m switch
|
||
{
|
||
ChargeMethodType.Ground => "地充",
|
||
ChargeMethodType.Rear => "尾充",
|
||
ChargeMethodType.Side => "侧充",
|
||
_ => "未知"
|
||
};
|
||
|
||
private static string FormatComm(CommunicationStatus s) => s switch
|
||
{
|
||
CommunicationStatus.Normal => "✓ 正常",
|
||
CommunicationStatus.Delayed => "⚠ 延迟",
|
||
CommunicationStatus.Timeout => "✗ 超时",
|
||
CommunicationStatus.Disconnected => "✗ 断开",
|
||
CommunicationStatus.Error => "✗ 错误",
|
||
_ => "? 未知"
|
||
};
|
||
|
||
private static string FormatMech(MechanismStatus s) => s switch
|
||
{
|
||
MechanismStatus.Extended => "◆ 伸出",
|
||
MechanismStatus.Retracted => "◇ 缩回",
|
||
MechanismStatus.Extending => "▶ 运动中",
|
||
_ => "? 未知"
|
||
};
|
||
|
||
private static string FormatAlarm(ChargeStation s)
|
||
{
|
||
if (!s.HasAlarm) return "正常";
|
||
return string.IsNullOrWhiteSpace(s.AlarmMessage) ? $"【{s.AlarmLevel}】" : s.AlarmMessage;
|
||
}
|
||
}
|
||
}
|