fix settings

This commit is contained in:
anlicheng 2026-03-19 22:44:50 +08:00
parent 2d90e70b69
commit a62d531493
4 changed files with 263 additions and 103 deletions

View File

@ -4,34 +4,58 @@
// //
// Created by on 2026/1/16. // Created by on 2026/1/16.
// //
import SwiftUI import SwiftUI
struct SettingsAccountView: View { struct SettingsAccountView: View {
@State var state: SettingsState = SettingsState() @State var state: SettingsState = SettingsState()
@Environment(UserContext.self) var userContext: UserContext @Environment(UserContext.self) var userContext: UserContext
@Environment(\.openWindow) var openWindow
var body: some View { var body: some View {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) { VStack(alignment: .leading, spacing: 24) {
Text("账户") // MARK: -
sectionHeader(title: "账户安全", icon: "shield.lefthalf.filled")
if let loginCredit = userContext.loginCredit { VStack(spacing: 0) {
switch loginCredit { if userContext.isLogined, let loginCredit = userContext.loginCredit {
case .token(let token): switch loginCredit {
TokenCreditView(token: token) case .token(let token):
case .accountAndPasword(let account, let password): TokenCreditView(token: token)
AccountCreditView(username: account) case .accountAndPasword(let account, _):
AccountCreditView(username: account)
}
} else {
//
Button {
self.openMainWindow(id: "main")
} label: {
Text("登录")
.fontWeight(.medium)
}
.buttonStyle(.borderedProminent)
} }
} }
.background(Color.primary.opacity(0.03))
.cornerRadius(12)
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.primary.opacity(0.05), lineWidth: 1))
// MARK: -
sectionHeader(title: "网络配置", icon: "network")
Text("网络") VStack(spacing: 16) {
HStack {
HStack(alignment: .top) { VStack(alignment: .leading, spacing: 4) {
Text("默认网络") Text("默认网络")
.font(.subheadline)
VStack(alignment: .leading) { .foregroundColor(.secondary)
Text(state.selectedNetwork.name)
.font(.headline)
}
Spacer()
Menu { Menu {
ForEach(state.networks, id: \.id) { network in ForEach(state.networks, id: \.id) { network in
Button(network.name) { Button(network.name) {
@ -39,104 +63,151 @@ struct SettingsAccountView: View {
} }
} }
} label: { } label: {
Text(state.selectedNetwork.name) Text("切换网络")
.padding() .font(.subheadline)
.background(Color.gray.opacity(0.2)) .padding(.horizontal, 12)
.cornerRadius(5) .padding(.vertical, 6)
.background(Capsule().fill(Color.blue.opacity(0.1)))
} }
.buttonStyle(.plain)
}
Divider()
HStack {
Button { Button {
} label: { } label: {
Text("进入管理平台") Label("进入管理平台", systemImage: "arrow.up.right.square")
} }
.buttonStyle(.plain)
.foregroundColor(.blue)
} Spacer()
Button {
} label: { Button("查看详情") {
Text("详情")
}
.buttonStyle(.bordered)
.controlSize(.small)
} }
} }
.padding(16)
.background(Color.primary.opacity(0.03))
.cornerRadius(12)
} }
.frame(maxWidth: 600) //
} }
.padding()
} }
//
private func openMainWindow(id: String) {
let window = NSApp.windows.first { win in
if let idStr = win.identifier?.rawValue {
return idStr.starts(with: id)
}
return false
}
if let window {
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
} else {
openWindow(id: id)
}
}
//
private func sectionHeader(title: String, icon: String) -> some View {
HStack {
Image(systemName: icon)
.foregroundColor(.blue)
Text(title)
.font(.system(size: 16, weight: .bold))
}
.padding(.leading, 4)
}
} }
// MARK: -
extension SettingsAccountView { extension SettingsAccountView {
// //
struct AccountCreditView: View { struct AccountRow: View {
@Environment(UserContext.self) var userContext: UserContext let icon: String
let title: String
let username: String let subtitle: String
var actions: AnyView
var body: some View { var body: some View {
HStack { HStack(spacing: 16) {
Image("logo") // Logo
.resizable() Circle()
.frame(width: 20, height: 20) .fill(Color.blue.gradient)
.frame(width: 32, height: 32)
.overlay(
Image(systemName: icon)
.foregroundColor(.white)
.font(.system(size: 14))
)
Text(username) VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline)
.foregroundColor(.secondary)
Text(subtitle)
.font(.system(size: 15, weight: .medium, design: .monospaced))
.lineLimit(1)
}
Spacer() Spacer()
Button { actions
} label: {
Text("修改密码")
}
Button {
Task { @MainActor in
try await VPNManager.shared.disableVpn()
self.userContext.isLogined = false
}
} label: {
Text("退出登陆")
}
} }
.frame(width: 400) .padding(16)
}
}
struct AccountCreditView: View {
@Environment(UserContext.self) var userContext: UserContext
let username: String
var body: some View {
AccountRow(icon: "person.fill", title: "当前登录账号", subtitle: username, actions: AnyView(
HStack(spacing: 12) {
Button("修改密码") {
}
.buttonStyle(.link)
Button("退出登录") {
Task { @MainActor in
try await userContext.logout()
}
}
.buttonStyle(.bordered)
.foregroundColor(.red)
}
))
} }
} }
// Token
struct TokenCreditView: View { struct TokenCreditView: View {
@Environment(UserContext.self) var userContext: UserContext @Environment(UserContext.self) var userContext: UserContext
let token: String let token: String
var body: some View { var body: some View {
HStack { AccountRow(icon: "key.horizontal.fill", title: "Token 登录", subtitle: token, actions: AnyView(
Image("logo") Button("退出登录") {
.resizable()
.frame(width: 20, height: 20)
Text(token)
Spacer()
Button {
Task { @MainActor in Task { @MainActor in
try await VPNManager.shared.disableVpn() try await userContext.logout()
self.userContext.isLogined = false
} }
} label: {
Text("退出登陆")
} }
.buttonStyle(.bordered)
} .foregroundColor(.red)
.frame(width: 400) ))
} }
} }
} }
#Preview {
SettingsAccountView()
}

