Files
StandardSence/StandardScene.Core/Charge/ChargeStationManagementForm.cs
T
zhaowei.huang a0dc1e6cd0 refactor: 插件 UI 从 WinForms 迁移到 CycleGUI,并修复代码质量问题
将 StandardScene 各插件的配置/监控窗体从 WinForms 迁移到 CycleGUI(删除 .Designer.cs/.resx,重写为 PanelBuilder 立即模式 UI,新增 CycleUiHelper 统一对话框)。

同时修复代码审核中的问题:
- 后台文件写入加锁 + try/catch(ButtonBoxManager / DoorManager,对齐 LoopViewer.SaveTasks 模式)
- CoderFieldsMetadata.cs 启用 #nullable enable,消除 CS8632 警告
- DummyCar 移除已废弃的 rightClickAction()/SetPosition()
- CarRemoteHelper.OpenVehicleWebPage 的 Process.Start 加 try/catch
- 重命名名不副实的 Mstsc()(现为打开网页)
- 统一弃元命名为 _
- TrafficInterlockViewer 改用稳定 Id(GUID)做选择/编辑,替代行索引
- csproj 改用 $(CGUILibDir) 解析 CycleGUI,绝对路径收敛到 Directory.Build.props

构建:dotnet build StandardScene.sln → 0 错误,30 警告(均为历史遗留)。
注:static 单例状态重构(审核第 8 项)暂未处理,留待单独任务。
2026-06-26 15:00:53 +08:00

456 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}