feat: 首页改为落地页并接入登录态

This commit is contained in:
AndyChen1024
2026-09-15 14:35:52 +08:00
parent 542f08c0a8
commit 2986340ad8
3 changed files with 178 additions and 47 deletions

View File

@@ -5,9 +5,22 @@ import DesignSystem
public struct HomeScreen: View {
private let viewModel: HomeViewModel
private let onNavigateToCheckin: () -> Void
private let onNavigateToPlan: () -> Void
private let onNavigateToAuth: () -> Void
public init(viewModel: HomeViewModel) {
@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 {
@@ -21,57 +34,153 @@ public struct HomeScreen: View {
switch viewModel.uiState {
case .loading:
LoadingView()
case .empty:
EmptyStateView(title: "今天还没有待打卡的习惯", systemImage: "checkmark.circle")
case .success(let data):
successContent(data)
case .error(let message):
ErrorView(message: message) { viewModel.load() }
case .success(let data, let isGuest):
successContent(data, isGuest: isGuest)
}
}
private func successContent(_ data: HomeData) -> some View {
List {
Section {
VStack(alignment: .leading, spacing: DS.Spacing.sm) {
Text(data.greeting)
.font(.largeTitle.bold())
Text(data.summary)
.font(.subheadline)
.foregroundStyle(.secondary)
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)
}
.padding(.vertical, DS.Spacing.sm)
IntroSection(intros: data.intros)
}
.padding(DS.Spacing.md)
}
.background(DS.Color.brandPaper)
.alert("登录提示", isPresented: $showLoginDialog) {
Button("取消", role: .cancel) {}
Button("去登录") { onNavigateToAuth() }
} message: {
Text("登录后使用此功能,是否前往登录?")
}
}
Section("今日打卡") {
ForEach(data.habits) { habit in
HabitRow(habit: habit)
}
}
private func handleModuleTap(_ module: HomeModule, isGuest: Bool) {
if isGuest {
showLoginDialog = true
return
}
switch module.destination {
case .checkin: onNavigateToCheckin()
case .plan: onNavigateToPlan()
case .about: break
}
}
}
private struct HabitRow: View {
let habit: Habit
// MARK: - Hero 卡
private struct HeroCard: View {
let data: HomeData
var body: some View {
HStack {
Image(systemName: habit.isCheckedInToday ? "checkmark.circle.fill" : "circle")
.foregroundStyle(habit.isCheckedInToday ? DS.Color.success : DS.Color.warning)
.font(.title3)
VStack(alignment: .leading, spacing: 2) {
Text(habit.title)
.font(.headline)
Text(habit.detail)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text("🔥 \(habit.streakDays) 天")
.font(.caption)
.foregroundStyle(.secondary)
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(.vertical, DS.Spacing.xs)
.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))
}
}