fix settings
This commit is contained in:
parent
2d90e70b69
commit
a62d531493
@ -4,34 +4,58 @@
|
||||
//
|
||||
// Created by 安礼成 on 2026/1/16.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsAccountView: View {
|
||||
@State var state: SettingsState = SettingsState()
|
||||
@Environment(UserContext.self) var userContext: UserContext
|
||||
@Environment(\.openWindow) var openWindow
|
||||
|
||||
var body: some View {
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading) {
|
||||
Text("账户")
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
// MARK: - 账户部分
|
||||
sectionHeader(title: "账户安全", icon: "shield.lefthalf.filled")
|
||||
|
||||
if let loginCredit = userContext.loginCredit {
|
||||
switch loginCredit {
|
||||
case .token(let token):
|
||||
TokenCreditView(token: token)
|
||||
case .accountAndPasword(let account, let password):
|
||||
AccountCreditView(username: account)
|
||||
VStack(spacing: 0) {
|
||||
if userContext.isLogined, let loginCredit = userContext.loginCredit {
|
||||
switch loginCredit {
|
||||
case .token(let token):
|
||||
TokenCreditView(token: token)
|
||||
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("网络")
|
||||
|
||||
HStack(alignment: .top) {
|
||||
Text("默认网络")
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("默认网络")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(state.selectedNetwork.name)
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Menu {
|
||||
ForEach(state.networks, id: \.id) { network in
|
||||
Button(network.name) {
|
||||
@ -39,104 +63,151 @@ struct SettingsAccountView: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(state.selectedNetwork.name)
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.cornerRadius(5)
|
||||
Text("切换网络")
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Capsule().fill(Color.blue.opacity(0.1)))
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
Text("进入管理平台")
|
||||
Label("进入管理平台", systemImage: "arrow.up.right.square")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
}
|
||||
|
||||
Button {
|
||||
Spacer()
|
||||
|
||||
} label: {
|
||||
Text("详情")
|
||||
Button("查看详情") {
|
||||
|
||||
}
|
||||
.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 {
|
||||
|
||||
// 基于账号密码的登陆
|
||||
struct AccountCreditView: View {
|
||||
@Environment(UserContext.self) var userContext: UserContext
|
||||
|
||||
let username: String
|
||||
// 统一的条目容器样式
|
||||
struct AccountRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
var actions: AnyView
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image("logo")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
HStack(spacing: 16) {
|
||||
// 模拟 Logo
|
||||
Circle()
|
||||
.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()
|
||||
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
Text("修改密码")
|
||||
}
|
||||
|
||||
Button {
|
||||
Task { @MainActor in
|
||||
try await VPNManager.shared.disableVpn()
|
||||
self.userContext.isLogined = false
|
||||
}
|
||||
} label: {
|
||||
Text("退出登陆")
|
||||
}
|
||||
actions
|
||||
}
|
||||
.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 {
|
||||
@Environment(UserContext.self) var userContext: UserContext
|
||||
let token: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image("logo")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text(token)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
AccountRow(icon: "key.horizontal.fill", title: "Token 登录", subtitle: token, actions: AnyView(
|
||||
Button("退出登录") {
|
||||
Task { @MainActor in
|
||||
try await VPNManager.shared.disableVpn()
|
||||
self.userContext.isLogined = false
|
||||
try await userContext.logout()
|
||||
}
|
||||
} label: {
|
||||
Text("退出登陆")
|
||||
}
|
||||
|
||||
}
|
||||
.frame(width: 400)
|
||||
.buttonStyle(.bordered)
|
||||
.foregroundColor(.red)
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsAccountView()
|
||||
}
|
||||
|
||||
@ -4,13 +4,11 @@
|
||||
//
|
||||
// Created by 安礼成 on 2026/1/16.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
||||
@State private var state = SettingsState()
|
||||
@State private var hovering = false
|
||||
@State private var selectedMenu: MenuItem = .accout
|
||||
|
||||
enum MenuItem: String, CaseIterable {
|
||||
@ -19,42 +17,127 @@ struct SettingsView: View {
|
||||
case device = "设备"
|
||||
case system = "软件"
|
||||
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 {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
List(MenuItem.allCases, id: \.self, selection: $selectedMenu) { menu in
|
||||
HStack(alignment: .center) {
|
||||
Text(menu.rawValue)
|
||||
// MARK: - 自定义侧边栏
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
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()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.frame(minWidth: 180, idealWidth: 200, maxWidth: 250)
|
||||
.background(VisualEffectView(material: .sidebar, blendingMode: .behindWindow))
|
||||
.navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 250)
|
||||
|
||||
} detail: {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Group {
|
||||
switch self.selectedMenu {
|
||||
case .accout:
|
||||
SettingsAccountView(state: self.state)
|
||||
case .network:
|
||||
SettingsNetworkView(state: self.state)
|
||||
case .device:
|
||||
SettingsDeviceView()
|
||||
case .system:
|
||||
SettingsSystemView()
|
||||
case .about:
|
||||
SettingsAboutView()
|
||||
// MARK: - 详情页转场
|
||||
ZStack(alignment: .topLeading) {
|
||||
// 背景保持统一
|
||||
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Group {
|
||||
// 使用 ID 辅助 SwiftUI 识别视图切换,触发 transition
|
||||
switch self.selectedMenu {
|
||||
case .accout:
|
||||
SettingsAccountView(state: self.state)
|
||||
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 {
|
||||
|
||||
@ -143,4 +143,10 @@ class UserContext {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 退出登陆
|
||||
func logout() async throws {
|
||||
try await VPNManager.shared.disableVpn()
|
||||
self.isLogined = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ struct punchnetApp: App {
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup(id: "mainWindow") {
|
||||
WindowGroup(id: "main") {
|
||||
// RootView()
|
||||
SettingsView()
|
||||
.navigationTitle("")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user