From 177f8932fa948349254a4c0423501fcb4b0f91a5 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Thu, 19 Mar 2026 17:39:52 +0800 Subject: [PATCH] fix network view --- punchnet/Views/Network/NetworkView.swift | 292 +++++++++++------------ 1 file changed, 141 insertions(+), 151 deletions(-) diff --git a/punchnet/Views/Network/NetworkView.swift b/punchnet/Views/Network/NetworkView.swift index 2f4f2fb..9cd4cab 100644 --- a/punchnet/Views/Network/NetworkView.swift +++ b/punchnet/Views/Network/NetworkView.swift @@ -4,11 +4,9 @@ import SwiftUI import Observation -// 连接状态 +// MARK: - 基础模型协议 (确保代码可编译,请根据实际 Model 调整) enum ConnectState { - case waitAuth - case connected - case disconnected + case waitAuth, connected, disconnected } // MARK: - 主网络视图 @@ -31,40 +29,31 @@ struct NetworkView: View { var body: some View { VStack(spacing: 0) { - // 头部:使用原生毛玻璃材质 + // 1. 头部区域 (Header) headerSection Divider() - // 内容区 - ZStack { - VisualEffectView(material: .windowBackground, blendingMode: .behindWindow) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 20) { - switch connectState { - case .waitAuth: - statusLoadingView - case .connected: - connectedContent - case .disconnected: - disconnectedContent - } - } - .padding(24) + // 2. 内容区域 (Content) + Group { + switch connectState { + case .waitAuth: + NetworkWaitAuthView(networkModel: networkModel) + case .connected: + connectedContent + case .disconnected: + disconnectedContent } - .scrollIndicators(.hidden) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(VisualEffectView(material: .windowBackground, blendingMode: .behindWindow)) } - .frame(minWidth: 550, minHeight: 480) + .frame(minWidth: 700, minHeight: 500) // 适当调大宽度以适应 SplitView .onAppear { syncState(vpnManager.vpnStatus) } .onChange(of: vpnManager.vpnStatus) { _, newStatus in - withAnimation(.snappy) { - syncState(newStatus) - } + withAnimation(.snappy) { syncState(newStatus) } } } } @@ -74,7 +63,6 @@ extension NetworkView { private var headerSection: some View { HStack(spacing: 16) { - // 状态指示器 statusIndicator VStack(alignment: .leading, spacing: 2) { @@ -109,7 +97,7 @@ extension NetworkView { .help("配置中心") } .padding(.horizontal, 20) - .padding(.vertical, 16) + .padding(.vertical, 14) .background(VisualEffectView(material: .headerView, blendingMode: .withinWindow)) } @@ -117,75 +105,58 @@ extension NetworkView { ZStack { Circle() .fill(connectState == .connected ? Color.green.opacity(0.15) : Color.primary.opacity(0.05)) - .frame(width: 40, height: 40) + .frame(width: 36, height: 36) Image(systemName: connectState == .connected ? "checkmark.shield.fill" : "shield.slash.fill") .symbolRenderingMode(.hierarchical) .foregroundStyle(connectState == .connected ? Color.green : Color.secondary) - .font(.system(size: 18)) - .symbolEffect(.bounce, value: connectState == .connected) + .font(.system(size: 16)) } } + @ViewBuilder private var connectedContent: some View { - VStack(spacing: 16) { - if showMode == .resource { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 220), spacing: 16)], spacing: 16) { + if showMode == .resource { + // 资源视图:网格布局 + ScrollView { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 16)], spacing: 16) { ForEach(networkModel.networkContext.resourceList, id: \.id) { res in ResourceItemCard(resource: res) } } - } else { - VStack(spacing: 0) { - let nodes = networkModel.networkContext.nodeList - ForEach(nodes, id: \.id) { node in - DeviceItemRow(node: node, isLast: node.id == nodes.last?.id) - } - } - .background(Color(NSColor.controlBackgroundColor).opacity(0.4)) - .cornerRadius(12) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.primary.opacity(0.08), lineWidth: 1)) + .padding(20) } + .transition(.opacity) + } else { + // 设备视图:双栏布局 + NetworkDeviceGroupView(networkModel: networkModel) + .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .opacity)) } } private var disconnectedContent: some View { - VStack(spacing: 24) { - Spacer().frame(height: 40) - + VStack(spacing: 20) { + Spacer() Image(systemName: "antenna.radiowaves.left.and.right") - .font(.system(size: 48, weight: .ultraLight)) + .font(.system(size: 40, weight: .ultraLight)) .foregroundStyle(.tertiary) .symbolEffect(.pulse, options: .repeating) - VStack(spacing: 8) { - Text("尚未接入网络").font(.headline) - Text("连接后即可访问内部资源与成员节点").font(.subheadline).foregroundColor(.secondary) - } + Text("尚未接入网络").font(.headline) Button(action: { startConnection() }) { if isConnecting { - ProgressView().controlSize(.small).frame(width: 110) + ProgressView().controlSize(.small).frame(width: 80) } else { - Text("建立安全连接").fontWeight(.medium).frame(width: 110) + Text("建立安全连接").frame(width: 80) } } .buttonStyle(.borderedProminent) - .controlSize(.large) .disabled(isConnecting) - Spacer() } } - private var statusLoadingView: some View { - VStack(spacing: 16) { - ProgressView() - Text("正在同步网络状态...").font(.subheadline).foregroundColor(.secondary) - } - .frame(maxWidth: .infinity, minHeight: 300) - } - private func syncState(_ status: VPNManager.VPNStatus) { switch status { case .connected: connectState = .connected @@ -198,13 +169,7 @@ extension NetworkView { isConnecting = true Task { do { - guard let session = userContext.networkSession else { - await MainActor.run { - isConnecting = false - } - return - } - + guard let session = userContext.networkSession else { return } try await networkModel.connect(networkSession: session) let context = networkModel.networkContext if let options = SystemConfig.getOptions( @@ -219,105 +184,130 @@ extension NetworkView { ) { try await vpnManager.enableVpn(options: options) } - } catch { - print("Connection error: \(error)") - } + } catch { print("Connection error: \(error)") } await MainActor.run { isConnecting = false } } } } -// MARK: - 子组件:资源卡片 -struct ResourceItemCard: View { - let resource: Resource - @State private var isHovered = false +// MARK: - 设备组视图 (NavigationSplitView) +struct NetworkDeviceGroupView: View { + @Bindable var networkModel: NetworkModel + @State private var selectedId: Int? var body: some View { - VStack(alignment: .leading, spacing: 10) { - HStack { - Image(systemName: "safari.fill") - .foregroundColor(.accentColor) - .font(.title3) - Spacer() - Circle() - .fill(Color.green) - .frame(width: 6, height: 6) - .shadow(color: .green.opacity(0.5), radius: 2) + NavigationSplitView { + List(networkModel.networkContext.nodeList, id: \.id, selection: $selectedId) { node in + NetworkNodeHeadView(node: node) + .tag(node.id) + } + .listStyle(.sidebar) + .navigationSplitViewColumnWidth(min: 200, ideal: 220) + } detail: { + if let selectedNode = networkModel.networkContext.nodeList.first(where: { $0.id == selectedId }) { + NetworkNodeDetailView(node: selectedNode) + } else { + ContentUnavailableView("选择成员设备", systemImage: "macbook.and.iphone", description: Text("查看详细网络信息和服务")) } - - Text(resource.name) - .font(.system(size: 13, weight: .semibold)) - .lineLimit(1) - - Text(resource.url) - .font(.system(size: 10, design: .monospaced)) - .foregroundColor(.secondary) - .lineLimit(1) } - .padding(14) - .background(isHovered ? Color.primary.opacity(0.05) : Color(NSColor.controlBackgroundColor).opacity(0.3)) - .cornerRadius(12) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(isHovered ? Color.accentColor.opacity(0.3) : Color.primary.opacity(0.08), lineWidth: 1)) - .onHover { isHovered = $0 } - .contextMenu { - Button("复制链接") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(resource.url, forType: .string) - } - Button("在浏览器打开") { - if let url = URL(string: resource.url) { - NSWorkspace.shared.open(url) - } + .onAppear { + if selectedId == nil { + selectedId = networkModel.networkContext.nodeList.first?.id } } } } -// MARK: - 子组件:设备行 -struct DeviceItemRow: View { - let node: Node - let isLast: Bool - @State private var isHovered = false - +// MARK: - 子组件 +struct NetworkNodeHeadView: View { + var node: Node var body: some View { - VStack(spacing: 0) { - HStack(spacing: 12) { - Image(systemName: "desktopcomputer") - .font(.title3) - .foregroundStyle(isHovered ? .primary : .secondary) - - VStack(alignment: .leading, spacing: 2) { - Text(node.name).font(.system(size: 13, weight: .medium)) - Text(node.ip).font(.system(size: 11, design: .monospaced)).foregroundColor(.secondary) - } - - Spacer() - - HStack(spacing: 4) { - Text("直连").font(.system(size: 9, weight: .bold)) - .padding(.horizontal, 4).padding(.vertical, 1) - .background(Color.accentColor.opacity(0.1)).foregroundColor(.accentColor).cornerRadius(3) - Circle().fill(Color.green).frame(width: 6, height: 6) - } - } - .padding(.vertical, 12) - .padding(.horizontal, 16) - .contentShape(Rectangle()) - .background(isHovered ? Color.primary.opacity(0.03) : Color.clear) - .onHover { isHovered = $0 } + HStack(spacing: 10) { + Circle() + .fill(node.connectionStatus == "在线" ? Color.green : Color.secondary.opacity(0.4)) + .frame(width: 8, height: 8) - if !isLast { - Divider().padding(.leading, 44).opacity(0.5) + VStack(alignment: .leading, spacing: 2) { + Text(node.name).font(.system(size: 13, weight: .medium)) + Text(node.ip).font(.system(size: 11, design: .monospaced)).foregroundColor(.secondary) } } - .contextMenu { - Button("复制 IP 地址") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(node.ip, forType: .string) + .padding(.vertical, 4) + } +} + +struct NetworkNodeDetailView: View { + @Environment(UserContext.self) var userContext + var node: Node + @State private var resources: [Resource] = [] + @State private var isLoading = false + + var body: some View { + List { + Section("节点信息") { + LabeledContent("连接状态", value: node.connectionStatus) + LabeledContent("虚拟IPv4", value: node.ip) + LabeledContent("系统环境", value: node.system ?? "未知") } - Button("终端 SSH 连接") { - /* 呼起终端逻辑 */ + + Section("提供的服务") { + if isLoading { + ProgressView().controlSize(.small) + } else if resources.isEmpty { + Text("该节点暂未发布资源").foregroundColor(.secondary).font(.callout) + } else { + ForEach(resources, id: \.id) { res in + VStack(alignment: .leading) { + Text(res.name).font(.body) + Text(res.url).font(.caption).foregroundColor(.secondary) + } + } + } } } + .task(id: node.id) { await loadNodeResources(id: node.id) } + } + + private func loadNodeResources(id: Int) async { + guard let session = userContext.networkSession else { return } + isLoading = true + defer { isLoading = false } + + let params: [String: Any] = [ + "client_id": SystemConfig.getClientId(), + "access_token": session.accessToken, + "id": id + ] + if let detail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NodeDetail.self) { + self.resources = detail.resourceList + } + } +} + +struct ResourceItemCard: View { + let resource: Resource + @State private var isHovered = false + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Image(systemName: "safari.fill").foregroundColor(.accentColor).font(.title3) + Text(resource.name).font(.headline).lineLimit(1) + Text(resource.url).font(.caption2).foregroundColor(.secondary).lineLimit(1) + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(NSColor.controlBackgroundColor).opacity(isHovered ? 0.8 : 0.4)) + .cornerRadius(10) + .onHover { isHovered = $0 } + } +} + +struct NetworkWaitAuthView: View { + @Bindable var networkModel: NetworkModel + var body: some View { + VStack(spacing: 16) { + ProgressView() + Text("等待认证确认中...").foregroundColor(.secondary) + } } }