This commit is contained in:
anlicheng 2026-03-19 18:43:42 +08:00
parent 177f8932fa
commit f144194734
3 changed files with 111 additions and 32 deletions

View File

@ -152,13 +152,16 @@ struct LoginAccountView: View {
struct LoginTokenView: View { struct LoginTokenView: View {
@Environment(UserContext.self) var userContext: UserContext @Environment(UserContext.self) var userContext: UserContext
@State private var token = "" @State private var token = ""
@State private var isLoading = false
var body: some View { var body: some View {
VStack(spacing: 20) { VStack(spacing: 20) {
CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill") CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill")
.frame(width: 280) .frame(width: 280)
Button(action: { /* Token login logic */ }) { Button(action: {
self.login()
}) {
Text("验证并连接") Text("验证并连接")
.fontWeight(.medium) .fontWeight(.medium)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -166,7 +169,7 @@ struct LoginTokenView: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.controlSize(.large) .controlSize(.large)
.frame(width: 280) .frame(width: 280)
.disabled(token.isEmpty) .disabled(token.isEmpty || isLoading)
} }
.onAppear { .onAppear {
if let cacheToken = self.userContext.loadCacheToken() { if let cacheToken = self.userContext.loadCacheToken() {
@ -175,6 +178,14 @@ struct LoginTokenView: View {
} }
} }
private func login() {
isLoading = true
Task {
try? await userContext.loginWithToken(token: token)
isLoading = false
}
}
} }
// MARK: - UI // MARK: - UI

View File

@ -10,6 +10,7 @@ import Observation
// //
struct Resource: Codable { struct Resource: Codable {
var uuid = UUID().uuidString
var id: Int var id: Int
var name: String var name: String
var url: String var url: String

View File

@ -4,7 +4,7 @@
import SwiftUI import SwiftUI
import Observation import Observation
// MARK: - ( Model ) // MARK: -
enum ConnectState { enum ConnectState {
case waitAuth, connected, disconnected case waitAuth, connected, disconnected
} }
@ -53,7 +53,9 @@ struct NetworkView: View {
syncState(vpnManager.vpnStatus) syncState(vpnManager.vpnStatus)
} }
.onChange(of: vpnManager.vpnStatus) { _, newStatus in .onChange(of: vpnManager.vpnStatus) { _, newStatus in
withAnimation(.snappy) { syncState(newStatus) } withAnimation(.snappy) {
syncState(newStatus)
}
} }
} }
} }
@ -74,7 +76,9 @@ extension NetworkView {
.font(.system(size: 11, design: .monospaced)) .font(.system(size: 11, design: .monospaced))
.foregroundColor(.secondary) .foregroundColor(.secondary)
} else { } else {
Text("PunchNet 服务未就绪").font(.caption).foregroundColor(.secondary) Text("PunchNet 服务未就绪")
.font(.caption)
.foregroundColor(.secondary)
} }
} }
@ -82,13 +86,17 @@ extension NetworkView {
if connectState == .connected { if connectState == .connected {
Picker("", selection: $showMode) { Picker("", selection: $showMode) {
ForEach(ShowMode.allCases, id: \.self) { Text($0.rawValue).tag($0) } ForEach(ShowMode.allCases, id: \.self) {
Text($0.rawValue).tag($0)
}
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.frame(width: 160) .frame(width: 160)
} }
Button { openWindow(id: "settings") } label: { Button {
openWindow(id: "settings")
} label: {
Image(systemName: "slider.horizontal.3") Image(systemName: "slider.horizontal.3")
.font(.system(size: 14)) .font(.system(size: 14))
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -119,14 +127,19 @@ extension NetworkView {
if showMode == .resource { if showMode == .resource {
// //
ScrollView { ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 16)], spacing: 16) { LazyVGrid(columns: [
ForEach(networkModel.networkContext.resourceList, id: \.id) { res in 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) ResourceItemCard(resource: res)
} }
} }
.padding(20) .padding(20)
} }
.transition(.opacity) .transition(.opacity)
.frame(maxWidth: .infinity)
} else { } else {
// //
NetworkDeviceGroupView(networkModel: networkModel) NetworkDeviceGroupView(networkModel: networkModel)
@ -142,13 +155,17 @@ extension NetworkView {
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
.symbolEffect(.pulse, options: .repeating) .symbolEffect(.pulse, options: .repeating)
Text("尚未接入网络").font(.headline) Text("尚未接入网络")
.font(.headline)
Button(action: { startConnection() }) { Button(action: { startConnection() }) {
if isConnecting { if isConnecting {
ProgressView().controlSize(.small).frame(width: 80) ProgressView()
.controlSize(.small)
.frame(width: 80)
} else { } else {
Text("建立安全连接").frame(width: 80) Text("建立安全连接")
.frame(width: 80)
} }
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
@ -169,8 +186,12 @@ extension NetworkView {
isConnecting = true isConnecting = true
Task { Task {
do { do {
guard let session = userContext.networkSession else { return } guard let session = userContext.networkSession else {
return
}
try await networkModel.connect(networkSession: session) try await networkModel.connect(networkSession: session)
let context = networkModel.networkContext let context = networkModel.networkContext
if let options = SystemConfig.getOptions( if let options = SystemConfig.getOptions(
networkId: UInt32(session.networkId), networkId: UInt32(session.networkId),
@ -184,8 +205,13 @@ extension NetworkView {
) { ) {
try await vpnManager.enableVpn(options: options) try await vpnManager.enableVpn(options: options)
} }
} catch { print("Connection error: \(error)") } } catch {
await MainActor.run { isConnecting = false } print("Connection error: \(error)")
}
await MainActor.run {
isConnecting = false
}
} }
} }
} }
@ -228,8 +254,12 @@ struct NetworkNodeHeadView: View {
.frame(width: 8, height: 8) .frame(width: 8, height: 8)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(node.name).font(.system(size: 13, weight: .medium)) Text(node.name)
Text(node.ip).font(.system(size: 11, design: .monospaced)).foregroundColor(.secondary) .font(.system(size: 13, weight: .medium))
Text(node.ip)
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.secondary)
} }
} }
.padding(.vertical, 4) .padding(.vertical, 4)
@ -252,32 +282,47 @@ struct NetworkNodeDetailView: View {
Section("提供的服务") { Section("提供的服务") {
if isLoading { if isLoading {
ProgressView().controlSize(.small) ProgressView()
.controlSize(.small)
} else if resources.isEmpty { } else if resources.isEmpty {
Text("该节点暂未发布资源").foregroundColor(.secondary).font(.callout) Text("该节点暂未发布资源")
.foregroundColor(.secondary)
.font(.callout)
} else { } else {
ForEach(resources, id: \.id) { res in ForEach(resources, id: \.id) { res in
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(res.name).font(.body) Text(res.name)
Text(res.url).font(.caption).foregroundColor(.secondary) .font(.body)
Text(res.url)
.font(.caption)
.foregroundColor(.secondary)
} }
} }
} }
} }
} }
.task(id: node.id) { await loadNodeResources(id: node.id) } .task(id: node.id) {
await loadNodeResources(id: node.id)
}
} }
private func loadNodeResources(id: Int) async { private func loadNodeResources(id: Int) async {
guard let session = userContext.networkSession else { return } guard let session = userContext.networkSession else {
return
}
isLoading = true isLoading = true
defer { isLoading = false } defer {
isLoading = false
}
let params: [String: Any] = [ let params: [String: Any] = [
"client_id": SystemConfig.getClientId(), "client_id": SystemConfig.getClientId(),
"access_token": session.accessToken, "access_token": session.accessToken,
"id": id "id": id
] ]
if let detail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NodeDetail.self) { if let detail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NodeDetail.self) {
self.resources = detail.resourceList self.resources = detail.resourceList
} }
@ -290,24 +335,46 @@ struct ResourceItemCard: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Image(systemName: "safari.fill").foregroundColor(.accentColor).font(.title3) Image(systemName: "safari.fill")
Text(resource.name).font(.headline).lineLimit(1) .foregroundColor(.accentColor)
Text(resource.url).font(.caption2).foregroundColor(.secondary).lineLimit(1) .font(.title3)
Text(resource.name)
.font(.headline)
.lineLimit(1)
.truncationMode(.tail)
Text(resource.url)
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.middle)
} }
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .leading) .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(Color(NSColor.controlBackgroundColor).opacity(isHovered ? 0.8 : 0.4)) .overlay(
.cornerRadius(10) RoundedRectangle(cornerRadius: 10)
.onHover { isHovered = $0 } .stroke(Color.gray, lineWidth: 1)
)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color(isHovered ? NSColor.selectedControlColor : NSColor.controlBackgroundColor))
)
.onHover {
isHovered = $0
}
} }
} }
struct NetworkWaitAuthView: View { struct NetworkWaitAuthView: View {
@Bindable var networkModel: NetworkModel @Bindable var networkModel: NetworkModel
var body: some View { var body: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
ProgressView() ProgressView()
Text("等待认证确认中...").foregroundColor(.secondary)
Text("等待认证确认中...")
.foregroundColor(.secondary)
} }
} }
} }