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