// // AccountSelectionView.swift // suixinkan // // Created by Codex on 2026/6/20. // import SwiftUI /// 账号选择视图,展示 v9 登录返回的多个景区/门店账号并提交用户选择。 struct AccountSelectionView: View { let payload: AccountSelectionPayload let isLoading: Bool let onCancel: () -> Void let onConfirm: (AccountSwitchAccount) -> Void @State private var selectedAccountId: String? private var selectedAccount: AccountSwitchAccount? { payload.accounts.first { $0.id == selectedAccountId } } var body: some View { NavigationStack { VStack(spacing: 0) { accountList bottomBar } .background(Color(hex: 0xF5F7FB).ignoresSafeArea()) .navigationTitle("选择账号") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("取消", action: onCancel) } } .onAppear { selectedAccountId = selectedAccountId ?? payload.accounts.first?.id } } .interactiveDismissDisabled(isLoading) } @ViewBuilder private var accountList: some View { if payload.accounts.isEmpty { AppContentUnavailableView( "暂无可用账号", systemImage: "person.crop.circle.badge.exclamationmark", description: Text("当前账号没有可用的景区账号或门店账号,请联系管理员。") ) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(AppMetrics.Spacing.xLarge) } else { ScrollView { LazyVStack(spacing: AppMetrics.Spacing.small) { ForEach(payload.accounts) { account in accountCard(account) } } .padding(AppMetrics.Spacing.medium) .padding(.bottom, 90) } } } private var bottomBar: some View { VStack(spacing: 0) { Divider() Button { guard let selectedAccount else { return } onConfirm(selectedAccount) } label: { Text("进入系统") .font(.system(size: AppMetrics.FontSize.body, weight: .semibold)) .foregroundStyle(.white) .frame(maxWidth: .infinity) .frame(height: AppMetrics.ControlSize.primaryButtonHeight) .background(canConfirm ? AppDesign.primary : Color(hex: 0xC9CED6), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button)) .padding(.horizontal, AppMetrics.Spacing.medium) .padding(.vertical, AppMetrics.FontSize.subheadline) } .buttonStyle(.plain) .disabled(!canConfirm) } .background(.white) } private var canConfirm: Bool { selectedAccount != nil && !isLoading } /// 渲染单个账号卡片,并处理选中状态。 private func accountCard(_ account: AccountSwitchAccount) -> some View { let selected = selectedAccountId == account.id return Button { selectedAccountId = account.id } label: { HStack(spacing: AppMetrics.FontSize.footnote) { avatarFallback(account) .frame(width: AppMetrics.ControlSize.primaryButtonHeight, height: AppMetrics.ControlSize.primaryButtonHeight) VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) { HStack(spacing: AppMetrics.Spacing.xSmall) { Text(account.title.isEmpty ? account.accountTypeLabel : account.title) .font(.system(size: AppMetrics.FontSize.body, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) .lineLimit(1) if account.isCurrent { tag("当前", foreground: AppDesign.primary, background: AppDesign.primarySoft) } } if !account.subtitle.isEmpty { Text(account.subtitle) .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textSecondary) .lineLimit(1) } if !account.phone.isEmpty { Text(account.phone) .font(.system(size: AppMetrics.FontSize.caption)) .foregroundStyle(Color(hex: 0x9AA1AA)) } } .frame(maxWidth: .infinity, alignment: .leading) VStack(alignment: .trailing, spacing: AppMetrics.Spacing.xSmall) { tag( account.isStoreUser ? "门店" : "景区", foreground: account.isStoreUser ? Color(hex: 0x0F9F6E) : Color(hex: 0x7C3AED), background: account.isStoreUser ? Color(hex: 0xE8F8F1) : Color(hex: 0xF3ECFF) ) Image(systemName: selected ? "checkmark.circle.fill" : "circle") .font(.system(size: AppMetrics.ControlSize.passwordIcon, weight: .semibold)) .foregroundStyle(selected ? AppDesign.primary : Color(hex: 0xB6BECA)) } } .padding(AppMetrics.FontSize.subheadline) .background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button)) .overlay { RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button) .stroke(selected ? AppDesign.primary : Color(hex: 0xE6ECF4), lineWidth: selected ? 1.4 : 1) } .shadow(color: Color(hex: 0xC8D7EA, alpha: selected ? 0.24 : 0.12), radius: selected ? 10 : 6, x: 0, y: 5) } .buttonStyle(.plain) } /// 构造账号头像占位视图,按景区或门店账号展示不同标识。 private func avatarFallback(_ account: AccountSwitchAccount) -> some View { ZStack { Circle() .fill(account.isStoreUser ? Color(hex: 0xE8F8F1) : Color(hex: 0xF3ECFF)) Text(account.isStoreUser ? "店" : "景") .font(.system(size: AppMetrics.FontSize.callout, weight: .semibold)) .foregroundStyle(account.isStoreUser ? Color(hex: 0x0F9F6E) : Color(hex: 0x7C3AED)) } } /// 构造账号类型和当前账号标记使用的胶囊标签。 private func tag(_ text: String, foreground: Color, background: Color) -> some View { Text(text) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(foreground) .padding(.horizontal, 7) .frame(height: AppMetrics.Spacing.sheet) .background(background, in: RoundedRectangle(cornerRadius: AppMetrics.Spacing.xxSmall)) } } #Preview { AccountSelectionView( payload: AccountSelectionPayload( tempToken: "token", accounts: [ AccountSwitchAccount( accountType: V9ScenicUser.accountTypeValue, businessUserId: 1, title: "西湖景区", subtitle: "摄影师", phone: "13800000000", avatar: "", scenicName: "西湖景区", storeId: nil, storeName: "", scenicId: 100, isCurrent: false ), AccountSwitchAccount( accountType: V9StoreUser.accountTypeValue, businessUserId: 2, title: "东门门店", subtitle: "西湖景区 · 店长", phone: "13900000000", avatar: "", scenicName: "西湖景区", storeId: 200, storeName: "东门门店", scenicId: 100, isCurrent: true ) ] ), isLoading: false, onCancel: {}, onConfirm: { _ in } ) }