191 lines
6.5 KiB
Swift
191 lines
6.5 KiB
Swift
//
|
||
// 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)
|
||
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: - 子组件
|
||
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)
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
}
|