View File

@ -4,13 +4,11 @@
// //
// Created by on 2026/1/16. // Created by on 2026/1/16.
// //
import SwiftUI import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@State private var columnVisibility: NavigationSplitViewVisibility = .all @State private var columnVisibility: NavigationSplitViewVisibility = .all
@State private var state = SettingsState() @State private var state = SettingsState()
@State private var hovering = false
@State private var selectedMenu: MenuItem = .accout @State private var selectedMenu: MenuItem = .accout
enum MenuItem: String, CaseIterable { enum MenuItem: String, CaseIterable {
@ -19,42 +17,127 @@ struct SettingsView: View {
case device = "设备" case device = "设备"
case system = "软件" case system = "软件"
case about = "关于" case about = "关于"
//
var icon: String {
switch self {
case .accout:
return "person.crop.circle.fill"
case .network:
return "network"
case .device:
return "laptopcomputer"
case .system:
return "gearshape.fill"
case .about:
return "info.circle.fill"
}
}
} }
var body: some View { var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) { NavigationSplitView(columnVisibility: $columnVisibility) {
List(MenuItem.allCases, id: \.self, selection: $selectedMenu) { menu in // MARK: -
HStack(alignment: .center) { VStack(alignment: .leading, spacing: 20) {
Text(menu.rawValue) Text("设置")
.font(.system(size: 24, weight: .bold))
.padding(.horizontal, 16)
.padding(.top, 24)
VStack(spacing: 4) {
ForEach(MenuItem.allCases, id: \.self) { menu in
SidebarItem(
icon: menu.icon,
title: menu.rawValue,
isSelected: selectedMenu == menu
)
.onTapGesture {
// 使 spring
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
self.selectedMenu = menu
}
}
}
Spacer() Spacer()
} }
.padding(.horizontal, 12)
} }
.listStyle(.sidebar) .background(VisualEffectView(material: .sidebar, blendingMode: .behindWindow))
.frame(minWidth: 180, idealWidth: 200, maxWidth: 250) .navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 250)
} detail: { } detail: {
VStack(alignment: .leading, spacing: 0) { // MARK: -
Group { ZStack(alignment: .topLeading) {
switch self.selectedMenu { //
case .accout: VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
SettingsAccountView(state: self.state) .ignoresSafeArea()
case .network:
SettingsNetworkView(state: self.state) VStack(alignment: .leading, spacing: 0) {
case .device: Group {
SettingsDeviceView() // 使 ID SwiftUI transition
case .system: switch self.selectedMenu {
SettingsSystemView() case .accout:
case .about: SettingsAccountView(state: self.state)
SettingsAboutView() case .network:
SettingsNetworkView(state: self.state)
case .device:
SettingsDeviceView()
case .system:
SettingsSystemView()
case .about:
SettingsAboutView()
}
} }
.id(selectedMenu) //
.transition(.asymmetric(
insertion: .move(edge: .bottom).combined(with: .opacity),
removal: .opacity
))
Spacer()
} }
Spacer() .padding(32) //
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
} }
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
} }
} }
}
// MARK: -
struct SidebarItem: View {
let icon: String
let title: String
let isSelected: Bool
@State private var isHovering = false
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 14, weight: .medium))
.frame(width: 20)
Text(title)
.font(.system(size: 14, weight: .medium))
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.foregroundColor(isSelected ? .white : .primary.opacity(0.8))
.background {
if isSelected {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Color.blue.gradient) //
} else if isHovering {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Color.primary.opacity(0.05))
}
}
.onHover {
isHovering = $0
}
.contentShape(Rectangle())
}
} }
#Preview { #Preview {

View File

@ -143,4 +143,10 @@ class UserContext {
return nil return nil
} }
// 退
func logout() async throws {
try await VPNManager.shared.disableVpn()
self.isLogined = false
}
} }

View File

@ -41,7 +41,7 @@ struct punchnetApp: App {
} }
var body: some Scene { var body: some Scene {
WindowGroup(id: "mainWindow") { WindowGroup(id: "main") {
// RootView() // RootView()
SettingsView() SettingsView()
.navigationTitle("") .navigationTitle("")