// // NetworkView.swift // punchnet import SwiftUI import Observation // 资源展示模式 enum NetworkShowMode: String, CaseIterable { case resource = "访问资源" case device = "成员设备" } // MARK: - 网络连接状态 enum NetworkConnectionPhase { case disconnected case connecting case connected case disconnecting } // MARK: - 主网络视图 struct NetworkView: View { @Environment(AppContext.self) var appContext: AppContext @Environment(\.openWindow) var openWindow @State private var networkModel = NetworkModel() var body: some View { @Bindable var networkModel = self.networkModel VStack(spacing: 0) { // 1. 头部区域 (Header) HStack(spacing: 16) { NetworkStatusBar(model: self.networkModel) Spacer() if self.networkModel.shouldShowModePicker { Picker("", selection: $networkModel.showMode) { ForEach(NetworkShowMode.allCases, id: \.self) { Text($0.rawValue).tag($0) } } .pickerStyle(.segmented) .frame(width: 160) } Button { openWindow(id: "settings") } label: { Image(systemName: "slider.horizontal.3") .font(.system(size: 14)) .foregroundColor(.secondary) } .buttonStyle(.plain) .help("配置中心") } .padding(.horizontal, 20) .padding(.vertical, 14) .background(VisualEffectView(material: .headerView, blendingMode: .withinWindow)) Divider() // 2. 内容区域 (Content) Group { switch self.networkModel.phase { case .connecting, .disconnecting: NetworkWaitAuthView(phase: self.networkModel.phase) case .connected: NetworkConnectedView(model: self.networkModel) case .disconnected: NetworkDisconnectedView(model: self.networkModel) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(VisualEffectView(material: .windowBackground, blendingMode: .behindWindow)) } .frame(minWidth: 700, minHeight: 500) // 适当调大宽度以适应 SplitView .task { await self.networkModel.activate(appContext: self.appContext) } .alert("提示", isPresented: self.errorPresented) { Button("确定", role: .cancel) { self.networkModel.clearError() } } message: { Text(self.networkModel.errorMessage ?? "") } } private var errorPresented: Binding { Binding( get: { self.networkModel.errorMessage != nil }, set: { isPresented in if !isPresented { self.networkModel.clearError() } } ) } } private struct NetworkStatusBar: View { var model: NetworkModel var body: some View { let isOnBinding = Binding( get: { self.model.isTunnelEnabled }, set: { newValue in Task { @MainActor in await self.model.setConnectionEnabled(newValue) } } ) HStack(spacing: 12) { // 左侧:状态指示器与文字 HStack(spacing: 20) { ZStack { Circle() .fill(self.model.isTunnelEnabled ? Color.green.opacity(0.15) : Color.primary.opacity(0.05)) .frame(width: 36, height: 36) Image(systemName: self.model.isTunnelEnabled ? "checkmark.shield.fill" : "shield.slash.fill") .symbolRenderingMode(.hierarchical) .foregroundStyle(self.model.isTunnelEnabled ? Color.green : Color.secondary) .font(.system(size: 16)) } VStack(alignment: .leading, spacing: 1) { if let networkSession = self.model.networkSession { Text(networkSession.networkName) .font(.system(size: 12, weight: .semibold)) Text("局域网IP: \(self.model.networkContext?.ip ?? "0.0.0.0")") .font(.system(size: 10, design: .monospaced)) .foregroundColor(.secondary) } else { Text("未登录网络") .font(.system(size: 12, weight: .semibold)) Text("登录后可建立连接") .font(.system(size: 10)) .foregroundColor(.secondary) } } } if self.model.networkSession != nil { exitNodeMenu } // 右侧:Switch 开关 // 注意:这里使用 Binding 手动接管连接/断开逻辑 Toggle("", isOn: isOnBinding) .toggleStyle(.switch) .controlSize(.small) // macOS 顶部栏或面板推荐使用 small 尺寸 .disabled(self.model.phase == .connecting || self.model.phase == .disconnecting || self.model.networkSession == nil) } .padding(.vertical, 5) } private var exitNodeMenu: some View { Menu { Button { Task { @MainActor in await self.model.updateExitNodeSelection(nil) } } label: { if self.model.selectedExitNode == nil { Label("不设置出口节点", systemImage: "checkmark") } else { Text("不设置出口节点") } } if !self.model.exitNodeOptions.isEmpty { Divider() ForEach(self.model.exitNodeOptions) { option in Button { Task { @MainActor in await self.model.updateExitNodeSelection(option.ip) } } label: { if self.model.selectedExitNode?.ip == option.ip { Label(option.nodeNameWithIp, systemImage: "checkmark") } else { Text(option.nodeNameWithIp) } } } } } label: { HStack(spacing: 10) { VStack(alignment: .leading, spacing: 3) { Text("出口节点") .font(.system(size: 10, weight: .medium)) .foregroundColor(.secondary) Text(self.model.exitNodeTitle) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.primary) .lineLimit(1) Text(self.model.exitNodeSubtitle) .font(.system(size: 10, design: .monospaced)) .foregroundColor(.secondary) .lineLimit(1) } Spacer(minLength: 0) if self.model.isUpdatingExitNode { ProgressView() .controlSize(.small) } else { Image(systemName: "chevron.down") .font(.system(size: 10, weight: .semibold)) .foregroundColor(.secondary) } } .padding(.horizontal, 12) .padding(.vertical, 8) .frame(width: 220, alignment: .leading) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color.primary.opacity(0.04)) ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.primary.opacity(0.06), lineWidth: 1) ) } .buttonStyle(.plain) .disabled(!self.model.canSelectExitNode) .opacity(self.model.canSelectExitNode ? 1 : 0.7) .help(self.model.exitNodeHelpText) } }