feat: 计划页对齐 android 并新增提交页

This commit is contained in:
AndyChen1024
2026-09-15 14:35:52 +08:00
parent 3e90f8edee
commit 4769aef59e
5 changed files with 564 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
import SwiftUI
import CoreModel
import CoreUI
import DesignSystem
public struct PlanScreen: View {
private let viewModel: PlanViewModel
private let onNavigateToAuth: () -> Void
private let makePlanSubmitViewModel: (PlanType, String) -> PlanSubmitViewModel
@State private var submitRoute: PlanSubmitRoute?
@State private var selectedGroup: PlanGroup?
public init(
viewModel: PlanViewModel,
onNavigateToAuth: @escaping () -> Void,
makePlanSubmitViewModel: @escaping (PlanType, String) -> PlanSubmitViewModel
) {
self.viewModel = viewModel
self.onNavigateToAuth = onNavigateToAuth
self.makePlanSubmitViewModel = makePlanSubmitViewModel
}
public var body: some View {
content
.navigationTitle("计划")
.task { viewModel.load() }
.navigationDestination(item: $submitRoute) { route in
PlanSubmitScreen(
viewModel: makePlanSubmitViewModel(route.type, route.periodKey),
type: route.type,
periodKey: route.periodKey
)
}
.sheet(item: $selectedGroup) { group in
GroupDetailSheet(group: group)
}
}
@ViewBuilder
private var content: some View {
switch viewModel.uiState {
case .loading:
LoadingView()
case .loggedOut:
LoggedOutView(onNavigateToAuth: onNavigateToAuth)
case .error(let message):
ErrorView(message: message) { viewModel.load() }
case .success(let record):
successContent(record)
}
}
private func successContent(_ record: PlanRecord) -> some View {
ScrollView {
VStack(spacing: DS.Spacing.md) {
Picker("类型", selection: Binding(get: { viewModel.selectedType }, set: { viewModel.selectType($0) })) {
ForEach(PlanType.allCases, id: \.self) { type in
Text(type.label).tag(type)
}
}
.pickerStyle(.segmented)
.labelsHidden()
PlanHead(record: record)
MyPlanStatusCard(record: record) {
submitRoute = PlanSubmitRoute(type: record.type, periodKey: record.periodKey)
}
if record.myChecked, let myRecord = record.myRecord {
UploadedPlanCard(record: myRecord)
}
if let message = record.message {
WindowMessage(message: message)
}
if record.groups.isEmpty {
EmptyStateView(title: "暂无小组", systemImage: "person.2")
} else {
Text("小组打卡")
.font(.subheadline.bold())
.foregroundStyle(DS.Color.brandDark)
.frame(maxWidth: .infinity, alignment: .leading)
ForEach(record.groups) { group in
GroupCard(group: group) { selectedGroup = group }
}
}
if record.unassignedCount > 0 {
Text("另有未分组 \(record.unassignedCount) 人")
.font(.caption)
.foregroundStyle(DS.Color.faintText)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(DS.Spacing.md)
}
.background(DS.Color.brandPaper)
}
}
private struct PlanSubmitRoute: Hashable {
let type: PlanType
let periodKey: String
}
// MARK: - 头部 / 状态卡
private struct PlanHead: View {
let record: PlanRecord
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(record.type.label)
.font(.title3.bold())
.foregroundStyle(DS.Color.brandDark)
Text(record.periodKey)
.font(.caption)
.foregroundStyle(DS.Color.brandMuted)
}
Spacer()
Text(record.myChecked ? "已提交" : "待提交")
.font(.caption)
.foregroundStyle(record.myChecked ? DS.Color.statusNormal : DS.Color.brandGold)
.padding(.horizontal, DS.Spacing.sm)
.padding(.vertical, 4)
.background(record.myChecked ? DS.Color.fillDone : DS.Color.fillUndone)
.clipShape(Capsule())
}
}
}
private struct MyPlanStatusCard: View {
let record: PlanRecord
let onSubmit: () -> Void
var body: some View {
HStack(spacing: DS.Spacing.md) {
Text(record.myChecked ? "✓" : "●")
.font(.headline)
.foregroundStyle(.white)
.frame(width: 44, height: 44)
.background(
record.myChecked
? AnyShapeStyle(LinearGradient(colors: [DS.Color.statusNormal, DS.Color.statusNormalBright], startPoint: .top, endPoint: .bottom))
: AnyShapeStyle(LinearGradient(colors: [DS.Color.danger, DS.Color.dangerBright], startPoint: .top, endPoint: .bottom))
)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(record.myChecked ? "已完成" : "未完成")
.font(.subheadline.bold())
.foregroundStyle(record.myChecked ? DS.Color.statusNormal : DS.Color.danger)
Text("我的\(record.type.label)")
.font(.caption)
.foregroundStyle(DS.Color.brandMuted)
}
Spacer()
Button(record.myChecked ? "修改" : "新增打卡", action: onSubmit)
.buttonStyle(.bordered)
.disabled(!record.canSubmit)
}
.padding(DS.Spacing.md)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
private struct UploadedPlanCard: View {
let record: MyPlanRecord
var body: some View {
VStack(alignment: .leading, spacing: DS.Spacing.sm) {
HStack {
Text("我上传的计划")
.font(.subheadline.bold())
.foregroundStyle(DS.Color.brandDark)
Spacer()
Text(record.status)
.font(.caption)
.foregroundStyle(DS.Color.brandGold)
.padding(.horizontal, DS.Spacing.sm)
.padding(.vertical, 2)
.background(DS.Color.fillUndone)
.clipShape(Capsule())
}
Text("\(record.submittedTime) 提交")
.font(.caption)
.foregroundStyle(DS.Color.brandMuted)
Text(record.content)
.font(.subheadline)
.foregroundStyle(DS.Color.brandBrown)
if !record.attachments.isEmpty {
HStack(spacing: DS.Spacing.sm) {
ForEach(record.attachments, id: \.self) { _ in
Text("🖼️")
.frame(width: 64, height: 64)
.background(DS.Color.brandChip)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}
}
}
.padding(DS.Spacing.md)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
private struct WindowMessage: View {
let message: String
var body: some View {
HStack(spacing: DS.Spacing.sm) {
Text("⏰")
Text(message)
.font(.caption)
.foregroundStyle(DS.Color.danger)
}
.padding(DS.Spacing.sm)
.frame(maxWidth: .infinity, alignment: .leading)
.background(DS.Color.fillUndone)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
// MARK: - 小组卡片 / 明细
private struct GroupCard: View {
let group: PlanGroup
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
HStack(spacing: DS.Spacing.sm) {
Text(String(group.name.prefix(1)))
.font(.subheadline.bold())
.foregroundStyle(DS.Color.brandGold)
.frame(width: 40, height: 40)
.background(DS.Gradient.groupTile)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
VStack(alignment: .leading, spacing: DS.Spacing.xs) {
Text(group.name)
.font(.subheadline.bold())
.foregroundStyle(DS.Color.brandDark)
ProgressView(value: Double(group.checkedCount), total: Double(max(group.memberCount, 1)))
.tint(DS.Color.brandGold)
}
Spacer()
Text("已打卡 \(group.checkedCount)")
.font(.caption2)
.foregroundStyle(DS.Color.statusNormal)
Text("\(group.memberCount)人")
.font(.caption2)
.foregroundStyle(DS.Color.brandGold)
Text("›")
.font(.headline)
.foregroundStyle(DS.Color.faintText)
}
.padding(DS.Spacing.md)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.buttonStyle(.plain)
}
}
private struct GroupDetailSheet: View {
let group: PlanGroup
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
List(group.members) { member in
HStack(spacing: DS.Spacing.sm) {
Text(String(member.name.prefix(1)))
.font(.subheadline.bold())
.foregroundStyle(member.filled ? DS.Color.statusNormal : DS.Color.brandGold)
.frame(width: 30, height: 30)
.background(member.filled ? DS.Color.fillDone : DS.Color.brandChip)
.clipShape(Circle())
Text(member.name)
.font(.subheadline)
.foregroundStyle(DS.Color.brandDark)
Spacer()
Text(member.filled ? "已完成" : "未完成")
.font(.caption)
.foregroundStyle(member.filled ? DS.Color.statusNormal : DS.Color.danger)
}
}
.navigationTitle(group.name)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("关闭") { dismiss() }
}
}
}
.presentationDetents([.medium])
}
}