Files
suixinkan_ios_new/suixinkan/Features/Tasks/Views/TaskDetailView.swift
汉秋 703078352c 从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +08:00

187 lines
7.3 KiB
Swift
Raw Permalink 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) private var taskAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
let taskId: Int
let summary: PhotographerTaskItem?
@StateObject 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
}
}