Files
suixinkan_ios_new/suixinkan/Features/Tasks/Views/TaskDetailView.swift
汉秋 ffb16eca29 新增任务模块,并接入首页任务管理路由
实现任务列表、详情与创建页面,对接 API,支持云端/本地附件,并添加单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:26:13 +08:00

187 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TaskDetailView.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import SwiftUI
///
struct TaskDetailView: View {
@Environment(TaskAPI.self) private var taskAPI
@Environment(ToastCenter.self) private var toastCenter
@Environment(\.globalLoading) private var globalLoading
let taskId: Int
let summary: PhotographerTaskItem?
@State private var viewModel = TaskDetailViewModel()
var body: some View {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
headerSection
orderSection
resultSection
}
.padding(AppMetrics.Spacing.pageHorizontal)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("任务详情")
.navigationBarTitleDisplayMode(.inline)
.task {
await globalLoading.withOptionalLoading(viewModel.detail == nil) {
await viewModel.load(api: taskAPI, taskId: taskId)
}
if let message = viewModel.errorMessage {
toastCenter.show(message)
}
}
}
///
private var headerSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
HStack(alignment: .top) {
Text(displayName)
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
.lineLimit(2)
Spacer()
Text(displayStatus)
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.padding(.horizontal, AppMetrics.Spacing.small)
.padding(.vertical, AppMetrics.Spacing.xxSmall)
.background(AppDesign.primarySoft, in: Capsule())
}
TaskInfoRow(title: "任务备注", value: detail?.photogRemark ?? summary?.photogRemark ?? "暂无")
TaskInfoRow(title: "创建时间", value: detail?.createdAt ?? summary?.createdAt ?? "暂无")
TaskInfoRow(title: "紧急小时", value: detail.map { "\($0.urgentHour) 小时" } ?? "暂无")
TaskInfoRow(title: "编辑人员", value: detail?.editorName.nonEmpty ?? summary?.editorName.nonEmpty ?? "暂无")
TaskInfoRow(title: "编辑电话", value: detail?.editorPhone.nonEmpty ?? "暂无")
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
///
private var orderSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("关联订单")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
TaskInfoRow(title: "项目名称", value: detail?.order?.projectName.nonEmpty ?? summary?.orderUserNickname.nonEmpty ?? "暂无")
TaskInfoRow(title: "订单号", value: detail?.order?.orderNumber.nonEmpty ?? summary?.orderNumber.nonEmpty ?? "暂无")
TaskInfoRow(title: "手机号", value: detail?.order?.userPhone.nonEmpty ?? summary?.orderUserPhone.nonEmpty ?? "暂无")
TaskInfoRow(title: "支付时间", value: detail?.order?.payTime.nonEmpty ?? "暂无")
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
///
private var resultSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("任务结果")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
if mediaItems.isEmpty {
Text("暂无任务结果文件")
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, AppMetrics.Spacing.small)
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 92), spacing: AppMetrics.Spacing.small)], spacing: AppMetrics.Spacing.small) {
ForEach(mediaItems) { media in
TaskMediaThumbView(media: media)
}
}
}
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
///
private var detail: TaskDetailResponse? {
viewModel.detail
}
///
private var displayName: String {
detail?.name.nonEmpty ?? summary?.name.nonEmpty ?? "未命名任务"
}
///
private var displayStatus: String {
detail?.taskStatusLabel.nonEmpty ?? summary?.statusName.nonEmpty ?? "未知"
}
///
private var mediaItems: [TaskMediaItem] {
if let detail {
return detail.taskResult
}
return summary?.media ?? []
}
}
///
private struct TaskInfoRow: View {
let title: String
let value: String
var body: some View {
HStack(alignment: .top, spacing: AppMetrics.Spacing.medium) {
Text(title)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
.frame(width: 72, alignment: .leading)
Text(value.isEmpty ? "暂无" : value)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
/// 使 Kingfisher
private struct TaskMediaThumbView: View {
let media: TaskMediaItem
var body: some View {
ZStack(alignment: .bottomLeading) {
RemoteImage(urlString: media.displayURL) {
Image(systemName: media.isVideo ? "video.fill" : "photo")
.font(.system(size: 28, weight: .semibold))
.foregroundStyle(AppDesign.placeholder)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(hex: 0xEEF2F7))
}
.frame(height: 92)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
if media.isVideo {
Image(systemName: "play.circle.fill")
.font(.system(size: 20, weight: .semibold))
.foregroundStyle(.white)
.padding(AppMetrics.Spacing.xSmall)
}
}
}
}
private extension String {
///
var nonEmpty: String? {
let text = trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
}