Files
shaguabijia-app-ios/App/Features/Welfare/WelfareView.swift
T
marco 98b7600797 初始化傻瓜比价正式版 iOS 客户端
对标安卓正式版、对接 app-server(8770)的 SwiftUI 客户端。iOS 无障碍受限、无比价核心,做自成一体:美团 CPS 导购 + 福利(签到/任务/金币)+ 查看(比价记录/省钱战绩)。
功能完整 M0-M5:脚手架、短信登录(JWT+Keychain+401 自动刷新)、首页券 feed 导购、比价记录+省钱战绩、福利、我的(资料/反馈/设置/注销)。
单 target、0 三方依赖、XcodeGen 生成工程;Debug/Release 双 plist 分流 ATS。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 00:54:11 +08:00

168 lines
6.0 KiB
Swift

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