98b7600797
对标安卓正式版、对接 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>
168 lines
6.0 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|