// // NetworkStatusBar.swift // punchnet // // Created by 安礼成 on 2026/4/17. // import SwiftUI 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) } } struct ExitNodeOption: Identifiable, Equatable { let id: Int let nodeName: String let ip: String let system: String? var nodeNameWithIp: String { "\(nodeName) (\(ip))" } }