Files
StandardSence/StandardScene.Core/Charge/ChargeStationManagementForm.cs
T

460 lines
20 KiB
C#
Raw Normal View History

2026-06-14 11:19:15 +08:00
using System;
using System.Collections.Generic;
2026-06-14 11:19:15 +08:00
using System.Drawing;
using System.IO;
2026-06-14 11:19:15 +08:00
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading.Tasks;
using FairyView;
using SimpleCore;
using StandardScene.Utils;
2026-06-14 11:19:15 +08:00
namespace StandardScene.Charge
{
/// <summary>
/// 充电桩管理界面(CycleGUI 版,替代原 WinForms 窗体)。
2026-06-14 11:19:15 +08:00
/// </summary>
public class ChargeStationManagementForm
2026-06-14 11:19:15 +08:00
{
private const string TableId = "charge-station-list";
private readonly string[] StatusFilterNames = { "全部状态", "空闲", "充电中", "故障", "离线" };
private readonly string[] TypeNames = { "FRLD高款充电桩", "FRLD矮款充电桩", "牧星充电桩" };
private readonly string[] MethodNames = { "地充", "尾充", "侧充" };
private readonly string[] CarTypeNames = { "FRLD充电", "牧星充电桩充电" };
private readonly ChargeStationDataService DataService = ChargeStationDataService.Instance;
private readonly Ping Ping = new Ping();
private Panel _panel;
private Panel _editDialog;
private List<ChargeStation> _snapshot = new List<ChargeStation>();
private volatile bool _refreshing;
private DateTime _lastFlush = DateTime.MinValue;
private readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(3);
private int _statusFilterIdx;
private string _searchText = "";
private string _statsText = "";
private string _status = "";
2026-06-14 11:19:15 +08:00
public void Show() => Open();
2026-06-14 11:19:15 +08:00
public static void Open() => (_instance ??= new ChargeStationManagementForm()).OpenCore();
private static ChargeStationManagementForm _instance;
private void OpenCore()
2026-06-14 11:19:15 +08:00
{
if (_panel != null)
2026-06-14 11:19:15 +08:00
{
try { _panel.BringToFront(); return; }
catch { _panel = null; }
2026-06-14 11:19:15 +08:00
}
_statusFilterIdx = 0;
_searchText = "";
_lastFlush = DateTime.MinValue;
2026-06-14 11:19:15 +08:00
var panel = GUI.DeclarePanel()
.ShowTitle("充电桩管理系统")
.ForceFloat()
.InitSize(1400, 760)
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_panel = panel;
panel.IfTerminalQuit(() => _panel = null);
2026-06-14 11:19:15 +08:00
panel.Define(pb =>
2026-06-14 11:19:15 +08:00
{
if (pb.Closing())
2026-06-14 11:19:15 +08:00
{
panel.Exit();
_panel = null;
return;
2026-06-14 11:19:15 +08:00
}
if (pb.Button("新增", distinct: "cs-add")) OpenEditDialog(null);
pb.SameLine(8);
if (pb.Button("刷新", distinct: "cs-refresh"))
2026-06-14 11:19:15 +08:00
{
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: FairyUiHelper.TableHeightPx(16), enableSearch: true);
if (!string.IsNullOrEmpty(_status))
{
pb.Separator();
pb.Label(_status);
2026-06-14 11:19:15 +08:00
}
pb.Panel.Repaint(repaintTimeMs: 1000);
});
2026-06-14 11:19:15 +08:00
}
private void EnsureSnapshotFresh()
2026-06-14 11:19:15 +08:00
{
if (_refreshing) return;
if (DateTime.Now - _lastFlush < FlushInterval) return;
_lastFlush = DateTime.Now;
_refreshing = true;
Task.Run(() =>
2026-06-14 11:19:15 +08:00
{
try
2026-06-14 11:19:15 +08:00
{
var list = DataService.GetAllStations().ToList();
foreach (var s in list)
2026-06-14 11:19:15 +08:00
{
try
2026-06-14 11:19:15 +08:00
{
s.CommStatus = Ping.Send(s.IpAddress, 1000).Status == IPStatus.Success
? CommunicationStatus.Normal : CommunicationStatus.Error;
2026-06-14 11:19:15 +08:00
}
catch { s.CommStatus = CommunicationStatus.Error; }
2026-06-14 11:19:15 +08:00
}
_snapshot = list;
}
catch (Exception ex) { _status = $"加载失败: {ex.Message}"; }
finally { _refreshing = false; _panel?.Repaint(); }
});
}
private 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 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 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}]")
.ForceFloat()
.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)}");
}
2026-06-14 11:19:15 +08:00
if (!string.IsNullOrEmpty(err)) { pb.Separator(); pb.Label(err); }
2026-06-14 11:19:15 +08:00
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;
2026-06-14 11:19:15 +08:00
string errorMessage;
bool ok = isAdd
? DataService.AddStation(station, out errorMessage)
: DataService.UpdateStation(station, out errorMessage, true);
2026-06-14 11:19:15 +08:00
if (!ok) { err = errorMessage; return; }
2026-06-14 11:19:15 +08:00
var mapSite = SimpleLib.GetSite(station.SiteId ?? 0);
if (mapSite != null)
2026-06-14 11:19:15 +08:00
{
mapSite.name = station.Name;
mapSite.fields["setVoltage"] = station.SetVoltage.ToString();
mapSite.fields["setElectricCurrent"] = station.SetElectricCurrent.ToString();
mapSite.fields["group"] = station.Enabled ? station.GroupCarType.ToString() : "禁用";
2026-06-14 11:19:15 +08:00
}
_lastFlush = DateTime.MinValue;
_status = "保存成功";
FairyUiHelper.Alert("提示", "保存成功");
dlg.Exit();
_editDialog = null;
_panel?.Repaint();
2026-06-14 11:19:15 +08:00
}
pb.SameLine(8);
if (pb.Button("取消", distinct: "cs-edit-cancel")) { dlg.Exit(); _editDialog = null; }
});
2026-06-14 11:19:15 +08:00
}
private 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)
2026-06-14 11:19:15 +08:00
{
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; }
2026-06-14 11:19:15 +08:00
if (isAdd && DataService.GetAllStations().Any(s => s.StationId == stationId))
{ err = $"充电桩编号 {stationId} 已存在"; return false; }
2026-06-14 11:19:15 +08:00
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; }
2026-06-14 11:19:15 +08:00
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; }
2026-06-14 11:19:15 +08:00
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;
2026-06-14 11:19:15 +08:00
}
private void ConfirmDelete(ChargeStation station)
2026-06-14 11:19:15 +08:00
{
FairyUiHelper.ConfirmThen($"确定删除充电桩 [{station.StationId}] {station.Name}", () =>
2026-06-14 11:19:15 +08:00
{
if (!DataService.DeleteStation(station.StationId, out var err))
2026-06-14 11:19:15 +08:00
{
FairyUiHelper.Alert("错误", $"删除失败: {err}");
return;
2026-06-14 11:19:15 +08:00
}
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");
2026-06-14 11:19:15 +08:00
}
_lastFlush = DateTime.MinValue;
_status = "删除成功";
_panel?.Repaint();
});
2026-06-14 11:19:15 +08:00
}
private void ExportData(PanelBuilder pb)
2026-06-14 11:19:15 +08:00
{
if (!pb.SaveFile("导出充电桩", "*.json;*.csv", out var path) || string.IsNullOrEmpty(path)) return;
2026-06-14 11:19:15 +08:00
try
{
var stations = DataService.GetAllStations();
if (path.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
2026-06-14 11:19:15 +08:00
{
var csv = new StringBuilder("编号,名称,类型,IP,端口,电压,电流,状态,启用,车型,站点ID,备注\n");
foreach (var s in stations)
2026-06-14 11:19:15 +08:00
{
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");
2026-06-14 11:19:15 +08:00
}
File.WriteAllText(path, csv.ToString(), Encoding.UTF8);
2026-06-14 11:19:15 +08:00
}
else
{
File.WriteAllText(path, Newtonsoft.Json.JsonConvert.SerializeObject(stations, Newtonsoft.Json.Formatting.Indented));
2026-06-14 11:19:15 +08:00
}
FairyUiHelper.Alert("提示", "导出成功");
2026-06-14 11:19:15 +08:00
}
catch (Exception ex) { FairyUiHelper.Alert("错误", $"导出失败: {ex.Message}"); }
2026-06-14 11:19:15 +08:00
}
private void ApplyRowColor(PanelBuilder.Row row, ChargeStation s)
2026-06-14 11:19:15 +08:00
{
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));
2026-06-14 11:19:15 +08:00
}
private ChargeStationStatus GetStatusFromFilterIndex(int idx) => idx switch
2026-06-14 11:19:15 +08:00
{
1 => ChargeStationStatus.Idle,
2 => ChargeStationStatus.Charging,
3 => ChargeStationStatus.Fault,
4 => ChargeStationStatus.Battery,
_ => ChargeStationStatus.Idle
};
2026-06-14 11:19:15 +08:00
private string GetStatusText(ChargeStationStatus status) => status switch
2026-06-14 11:19:15 +08:00
{
ChargeStationStatus.Idle => "空闲",
ChargeStationStatus.Charging => "充电中",
ChargeStationStatus.Fault => "故障",
ChargeStationStatus.Battery => "AGV电池已接入",
_ => "未知"
};
2026-06-14 11:19:15 +08:00
private string GetTypeText(ChargeStationType type) => type switch
2026-06-14 11:19:15 +08:00
{
ChargeStationType.FRLDTall => "FRLD高款充电桩",
ChargeStationType.FRLDShort => "FRLD矮款充电桩",
ChargeStationType.MuXing => "牧星充电桩",
_ => "未知"
};
2026-06-14 11:19:15 +08:00
private string GetMethodText(ChargeMethodType m) => m switch
2026-06-14 11:19:15 +08:00
{
ChargeMethodType.Ground => "地充",
ChargeMethodType.Rear => "尾充",
ChargeMethodType.Side => "侧充",
_ => "未知"
};
2026-06-14 11:19:15 +08:00
private string FormatComm(CommunicationStatus s) => s switch
2026-06-14 11:19:15 +08:00
{
CommunicationStatus.Normal => "✓ 正常",
CommunicationStatus.Delayed => "⚠ 延迟",
CommunicationStatus.Timeout => "✗ 超时",
CommunicationStatus.Disconnected => "✗ 断开",
CommunicationStatus.Error => "✗ 错误",
_ => "? 未知"
};
2026-06-14 11:19:15 +08:00
private string FormatMech(MechanismStatus s) => s switch
2026-06-14 11:19:15 +08:00
{
MechanismStatus.Extended => "◆ 伸出",
MechanismStatus.Retracted => "◇ 缩回",
MechanismStatus.Extending => "▶ 运动中",
_ => "? 未知"
};
2026-06-14 11:19:15 +08:00
private string FormatAlarm(ChargeStation s)
2026-06-14 11:19:15 +08:00
{
if (!s.HasAlarm) return "正常";
return string.IsNullOrWhiteSpace(s.AlarmMessage) ? $"【{s.AlarmLevel}】" : s.AlarmMessage;
2026-06-14 11:19:15 +08:00
}
}
}