From 1520343c29457cf12056cb1bbf97ee21c0f85a84 Mon Sep 17 00:00:00 2001 From: wwmmzz <270447914@qq.com> Date: Sun, 24 May 2026 14:01:27 +0800 Subject: [PATCH] Show routing import errors in modal dialog --- v2rayN/ServiceLib/Enums/EViewAction.cs | 1 + .../ViewModels/RoutingRuleSettingViewModel.cs | 76 ++++++++++++++++++- v2rayN/v2rayN.Desktop/Common/UI.cs | 6 ++ .../v2rayN.Desktop/Views/MainWindow.axaml.cs | 9 +++ .../Views/MessageBoxDialog.axaml.cs | 7 +- .../Views/RoutingRuleSettingWindow.axaml.cs | 9 +++ v2rayN/v2rayN/Views/MainWindow.xaml.cs | 9 +++ .../Views/RoutingRuleSettingWindow.xaml.cs | 9 +++ 8 files changed, 123 insertions(+), 3 deletions(-) diff --git a/v2rayN/ServiceLib/Enums/EViewAction.cs b/v2rayN/ServiceLib/Enums/EViewAction.cs index fba340dc..cc145e68 100644 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ b/v2rayN/ServiceLib/Enums/EViewAction.cs @@ -33,4 +33,5 @@ public enum EViewAction DispatcherRefreshServersBiz, DispatcherRefreshIcon, DispatcherShowMsg, + ShowMessage, } diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs index 3baf79c6..2a0ee5fd 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs @@ -305,13 +305,16 @@ public class RoutingRuleSettingViewModel : MyReactiveObject } if (clipboardData.IsNullOrEmpty()) { + await ShowImportErrorAsync($"{ResUI.OperationFailed}: {ResUI.FailedReadConfiguration}"); return -1; } - var lstRules = JsonUtils.Deserialize>(clipboardData); - if (lstRules == null) + + if (!TryDeserializeRoutingRules(clipboardData, out var lstRules, out var errorMsg)) { + await ShowImportErrorAsync($"{ResUI.OperationFailed}: {errorMsg}"); return -1; } + foreach (var rule in lstRules) { rule.Id = Utils.GetGuid(false); @@ -328,5 +331,74 @@ public class RoutingRuleSettingViewModel : MyReactiveObject return 0; } + private async Task ShowImportErrorAsync(string message) + { + if (_updateView != null) + { + await _updateView.Invoke(EViewAction.ShowMessage, message); + return; + } + + NoticeManager.Instance.Enqueue(message); + } + + private static bool TryDeserializeRoutingRules(string clipboardData, out List rules, out string errorMsg) + { + rules = []; + errorMsg = string.Empty; + + var trimmed = clipboardData.Trim(); + if (trimmed.IsNullOrEmpty()) + { + errorMsg = ResUI.FailedReadConfiguration; + return false; + } + + if (trimmed.StartsWith('{')) + { + if (trimmed.Contains("\"routing\"", StringComparison.OrdinalIgnoreCase) + || trimmed.Contains("\"route\"", StringComparison.OrdinalIgnoreCase)) + { + errorMsg = "你粘贴的是完整配置文件,不是路由规则数组。请只粘贴 routing.rules 或 route.rules 的 JSON 数组。"; + } + else + { + errorMsg = "当前内容是 JSON 对象。路由规则导入需要顶层 JSON 数组,例如 [ { \"outboundTag\": \"proxy\" } ]。"; + } + return false; + } + + try + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + }; + + rules = JsonSerializer.Deserialize>(trimmed, options) ?? []; + if (rules.Count == 0) + { + errorMsg = "未解析到任何路由规则,请确认内容是非空 JSON 数组。"; + return false; + } + + return true; + } + catch (JsonException ex) + { + var location = ex.LineNumber is not null && ex.BytePositionInLine is not null + ? $"第 {ex.LineNumber + 1} 行,第 {ex.BytePositionInLine + 1} 列" + : "未知位置"; + errorMsg = $"JSON 解析错误,{location}。{ex.Message}"; + return false; + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + } + #endregion Import rules } diff --git a/v2rayN/v2rayN.Desktop/Common/UI.cs b/v2rayN/v2rayN.Desktop/Common/UI.cs index 990e3c68..d34baa61 100644 --- a/v2rayN/v2rayN.Desktop/Common/UI.cs +++ b/v2rayN/v2rayN.Desktop/Common/UI.cs @@ -7,6 +7,12 @@ internal class UI { private static readonly string caption = Global.AppName; + public static async Task Show(Window owner, string msg) + { + var box = new MessageBoxDialog(caption, msg, true); + await box.ShowDialog(owner); + } + public static async Task ShowYesNo(Window owner, string msg) { var box = new MessageBoxDialog(caption, msg); diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs index ba568f1f..7703e9e5 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs @@ -247,6 +247,15 @@ public partial class MainWindow : WindowBase case EViewAction.AddServerViaClipboard: await AddServerViaClipboardAsync(); break; + + case EViewAction.ShowMessage: + if (obj is null) + { + return false; + } + + await UI.Show(this, obj.ToString() ?? string.Empty); + break; } return await Task.FromResult(true); diff --git a/v2rayN/v2rayN.Desktop/Views/MessageBoxDialog.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MessageBoxDialog.axaml.cs index 768a8e1d..c0e66148 100644 --- a/v2rayN/v2rayN.Desktop/Views/MessageBoxDialog.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MessageBoxDialog.axaml.cs @@ -9,13 +9,18 @@ public partial class MessageBoxDialog : Window { } - public MessageBoxDialog(string caption, string message) + public MessageBoxDialog(string caption, string message, bool okOnly = false) { InitializeComponent(); Title = caption; txtMessage.Text = message; + if (okOnly) + { + btnNo.IsVisible = false; + } + btnYes.Click += BtnYes_Click; btnNo.Click += BtnNo_Click; } diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs index c2d90a33..875e2000 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs @@ -115,6 +115,15 @@ public partial class RoutingRuleSettingWindow : WindowBase