import SwiftUI /// 福利:金币余额(只进不出,无提现出口)+ 每日签到 + 任务。未登录拦截。 struct WelfareView: View { @Environment(SessionStore.self) private var session @State private var vm = WelfareViewModel() var body: some View { NavigationStack { Group { if session.isLoggedIn { content } else { LoginRequiredView(message: "登录后领取签到与任务奖励,查看省钱战绩") } } .navigationTitle("福利") } .task(id: session.isLoggedIn) { // 登录态变化(含换用户)/ 每次进入都重载;登出清空,防残留上一用户数据 if session.isLoggedIn { await vm.loadAll() } else { vm.reset() } } } @ViewBuilder private var content: some View { List { Section { coinCard } Section("每日签到") { signinSection } Section("任务") { if vm.tasks.isEmpty { Text("暂无任务").font(.subheadline).foregroundStyle(.secondary) } else { ForEach(vm.tasks) { taskRow($0) } } } } .listStyle(.insetGrouped) .refreshable { await vm.loadAll() } .overlay(alignment: .bottom) { toastView } } private var coinCard: some View { NavigationLink { CoinHistoryView() } label: { HStack(spacing: 12) { Image(systemName: "circle.hexagongrid.fill") .font(.title2) .foregroundStyle(AppColors.coin) VStack(alignment: .leading, spacing: 2) { Text("我的金币").font(.caption).foregroundStyle(.secondary) Text("\(vm.account?.coinBalance ?? 0)").font(.title3.bold()) } Spacer() Text("累计赚 \(vm.account?.totalCoinEarned ?? 0)") .font(.caption).foregroundStyle(.secondary) } } } @ViewBuilder private var signinSection: some View { VStack(spacing: 14) { if let steps = vm.signin?.steps, !steps.isEmpty { HStack(alignment: .top) { ForEach(steps, id: \.day) { step in VStack(spacing: 4) { ZStack { Circle().fill(circleColor(step.status)).frame(width: 34, height: 34) if step.status == "claimed" { Image(systemName: "checkmark").font(.caption).foregroundStyle(.white) } else { Text("\(step.coin)") .font(.caption2) .foregroundStyle(step.status == "today" ? .black : AppColors.gray500) } } Text("第\(step.day)天").font(.caption2).foregroundStyle(.secondary) } .frame(maxWidth: .infinity) } } } Button { Task { await vm.doSignin() } } label: { Text(signinButtonText) .font(.subheadline.bold()) .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(signinEnabled ? AppColors.primary : AppColors.gray200) .foregroundStyle(signinEnabled ? .black : AppColors.gray500) .clipShape(RoundedRectangle(cornerRadius: 10)) } .disabled(!signinEnabled) } .padding(.vertical, 4) } private func taskRow(_ task: TaskDTO) -> some View { HStack { VStack(alignment: .leading, spacing: 2) { Text(WelfareCatalog.taskTitle(task.taskKey)).font(.subheadline) let desc = WelfareCatalog.taskDesc(task.taskKey) if !desc.isEmpty { Text(desc).font(.caption).foregroundStyle(.secondary) } } Spacer() if task.claimed { Text("已领取").font(.caption).foregroundStyle(.secondary) } else { Button { Task { await vm.claimTask(task.taskKey) } } label: { Text("领\(task.coin)金币") .font(.caption.bold()) .padding(.horizontal, 12) .padding(.vertical, 6) .background(AppColors.primary) .foregroundStyle(.black) .clipShape(Capsule()) } .buttonStyle(.plain) } } } @ViewBuilder private var toastView: some View { if let toast = vm.toast { Text(toast) .font(.subheadline) .padding(.horizontal, 16) .padding(.vertical, 10) .background(.black.opacity(0.8)) .foregroundStyle(.white) .clipShape(Capsule()) .padding(.bottom, 30) .task(id: toast) { try? await Task.sleep(for: .seconds(2)) vm.clearToast() } } } private var signinEnabled: Bool { vm.signin?.canClaim == true && !vm.signinInProgress } private var signinButtonText: String { if vm.signin?.todaySigned == true { return "今日已签到" } if let coin = vm.signin?.todayCoin { return "签到领 \(coin) 金币" } return "签到" } private func circleColor(_ status: String) -> Color { switch status { case "claimed": return AppColors.success case "today": return AppColors.primary default: return AppColors.gray200 } } }