初始提交
This commit is contained in:
636
suixinkan/Features/Profile/Views/ProfileView.swift
Normal file
636
suixinkan/Features/Profile/Views/ProfileView.swift
Normal file
@ -0,0 +1,636 @@
|
||||
//
|
||||
// ProfileView.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 个人信息页视图,展示用户资料、账号状态、实名认证状态和退出登录入口。
|
||||
struct ProfileView: View {
|
||||
@Environment(AppSession.self) private var appSession
|
||||
@Environment(AccountContext.self) private var accountContext
|
||||
@Environment(PermissionContext.self) private var permissionContext
|
||||
@Environment(ScenicSpotContext.self) private var scenicSpotContext
|
||||
@Environment(AppRouter.self) private var appRouter
|
||||
@Environment(ProfileAPI.self) private var profileAPI
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@Environment(AuthSessionCoordinator.self) private var authSessionCoordinator
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@State private var viewModel = ProfileViewModel()
|
||||
@State private var showsPasswordSheet = false
|
||||
@State private var showsLogoutConfirm = false
|
||||
@FocusState private var nicknameFocused: Bool
|
||||
|
||||
private var contentMaxWidth: CGFloat {
|
||||
horizontalSizeClass == .regular ? 720 : .infinity
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 22) {
|
||||
header
|
||||
infoCard
|
||||
logoutButton
|
||||
}
|
||||
.padding(.horizontal, 19)
|
||||
.padding(.top, 24)
|
||||
.padding(.bottom, 24)
|
||||
.frame(maxWidth: contentMaxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(profileBackground.ignoresSafeArea())
|
||||
.navigationTitle("个人信息")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
await reloadProfile(showToast: false)
|
||||
}
|
||||
.refreshable {
|
||||
await reloadProfile(showToast: true)
|
||||
}
|
||||
.sheet(isPresented: $showsPasswordSheet) {
|
||||
PasswordUpdateSheet { password in
|
||||
await updatePassword(password)
|
||||
}
|
||||
}
|
||||
.confirmationDialog("确认退出当前账号?", isPresented: $showsLogoutConfirm, titleVisibility: .visible) {
|
||||
Button("退出登录", role: .destructive) {
|
||||
authSessionCoordinator.logout(
|
||||
appSession: appSession,
|
||||
accountContext: accountContext,
|
||||
permissionContext: permissionContext,
|
||||
scenicSpotContext: scenicSpotContext,
|
||||
appRouter: appRouter,
|
||||
toastCenter: toastCenter
|
||||
)
|
||||
}
|
||||
Button("取消", role: .cancel) {}
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.isLoading && viewModel.userInfo == nil {
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.white.opacity(0.35))
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.isEditingProfile) { _, isEditing in
|
||||
guard isEditing else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||
nicknameFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var profileBackground: some View {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(hex: 0xF7FAFF),
|
||||
Color(hex: 0xF3F7FD),
|
||||
Color.white
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(alignment: .center, spacing: 14) {
|
||||
avatarImage
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if viewModel.isEditingProfile {
|
||||
TextField("请输入昵称", text: nicknameBinding)
|
||||
.font(.system(size: 24, weight: .semibold))
|
||||
.foregroundStyle(Color(hex: 0x252525))
|
||||
.tint(AppDesign.primary)
|
||||
.textFieldStyle(.plain)
|
||||
.focused($nicknameFocused)
|
||||
.submitLabel(.done)
|
||||
.padding(.horizontal, 10)
|
||||
.frame(height: 42)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 8))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(AppDesign.primary, lineWidth: 1)
|
||||
}
|
||||
.onSubmit {
|
||||
Task { await saveProfileEdits() }
|
||||
}
|
||||
} else {
|
||||
Text(viewModel.displayNickname)
|
||||
.font(.system(size: 24, weight: .semibold))
|
||||
.foregroundStyle(Color(hex: 0x252525))
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Text("UID: \(accountContext.profile?.userId.isEmpty == false ? accountContext.profile?.userId ?? "--" : "--")")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(Color(hex: 0x606A7A))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.layoutPriority(1)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button {
|
||||
if viewModel.isEditingProfile {
|
||||
Task { await saveProfileEdits() }
|
||||
} else {
|
||||
viewModel.beginEditing()
|
||||
}
|
||||
} label: {
|
||||
ZStack {
|
||||
if viewModel.isSaving && viewModel.isEditingProfile {
|
||||
ProgressView()
|
||||
.tint(.white)
|
||||
.scaleEffect(0.82)
|
||||
} else {
|
||||
Image(systemName: viewModel.isEditingProfile ? "checkmark" : "pencil")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [Color(hex: 0x238BFF), Color(hex: 0x006BFF)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
in: Circle()
|
||||
)
|
||||
.shadow(color: Color(hex: 0x0073FF, alpha: 0.24), radius: 10, x: 0, y: 5)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(viewModel.isSaving)
|
||||
.opacity(viewModel.isSaving ? 0.6 : 1)
|
||||
.accessibilityLabel(viewModel.isEditingProfile ? "完成编辑" : "编辑个人信息")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 21)
|
||||
.frame(minHeight: 132)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 17, style: .continuous)
|
||||
.fill(.white)
|
||||
ProfileHeaderWaveShape()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(hex: 0xBFDFFF, alpha: 0.18),
|
||||
Color(hex: 0x8EC4FF, alpha: 0.28)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
ProfileHeaderWaveShape(phase: 0.35)
|
||||
.fill(Color(hex: 0xE4F1FF, alpha: 0.62))
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 17, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 17, style: .continuous)
|
||||
.stroke(Color(hex: 0xE4ECF7), lineWidth: 1)
|
||||
}
|
||||
.shadow(color: Color(hex: 0xC8D7EA, alpha: 0.32), radius: 16, x: 0, y: 8)
|
||||
}
|
||||
|
||||
private var avatarImage: some View {
|
||||
Button {
|
||||
toastCenter.show("头像上传待接入阿里云 OSS")
|
||||
} label: {
|
||||
AsyncAvatarImage(urlString: viewModel.displayAvatarURL)
|
||||
.frame(width: 86, height: 86)
|
||||
.clipShape(Circle())
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(.white, lineWidth: 3)
|
||||
}
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
Image(systemName: "camera.fill")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 26, height: 26)
|
||||
.background(AppDesign.primary, in: Circle())
|
||||
.overlay {
|
||||
Circle().stroke(.white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("头像")
|
||||
}
|
||||
|
||||
private var infoCard: some View {
|
||||
VStack(spacing: 0) {
|
||||
infoRow(title: "姓名") {
|
||||
plainInfoValue(viewModel.displayRealName)
|
||||
}
|
||||
|
||||
divider
|
||||
|
||||
Button {
|
||||
toastCenter.show("账号切换待接入")
|
||||
} label: {
|
||||
infoRow(title: "当前账号") {
|
||||
VStack(alignment: .trailing, spacing: 3) {
|
||||
rowValueText(currentAccountDisplayName)
|
||||
Text(currentAccountTypeText)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
rowChevron
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
divider
|
||||
|
||||
infoRow(title: "手机号") {
|
||||
plainInfoValue(viewModel.displayPhone)
|
||||
}
|
||||
|
||||
divider
|
||||
|
||||
Button {
|
||||
showsPasswordSheet = true
|
||||
} label: {
|
||||
infoRow(title: "修改密码") {
|
||||
rowChevron
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
divider
|
||||
|
||||
Button {
|
||||
toastCenter.show("实名认证页面待接入")
|
||||
} label: {
|
||||
infoRow(title: "认证状态") {
|
||||
realNameStatusView
|
||||
rowChevron
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
divider
|
||||
|
||||
infoRow(title: "账号状态") {
|
||||
accountStatusView
|
||||
}
|
||||
|
||||
divider
|
||||
|
||||
infoRow(title: "当前景区") {
|
||||
scenicStatusView
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 4)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 17, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 17, style: .continuous)
|
||||
.stroke(Color(hex: 0xEEF3FA), lineWidth: 1)
|
||||
}
|
||||
.shadow(color: Color(hex: 0xC8D7EA, alpha: 0.30), radius: 16, x: 0, y: 8)
|
||||
}
|
||||
|
||||
private var divider: some View {
|
||||
Rectangle()
|
||||
.fill(Color(hex: 0xE8EDF4))
|
||||
.frame(height: 1)
|
||||
}
|
||||
|
||||
private var logoutButton: some View {
|
||||
Button {
|
||||
showsLogoutConfirm = true
|
||||
} label: {
|
||||
Text("退出登录")
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundStyle(Color(hex: 0xFF3B3B))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 58)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 17, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 17, style: .continuous)
|
||||
.stroke(Color(hex: 0xEEF3FA), lineWidth: 1)
|
||||
}
|
||||
.shadow(color: Color(hex: 0xC8D7EA, alpha: 0.24), radius: 14, x: 0, y: 7)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
/// 构造资料信息行,统一左右布局和点击区域。
|
||||
private func infoRow<Content: View>(title: String, @ViewBuilder content: () -> Content) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Text(title)
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundStyle(Color(hex: 0x101827))
|
||||
.frame(width: 104, alignment: .leading)
|
||||
Spacer(minLength: 4)
|
||||
content()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(height: 69)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
/// 根据文本是否为空占位,选择普通文本或状态徽标展示。
|
||||
private func plainInfoValue(_ text: String) -> some View {
|
||||
if text == "--" {
|
||||
statusBadge(text: text, foreground: Color(hex: 0x606A7A), background: Color(hex: 0xF0F2F6))
|
||||
} else {
|
||||
rowValueText(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造资料行右侧的普通值文本。
|
||||
private func rowValueText(_ text: String) -> some View {
|
||||
Text(text)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundStyle(Color(hex: 0x333333))
|
||||
.lineLimit(1)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
private var rowChevron: some View {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 22, weight: .medium))
|
||||
.foregroundStyle(Color(hex: 0x9AA1AA))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var realNameStatusView: some View {
|
||||
if viewModel.realNameInfo == nil {
|
||||
Text(viewModel.realNameStatusText)
|
||||
.font(.system(size: 17, weight: .medium))
|
||||
.foregroundStyle(Color(hex: 0xFF7B00))
|
||||
.lineLimit(1)
|
||||
} else {
|
||||
statusBadge(
|
||||
text: viewModel.realNameStatusText,
|
||||
icon: viewModel.realNameInfo?.auditStatus == 2 ? "checkmark.shield.fill" : nil,
|
||||
foreground: realNameStatusColor,
|
||||
background: realNameStatusBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var accountStatusView: some View {
|
||||
let text = viewModel.accountStatusText
|
||||
return Group {
|
||||
if text == "--" {
|
||||
statusBadge(text: text, foreground: Color(hex: 0x606A7A), background: Color(hex: 0xF0F2F6))
|
||||
} else {
|
||||
statusBadge(text: text, icon: "checkmark.circle.fill", foreground: Color(hex: 0x14964A), background: Color(hex: 0xDDF8E9))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var scenicStatusView: some View {
|
||||
let text = nonEmpty(accountContext.currentScenic?.name) ?? "--"
|
||||
return Group {
|
||||
if text == "--" {
|
||||
statusBadge(text: text, foreground: Color(hex: 0x606A7A), background: Color(hex: 0xF0F2F6))
|
||||
} else {
|
||||
statusBadge(text: text, icon: "mappin.circle.fill", foreground: AppDesign.primary, background: Color(hex: 0xE7F1FF))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造状态徽标,支持可选图标、前景色和背景色。
|
||||
private func statusBadge(text: String, icon: String? = nil, foreground: Color, background: Color) -> some View {
|
||||
HStack(spacing: 7) {
|
||||
if let icon {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
}
|
||||
Text(text)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundStyle(foreground)
|
||||
.padding(.horizontal, icon == nil ? 14 : 12)
|
||||
.frame(height: 34)
|
||||
.background(background, in: Capsule())
|
||||
}
|
||||
|
||||
private var realNameStatusColor: Color {
|
||||
guard let realNameInfo = viewModel.realNameInfo else { return Color(hex: 0xFF7B00) }
|
||||
switch realNameInfo.auditStatus {
|
||||
case 2:
|
||||
return Color(hex: 0x14964A)
|
||||
case 3:
|
||||
return Color(hex: 0xEF4444)
|
||||
default:
|
||||
return Color(hex: 0xFF7B00)
|
||||
}
|
||||
}
|
||||
|
||||
private var realNameStatusBackground: Color {
|
||||
guard let realNameInfo = viewModel.realNameInfo else { return Color(hex: 0xF4F4F4) }
|
||||
switch realNameInfo.auditStatus {
|
||||
case 2:
|
||||
return Color(hex: 0xDDF8E9)
|
||||
case 3:
|
||||
return Color(hex: 0xFFE7E7)
|
||||
default:
|
||||
return Color(hex: 0xFFF0E2)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentAccountDisplayName: String {
|
||||
nonEmpty(accountContext.currentStore?.name)
|
||||
?? nonEmpty(accountContext.currentScenic?.name)
|
||||
?? accountContext.profile?.displayName
|
||||
?? viewModel.displayNickname
|
||||
}
|
||||
|
||||
private var currentAccountTypeText: String {
|
||||
if accountContext.currentStore != nil {
|
||||
return "门店账号"
|
||||
}
|
||||
if accountContext.currentScenic != nil {
|
||||
return "景区账号"
|
||||
}
|
||||
return nonEmpty(viewModel.userInfo?.roleName) ?? "账号"
|
||||
}
|
||||
|
||||
private var nicknameBinding: Binding<String> {
|
||||
Binding(
|
||||
get: { viewModel.editingNickname },
|
||||
set: { viewModel.editingNickname = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
/// 重新拉取个人资料,并同步更新全局账号资料。
|
||||
private func reloadProfile(showToast: Bool) async {
|
||||
do {
|
||||
try await viewModel.reload(api: profileAPI)
|
||||
if let userInfo = viewModel.userInfo {
|
||||
authSessionCoordinator.refreshCachedProfile(from: userInfo, accountContext: accountContext)
|
||||
} else {
|
||||
accountContext.replaceProfile(viewModel.accountProfileFallback(accountContext.profile))
|
||||
}
|
||||
if showToast {
|
||||
toastCenter.show("个人信息已刷新")
|
||||
}
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存昵称编辑内容,并在成功后刷新全局账号展示。
|
||||
private func saveProfileEdits() async {
|
||||
do {
|
||||
try await viewModel.saveProfile(api: profileAPI)
|
||||
if let userInfo = viewModel.userInfo {
|
||||
authSessionCoordinator.refreshCachedProfile(from: userInfo, accountContext: accountContext)
|
||||
} else {
|
||||
accountContext.replaceProfile(viewModel.accountProfileFallback(accountContext.profile))
|
||||
}
|
||||
toastCenter.show("个人信息已保存")
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交新密码,并在成功后关闭密码弹窗。
|
||||
private func updatePassword(_ password: String) async {
|
||||
do {
|
||||
try await viewModel.updatePassword(password, api: profileAPI)
|
||||
showsPasswordSheet = false
|
||||
toastCenter.show("密码修改成功")
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 去除空白字符后返回非空字符串。
|
||||
private func nonEmpty(_ value: String?) -> String? {
|
||||
let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步头像视图,负责加载网络头像并在失败或空地址时展示占位图。
|
||||
private struct AsyncAvatarImage: View {
|
||||
let urlString: String
|
||||
|
||||
var body: some View {
|
||||
if let url = URL(string: urlString), !urlString.isEmpty {
|
||||
AsyncImage(url: url) { phase in
|
||||
switch phase {
|
||||
case let .success(image):
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
case .failure, .empty:
|
||||
placeholder
|
||||
@unknown default:
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
} else {
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
|
||||
private var placeholder: some View {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 44, weight: .semibold))
|
||||
.foregroundStyle(AppDesign.primary)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(AppDesign.primarySoft)
|
||||
}
|
||||
}
|
||||
|
||||
/// 个人信息头部背景波浪形状,用于还原旧版蓝白卡片视觉。
|
||||
private struct ProfileHeaderWaveShape: Shape {
|
||||
var phase: CGFloat = 0
|
||||
|
||||
/// 根据当前矩形尺寸生成波浪路径。
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
let startY = rect.height * (0.94 - phase * 0.16)
|
||||
path.move(to: CGPoint(x: rect.width * (0.26 + phase * 0.16), y: rect.height))
|
||||
path.addCurve(
|
||||
to: CGPoint(x: rect.width, y: rect.height * (0.26 + phase * 0.08)),
|
||||
control1: CGPoint(x: rect.width * (0.55 + phase * 0.06), y: startY),
|
||||
control2: CGPoint(x: rect.width * (0.70 + phase * 0.08), y: rect.height * (0.40 + phase * 0.14))
|
||||
)
|
||||
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改密码弹窗,负责采集新密码并提交给外部动作。
|
||||
private struct PasswordUpdateSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var password = ""
|
||||
let onSubmit: (String) async -> Void
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SecureField("请输入新密码", text: $password)
|
||||
.textContentType(.newPassword)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 48)
|
||||
.background(Color(hex: 0xF5F7FA), in: RoundedRectangle(cornerRadius: 8))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color(hex: 0xE2E8F0), lineWidth: 1)
|
||||
}
|
||||
|
||||
Text("密码至少 6 位。")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(16)
|
||||
.navigationTitle("修改密码")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("取消") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("完成") {
|
||||
Task { await onSubmit(password) }
|
||||
}
|
||||
.disabled(password.trimmingCharacters(in: .whitespacesAndNewlines).count < 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.height(220)])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
ProfileView()
|
||||
.environment(AppSession())
|
||||
.environment(AccountContext())
|
||||
.environment(PermissionContext())
|
||||
.environment(ScenicSpotContext())
|
||||
.environment(AppRouter())
|
||||
.environment(ToastCenter())
|
||||
.environment(ProfileAPI(client: APIClient()))
|
||||
.environment(AuthSessionCoordinator())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user