Compare commits
No commits in common. "177f8932fa948349254a4c0423501fcb4b0f91a5" and "3558d3c102930c19d26a7e2ac5d4eaa50416bcef" have entirely different histories.
177f8932fa
...
3558d3c102
@ -1,313 +1,366 @@
|
|||||||
//
|
//
|
||||||
// NetworkView.swift
|
// NetworkView.swift
|
||||||
// punchnet
|
// punchnet
|
||||||
import SwiftUI
|
//
|
||||||
import Observation
|
// Created by 安礼成 on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
// MARK: - 基础模型协议 (确保代码可编译,请根据实际 Model 调整)
|
import SwiftUI
|
||||||
|
|
||||||
|
// 连接状态
|
||||||
enum ConnectState {
|
enum ConnectState {
|
||||||
case waitAuth, connected, disconnected
|
case waitAuth
|
||||||
|
case connected
|
||||||
|
case disconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 主网络视图
|
|
||||||
struct NetworkView: View {
|
struct NetworkView: View {
|
||||||
@Environment(UserContext.self) var userContext
|
@Environment(UserContext.self) var userContext: UserContext
|
||||||
@Environment(AppContext.self) var appContext
|
|
||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
|
|
||||||
@State private var networkModel = NetworkModel()
|
@State private var networkModel = NetworkModel()
|
||||||
@State private var showMode: ShowMode = .resource
|
@State private var showMode: ShowMode = .resource
|
||||||
|
// 状态管理
|
||||||
@State private var connectState: ConnectState = .disconnected
|
@State private var connectState: ConnectState = .disconnected
|
||||||
@State private var isConnecting: Bool = false
|
@State private var isOn: Bool = false
|
||||||
|
@State private var vpnManager: VPNManager = VPNManager.shared
|
||||||
|
|
||||||
private var vpnManager = VPNManager.shared
|
// 展示状态
|
||||||
|
enum ShowMode {
|
||||||
enum ShowMode: String, CaseIterable {
|
case resource
|
||||||
case resource = "访问资源"
|
case device
|
||||||
case device = "成员设备"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack {
|
||||||
// 1. 头部区域 (Header)
|
HStack {
|
||||||
headerSection
|
|
||||||
|
|
||||||
Divider()
|
VStack {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Text(userContext.networkSession?.networkName ?? "未知")
|
||||||
|
Text(">")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Toggle("", isOn: $isOn)
|
||||||
|
.toggleStyle(SwitchToggleStyle(tint: .green))
|
||||||
|
.disabled(true)
|
||||||
|
|
||||||
|
Text("已连接")
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 320)
|
||||||
|
|
||||||
|
if self.connectState == .connected {
|
||||||
|
// 显示设备和资源选项
|
||||||
|
HStack {
|
||||||
|
Button {
|
||||||
|
self.showMode = .resource
|
||||||
|
} label: {
|
||||||
|
Text("资源")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
self.showMode = .device
|
||||||
|
} label: {
|
||||||
|
Text("设备")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 内容区域 (Content)
|
|
||||||
Group {
|
Group {
|
||||||
switch connectState {
|
switch self.connectState {
|
||||||
case .waitAuth:
|
case .waitAuth:
|
||||||
NetworkWaitAuthView(networkModel: networkModel)
|
NetworkWaitAuthView(networkModel: self.networkModel)
|
||||||
case .connected:
|
case .connected:
|
||||||
connectedContent
|
Group {
|
||||||
|
switch self.showMode {
|
||||||
|
case .resource:
|
||||||
|
NetworkResourceGroupView(networkModel: self.networkModel)
|
||||||
|
case .device:
|
||||||
|
NetworkDeviceGroupView(networkModel: self.networkModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
disconnectedContent
|
NetworkDisconnctedView(networkModel: self.networkModel)
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.background(VisualEffectView(material: .windowBackground, blendingMode: .behindWindow))
|
|
||||||
}
|
|
||||||
.frame(minWidth: 700, minHeight: 500) // 适当调大宽度以适应 SplitView
|
|
||||||
.onAppear {
|
|
||||||
syncState(vpnManager.vpnStatus)
|
|
||||||
}
|
|
||||||
.onChange(of: vpnManager.vpnStatus) { _, newStatus in
|
|
||||||
withAnimation(.snappy) { syncState(newStatus) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 视图组件扩展
|
|
||||||
extension NetworkView {
|
|
||||||
|
|
||||||
private var headerSection: some View {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
statusIndicator
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(userContext.networkSession?.networkName ?? "未连接网络")
|
|
||||||
.font(.system(size: 14, weight: .semibold))
|
|
||||||
|
|
||||||
if connectState == .connected {
|
|
||||||
Text("虚拟局域网 IP: \(networkModel.networkContext.ip)")
|
|
||||||
.font(.system(size: 11, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
} else {
|
|
||||||
Text("PunchNet 服务未就绪").font(.caption).foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top, 10)
|
||||||
|
.padding(.leading, 10)
|
||||||
|
.onChange(of: vpnManager.vpnStatus) { _, newState in
|
||||||
|
NSLog("print view change: \(newState)")
|
||||||
|
switch newState {
|
||||||
|
case .connected:
|
||||||
|
self.connectState = .connected
|
||||||
|
self.isOn = true
|
||||||
|
case .disconnected:
|
||||||
|
self.connectState = .disconnected
|
||||||
|
self.isOn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
if self.connectState == .connected {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button {
|
||||||
|
openWindow(id: "settings")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "gearshape")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if connectState == .connected {
|
// 网络处于未连接状态
|
||||||
Picker("", selection: $showMode) {
|
struct NetworkDisconnctedView: View {
|
||||||
ForEach(ShowMode.allCases, id: \.self) { Text($0.rawValue).tag($0) }
|
@Bindable var networkModel: NetworkModel
|
||||||
}
|
@Environment(AppContext.self) var appContext: AppContext
|
||||||
.pickerStyle(.segmented)
|
@Environment(UserContext.self) var userContext: UserContext
|
||||||
.frame(width: 160)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button { openWindow(id: "settings") } label: {
|
@State private var showAlert = false
|
||||||
Image(systemName: "slider.horizontal.3")
|
@State private var errorMessage = ""
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.help("配置中心")
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 14)
|
|
||||||
.background(VisualEffectView(material: .headerView, blendingMode: .withinWindow))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var statusIndicator: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Color.clear
|
||||||
.fill(connectState == .connected ? Color.green.opacity(0.15) : Color.primary.opacity(0.05))
|
|
||||||
.frame(width: 36, height: 36)
|
|
||||||
|
|
||||||
Image(systemName: connectState == .connected ? "checkmark.shield.fill" : "shield.slash.fill")
|
VStack {
|
||||||
.symbolRenderingMode(.hierarchical)
|
Button {
|
||||||
.foregroundStyle(connectState == .connected ? Color.green : Color.secondary)
|
Task { @MainActor in
|
||||||
.font(.system(size: 16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var connectedContent: some View {
|
|
||||||
if showMode == .resource {
|
|
||||||
// 资源视图:网格布局
|
|
||||||
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 {
|
|
||||||
// 设备视图:双栏布局
|
|
||||||
NetworkDeviceGroupView(networkModel: networkModel)
|
|
||||||
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .opacity))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var disconnectedContent: 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: { startConnection() }) {
|
|
||||||
if isConnecting {
|
|
||||||
ProgressView().controlSize(.small).frame(width: 80)
|
|
||||||
} else {
|
|
||||||
Text("建立安全连接").frame(width: 80)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.disabled(isConnecting)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func syncState(_ status: VPNManager.VPNStatus) {
|
|
||||||
switch status {
|
|
||||||
case .connected: connectState = .connected
|
|
||||||
case .disconnected: connectState = .disconnected
|
|
||||||
@unknown default: connectState = .disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startConnection() {
|
|
||||||
isConnecting = true
|
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
guard let session = userContext.networkSession else { return }
|
try await self.connect()
|
||||||
try await networkModel.connect(networkSession: session)
|
try await self.startVpn()
|
||||||
let context = networkModel.networkContext
|
} catch let err {
|
||||||
if let options = SystemConfig.getOptions(
|
self.showAlert = true
|
||||||
networkId: UInt32(session.networkId),
|
self.errorMessage = err.localizedDescription
|
||||||
networkDomain: session.networkDomain,
|
}
|
||||||
ip: context.ip,
|
}
|
||||||
maskLen: context.maskLen,
|
} label: {
|
||||||
accessToken: session.accessToken,
|
Text("连接")
|
||||||
identityId: context.identityId,
|
.font(.system(size: 14, weight: .regular))
|
||||||
hostname: context.hostname,
|
.padding([.top, .bottom], 8)
|
||||||
noticePort: appContext.noticePort
|
.padding([.leading, .trailing], 30)
|
||||||
) {
|
.foregroundColor(.white)
|
||||||
try await vpnManager.enableVpn(options: options)
|
|
||||||
|
}
|
||||||
|
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
|
||||||
|
.cornerRadius(5)
|
||||||
|
.frame(width: 120, height: 35)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
try await VPNManager.shared.disableVpn()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("关闭")
|
||||||
|
.font(.system(size: 14, weight: .regular))
|
||||||
|
.padding([.top, .bottom], 8)
|
||||||
|
.padding([.leading, .trailing], 30)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
}
|
||||||
|
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
|
||||||
|
.cornerRadius(5)
|
||||||
|
.frame(width: 120, height: 35)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(isPresented: $showAlert) {
|
||||||
|
Alert(title: Text("提示"), message: Text(self.errorMessage))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func connect() async throws {
|
||||||
|
guard let networkSession = userContext.networkSession else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try await networkModel.connect(networkSession: networkSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行登陆操作
|
||||||
|
private func startVpn() async throws {
|
||||||
|
guard let networkSession = userContext.networkSession else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let networkContext = networkModel.networkContext
|
||||||
|
let options = SystemConfig.getOptions(networkId: UInt32(networkSession.networkId),
|
||||||
|
networkDomain: networkSession.networkDomain,
|
||||||
|
ip: networkContext.ip,
|
||||||
|
maskLen: networkContext.maskLen,
|
||||||
|
accessToken: networkSession.accessToken,
|
||||||
|
identityId: networkContext.identityId,
|
||||||
|
hostname: networkContext.hostname,
|
||||||
|
noticePort: self.appContext.noticePort)
|
||||||
|
// token存在则优先使用token
|
||||||
|
try await VPNManager.shared.enableVpn(options: options!)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络处于连接状态
|
||||||
|
|
||||||
|
// 显示资源信息
|
||||||
|
struct NetworkResourceGroupView: View {
|
||||||
|
@Bindable var networkModel: NetworkModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 8) {
|
||||||
|
ForEach(self.networkModel.networkContext.resourceList, id: \.id) { resource in
|
||||||
|
NetworkResourceView(resource: resource)
|
||||||
}
|
}
|
||||||
} catch { print("Connection error: \(error)") }
|
|
||||||
await MainActor.run { isConnecting = false }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 设备组视图 (NavigationSplitView)
|
struct NetworkResourceView: View {
|
||||||
|
var resource: Resource
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text(resource.connectionStatus)
|
||||||
|
|
||||||
|
Text(resource.name)
|
||||||
|
.font(.system(size: 14, weight: .regular))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(resource.url)
|
||||||
|
.font(.system(size: 14, weight: .regular))
|
||||||
|
.padding(.leading, 30)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示设备信息
|
||||||
struct NetworkDeviceGroupView: View {
|
struct NetworkDeviceGroupView: View {
|
||||||
@Bindable var networkModel: NetworkModel
|
@Bindable var networkModel: NetworkModel
|
||||||
@State private var selectedId: Int?
|
@State private var selectedId: Int?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationSplitView {
|
NavigationSplitView {
|
||||||
List(networkModel.networkContext.nodeList, id: \.id, selection: $selectedId) { node in
|
List(self.networkModel.networkContext.nodeList, id: \.id, selection: $selectedId) { node in
|
||||||
NetworkNodeHeadView(node: node)
|
NetworkNodeHeadView(node: node)
|
||||||
.tag(node.id)
|
|
||||||
}
|
}
|
||||||
.listStyle(.sidebar)
|
.listStyle(.sidebar)
|
||||||
.navigationSplitViewColumnWidth(min: 200, ideal: 220)
|
.onChange(of: selectedId) {
|
||||||
} detail: {
|
self.networkModel.changeSelectedNode(nodeId: selectedId)
|
||||||
if let selectedNode = networkModel.networkContext.nodeList.first(where: { $0.id == selectedId }) {
|
|
||||||
NetworkNodeDetailView(node: selectedNode)
|
|
||||||
} else {
|
|
||||||
ContentUnavailableView("选择成员设备", systemImage: "macbook.and.iphone", description: Text("查看详细网络信息和服务"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if selectedId == nil {
|
if selectedId == nil {
|
||||||
selectedId = networkModel.networkContext.nodeList.first?.id
|
selectedId = self.networkModel.networkContext.nodeList.first?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} detail: {
|
||||||
|
NetworkNodeDetailView(node: networkModel.selectedNode)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 子组件
|
|
||||||
struct NetworkNodeHeadView: View {
|
struct NetworkNodeHeadView: View {
|
||||||
var node: Node
|
var node: 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) {
|
var body: some View {
|
||||||
Text(node.name).font(.system(size: 13, weight: .medium))
|
VStack {
|
||||||
Text(node.ip).font(.system(size: 11, design: .monospaced)).foregroundColor(.secondary)
|
HStack {
|
||||||
|
Text(node.connectionStatus)
|
||||||
|
|
||||||
|
Text(node.name)
|
||||||
|
.font(.system(size: 14, weight: .regular))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(node.ip)
|
||||||
|
.font(.system(size: 14, weight: .regular))
|
||||||
|
.padding(.leading, 30)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NetworkNodeDetailView: View {
|
struct NetworkNodeDetailView: View {
|
||||||
@Environment(UserContext.self) var userContext
|
@Environment(UserContext.self) var userContext: UserContext
|
||||||
var node: Node
|
var node: Node?
|
||||||
@State private var resources: [Resource] = []
|
@State private var resources: [Resource] = []
|
||||||
@State private var isLoading = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let node {
|
||||||
List {
|
List {
|
||||||
Section("节点信息") {
|
Section {
|
||||||
LabeledContent("连接状态", value: node.connectionStatus)
|
HStack {
|
||||||
LabeledContent("虚拟IPv4", value: node.ip)
|
Text("连接状态")
|
||||||
LabeledContent("系统环境", value: node.system ?? "未知")
|
Text("\(node.connectionStatus)")
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("提供的服务") {
|
HStack {
|
||||||
if isLoading {
|
Text("虚拟IPv4")
|
||||||
ProgressView().controlSize(.small)
|
Text("\(node.ip)")
|
||||||
} else if resources.isEmpty {
|
Spacer()
|
||||||
Text("该节点暂未发布资源").foregroundColor(.secondary).font(.callout)
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("操作系统")
|
||||||
|
Text(node.system ?? "未知")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("服务列表") {
|
||||||
|
ForEach(resources, id: \.id) { resource in
|
||||||
|
HStack {
|
||||||
|
Text("\(resource.name)")
|
||||||
|
Text("\(resource.url)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: node.id) {
|
||||||
|
await self.loadNodeResources(id: node.id)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ForEach(resources, id: \.id) { res in
|
EmptyView()
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(res.name).font(.body)
|
|
||||||
Text(res.url).font(.caption).foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
.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 networkSession = userContext.networkSession else {
|
||||||
isLoading = true
|
return
|
||||||
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": networkSession.accessToken,
|
||||||
"id": id
|
"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 {
|
if let nodeDetail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NodeDetail.self) {
|
||||||
let resource: Resource
|
self.resources = nodeDetail.resourceList
|
||||||
@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 {
|
struct NetworkWaitAuthView: View {
|
||||||
@Bindable var networkModel: NetworkModel
|
@Bindable var networkModel: NetworkModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 16) {
|
Color.clear
|
||||||
ProgressView()
|
.overlay {
|
||||||
Text("等待认证确认中...").foregroundColor(.secondary)
|
Text("等待确认中")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NetworkView()
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user