feat: 我的页对齐 android
This commit is contained in:
168
IosClientKit/Sources/FeatureProfile/ProfileScreen.swift
Normal file
168
IosClientKit/Sources/FeatureProfile/ProfileScreen.swift
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
IosClientKit/Sources/FeatureProfile/ProfileUiState.swift
Normal file
9
IosClientKit/Sources/FeatureProfile/ProfileUiState.swift
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import CoreModel
|
||||||
|
|
||||||
|
/// 我的页状态。
|
||||||
|
public enum ProfileUiState {
|
||||||
|
case loading
|
||||||
|
case success(UserProfile)
|
||||||
|
case loggedOut
|
||||||
|
case error(String)
|
||||||
|
}
|
||||||
60
IosClientKit/Sources/FeatureProfile/ProfileViewModel.swift
Normal file
60
IosClientKit/Sources/FeatureProfile/ProfileViewModel.swift
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import CoreDomain
|
||||||
|
import CoreModel
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
public final class ProfileViewModel {
|
||||||
|
public private(set) var uiState: ProfileUiState = .loading
|
||||||
|
|
||||||
|
private let profileUseCase: ProfileUseCase
|
||||||
|
private let sessionUseCase: SessionUseCase
|
||||||
|
private var loadTask: Task<Void, Never>?
|
||||||
|
private var dataTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
public init(profileUseCase: ProfileUseCase, sessionUseCase: SessionUseCase) {
|
||||||
|
self.profileUseCase = profileUseCase
|
||||||
|
self.sessionUseCase = sessionUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
public func load() {
|
||||||
|
loadTask?.cancel()
|
||||||
|
loadTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
do {
|
||||||
|
for try await isLoggedIn in self.sessionUseCase.observeIsLoggedIn() {
|
||||||
|
self.handleLoginState(isLoggedIn)
|
||||||
|
}
|
||||||
|
} catch is CancellationError {
|
||||||
|
} catch {
|
||||||
|
self.uiState = .error(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func logout() {
|
||||||
|
Task { [weak self] in
|
||||||
|
try? await self?.sessionUseCase.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleLoginState(_ isLoggedIn: Bool) {
|
||||||
|
dataTask?.cancel()
|
||||||
|
guard isLoggedIn else {
|
||||||
|
uiState = .loggedOut
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uiState = .loading
|
||||||
|
dataTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
do {
|
||||||
|
for try await profile in self.profileUseCase.observeProfile() {
|
||||||
|
self.uiState = .success(profile)
|
||||||
|
}
|
||||||
|
} catch is CancellationError {
|
||||||
|
} catch {
|
||||||
|
self.uiState = .error(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user