This commit is contained in:
anlicheng 2026-03-24 01:03:48 +08:00
parent fbbef96aa9
commit f00f65985e
4 changed files with 230 additions and 178 deletions

View File

@ -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 []
}
}

View File

@ -13,7 +13,7 @@ class AppContext {
var noticePort: Int
// "/connect"
var networkContext: NetworkContext?
var networkContext: SDLAPIClient.NetworkContext?
var loginCredit: Credit?
var networkSession: SDLAPIClient.NetworkSession?

View File

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

View File

@ -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 {