Files
ios-client/IosClientKit/Sources/FeatureProfile/ProfileScreen.swift
2026-09-15 14:35:52 +08:00

169 lines
5.6 KiB
Swift

import SwiftUI
import CoreModel
import CoreUI
import DesignSystem
public struct ProfileScreen: View {
private let viewModel: ProfileViewModel
private let onNavigateToAuth: () -> Void
@State private var showLogoutConfirm = false
public init(viewModel: ProfileViewModel, onNavigateToAuth: @escaping () -> Void) {
self.viewModel = viewModel
self.onNavigateToAuth = onNavigateToAuth
}
public var body: some View {
content
.navigationTitle("我的")
.task { viewModel.load() }
}
@ViewBuilder
private var content: some View {
switch viewModel.uiState {
case .loading:
LoadingView()
case .loggedOut:
LoggedOutView(onNavigateToAuth: onNavigateToAuth)
case .error(let message):
ErrorView(message: message) { viewModel.load() }
case .success(let profile):
successContent(profile)
}
}
private func successContent(_ profile: UserProfile) -> some View {
ScrollView {
VStack(spacing: DS.Spacing.md) {
ProfileHeaderCard(profile: profile)
InfoCard(profile: profile)
if !profile.classGroups.isEmpty {
ClassGroupCard(groups: profile.classGroups)
}
Button("退出登录", role: .destructive) {
showLogoutConfirm = true
}
.frame(maxWidth: .infinity)
.buttonStyle(.bordered)
}
.padding(DS.Spacing.md)
}
.background(DS.Color.brandPaper)
.alert("退出登录", isPresented: $showLogoutConfirm) {
Button("取消", role: .cancel) {}
Button("确定", role: .destructive) { viewModel.logout() }
} message: {
Text("确定要退出登录吗?退出后需要重新登录。")
}
}
}
// MARK: - 头像卡
private struct ProfileHeaderCard: View {
let profile: UserProfile
var body: some View {
HStack(spacing: DS.Spacing.md) {
ZStack(alignment: .bottomTrailing) {
Text(String(profile.displayName.prefix(1)))
.font(.title.bold())
.foregroundStyle(.white)
.frame(width: 56, height: 56)
.background(DS.Color.brandGold)
.clipShape(Circle())
Text("📷")
.font(.caption2)
.frame(width: 20, height: 20)
.background(DS.Color.brandGold)
.clipShape(Circle())
.overlay(Circle().stroke(DS.Color.brandPaper, lineWidth: 2))
}
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: DS.Spacing.xs) {
Text(profile.displayName)
.font(.title3.bold())
.foregroundStyle(DS.Color.brandDark)
Text(profile.role)
.font(.caption.bold())
.foregroundStyle(DS.Color.brandGold)
.padding(.horizontal, DS.Spacing.sm)
.padding(.vertical, 2)
.background(DS.Color.brandChip)
.clipShape(Capsule())
}
Text(profile.studentId)
.font(.caption)
.foregroundStyle(DS.Color.brandMuted)
}
Spacer()
}
.padding(DS.Spacing.md)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
// MARK: - 信息卡
private struct InfoCard: View {
let profile: UserProfile
var body: some View {
VStack(alignment: .leading, spacing: DS.Spacing.sm) {
Text("个人信息")
.font(.subheadline.bold())
.foregroundStyle(DS.Color.brandDark)
LabeledRow(label: "姓名", value: profile.name)
LabeledRow(label: "学号", value: profile.studentId)
LabeledRow(label: "手机号", value: profile.phone ?? "未绑定")
LabeledRow(label: "角色", value: profile.role)
LabeledRow(label: "小组", value: profile.group ?? "未分组")
LabeledRow(label: "登录方式", value: profile.loginType)
}
.padding(DS.Spacing.md)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
private struct ClassGroupCard: View {
let groups: [ClassGroup]
var body: some View {
VStack(alignment: .leading, spacing: DS.Spacing.sm) {
Text("小组分组")
.font(.subheadline.bold())
.foregroundStyle(DS.Color.brandDark)
ForEach(groups) { group in
LabeledRow(label: group.period, value: "\(group.group) · \(group.role)")
}
}
.padding(DS.Spacing.md)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
private struct LabeledRow: View {
let label: String
let value: String
var body: some View {
HStack {
Text(label)
.font(.subheadline)
.foregroundStyle(DS.Color.brandMuted)
Spacer()
Text(value)
.font(.subheadline)
.foregroundStyle(DS.Color.brandDark)
}
.padding(.vertical, DS.Spacing.xs)
}
}