From edb2c08933f7e56d6911c54da4c57d7fc06493f1 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Sat, 3 Oct 2026 19:04:40 +0800 Subject: [PATCH] =?UTF-8?q?refactor(rules):=20outcome=E2=86=92=E6=96=87?= =?UTF-8?q?=E6=A1=88=E6=98=A0=E5=B0=84=E7=A7=BB=E5=87=BA=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E8=90=BD=E5=9B=9E=20outcome=20?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=97=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProxyRuntimeController+CustomRules.swift | 49 +++++++++++++++++++ .../App/RulesOperationStatusView.swift | 45 ----------------- .../Tests/RulesPresentationTests.swift | 33 +++++++++++++ 3 files changed, 82 insertions(+), 45 deletions(-) diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift index 606d39ca..b516ec31 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift @@ -27,6 +27,55 @@ struct RuleDocumentCommit: Sendable { let document: CustomRuleDocument? } +// MARK: - 结果呈现文案 + +// outcome→文案映射与 outcome 类型同住,供状态行与编辑器两个视图共享。 + +extension RulesCommitFeedback { + var summary: String { + guard outcome.isSuccess else { return outcome.rulesMessage } + let count: String + switch operation { + case .enablement(let enabled): + count = String.localizedStringWithFormat( + RulesCopy.text(enabled ? "已启用 %lld 条规则" : "已禁用 %lld 条规则"), Int64(changedCount)) + case .add: count = RulesCopy.text("已新增规则") + case .edit: count = RulesCopy.text("已编辑规则") + case .delete: + count = String.localizedStringWithFormat( + RulesCopy.text("已删除 %lld 条自定义规则"), Int64(changedCount)) + } + return count + " · " + RulesCopy.text("已保存") + } +} + +extension CustomRuleUpdateOutcome { + var rulesMessage: String { + switch self { + case .saved: RulesCopy.text("已保存") + case .persistenceFailed: RulesCopy.text("保存失败,规则未更改") + case .busy: RulesCopy.text("正在更新规则…") + case .invalidDocument, .rejected: RulesCopy.text("无法更新规则") + } + } + + var failureDetail: String? { + switch self { + case .invalidDocument(let detail): detail + case .rejected(let rejected): rejected.map(\.explanation).joined(separator: "\n") + default: nil + } + } + + var nextStep: String? { + switch self { + case .persistenceFailed, .busy: "请重试规则操作。" + case .invalidDocument, .rejected: "请检查规则数据后再操作。" + default: nil + } + } +} + // MARK: - 自定义规则命令面 extension ProxyRuntimeController { diff --git a/ShadowsocksX-NG2/App/RulesOperationStatusView.swift b/ShadowsocksX-NG2/App/RulesOperationStatusView.swift index 05d0a71e..05eea10b 100644 --- a/ShadowsocksX-NG2/App/RulesOperationStatusView.swift +++ b/ShadowsocksX-NG2/App/RulesOperationStatusView.swift @@ -103,48 +103,3 @@ struct RulesCollectionIssuesView: View { } } } - -extension RulesCommitFeedback { - var summary: String { - guard outcome.isSuccess else { return outcome.rulesMessage } - let count: String - switch operation { - case .enablement(let enabled): - count = String.localizedStringWithFormat( - RulesCopy.text(enabled ? "已启用 %lld 条规则" : "已禁用 %lld 条规则"), Int64(changedCount)) - case .add: count = RulesCopy.text("已新增规则") - case .edit: count = RulesCopy.text("已编辑规则") - case .delete: - count = String.localizedStringWithFormat( - RulesCopy.text("已删除 %lld 条自定义规则"), Int64(changedCount)) - } - return count + " · " + RulesCopy.text("已保存") - } -} - -extension CustomRuleUpdateOutcome { - var rulesMessage: String { - switch self { - case .saved: RulesCopy.text("已保存") - case .persistenceFailed: RulesCopy.text("保存失败,规则未更改") - case .busy: RulesCopy.text("正在更新规则…") - case .invalidDocument, .rejected: RulesCopy.text("无法更新规则") - } - } - - var failureDetail: String? { - switch self { - case .invalidDocument(let detail): detail - case .rejected(let rejected): rejected.map(\.explanation).joined(separator: "\n") - default: nil - } - } - - var nextStep: String? { - switch self { - case .persistenceFailed, .busy: "请重试规则操作。" - case .invalidDocument, .rejected: "请检查规则数据后再操作。" - default: nil - } - } -} diff --git a/ShadowsocksX-NG2/Tests/RulesPresentationTests.swift b/ShadowsocksX-NG2/Tests/RulesPresentationTests.swift index 3078e915..73e071db 100644 --- a/ShadowsocksX-NG2/Tests/RulesPresentationTests.swift +++ b/ShadowsocksX-NG2/Tests/RulesPresentationTests.swift @@ -58,6 +58,39 @@ final class RulesPresentationTests: XCTestCase { XCTAssertNil(workflow.selectedRelationshipRow) } + func testOutcomeCopyMappingCoversEveryCase() throws { + XCTAssertEqual(CustomRuleUpdateOutcome.saved.rulesMessage, RulesCopy.text("已保存")) + XCTAssertEqual( + CustomRuleUpdateOutcome.persistenceFailed.rulesMessage, RulesCopy.text("保存失败,规则未更改")) + XCTAssertEqual( + CustomRuleUpdateOutcome.persistenceFailed.nextStep, RulesCopy.text("请重试规则操作。")) + XCTAssertNil(CustomRuleUpdateOutcome.persistenceFailed.failureDetail) + XCTAssertEqual(CustomRuleUpdateOutcome.busy.rulesMessage, RulesCopy.text("正在更新规则…")) + let invalid = CustomRuleUpdateOutcome.invalidDocument(detail: "detail-1") + XCTAssertEqual(invalid.rulesMessage, RulesCopy.text("无法更新规则")) + XCTAssertEqual(invalid.failureDetail, "detail-1") + XCTAssertEqual(invalid.nextStep, RulesCopy.text("请检查规则数据后再操作。")) + let rejected = CustomRuleUpdateOutcome.rejected([ + RejectedCustomRule( + rule: CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.test")), + reason: .duplicate, explanation: "r1"), + RejectedCustomRule( + rule: CustomRule(action: .direct, match: try RuleMatch(domainExact: "b.test")), + reason: .duplicate, explanation: "r2"), + ]) + XCTAssertEqual(rejected.failureDetail, "r1\nr2") + XCTAssertEqual(rejected.nextStep, RulesCopy.text("请检查规则数据后再操作。")) + XCTAssertNil(CustomRuleUpdateOutcome.saved.failureDetail) + XCTAssertNil(CustomRuleUpdateOutcome.saved.nextStep) + let success = RulesCommitFeedback( + outcome: .saved, operation: .enablement(true), changedCount: 3) + XCTAssertEqual( + success.summary, + RulesCopy.text("已启用 3 条规则") + " · " + RulesCopy.text("已保存")) + let failure = RulesCommitFeedback(outcome: .busy, operation: .delete, changedCount: 1) + XCTAssertEqual(failure.summary, RulesCopy.text("正在更新规则…")) + } + } private final class ReportFixtureLoader: @unchecked Sendable {