// // AccountSwitchView.swift // suixinkan // // Created by Codex on 2026/6/22. // import SwiftUI /// 账号切换页面,展示当前登录主体下可进入的景区账号和门店账号。 struct AccountSwitchView: View { @Environment(AppSession.self) private var appSession @Environment(AccountContext.self) private var accountContext @Environment(PermissionContext.self) private var permissionContext @Environment(AppRouter.self) private var appRouter @Environment(ProfileAPI.self) private var profileAPI @Environment(AuthAPI.self) private var authAPI @Environment(AccountContextAPI.self) private var accountContextAPI @Environment(AuthSessionCoordinator.self) private var authSessionCoordinator @Environment(ToastCenter.self) private var toastCenter @Environment(\.globalLoading) private var globalLoading @Environment(\.dismiss) private var dismiss @State private var viewModel = AccountSwitchViewModel() private var selectedAccount: AccountSwitchAccount? { viewModel.selectedAccount } var body: some View { VStack(spacing: 0) { accountList bottomBar } .background(Color(hex: 0xF5F7FB).ignoresSafeArea()) .navigationTitle("账号切换") .navigationBarTitleDisplayMode(.inline) .task { await loadAccounts() } } @ViewBuilder private var accountList: some View { if viewModel.accounts.isEmpty && !viewModel.loading { ContentUnavailableView( "暂无可切换账号", systemImage: "person.crop.circle.badge.exclamationmark", description: Text("当前登录账号下没有其他可切换账号。") ) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(24) } else { ScrollView { LazyVStack(spacing: 12) { ForEach(viewModel.accounts) { account in accountCard(account) } } .padding(16) .padding(.bottom, 92) } .refreshable { await loadAccounts(force: true) } } } private var bottomBar: some View { VStack(spacing: 0) { Divider() Button { Task { await confirmSelection() } } label: { Text("确认切换") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white) .frame(maxWidth: .infinity) .frame(height: 50) .background(canConfirm ? AppDesign.primary : Color(hex: 0xC9CED6), in: RoundedRectangle(cornerRadius: 12)) .padding(.horizontal, 16) .padding(.vertical, 14) } .buttonStyle(.plain) .disabled(!canConfirm) } .background(.white) } private var canConfirm: Bool { selectedAccount != nil && !viewModel.loading && !viewModel.switching } /// 构造单个账号卡片。 private func accountCard(_ account: AccountSwitchAccount) -> some View { let selected = viewModel.selectedAccountId == account.id return Button { viewModel.select(account) } label: { HStack(spacing: 13) { accountAvatar(account) VStack(alignment: .leading, spacing: 5) { HStack(spacing: 8) { Text(nonEmpty(account.title) ?? account.accountTypeLabel) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) .lineLimit(1) if account.isCurrent || isCurrentAccount(account) { tag("当前", foreground: AppDesign.primary, background: AppDesign.primarySoft) } } if !account.subtitle.isEmpty { Text(account.subtitle) .font(.system(size: 13)) .foregroundStyle(AppDesign.textSecondary) .lineLimit(1) } if !account.phone.isEmpty { Text(account.phone) .font(.system(size: 12)) .foregroundStyle(Color(hex: 0x9AA1AA)) } } .frame(maxWidth: .infinity, alignment: .leading) VStack(alignment: .trailing, spacing: 8) { 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: 22, weight: .semibold)) .foregroundStyle(selected ? AppDesign.primary : Color(hex: 0xB6BECA)) } } .padding(14) .background(.white, in: RoundedRectangle(cornerRadius: 12)) .overlay { RoundedRectangle(cornerRadius: 12) .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) } @ViewBuilder /// 构造账号头像,网络图加载失败时显示账号类型首字。 private func accountAvatar(_ account: AccountSwitchAccount) -> some View { RemoteImage(urlString: account.avatar) { avatarFallback(account) } .frame(width: 50, height: 50) .clipShape(Circle()) } /// 构造账号头像占位。 private func avatarFallback(_ account: AccountSwitchAccount) -> some View { ZStack { Circle() .fill(account.isStoreUser ? Color(hex: 0xE8F8F1) : Color(hex: 0xF3ECFF)) Text(account.isStoreUser ? "店" : "景") .font(.system(size: 17, 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: 22) .background(background, in: RoundedRectangle(cornerRadius: 4)) } /// 拉取账号列表。 private func loadAccounts(force: Bool = false) async { do { try await globalLoading.withOptionalLoading(!force && viewModel.accounts.isEmpty, message: "加载中...") { try await viewModel.load(api: profileAPI, force: force, currentAccountId: currentAccountId) } } catch is CancellationError { return } catch { toastCenter.show(error.localizedDescription) } } /// 确认账号切换,并刷新全局登录上下文。 private func confirmSelection() async { guard let account = selectedAccount else { return } if isCurrentAccount(account) { dismiss() return } do { try await globalLoading.withLoading(message: "账号切换中...") { let response = try await viewModel.switchAccount(account, api: authAPI) let username = nonEmpty(accountContext.profile?.phone) ?? nonEmpty(account.phone) ?? "" try await authSessionCoordinator.completeLogin( with: response, username: username, privacyAgreementAccepted: authSessionCoordinator.loginPreferences().privacyAgreementAccepted, appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, profileAPI: profileAPI, accountContextAPI: accountContextAPI ) } appRouter.reset() toastCenter.show("账号已切换") dismiss() } catch is CancellationError { return } catch { toastCenter.show(error.localizedDescription) } } private var currentAccountId: String? { guard let current = accountContext.profile else { return nil } if let store = accountContext.currentStore { return "\(V9StoreUser.accountTypeValue)_\(store.id)" } if let scenic = accountContext.currentScenic { return "\(V9ScenicUser.accountTypeValue)_\(scenic.id)" } return current.userId.isEmpty ? nil : current.userId } /// 判断账号是否为当前已选账号。 private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool { account.isCurrent || viewModel.isCurrent(account, currentAccountId: currentAccountId) } /// 返回去空白后的非空文本。 private func nonEmpty(_ value: String?) -> String? { let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return text.isEmpty ? nil : text } }