// // NetworkConnectedView.swift // punchnet // // Created by 安礼成 on 2026/4/17. // import SwiftUI struct NetworkConnectedView: View { var model: NetworkModel var body: some View { if self.model.showMode == .resource { // 资源视图:网格布局 ScrollView { LazyVGrid(columns: [ GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8) ], spacing: 10) { ForEach(self.model.resourceList, id: \.uuid) { res in ResourceItemCard(resource: res) } } .padding(20) } .transition(.opacity) .frame(maxWidth: .infinity) } else { // 设备视图:双栏布局 NetworkDeviceGroupView(model: self.model) .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .opacity)) } } } // MARK: - 设备组视图 (NavigationSplitView) private struct NetworkDeviceGroupView: View { var model: NetworkModel // 侧边栏宽度 private let sidebarWidth: CGFloat = 240 var body: some View { let selectedIdBinding = Binding( get: { self.model.selectedNodeId }, set: { newValue in self.model.selectNode(id: newValue) } ) HStack(spacing: 0) { // --- 1. 自定义侧边栏 (Sidebar) --- VStack(alignment: .leading, spacing: 0) { // 顶部留白:避开 macOS 窗口左上角的红绿灯按钮 // 如果你的 WindowStyle 是 .hiddenTitleBar,这个 Padding 非常重要 Color.clear.frame(height: 28) List(self.model.nodeList, id: \.id, selection: selectedIdBinding) { node in NetworkNodeHeadView(node: node) // 技巧:在 HStack 方案中,tag 配合 List 的 selection 依然有效 .tag(node.id) .listRowSeparator(.hidden) } .listStyle(.inset) // 使用 inset 样式在自定义侧边栏中更美观 .scrollContentBackground(.hidden) // 隐藏默认白色背景,显示下方的磨砂材质 } .frame(width: sidebarWidth) Divider() // 分割线 // --- 2. 详情区域 (Detail) --- ZStack { if let selectedNode = self.model.selectedNode { NetworkNodeDetailView(model: self.model, node: selectedNode) .transition(.opacity.animation(.easeInOut(duration: 0.2))) } else { ContentUnavailableView( "选择成员设备", systemImage: "macbook.and.iphone", description: Text("查看详细网络信息和服务") ) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(nsColor: .windowBackgroundColor)) // 详情页使用标准窗口背景色 } .ignoresSafeArea() // 真正顶到最上方 } } // MARK: - 子组件 private struct NetworkNodeHeadView: View { var node: NetworkContext.Node var body: some View { HStack(spacing: 10) { Circle() .fill(node.connectionStatus == "在线" ? Color.green : Color.secondary.opacity(0.4)) .frame(width: 8, height: 8) 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) } } .padding(.vertical, 4) } } private struct NetworkNodeDetailView: View { var model: NetworkModel var node: NetworkContext.Node var body: some View { List { Section("节点信息") { LabeledContent("连接状态", value: node.connectionStatus) LabeledContent("虚拟IPv4", value: node.ip) LabeledContent("系统环境", value: node.system ?? "未知") } Section("提供的服务") { if self.model.isLoadingResources(for: node.id) { ProgressView() .controlSize(.small) } else if self.model.resources(for: node.id).isEmpty { Text("该节点暂未发布资源") .foregroundColor(.secondary) .font(.callout) } else { ForEach(self.model.resources(for: node.id), id: \.id) { res in VStack(alignment: .leading) { Text(res.name) .font(.body) Text(res.url) .font(.caption) .foregroundColor(.secondary) } } } } } .task(id: self.node.id) { await self.model.loadResourcesIfNeeded(for: self.node.id) } } } private struct ResourceItemCard: View { let resource: NetworkContext.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) .truncationMode(.tail) Text(resource.url) .font(.caption2) .foregroundColor(.secondary) .lineLimit(1) .truncationMode(.middle) } .padding() .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.gray, lineWidth: 1) ) .background( RoundedRectangle(cornerRadius: 10) .fill(Color(isHovered ? NSColor.selectedControlColor : NSColor.controlBackgroundColor)) ) .onHover { isHovered = $0 } } }