初始提交
This commit is contained in:
216
suixinkan/Features/Auth/Views/AccountSelectionView.swift
Normal file
216
suixinkan/Features/Auth/Views/AccountSelectionView.swift
Normal file
@ -0,0 +1,216 @@
|
||||
//
|
||||
// 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 {
|
||||
ContentUnavailableView(
|
||||
"暂无可用账号",
|
||||
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: {
|
||||
HStack(spacing: AppMetrics.Spacing.xSmall) {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.tint(.white)
|
||||
}
|
||||
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 }
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user