View代码拆分

This commit is contained in:
anlicheng 2026-04-17 15:56:56 +08:00
parent 809d85dff5
commit 46baa8f9a7
5 changed files with 403 additions and 374 deletions

View 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
}
}
}

View File

@ -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()
}
}
}

View 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))"
}
}

View File

@ -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)
}
}
}

View 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)
}
}
}