fix network view
This commit is contained in:
parent
2b96a5a8cf
commit
177f8932fa
@ -4,11 +4,9 @@
|
||||
import SwiftUI
|
||||
import Observation
|
||||
|
||||
// 连接状态
|
||||
// MARK: - 基础模型协议 (确保代码可编译,请根据实际 Model 调整)
|
||||
enum ConnectState {
|
||||
case waitAuth
|
||||
case connected
|
||||
case disconnected
|
||||
case waitAuth, connected, disconnected
|
||||
}
|
||||
|
||||
// MARK: - 主网络视图
|
||||
@ -31,40 +29,31 @@ struct NetworkView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// 头部:使用原生毛玻璃材质
|
||||
// 1. 头部区域 (Header)
|
||||
headerSection
|
||||
|
||||
Divider()
|
||||
|
||||
// 内容区
|
||||
ZStack {
|
||||
VisualEffectView(material: .windowBackground, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// 2. 内容区域 (Content)
|
||||
Group {
|
||||
switch connectState {
|
||||
case .waitAuth:
|
||||
statusLoadingView
|
||||
NetworkWaitAuthView(networkModel: networkModel)
|
||||
case .connected:
|
||||
connectedContent
|
||||
case .disconnected:
|
||||
disconnectedContent
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(VisualEffectView(material: .windowBackground, blendingMode: .behindWindow))
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 550, minHeight: 480)
|
||||
.frame(minWidth: 700, minHeight: 500) // 适当调大宽度以适应 SplitView
|
||||
.onAppear {
|
||||
syncState(vpnManager.vpnStatus)
|
||||
}
|
||||
.onChange(of: vpnManager.vpnStatus) { _, newStatus in
|
||||
withAnimation(.snappy) {
|
||||
syncState(newStatus)
|
||||
}
|
||||
withAnimation(.snappy) { syncState(newStatus) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,7 +63,6 @@ extension NetworkView {
|
||||
|
||||
private var headerSection: some View {
|
||||
HStack(spacing: 16) {
|
||||
// 状态指示器
|
||||
statusIndicator
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
@ -109,7 +97,7 @@ extension NetworkView {
|
||||
.help("配置中心")
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
.padding(.vertical, 14)
|
||||
.background(VisualEffectView(material: .headerView, blendingMode: .withinWindow))
|
||||
}
|
||||
|
||||
@ -117,75 +105,58 @@ extension NetworkView {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(connectState == .connected ? Color.green.opacity(0.15) : Color.primary.opacity(0.05))
|
||||
.frame(width: 40, height: 40)
|
||||
.frame(width: 36, height: 36)
|
||||
|
||||
Image(systemName: connectState == .connected ? "checkmark.shield.fill" : "shield.slash.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(connectState == .connected ? Color.green : Color.secondary)
|
||||
.font(.system(size: 18))
|
||||
.symbolEffect(.bounce, value: connectState == .connected)
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var connectedContent: some View {
|
||||
VStack(spacing: 16) {
|
||||
if showMode == .resource {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 220), spacing: 16)], spacing: 16) {
|
||||
// 资源视图:网格布局
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 16)], spacing: 16) {
|
||||
ForEach(networkModel.networkContext.resourceList, id: \.id) { res in
|
||||
ResourceItemCard(resource: res)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
let nodes = networkModel.networkContext.nodeList
|
||||
ForEach(nodes, id: \.id) { node in
|
||||
DeviceItemRow(node: node, isLast: node.id == nodes.last?.id)
|
||||
}
|
||||
}
|
||||
.background(Color(NSColor.controlBackgroundColor).opacity(0.4))
|
||||
.cornerRadius(12)
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.primary.opacity(0.08), lineWidth: 1))
|
||||
}
|
||||
// 设备视图:双栏布局
|
||||
NetworkDeviceGroupView(networkModel: networkModel)
|
||||
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .opacity))
|
||||
}
|
||||
}
|
||||
|
||||
private var disconnectedContent: some View {
|
||||
VStack(spacing: 24) {
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.font(.system(size: 48, weight: .ultraLight))
|
||||
.font(.system(size: 40, weight: .ultraLight))
|
||||
.foregroundStyle(.tertiary)
|
||||
.symbolEffect(.pulse, options: .repeating)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("尚未接入网络").font(.headline)
|
||||
Text("连接后即可访问内部资源与成员节点").font(.subheadline).foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Button(action: { startConnection() }) {
|
||||
if isConnecting {
|
||||
ProgressView().controlSize(.small).frame(width: 110)
|
||||
ProgressView().controlSize(.small).frame(width: 80)
|
||||
} else {
|
||||
Text("建立安全连接").fontWeight(.medium).frame(width: 110)
|
||||
Text("建立安全连接").frame(width: 80)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.disabled(isConnecting)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
private var statusLoadingView: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
Text("正在同步网络状态...").font(.subheadline).foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 300)
|
||||
}
|
||||
|
||||
private func syncState(_ status: VPNManager.VPNStatus) {
|
||||
switch status {
|
||||
case .connected: connectState = .connected
|
||||
@ -198,13 +169,7 @@ extension NetworkView {
|
||||
isConnecting = true
|
||||
Task {
|
||||
do {
|
||||
guard let session = userContext.networkSession else {
|
||||
await MainActor.run {
|
||||
isConnecting = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let session = userContext.networkSession else { return }
|
||||
try await networkModel.connect(networkSession: session)
|
||||
let context = networkModel.networkContext
|
||||
if let options = SystemConfig.getOptions(
|
||||
@ -219,105 +184,130 @@ extension NetworkView {
|
||||
) {
|
||||
try await vpnManager.enableVpn(options: options)
|
||||
}
|
||||
} catch {
|
||||
print("Connection error: \(error)")
|
||||
}
|
||||
} catch { print("Connection error: \(error)") }
|
||||
await MainActor.run { isConnecting = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 子组件:资源卡片
|
||||
struct ResourceItemCard: View {
|
||||
let resource: Resource
|
||||
@State private var isHovered = false
|
||||
// MARK: - 设备组视图 (NavigationSplitView)
|
||||
struct NetworkDeviceGroupView: View {
|
||||
@Bindable var networkModel: NetworkModel
|
||||
@State private var selectedId: Int?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Image(systemName: "safari.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.title3)
|
||||
Spacer()
|
||||
Circle()
|
||||
.fill(Color.green)
|
||||
.frame(width: 6, height: 6)
|
||||
.shadow(color: .green.opacity(0.5), radius: 2)
|
||||
NavigationSplitView {
|
||||
List(networkModel.networkContext.nodeList, id: \.id, selection: $selectedId) { node in
|
||||
NetworkNodeHeadView(node: node)
|
||||
.tag(node.id)
|
||||
}
|
||||
|
||||
Text(resource.name)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
|
||||
Text(resource.url)
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.listStyle(.sidebar)
|
||||
.navigationSplitViewColumnWidth(min: 200, ideal: 220)
|
||||
} detail: {
|
||||
if let selectedNode = networkModel.networkContext.nodeList.first(where: { $0.id == selectedId }) {
|
||||
NetworkNodeDetailView(node: selectedNode)
|
||||
} else {
|
||||
ContentUnavailableView("选择成员设备", systemImage: "macbook.and.iphone", description: Text("查看详细网络信息和服务"))
|
||||
}
|
||||
.padding(14)
|
||||
.background(isHovered ? Color.primary.opacity(0.05) : Color(NSColor.controlBackgroundColor).opacity(0.3))
|
||||
.cornerRadius(12)
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(isHovered ? Color.accentColor.opacity(0.3) : Color.primary.opacity(0.08), lineWidth: 1))
|
||||
.onHover { isHovered = $0 }
|
||||
.contextMenu {
|
||||
Button("复制链接") {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(resource.url, forType: .string)
|
||||
}
|
||||
Button("在浏览器打开") {
|
||||
if let url = URL(string: resource.url) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
.onAppear {
|
||||
if selectedId == nil {
|
||||
selectedId = networkModel.networkContext.nodeList.first?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 子组件:设备行
|
||||
struct DeviceItemRow: View {
|
||||
let node: Node
|
||||
let isLast: Bool
|
||||
@State private var isHovered = false
|
||||
|
||||
// MARK: - 子组件
|
||||
struct NetworkNodeHeadView: View {
|
||||
var node: Node
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "desktopcomputer")
|
||||
.font(.title3)
|
||||
.foregroundStyle(isHovered ? .primary : .secondary)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
struct NetworkNodeDetailView: View {
|
||||
@Environment(UserContext.self) var userContext
|
||||
var node: Node
|
||||
@State private var resources: [Resource] = []
|
||||
@State private var isLoading = false
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text("直连").font(.system(size: 9, weight: .bold))
|
||||
.padding(.horizontal, 4).padding(.vertical, 1)
|
||||
.background(Color.accentColor.opacity(0.1)).foregroundColor(.accentColor).cornerRadius(3)
|
||||
Circle().fill(Color.green).frame(width: 6, height: 6)
|
||||
var body: some View {
|
||||
List {
|
||||
Section("节点信息") {
|
||||
LabeledContent("连接状态", value: node.connectionStatus)
|
||||
LabeledContent("虚拟IPv4", value: node.ip)
|
||||
LabeledContent("系统环境", value: node.system ?? "未知")
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 16)
|
||||
.contentShape(Rectangle())
|
||||
.background(isHovered ? Color.primary.opacity(0.03) : Color.clear)
|
||||
.onHover { isHovered = $0 }
|
||||
|
||||
if !isLast {
|
||||
Divider().padding(.leading, 44).opacity(0.5)
|
||||
Section("提供的服务") {
|
||||
if isLoading {
|
||||
ProgressView().controlSize(.small)
|
||||
} else if resources.isEmpty {
|
||||
Text("该节点暂未发布资源").foregroundColor(.secondary).font(.callout)
|
||||
} else {
|
||||
ForEach(resources, id: \.id) { res in
|
||||
VStack(alignment: .leading) {
|
||||
Text(res.name).font(.body)
|
||||
Text(res.url).font(.caption).foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button("复制 IP 地址") {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(node.ip, forType: .string)
|
||||
}
|
||||
Button("终端 SSH 连接") {
|
||||
/* 呼起终端逻辑 */
|
||||
}
|
||||
}
|
||||
.task(id: node.id) { await loadNodeResources(id: node.id) }
|
||||
}
|
||||
|
||||
private func loadNodeResources(id: Int) async {
|
||||
guard let session = userContext.networkSession else { return }
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
let params: [String: Any] = [
|
||||
"client_id": SystemConfig.getClientId(),
|
||||
"access_token": session.accessToken,
|
||||
"id": id
|
||||
]
|
||||
if let detail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NodeDetail.self) {
|
||||
self.resources = detail.resourceList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResourceItemCard: View {
|
||||
let resource: 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)
|
||||
Text(resource.url).font(.caption2).foregroundColor(.secondary).lineLimit(1)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(NSColor.controlBackgroundColor).opacity(isHovered ? 0.8 : 0.4))
|
||||
.cornerRadius(10)
|
||||
.onHover { isHovered = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkWaitAuthView: View {
|
||||
@Bindable var networkModel: NetworkModel
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
Text("等待认证确认中...").foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user