2026-06-26 15:00:53 +08:00
|
|
|
|
using System;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
2026-06-26 15:00:53 +08:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using CycleGUI;
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
using SimpleCore.Library;
|
|
|
|
|
|
using StandardScene.Model;
|
|
|
|
|
|
using StandardScene.Utils;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
|
|
|
|
|
namespace LoopViewerApp
|
|
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 环线/循环任务配置管理界面(CycleGUI 版,替代原 WinForms <c>LoopViewer</c> 窗体)。
|
|
|
|
|
|
/// <list type="bullet">
|
|
|
|
|
|
/// <item>维护 <c>tasklist.json</c>(<see cref="List{T}"/> of <see cref="LoopTask"/>)的增 / 改 / 删;与 <c>AbstractLoopMission</c> 读取同一文件。</item>
|
|
|
|
|
|
/// <item>单实例:再次打开则把已有面板置前。</item>
|
|
|
|
|
|
/// <item>勾选多行后「删除选中」可批量删除(保留原 ListView 多选删除能力);每行「编辑」按钮打开编辑对话框。</item>
|
|
|
|
|
|
/// <item>文件写入放后台线程,绝不阻塞渲染线程(避免界面卡死)。</item>
|
|
|
|
|
|
/// </list>
|
|
|
|
|
|
/// 沿用 <c>DeliveryViewer</c> 的同套模式(单实例面板、<c>pb.Table</c>、<c>CycleUiHelper.ConfirmThen</c>),不另造轮子。
|
|
|
|
|
|
/// 保留可实例化 + <see cref="Show"/> 以兼容既有调用 <c>new LoopViewer().Show()</c>。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class LoopViewer
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
private const string TableId = "loop-task-list";
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
// 与 AbstractLoopMission 完全一致的读取路径,保证“写哪儿、它就读哪儿”。
|
|
|
|
|
|
private static string JsonPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tasklist.json");
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
private static readonly object SaveLock = new object();
|
|
|
|
|
|
// 直接取自枚举,自动与 TaskKind / TaskStartType 保持同步(含 Charge),无需手写列表。
|
|
|
|
|
|
private static readonly string[] KindNames = Enum.GetNames(typeof(TaskKind));
|
|
|
|
|
|
private static readonly string[] StartTypeNames = Enum.GetNames(typeof(TaskStartType));
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
private static Panel _panel;
|
|
|
|
|
|
private static Panel _dialog; // 新增/编辑对话框,限单实例
|
|
|
|
|
|
private static List<LoopTask> _tasks = new List<LoopTask>(); // 仅渲染线程读写
|
|
|
|
|
|
private static readonly HashSet<int> _selected = new HashSet<int>(); // 仅渲染线程读写,存被勾选任务的 Id
|
|
|
|
|
|
private static volatile string _status = "";
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
/// <summary>打开(或置前)任务管理面板。兼容原 <c>new LoopViewer().Show()</c> 调用方式。</summary>
|
|
|
|
|
|
public void Show() => Open();
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
/// <summary>打开(或置前)任务管理面板。</summary>
|
|
|
|
|
|
public static void Open()
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
if (_panel != null)
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
try
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_panel.BringToFront();
|
2026-06-14 11:19:15 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
catch
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_panel = null;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
}
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_selected.Clear();
|
|
|
|
|
|
LoadTasks();
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
var panel = GUI.DeclarePanel()
|
|
|
|
|
|
.ShowTitle("任务列表管理器")
|
|
|
|
|
|
.SetDefaultDocking(Panel.Docking.None)
|
|
|
|
|
|
.InitSize(1080, 620)
|
|
|
|
|
|
.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
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
panel.Define(pb =>
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
if (pb.Closing())
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
panel.Exit();
|
|
|
|
|
|
_panel = null;
|
|
|
|
|
|
return;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
if (pb.Button("新增任务", distinct: "loop-add"))
|
|
|
|
|
|
OpenEditDialog(null);
|
|
|
|
|
|
pb.SameLine(12);
|
|
|
|
|
|
if (pb.Button("删除选中", distinct: "loop-del-selected"))
|
|
|
|
|
|
ConfirmDeleteSelected();
|
|
|
|
|
|
pb.SameLine(16);
|
|
|
|
|
|
pb.Label($"共 {_tasks.Count} 个任务,已选 {_selected.Count} 个");
|
|
|
|
|
|
|
|
|
|
|
|
pb.Table(TableId,
|
|
|
|
|
|
new[] { "选择", "ID", "任务类别", "当前站点", "目标站点", "流量控制", "优先级", "途径点", "启动类型", "操作" },
|
|
|
|
|
|
_tasks.Count, (row, i) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = _tasks[i];
|
|
|
|
|
|
var id = t.Id;
|
|
|
|
|
|
|
|
|
|
|
|
var sel = _selected.Contains(id);
|
|
|
|
|
|
if (row.Checkbox(ref sel, "勾选以批量删除"))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (sel) _selected.Add(id);
|
|
|
|
|
|
else _selected.Remove(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
row.Label($"{t.Id}");
|
|
|
|
|
|
row.Label($"{t.Kind}");
|
|
|
|
|
|
row.Label($"{t.CurrentStationId}");
|
|
|
|
|
|
row.Label($"{t.TargetStationId}");
|
|
|
|
|
|
row.Label($"{t.TrafficControl}");
|
|
|
|
|
|
row.Label($"{t.Priority}");
|
|
|
|
|
|
row.Label(t.IsViaPoint ? "是" : "否");
|
|
|
|
|
|
row.Label($"{t.StartType}");
|
|
|
|
|
|
|
|
|
|
|
|
if (row.ButtonGroup(new[] { "编辑" }, new[] { "编辑该任务" }) == 0)
|
|
|
|
|
|
OpenEditDialog(t);
|
|
|
|
|
|
}, height: 18, enableSearch: true);
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(_status))
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
pb.Separator();
|
|
|
|
|
|
pb.Label(_status);
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
// 事件驱动为主,配合较慢的节流重绘即可保证后台保存结果/状态及时反映。
|
|
|
|
|
|
pb.Panel.Repaint(repaintTimeMs: 500);
|
|
|
|
|
|
});
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
/// <summary>对选中项发起二次确认后删除(保留原多选删除的提示文案)。</summary>
|
|
|
|
|
|
private static void ConfirmDeleteSelected()
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
if (_selected.Count == 0)
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_status = "未选择任何任务";
|
|
|
|
|
|
_panel?.Repaint();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
string prompt;
|
|
|
|
|
|
if (_selected.Count == 1)
|
|
|
|
|
|
prompt = $"确认删除任务 ID={_selected.First()}?";
|
|
|
|
|
|
else
|
|
|
|
|
|
prompt = $"确认删除所选 {_selected.Count} 个任务?";
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
// 删除仅做内存列表增删(极快,可在渲染线程执行);真正的文件写入在 SaveTasks 内部放后台线程。
|
|
|
|
|
|
CycleUiHelper.ConfirmThen(prompt, DeleteSelected);
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
private static void DeleteSelected()
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
var removed = _tasks.RemoveAll(t => _selected.Contains(t.Id));
|
|
|
|
|
|
_selected.Clear();
|
|
|
|
|
|
SaveTasks();
|
|
|
|
|
|
_status = $"已删除 {removed} 个任务";
|
|
|
|
|
|
_panel?.Repaint();
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-26 15:00:53 +08:00
|
|
|
|
/// 打开「新增 / 编辑」对话框(置顶非模态、限单实例)。<paramref name="existing"/> 为 null 表示新增,否则编辑该任务(保留其 Id)。
|
|
|
|
|
|
/// 每次打开都是全新面板:<c>defaultText</c> 能正确初始化,规避立即模式下文本框缓冲难以重置的问题。
|
2026-06-14 11:19:15 +08:00
|
|
|
|
/// </summary>
|
2026-06-26 15:00:53 +08:00
|
|
|
|
private static void OpenEditDialog(LoopTask existing)
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
if (_dialog != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
try { _dialog.BringToFront(); return; }
|
|
|
|
|
|
catch { _dialog = null; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool isAdd = existing == null;
|
|
|
|
|
|
|
|
|
|
|
|
int kindIdx = isAdd ? 0 : Math.Max(0, Array.IndexOf(KindNames, existing.Kind.ToString()));
|
|
|
|
|
|
int startIdx = Math.Max(0, Array.IndexOf(StartTypeNames,
|
|
|
|
|
|
(isAdd ? TaskStartType.AutoLoop : existing.StartType).ToString()));
|
|
|
|
|
|
string curText = (isAdd ? 0 : Clamp(existing.CurrentStationId, 0, 1000000)).ToString();
|
|
|
|
|
|
string tgtText = (isAdd ? 0 : Clamp(existing.TargetStationId, 0, 1000000)).ToString();
|
|
|
|
|
|
string trafficText = (isAdd ? 0 : Clamp(existing.TrafficControl, 0, 1000)).ToString();
|
|
|
|
|
|
string priText = (isAdd ? 1 : Clamp(existing.Priority, 0, 100)).ToString();
|
|
|
|
|
|
bool via = !isAdd && existing.IsViaPoint;
|
|
|
|
|
|
string err = "";
|
|
|
|
|
|
|
|
|
|
|
|
// 不用 Modal:原生「模态弹窗 + 标题栏关闭X」的 EndPopup 配对 bug 会断言崩溃。
|
|
|
|
|
|
// 也不用 TopMost:置顶视口带 NoAutoMerge,会让 DropdownBox 的下拉弹窗落到独立非置顶视口里、被对话框挡在后面(看不到选项)。
|
|
|
|
|
|
// 故采用与 DeliveryViewer 相同的普通浮动面板(非模态、不停靠):Begin/End 路径,X 关闭干净,下拉弹窗 z 序正常。
|
|
|
|
|
|
var dlg = GUI.DeclarePanel()
|
|
|
|
|
|
.ShowTitle(isAdd ? "新增任务" : $"编辑任务 ID: {existing.Id}")
|
|
|
|
|
|
.SetDefaultDocking(Panel.Docking.None)
|
|
|
|
|
|
.InitSize(420, 380)
|
|
|
|
|
|
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
|
|
|
|
|
|
_dialog = dlg;
|
|
|
|
|
|
dlg.IfTerminalQuit(() => _dialog = null);
|
|
|
|
|
|
dlg.Define(pb =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (pb.Closing())
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
dlg.Exit();
|
|
|
|
|
|
_dialog = null;
|
|
|
|
|
|
return;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
// 控件 id 由 ImHashStr(prompt) 经 Encoding.ASCII 计算:中文会被压成 '?',导致同字数纯中文标签
|
|
|
|
|
|
// (如“当前站点/目标站点”“任务类别/启动类型”)哈希相同而抛 Duplicated id。故各标签加唯一 ASCII 序号前缀以区分。
|
|
|
|
|
|
pb.DropdownBox("1. 任务类别", KindNames, ref kindIdx);
|
|
|
|
|
|
var (c, _) = pb.TextInput("2. 当前站点 (0~1000000)", curText, alwaysReturnString: true);
|
|
|
|
|
|
curText = c;
|
|
|
|
|
|
var (tg, _) = pb.TextInput("3. 目标站点 (0~1000000)", tgtText, alwaysReturnString: true);
|
|
|
|
|
|
tgtText = tg;
|
|
|
|
|
|
var (tf, _) = pb.TextInput("4. 流量控制 (0~1000)", trafficText, alwaysReturnString: true);
|
|
|
|
|
|
trafficText = tf;
|
|
|
|
|
|
var (pr, _) = pb.TextInput("5. 优先级 (0~100)", priText, alwaysReturnString: true);
|
|
|
|
|
|
priText = pr;
|
|
|
|
|
|
pb.CheckBox("6. 途径点", ref via);
|
|
|
|
|
|
pb.DropdownBox("7. 启动类型", StartTypeNames, ref startIdx);
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(err))
|
|
|
|
|
|
{
|
|
|
|
|
|
pb.Separator();
|
|
|
|
|
|
pb.Label(err);
|
|
|
|
|
|
}
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
pb.Separator();
|
|
|
|
|
|
if (pb.Button("保存", distinct: "loop-edit-save"))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!TryParseClamp(curText, 0, 1000000, out var cur)) { err = "当前站点需为 0~1000000 的整数"; return; }
|
|
|
|
|
|
if (!TryParseClamp(tgtText, 0, 1000000, out var tgt)) { err = "目标站点需为 0~1000000 的整数"; return; }
|
|
|
|
|
|
if (!TryParseClamp(trafficText, 0, 1000, out var traffic)) { err = "流量控制需为 0~1000 的整数"; return; }
|
|
|
|
|
|
if (!TryParseClamp(priText, 0, 100, out var pri)) { err = "优先级需为 0~100 的整数"; return; }
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
Enum.TryParse<TaskKind>(KindNames[kindIdx], out var kind);
|
|
|
|
|
|
Enum.TryParse<TaskStartType>(StartTypeNames[startIdx], out var st);
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
if (isAdd)
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_tasks.Add(new LoopTask
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = GetNextTaskId(),
|
|
|
|
|
|
Kind = kind,
|
|
|
|
|
|
CurrentStationId = cur,
|
|
|
|
|
|
TargetStationId = tgt,
|
|
|
|
|
|
TrafficControl = traffic,
|
|
|
|
|
|
Priority = pri,
|
|
|
|
|
|
IsViaPoint = via,
|
|
|
|
|
|
StartType = st
|
|
|
|
|
|
});
|
|
|
|
|
|
_status = "已新增任务";
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
else
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
existing.Kind = kind;
|
|
|
|
|
|
existing.CurrentStationId = cur;
|
|
|
|
|
|
existing.TargetStationId = tgt;
|
|
|
|
|
|
existing.TrafficControl = traffic;
|
|
|
|
|
|
existing.Priority = pri;
|
|
|
|
|
|
existing.IsViaPoint = via;
|
|
|
|
|
|
existing.StartType = st;
|
|
|
|
|
|
_status = $"已保存任务 ID={existing.Id}";
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
SaveTasks();
|
|
|
|
|
|
dlg.Exit();
|
|
|
|
|
|
_dialog = null;
|
|
|
|
|
|
_panel?.Repaint();
|
|
|
|
|
|
}
|
|
|
|
|
|
pb.SameLine(8);
|
|
|
|
|
|
if (pb.Button("取消", distinct: "loop-edit-cancel"))
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
dlg.Exit();
|
|
|
|
|
|
_dialog = null;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
});
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
/// <summary>下一个可用任务 Id(当前最大 Id + 1,空表则为 1)。</summary>
|
|
|
|
|
|
private static int GetNextTaskId() => _tasks.Count == 0 ? 1 : _tasks.Max(t => t.Id) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
private static void LoadTasks()
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
var path = JsonPath;
|
|
|
|
|
|
if (!File.Exists(path))
|
|
|
|
|
|
File.WriteAllText(path, "[]");
|
|
|
|
|
|
|
|
|
|
|
|
var text = File.ReadAllText(path);
|
|
|
|
|
|
_tasks = JsonConvert.DeserializeObject<List<LoopTask>>(text) ?? new List<LoopTask>();
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_tasks = new List<LoopTask>();
|
|
|
|
|
|
_status = "加载 tasklist.json 失败,详见日志";
|
|
|
|
|
|
Diagnosis.Post($"LoopViewer 加载 tasklist.json 异常: {ExceptionFormatter.FormatEx(ex)}");
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
/// <summary>序列化在渲染线程完成(极快),文件写入放后台线程,避免阻塞渲染线程。</summary>
|
|
|
|
|
|
private static void SaveTasks()
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
string json;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
json = JsonConvert.SerializeObject(_tasks, Formatting.Indented);
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_status = "保存失败,详见日志";
|
|
|
|
|
|
Diagnosis.Post($"LoopViewer 序列化 tasklist.json 异常: {ExceptionFormatter.FormatEx(ex)}");
|
|
|
|
|
|
return;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
var path = JsonPath;
|
|
|
|
|
|
Task.Run(() =>
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
try
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
lock (SaveLock)
|
|
|
|
|
|
File.WriteAllText(path, json);
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
catch (Exception ex)
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
_status = "保存失败,详见日志";
|
|
|
|
|
|
Diagnosis.Post($"LoopViewer 保存 tasklist.json 异常: {ExceptionFormatter.FormatEx(ex)}");
|
|
|
|
|
|
_panel?.Repaint();
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
});
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
private static int Clamp(int v, int min, int max) => v < min ? min : (v > max ? max : v);
|
2026-06-14 11:19:15 +08:00
|
|
|
|
|
2026-06-26 15:00:53 +08:00
|
|
|
|
private static bool TryParseClamp(string s, int min, int max, out int value)
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
if (int.TryParse((s ?? "").Trim(), out value))
|
2026-06-14 11:19:15 +08:00
|
|
|
|
{
|
2026-06-26 15:00:53 +08:00
|
|
|
|
value = Clamp(value, min, max);
|
|
|
|
|
|
return true;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
value = min;
|
|
|
|
|
|
return false;
|
2026-06-14 11:19:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-26 15:00:53 +08:00
|
|
|
|
}
|