import SwiftUI /// 装配当前节点的详情会话;查询、参数装载与命令均留在共用视图之外。 struct CatalogNodeDetailView: View { @Environment(\.locale) private var locale @Environment(\.timeZone) private var timeZone @ObservedObject var workflow: CatalogWorkflow @ObservedObject var activation: ActivationFeedbackState let nodeID: NodeID let isActiveTarget: Bool /// 子树含活动目标(严格后代),与行级空心闪电同源判定。 let containsActiveTarget: Bool let errors: ErrorAlertPresenter @StateObject private var parameters = ServerDetailParameters() private var node: CatalogTreeNode? { workflow.tree.node(withID: nodeID) } private var eligibility: ActivationEligibility? { workflow.activationEligibility(for: nodeID) } var body: some View { if let node { Group { if let module = workflow.connectivity { ObservedConnectivityProperties( module: module, node: node, serverName: workflow.displayName(for:) ) { properties in detailView(for: node, connectivity: properties) } } else { detailView(for: node) } } .onAppear { if !node.isGroup { parameters.showServer(nodeID, load: workflow.serverDetailPluginOptions) } } .onReceive(workflow.serverDetailChanges) { affected in parameters.didRefresh(affectedServers: affected, load: workflow.serverDetailPluginOptions) } } } private func detailView( for node: CatalogTreeNode, connectivity: NodeDetailPresentation.Properties? = nil ) -> some View { var detail = presentation(for: node) if let connectivity { detail.properties.append(connectivity) } return NodeDetailView(detail: detail) { primaryActions(for: node) } secondaryActions: { if let module = workflow.connectivity { ConnectivityTestButton(module: module, node: node) } Button( workflow.isFavorite(nodeID) ? String(localized: "取消收藏", table: "ServerDetails") : String(localized: "收藏", table: "ServerDetails") ) { do { try workflow.setFavorite(nodeID, isFavorite: !workflow.isFavorite(nodeID)) } catch { errors.present(error) } } if !node.isGroup && parameters.failure != nil { Button(String(localized: "重新加载", table: "ServerDetails")) { parameters.reload(load: workflow.serverDetailPluginOptions) } } } feedback: { ActivationDetailFeedbackView( messages: detail.activationMessages, completion: activation.completion?.targetID == nodeID ? activation.completion : nil) if let subscription = subscriptionSummary(for: node) { SubscriptionRefreshFeedbackView( isRefreshing: workflow.refreshingSubscriptionIDs.contains(subscription.id), completion: workflow.subscriptionRefreshCompletions[subscription.id] ) .id(subscription.id) } } propertiesFooter: { group in if group.id == .subscription, let fraction = subscriptionSummary(for: node)?.information?.usedFraction { ProgressView(value: fraction) { Text("流量消耗") } currentValueLabel: { Text(fraction, format: .percent.precision(.fractionLength(0))) } .progressViewStyle(.linear) .tint(.accentColor) } else { nil } } } @ViewBuilder private func primaryActions(for node: CatalogTreeNode) -> some View { Button( activation.pendingTargetID == nodeID ? String(localized: "激活中…", table: "ServerDetails") : String(localized: "激活", table: "ServerDetails"), systemImage: "bolt.fill" ) { Task { @MainActor in do { _ = try await activation.activate(nodeID, via: workflow) } catch { // 激活状态记录失败,详情在 feedback 区域显示结果。 } } } .disabled(!(eligibility?.canActivate ?? false) || activation.isPending) if let subscription = subscriptionSummary(for: node) { Button( workflow.refreshingSubscriptionIDs.contains(subscription.id) ? String(localized: "更新中…", table: "ServerDetails") : String(localized: "更新", table: "ServerDetails"), systemImage: "arrow.clockwise" ) { Task { await workflow.refreshSubscription(subscription.id) } } .disabled(workflow.refreshingSubscriptionIDs.contains(subscription.id)) } } private func presentation(for node: CatalogTreeNode) -> NodeDetailPresentation { let server = node.isGroup ? nil : workflow.serverDetailPresentation(for: nodeID) let subscription = subscriptionSummary(for: node) var detail = NodeDetailPresentation( node: node, server: server, eligibility: eligibility, isActiveTarget: isActiveTarget, containsActiveTarget: containsActiveTarget, subscription: subscription, locale: locale, timeZone: timeZone) if let server, server.plugin.selection != .none { if parameters.failure != nil { detail.appendPluginParameterFailure() } else { detail.appendPluginParameters(parameters.options.rawText) } } return detail } private func subscriptionSummary(for node: CatalogTreeNode) -> SubscriptionSummary? { guard node.isRootSubscriptionGroup else { return nil } return workflow.subscriptions.first { $0.groupID == node.id } } } /// 只观察测试资料并生成属性;布局由通用节点详情负责。 private struct ObservedConnectivityProperties: View { @Environment(\.locale) private var locale @Environment(\.timeZone) private var timeZone @ObservedObject var module: ServerConnectivity let node: CatalogTreeNode let serverName: (NodeID) -> String @ViewBuilder let content: (NodeDetailPresentation.Properties) -> Content var body: some View { if node.isGroup { TimelineView(.periodic(from: .now, by: 1)) { context in content( NodeDetailPresentation.connectivitySummary( module.summary(for: node.connectivityServerIDs, at: context.date), serverName: serverName, locale: locale, timeZone: timeZone)) } } else { content( NodeDetailPresentation.connectivityHistory( module.history(for: node.id), locale: locale, timeZone: timeZone)) } } }