fix settings
This commit is contained in:
parent
2d90e70b69
commit
a62d531493
@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -143,4 +143,10 @@ class UserContext {
|
|||||||
return nil
|
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 {
|
var body: some Scene {
|
||||||
WindowGroup(id: "mainWindow") {
|
WindowGroup(id: "main") {
|
||||||
// RootView()
|
// RootView()
|
||||||
SettingsView()
|
SettingsView()
|
||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user