View代码拆分
This commit is contained in:
parent
809d85dff5
commit
46baa8f9a7
190
punchnet/Features/Network/Views/NetworkConnectedView.swift
Normal file
190
punchnet/Features/Network/Views/NetworkConnectedView.swift
Normal file
@ -0,0 +1,190 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
//
|
||||
// NetworkDisconnectedView.swift
|
||||
// punchnet
|
||||
//
|
||||
// Created by 安礼成 on 2026/4/17.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct NetworkDisconnectedView: View {
|
||||
var model: NetworkModel
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.font(.system(size: 40, weight: .ultraLight))
|
||||
.foregroundStyle(.tertiary)
|
||||
.symbolEffect(.pulse, options: .repeating)
|
||||
|
||||
Text("尚未接入网络")
|
||||
.font(.headline)
|
||||
|
||||
Button(action: {
|
||||
Task { @MainActor in
|
||||
await self.model.connect()
|
||||
}
|
||||
}) {
|
||||
Text("建立安全连接")
|
||||
.frame(width: 80)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(self.model.phase == .connecting || self.model.networkSession == nil)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
157
punchnet/Features/Network/Views/NetworkStatusBar.swift
Normal file
157
punchnet/Features/Network/Views/NetworkStatusBar.swift
Normal file
@ -0,0 +1,157 @@
|
||||
//
|
||||
// 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))"
|
||||
}
|
||||
}
|
||||
@ -18,8 +18,6 @@ enum NetworkConnectionPhase {
|
||||
case disconnecting
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - 主网络视图
|
||||
struct NetworkView: View {
|
||||
@Environment(AppContext.self) var appContext: AppContext
|
||||
@ -102,377 +100,5 @@ struct NetworkView: View {
|
||||
}
|
||||
}
|
||||
|
||||
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))"
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkDisconnectedView: View {
|
||||
var model: NetworkModel
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.font(.system(size: 40, weight: .ultraLight))
|
||||
.foregroundStyle(.tertiary)
|
||||
.symbolEffect(.pulse, options: .repeating)
|
||||
|
||||
Text("尚未接入网络")
|
||||
.font(.headline)
|
||||
|
||||
Button(action: {
|
||||
Task { @MainActor in
|
||||
await self.model.connect()
|
||||
}
|
||||
}) {
|
||||
Text("建立安全连接")
|
||||
.frame(width: 80)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(self.model.phase == .connecting || self.model.networkSession == nil)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkWaitAuthView: View {
|
||||
var phase: NetworkConnectionPhase
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
|
||||
Text(self.phase == .disconnecting ? "正在断开网络..." : "正在建立安全连接...")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
punchnet/Features/Network/Views/NetworkWaitAuthView.swift
Normal file
20
punchnet/Features/Network/Views/NetworkWaitAuthView.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// NetworkWaitAuthView.swift
|
||||
// punchnet
|
||||
//
|
||||
// Created by 安礼成 on 2026/4/17.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct NetworkWaitAuthView: View {
|
||||
var phase: NetworkConnectionPhase
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
|
||||
Text(self.phase == .disconnecting ? "正在断开网络..." : "正在建立安全连接...")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user