187 lines
5.6 KiB
Swift
187 lines
5.6 KiB
Swift
import SwiftUI
|
|
import CoreModel
|
|
import CoreUI
|
|
import DesignSystem
|
|
|
|
public struct HomeScreen: View {
|
|
private let viewModel: HomeViewModel
|
|
private let onNavigateToCheckin: () -> Void
|
|
private let onNavigateToPlan: () -> Void
|
|
private let onNavigateToAuth: () -> Void
|
|
|
|
@State private var showLoginDialog = false
|
|
|
|
public init(
|
|
viewModel: HomeViewModel,
|
|
onNavigateToCheckin: @escaping () -> Void,
|
|
onNavigateToPlan: @escaping () -> Void,
|
|
onNavigateToAuth: @escaping () -> Void
|
|
) {
|
|
self.viewModel = viewModel
|
|
self.onNavigateToCheckin = onNavigateToCheckin
|
|
self.onNavigateToPlan = onNavigateToPlan
|
|
self.onNavigateToAuth = onNavigateToAuth
|
|
}
|
|
|
|
public var body: some View {
|
|
content
|
|
.navigationTitle("首页")
|
|
.task { viewModel.load() }
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var content: some View {
|
|
switch viewModel.uiState {
|
|
case .loading:
|
|
LoadingView()
|
|
case .error(let message):
|
|
ErrorView(message: message) { viewModel.load() }
|
|
case .success(let data, let isGuest):
|
|
successContent(data, isGuest: isGuest)
|
|
}
|
|
}
|
|
|
|
private func successContent(_ data: HomeData, isGuest: Bool) -> some View {
|
|
ScrollView {
|
|
VStack(spacing: DS.Spacing.md) {
|
|
HeroCard(data: data)
|
|
ModuleGrid(modules: data.modules) { module in
|
|
handleModuleTap(module, isGuest: isGuest)
|
|
}
|
|
IntroSection(intros: data.intros)
|
|
}
|
|
.padding(DS.Spacing.md)
|
|
}
|
|
.background(DS.Color.brandPaper)
|
|
.alert("登录提示", isPresented: $showLoginDialog) {
|
|
Button("取消", role: .cancel) {}
|
|
Button("去登录") { onNavigateToAuth() }
|
|
} message: {
|
|
Text("登录后使用此功能,是否前往登录?")
|
|
}
|
|
}
|
|
|
|
private func handleModuleTap(_ module: HomeModule, isGuest: Bool) {
|
|
if isGuest {
|
|
showLoginDialog = true
|
|
return
|
|
}
|
|
switch module.destination {
|
|
case .checkin: onNavigateToCheckin()
|
|
case .plan: onNavigateToPlan()
|
|
case .about: break
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Hero 卡
|
|
|
|
private struct HeroCard: View {
|
|
let data: HomeData
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: DS.Spacing.sm) {
|
|
Text(data.greeting)
|
|
.font(.caption.bold())
|
|
.foregroundStyle(DS.Color.brandLightAmber)
|
|
.padding(.horizontal, DS.Spacing.sm)
|
|
.padding(.vertical, DS.Spacing.xs)
|
|
.background(DS.Color.brandGold.opacity(0.18))
|
|
.clipShape(Capsule())
|
|
Text(data.displayName)
|
|
.font(.title2.bold())
|
|
.foregroundStyle(.white)
|
|
Text(data.encouragement)
|
|
.font(.subheadline)
|
|
.foregroundStyle(DS.Color.brandCream)
|
|
}
|
|
.padding(DS.Spacing.lg)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(DS.Gradient.hero)
|
|
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
|
}
|
|
}
|
|
|
|
// MARK: - 模块宫格
|
|
|
|
private struct ModuleGrid: View {
|
|
let modules: [HomeModule]
|
|
let onTap: (HomeModule) -> Void
|
|
|
|
private let columns = Array(repeating: GridItem(.flexible(), spacing: DS.Spacing.sm), count: 3)
|
|
|
|
var body: some View {
|
|
LazyVGrid(columns: columns, spacing: DS.Spacing.sm) {
|
|
ForEach(modules) { module in
|
|
ModuleItem(module: module, onTap: onTap)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ModuleItem: View {
|
|
let module: HomeModule
|
|
let onTap: (HomeModule) -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: DS.Spacing.sm) {
|
|
Text(module.emoji)
|
|
.font(.title2)
|
|
.frame(width: 44, height: 44)
|
|
.background(DS.Color.brandChip)
|
|
.clipShape(Circle())
|
|
Text(module.title)
|
|
.font(.footnote.bold())
|
|
.foregroundStyle(DS.Color.brandDark)
|
|
Text(module.description)
|
|
.font(.caption2)
|
|
.foregroundStyle(DS.Color.brandMuted)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, DS.Spacing.md)
|
|
.background(Color.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { onTap(module) }
|
|
}
|
|
}
|
|
|
|
// MARK: - 产品介绍
|
|
|
|
private struct IntroSection: View {
|
|
let intros: [IntroCard]
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: DS.Spacing.sm) {
|
|
Text("产品介绍")
|
|
.font(.title3.bold())
|
|
.foregroundStyle(DS.Color.brandDark)
|
|
ForEach(intros, id: \.title) { intro in
|
|
IntroItem(intro: intro)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
private struct IntroItem: View {
|
|
let intro: IntroCard
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: DS.Spacing.xs) {
|
|
Text(intro.title)
|
|
.font(.subheadline.bold())
|
|
.foregroundStyle(DS.Color.brandGold)
|
|
Text(intro.body)
|
|
.font(.subheadline)
|
|
.foregroundStyle(DS.Color.brandBrown)
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(DS.Spacing.md)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Color.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
}
|
|
}
|