Initial commit

This commit is contained in:
2026-06-22 11:28:01 +08:00
commit ace9c94359
84 changed files with 8899 additions and 0 deletions

View 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 }
)
}