diff --git a/punchnet/Networking/SDLAPIClient+Network.swift b/punchnet/Networking/SDLAPIClient+Network.swift new file mode 100644 index 0000000..76cbbf9 --- /dev/null +++ b/punchnet/Networking/SDLAPIClient+Network.swift @@ -0,0 +1,119 @@ +// +// SDLAPIClient+Network.swift +// punchnet +// +// Created by 安礼成 on 2026/3/24. +// +import Foundation + +extension SDLAPIClient { + + // 用来做临时的数据解析 + struct NetworkContext: Codable { + let ip: String + let maskLen: UInt8 + // 主机名称 + let hostname: String + let identityId: UInt32 + let resourceList: [Resource] + let nodeList: [Node] + + // 资源列表 + struct Resource: Codable { + var uuid = UUID().uuidString + var id: Int + var name: String + var url: String + var connectionStatus: String + + enum CodingKeys: String, CodingKey { + case id + case name + case url + case connectionStatus = "connection_status" + } + } + + // 设备列表 + struct Node: Codable { + var id: Int + var name: String + var ip: String + var system: String? + var connectionStatus: String + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + enum CodingKeys: String, CodingKey { + case id + case name + case ip + case system + case connectionStatus = "connection_status" + } + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + } + + enum CodingKeys: String, CodingKey { + case ip + case maskLen = "mask_len" + case hostname + case identityId = "identity_id" + case resourceList = "resource_list" + case nodeList = "node_list" + } + + static func `default`() -> Self { + return .init(ip: "", maskLen: 24, hostname: "", identityId: 0, resourceList: [], nodeList: []) + } + + // 节点详情 + struct NodeDetail: Codable { + let id: Int + let name: String + let ip: String + let system: String? + let connectionStatus: String + let resourceList: [Resource] + + enum CodingKeys: String, CodingKey { + case id + case name + case ip + case system + case connectionStatus = "connection_status" + case resourceList = "resource_list" + } + } + + } + + static func connectNetwork(networkSession: NetworkSession) async throws -> NetworkContext { + let params: [String: Any] = [ + "client_id": SystemConfig.getClientId(), + "access_token": networkSession.accessToken + ] + + return try await SDLAPIClient.doPost(path: "/connect", params: params, as: NetworkContext.self) + } + + static func loadNodeResources(accesToken: String, id: Int) async -> [NetworkContext.Resource] { + let params: [String: Any] = [ + "client_id": SystemConfig.getClientId(), + "access_token": accesToken, + "id": id + ] + + if let detail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NetworkContext.NodeDetail.self) { + return detail.resourceList + } + + return [] + } + +} diff --git a/punchnet/Views/AppContext.swift b/punchnet/Views/AppContext.swift index 28e4351..5af0e1a 100644 --- a/punchnet/Views/AppContext.swift +++ b/punchnet/Views/AppContext.swift @@ -13,7 +13,7 @@ class AppContext { var noticePort: Int // 调用 "/connect" 之后的网络信息 - var networkContext: NetworkContext? + var networkContext: SDLAPIClient.NetworkContext? var loginCredit: Credit? var networkSession: SDLAPIClient.NetworkSession? diff --git a/punchnet/Views/Network/NetworkModel.swift b/punchnet/Views/Network/NetworkModel.swift index f1a2076..9c09dc4 100644 --- a/punchnet/Views/Network/NetworkModel.swift +++ b/punchnet/Views/Network/NetworkModel.swift @@ -8,96 +8,12 @@ import Foundation import Observation -// 资源列表 -struct Resource: Codable { - var uuid = UUID().uuidString - var id: Int - var name: String - var url: String - var connectionStatus: String - - enum CodingKeys: String, CodingKey { - case id - case name - case url - case connectionStatus = "connection_status" - } -} - -// 设备列表 -struct Node: Codable { - var id: Int - var name: String - var ip: String - var system: String? - var connectionStatus: String - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - enum CodingKeys: String, CodingKey { - case id - case name - case ip - case system - case connectionStatus = "connection_status" - } - - static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.id == rhs.id - } -} - -// 用来做临时的数据解析 -struct NetworkContext: Codable { - let ip: String - let maskLen: UInt8 - // 主机名称 - let hostname: String - let identityId: UInt32 - let resourceList: [Resource] - let nodeList: [Node] - - enum CodingKeys: String, CodingKey { - case ip - case maskLen = "mask_len" - case hostname - case identityId = "identity_id" - case resourceList = "resource_list" - case nodeList = "node_list" - } - - static func `default`() -> Self { - return .init(ip: "", maskLen: 24, hostname: "", identityId: 0, resourceList: [], nodeList: []) - } - -} - -// 节点详情 -struct NodeDetail: Codable { - let id: Int - let name: String - let ip: String - let system: String? - let connectionStatus: String - let resourceList: [Resource] - - enum CodingKeys: String, CodingKey { - case id - case name - case ip - case system - case connectionStatus = "connection_status" - case resourceList = "resource_list" - } -} - @Observable class NetworkModel { + // 当前选中的设备 - var selectedNode: Node? - var networkContext: NetworkContext = .default() + var selectedNode: SDLAPIClient.NetworkContext.Node? + var networkContext: SDLAPIClient.NetworkContext = .default() init() { @@ -111,15 +27,12 @@ class NetworkModel { } } - func connect(networkSession: SDLAPIClient.NetworkSession) async throws -> NetworkContext { + func connect(networkSession: SDLAPIClient.NetworkSession) async throws { let params: [String: Any] = [ "client_id": SystemConfig.getClientId(), "access_token": networkSession.accessToken ] - - self.networkContext = try await SDLAPIClient.doPost(path: "/connect", params: params, as: NetworkContext.self) - - return self.networkContext + self.networkContext = try await SDLAPIClient.connectNetwork(networkSession: networkSession) } } diff --git a/punchnet/Views/Network/NetworkView.swift b/punchnet/Views/Network/NetworkView.swift index 011e18c..eab1cbd 100644 --- a/punchnet/Views/Network/NetworkView.swift +++ b/punchnet/Views/Network/NetworkView.swift @@ -25,7 +25,6 @@ struct NetworkView: View { @State private var networkModel = NetworkModel() @State private var showMode: NetworkShowMode = .resource @State private var connectState: ConnectState = .disconnected - @State private var isConnecting: Bool = false private var vpnManager = VPNManager.shared @@ -93,52 +92,9 @@ struct NetworkView: View { case .waitAuth: NetworkWaitAuthView(networkModel: networkModel) case .connected: - if showMode == .resource { - // 资源视图:网格布局 - ScrollView { - LazyVGrid(columns: [ - GridItem(.flexible(), spacing: 8), - GridItem(.flexible(), spacing: 8), - GridItem(.flexible(), spacing: 8) - ], spacing: 10) { - ForEach(networkModel.networkContext.resourceList, id: \.uuid) { res in - ResourceItemCard(resource: res) - } - } - .padding(20) - } - .transition(.opacity) - .frame(maxWidth: .infinity) - } else { - // 设备视图:双栏布局 - NetworkDeviceGroupView(networkModel: networkModel) - .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .opacity)) - } + NetworkConnectedView(showMode: $showMode, networkModel: networkModel) case .disconnected: - 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: { startConnection() }) { - if isConnecting { - ProgressView() - .controlSize(.small) - .frame(width: 80) - } else { - Text("建立安全连接") - .frame(width: 80) - } - } - .buttonStyle(.borderedProminent) - .disabled(isConnecting) - Spacer() - } + NetworkDisconnectedView() } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -162,41 +118,106 @@ struct NetworkView: View { @unknown default: connectState = .disconnected } } +} + +struct NetworkConnectedView: View { + @Binding var showMode: NetworkShowMode + @Bindable var networkModel: NetworkModel - private func startConnection() { - isConnecting = true - Task { - do { - guard let session = appContext.networkSession else { - return + var body: some View { + if showMode == .resource { + // 资源视图:网格布局 + ScrollView { + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 8), + GridItem(.flexible(), spacing: 8), + GridItem(.flexible(), spacing: 8) + ], spacing: 10) { + ForEach(networkModel.networkContext.resourceList, id: \.uuid) { res in + ResourceItemCard(resource: res) + } } - - let context = try await networkModel.connect(networkSession: session) - - // 登陆后需要保存到app的上线文 - self.appContext.networkContext = context - - if let options = SystemConfig.getOptions( - networkId: UInt32(session.networkId), - networkDomain: session.networkDomain, - ip: context.ip, - maskLen: context.maskLen, - accessToken: session.accessToken, - identityId: context.identityId, - hostname: context.hostname, - noticePort: appContext.noticePort - ) { - try await vpnManager.enableVpn(options: options) - } - } catch { - print("Connection error: \(error)") - } - - await MainActor.run { - isConnecting = false + .padding(20) } + .transition(.opacity) + .frame(maxWidth: .infinity) + } else { + // 设备视图:双栏布局 + NetworkDeviceGroupView(networkModel: networkModel) + .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .opacity)) } } + +} + +struct NetworkDisconnectedView: View { + @State private var isConnecting: Bool = false + @Environment(AppContext.self) private var appContext: AppContext + + 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 startConnection() + } + }) { + if isConnecting { + ProgressView() + .controlSize(.small) + .frame(width: 80) + } else { + Text("建立安全连接") + .frame(width: 80) + } + } + .buttonStyle(.borderedProminent) + .disabled(isConnecting) + Spacer() + } + } + + private func startConnection() async { + self.isConnecting = true + defer { + self.isConnecting = false + } + + do { + guard let session = appContext.networkSession else { + return + } + + let context = try await SDLAPIClient.connectNetwork(networkSession: session) + + // 登陆后需要保存到app的上线文 + self.appContext.networkContext = context + + if let options = SystemConfig.getOptions( + networkId: UInt32(session.networkId), + networkDomain: session.networkDomain, + ip: context.ip, + maskLen: context.maskLen, + accessToken: session.accessToken, + identityId: context.identityId, + hostname: context.hostname, + noticePort: appContext.noticePort + ) { + try await VPNManager.shared.enableVpn(options: options) + } + } catch { + print("Connection error: \(error)") + } + } + } // MARK: - 设备组视图 (NavigationSplitView) @@ -229,7 +250,7 @@ struct NetworkDeviceGroupView: View { // MARK: - 子组件 struct NetworkNodeHeadView: View { - var node: Node + var node: SDLAPIClient.NetworkContext.Node var body: some View { HStack(spacing: 10) { Circle() @@ -252,8 +273,8 @@ struct NetworkNodeHeadView: View { struct NetworkNodeDetailView: View { @Environment(AppContext.self) private var appContext: AppContext - var node: Node - @State private var resources: [Resource] = [] + var node: SDLAPIClient.NetworkContext.Node + @State private var resources: [SDLAPIClient.NetworkContext.Resource] = [] @State private var isLoading = false var body: some View { @@ -296,9 +317,9 @@ struct NetworkNodeDetailView: View { return } - isLoading = true + self.isLoading = true defer { - isLoading = false + self.isLoading = false } let params: [String: Any] = [ @@ -307,14 +328,13 @@ struct NetworkNodeDetailView: View { "id": id ] - if let detail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NodeDetail.self) { - self.resources = detail.resourceList - } + self.resources = await SDLAPIClient.loadNodeResources(accesToken: session.accessToken, id: id) } + } struct ResourceItemCard: View { - let resource: Resource + let resource: SDLAPIClient.NetworkContext.Resource @State private var isHovered = false var body: some View {