Files
suixinkan_ios_new/suixinkan/Features/Assets/Views/MediaLibraryViews.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

709 lines
29 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.

//
// MediaLibraryViews.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import PhotosUI
import SwiftUI
///
struct MediaLibraryView: View {
let kind: MediaLibraryKind
@Environment(\.assetsAPI) private var assetsAPI
@EnvironmentObject private var router: RouterPath
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
@StateObject private var viewModel: MediaLibraryViewModel
@State private var selectedItem: MediaLibraryItem?
///
init(kind: MediaLibraryKind = .material) {
self.kind = kind
_viewModel = StateObject(wrappedValue: MediaLibraryViewModel(kind: kind))
}
var body: some View {
ScrollView {
LazyVStack(spacing: AppMetrics.Spacing.medium) {
filterSection
statsSection
contentSection
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle(kind.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
router.navigate(to: .home(kind == .sample ? .sampleUpload : .materialUpload))
} label: {
Image(systemName: "plus")
}
.accessibilityLabel(kind.uploadTitle)
}
}
.refreshable { await reload(showLoading: false) }
.task {
guard viewModel.items.isEmpty else { return }
await reload(showLoading: true)
}
.appNavigationDestination(item: $selectedItem) { item in
MediaLibraryDetailView(item: item, listViewModel: viewModel)
}
}
///
private var filterSection: some View {
VStack(spacing: AppMetrics.Spacing.small) {
HStack(spacing: AppMetrics.Spacing.small) {
Image(systemName: "magnifyingglass")
.foregroundStyle(AppDesign.placeholder)
TextField(kind.searchPlaceholder, text: $viewModel.keyword)
.submitLabel(.search)
.onSubmit { Task { await reload(showLoading: true) } }
Button("搜索") {
Task { await reload(showLoading: true) }
}
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
.appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: AppMetrics.ControlSize.inputHeight)
Picker("审核状态", selection: $viewModel.selectedAuditFilter) {
ForEach(MediaLibraryAuditFilter.allCases) { filter in
Text(filter.title).tag(filter)
}
}
.pickerStyle(.segmented)
.onChange(of: viewModel.selectedAuditFilter) { _ in
Task { await reload(showLoading: true) }
}
}
}
///
@ViewBuilder
private var statsSection: some View {
if let info = viewModel.orderInfo {
HStack(spacing: AppMetrics.Spacing.small) {
MediaStatCard(title: "订单数", value: "\(info.totalNum)")
MediaStatCard(title: "均价", value: String(format: "%.2f", info.avgOrderAmount))
MediaStatCard(title: "退款", value: String(format: "%.2f", info.refundTotal))
}
}
}
///
@ViewBuilder
private var contentSection: some View {
if viewModel.items.isEmpty {
AssetsEmptyState(title: kind.emptyTitle, message: viewModel.errorMessage ?? "当前筛选条件下没有\(kind == .sample ? "样片" : "素材")")
} else {
ForEach(viewModel.items) { item in
MediaLibraryCard(item: item) {
selectedItem = item
} toggleAction: {
Task { await toggleListing(item) }
}
.onAppear {
guard item.id == viewModel.items.last?.id else { return }
Task { await viewModel.loadMore(api: assetsAPI) }
}
}
if viewModel.isLoadingMore {
ProgressView()
.padding(.vertical, AppMetrics.Spacing.small)
}
}
}
///
private func reload(showLoading: Bool) async {
await globalLoading.withOptionalLoading(showLoading) {
await viewModel.reload(api: assetsAPI)
}
if let message = viewModel.errorMessage {
toastCenter.show(message)
}
}
///
private func toggleListing(_ item: MediaLibraryItem) async {
let success = await globalLoading.withLoading {
await viewModel.toggleListing(item, api: assetsAPI)
}
toastCenter.show(success ? "状态已更新" : (viewModel.errorMessage ?? "操作失败"))
}
}
///
private struct MediaStatCard: View {
let title: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) {
Text(title)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
Text(value)
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
}
///
private struct MediaLibraryCard: View {
let item: MediaLibraryItem
let openAction: () -> Void
let toggleAction: () -> Void
var body: some View {
Button(action: openAction) {
HStack(spacing: AppMetrics.Spacing.medium) {
RemoteImage(urlString: item.coverUrl, contentMode: .fill) {
Image(systemName: "photo")
.font(.system(size: 28, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppDesign.primarySoft)
}
.frame(width: 74, height: 74)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) {
Text(item.name)
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
.lineLimit(2)
Text(item.scenicSpotName.assetsNonEmpty ?? item.projectName.assetsNonEmpty ?? "未关联打卡点")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
.lineLimit(1)
HStack(spacing: AppMetrics.Spacing.xSmall) {
MediaStatusBadge(text: item.status.auditTitle, color: item.status.auditColor)
MediaStatusBadge(text: item.listing.title, color: item.listing == .online ? AppDesign.success : AppDesign.placeholder)
}
}
Spacer()
Button(action: toggleAction) {
Image(systemName: item.listing == .online ? "arrow.down.circle" : "arrow.up.circle")
.frame(width: AppMetrics.ControlSize.iconTapArea, height: AppMetrics.ControlSize.iconTapArea)
}
.buttonStyle(.plain)
.foregroundStyle(item.isApproved ? AppDesign.primary : AppDesign.placeholder)
.disabled(!item.isApproved)
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
.buttonStyle(.plain)
}
}
///
private struct MediaStatusBadge: View {
let text: String
let color: Color
var body: some View {
Text(text)
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(color)
.padding(.horizontal, AppMetrics.Spacing.xSmall)
.padding(.vertical, AppMetrics.Spacing.xxSmall)
.background(color.opacity(0.1), in: Capsule())
}
}
///
struct MediaLibraryDetailView: View {
let item: MediaLibraryItem
let listViewModel: MediaLibraryViewModel
@Environment(\.assetsAPI) private var assetsAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.dismiss) private var dismiss
@Environment(\.globalLoading) private var globalLoading
@State private var isEditing = false
var body: some View {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
headerSection
mediaSection
actionSection
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle(listViewModel.kind.detailTitle)
.navigationBarTitleDisplayMode(.inline)
.task {
await globalLoading.withOptionalLoading(listViewModel.selectedDetail?.id != item.id) {
await listViewModel.loadDetail(id: item.id, api: assetsAPI)
}
}
.sheet(isPresented: $isEditing) {
MediaLibraryUploadView(kind: listViewModel.kind, editingDetail: listViewModel.selectedDetail)
}
}
///
private var headerSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
RemoteImage(urlString: currentCoverURL, contentMode: .fill) {
Image(systemName: "photo")
.font(.system(size: 40, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppDesign.primarySoft)
}
.frame(height: 180)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
Text(currentName)
.font(.system(size: AppMetrics.FontSize.title2, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
Text(listViewModel.selectedDetail?.description.assetsNonEmpty ?? "暂无描述")
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
HStack {
MediaStatusBadge(text: currentStatus.auditTitle, color: currentStatus.auditColor)
MediaStatusBadge(text: currentListing.title, color: currentListing == .online ? AppDesign.success : AppDesign.placeholder)
}
if listViewModel.kind == .sample {
sampleSummaryBadges
}
detailMetaRows
}
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
///
private var detailMetaRows: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) {
if let detail = listViewModel.selectedDetail {
MediaDetailMetaRow(title: "项目", value: detail.projectName.assetsNonEmpty ?? "--")
MediaDetailMetaRow(title: "景区", value: detail.scenicName.assetsNonEmpty ?? "--")
MediaDetailMetaRow(title: "上传人", value: detail.uploaderName.assetsNonEmpty ?? "--")
MediaDetailMetaRow(title: "创建时间", value: detail.createdAt.assetsNonEmpty ?? "--")
} else {
MediaDetailMetaRow(title: "关联", value: item.projectName.assetsNonEmpty ?? item.scenicSpotName.assetsNonEmpty ?? "--")
MediaDetailMetaRow(title: "创建时间", value: item.createdAt.assetsNonEmpty ?? "--")
}
}
}
///
private var sampleSummaryBadges: some View {
let media = listViewModel.selectedDetail?.mediaList ?? []
let videoCount = media.filter(\.isVideo).count
let imageCount = media.count - videoCount
return HStack(spacing: AppMetrics.Spacing.xSmall) {
MediaStatusBadge(text: "图片 \(imageCount)", color: AppDesign.success)
MediaStatusBadge(text: "视频 \(videoCount)", color: AppDesign.warning)
MediaStatusBadge(text: "总计 \(media.count)", color: AppDesign.primary)
}
}
///
@ViewBuilder
private var mediaSection: some View {
let media = listViewModel.selectedDetail?.mediaList ?? []
if media.isEmpty {
AssetsEmptyState(title: "暂无媒体文件", message: "详情接口暂未返回\(listViewModel.kind == .sample ? "样片" : "素材")文件。")
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 110), spacing: AppMetrics.Spacing.small)], spacing: AppMetrics.Spacing.small) {
ForEach(media) { file in
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) {
RemoteImage(urlString: file.thumbnailUrl.assetsNonEmpty ?? file.showUrl.assetsNonEmpty ?? file.ossUrl, contentMode: .fill) {
Image(systemName: file.isVideo ? "play.rectangle.fill" : "photo")
.font(.system(size: 28, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppDesign.primarySoft)
}
.frame(height: 96)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
Text(file.originalName)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
.lineLimit(1)
}
.padding(AppMetrics.Spacing.xSmall)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
}
}
}
}
///
private var actionSection: some View {
VStack(spacing: AppMetrics.Spacing.small) {
if listViewModel.kind == .material {
Button("编辑素材") {
isEditing = true
}
.frame(maxWidth: .infinity, minHeight: AppMetrics.ControlSize.primaryButtonHeight)
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
.foregroundStyle(.white)
}
HStack(spacing: AppMetrics.Spacing.small) {
Button(currentListing == .online ? "下架" : "上架") {
Task { await toggleListing() }
}
.frame(maxWidth: .infinity, minHeight: AppMetrics.ControlSize.sheetButtonHeight)
.background(Color.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
Button("删除", role: .destructive) {
Task { await delete() }
}
.frame(maxWidth: .infinity, minHeight: AppMetrics.ControlSize.sheetButtonHeight)
.background(Color.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
}
}
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
}
private var currentName: String { listViewModel.selectedDetail?.name ?? item.name }
private var currentCoverURL: String { listViewModel.selectedDetail?.coverUrl ?? item.coverUrl }
private var currentStatus: Int { listViewModel.selectedDetail?.status ?? item.status }
private var currentListing: MediaListingStatus { listViewModel.selectedDetail?.listing ?? item.listing }
///
private func toggleListing() async {
guard let detail = listViewModel.selectedDetail else { return }
let success = await globalLoading.withLoading {
await listViewModel.toggleListing(detail: detail, api: assetsAPI)
}
toastCenter.show(success ? "状态已更新" : (listViewModel.errorMessage ?? "操作失败"))
}
///
private func delete() async {
let success = await globalLoading.withLoading {
await listViewModel.delete(id: item.id, api: assetsAPI)
}
toastCenter.show(success ? "已删除" : (listViewModel.errorMessage ?? "删除失败"))
if success { dismiss() }
}
}
///
struct MediaLibraryUploadView: View {
let kind: MediaLibraryKind
let editingDetail: MediaLibraryDetail?
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var scenicSpotContext: ScenicSpotContext
@Environment(\.assetsAPI) private var assetsAPI
@Environment(\.ossUploadService) private var uploadService
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.dismiss) private var dismiss
@Environment(\.globalLoading) private var globalLoading
@StateObject private var viewModel: MediaLibraryEditorViewModel
@State private var coverSelection: PhotosPickerItem?
@State private var mediaSelection: [PhotosPickerItem] = []
///
init(kind: MediaLibraryKind = .material, editingDetail: MediaLibraryDetail? = nil) {
self.kind = kind
self.editingDetail = editingDetail
_viewModel = StateObject(wrappedValue: MediaLibraryEditorViewModel(detail: editingDetail))
}
var body: some View {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
formSection
spotSection
projectSection
coverSection
mediaSection
submitButton
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle(viewModel.isEditing ? "编辑素材" : kind.uploadTitle)
.navigationBarTitleDisplayMode(.inline)
.task {
guard kind == .sample, viewModel.projects.isEmpty else { return }
await viewModel.loadProjects(scenicId: accountContext.currentScenic?.id, api: assetsAPI)
if let message = viewModel.errorMessage {
toastCenter.show(message)
}
}
.onChange(of: coverSelection) { item in
Task { await loadCover(item) }
}
.onChange(of: mediaSelection) { items in
Task { await loadMedia(items) }
}
}
///
private var formSection: some View {
VStack(spacing: AppMetrics.Spacing.small) {
TextField(kind.namePlaceholder, text: $viewModel.name)
.appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: AppMetrics.ControlSize.inputHeight)
if kind == .material {
TextField("标签,用逗号分隔", text: $viewModel.tagsText)
.appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: AppMetrics.ControlSize.inputHeight)
}
TextField("描述", text: $viewModel.description, axis: .vertical)
.lineLimit(3...5)
.appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: 88)
}
}
///
@ViewBuilder
private var projectSection: some View {
if kind == .sample {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("关联项目")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
if viewModel.projects.isEmpty {
AssetsEmptyState(title: "暂无可选项目", message: viewModel.isLoadingProjects ? "正在加载项目..." : "当前景区暂未加载到可关联项目。")
} else {
Picker("关联项目", selection: $viewModel.selectedProjectId) {
Text("请选择").tag(nil as Int?)
ForEach(viewModel.projects) { project in
Text(project.name.assetsNonEmpty ?? "项目\(project.id)").tag(Optional(project.id))
}
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
}
}
}
///
private var spotSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("打卡点")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
if scenicSpotContext.spots.isEmpty {
AssetsEmptyState(title: "暂无打卡点", message: "当前景区暂未加载到可选打卡点。")
} else {
Picker("打卡点", selection: $viewModel.selectedSpotId) {
Text("请选择").tag(nil as Int?)
ForEach(scenicSpotContext.spots) { spot in
Text(spot.name).tag(Optional(spot.id))
}
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
}
}
///
private var coverSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("封面")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
PhotosPicker(selection: $coverSelection, matching: .images) {
ZStack {
RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card)
.fill(Color.white)
if let coverFile = viewModel.coverFile, let image = UIImage(data: coverFile.data) {
Image(uiImage: image)
.resizable()
.scaledToFill()
} else if let url = editingDetail?.coverUrl, !url.isEmpty {
RemoteImage(urlString: url, contentMode: .fill) {
Image(systemName: "photo")
.font(.system(size: 34, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
} else {
Label("选择封面", systemImage: "photo.badge.plus")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
}
.frame(height: 170)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
}
}
///
private var mediaSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
HStack {
Text("媒体文件")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
Spacer()
PhotosPicker(selection: $mediaSelection, maxSelectionCount: 20, matching: .any(of: [.images, .videos])) {
Label("添加", systemImage: "plus")
}
}
if viewModel.mediaFiles.isEmpty {
AssetsEmptyState(title: "未选择\(kind == .sample ? "样片" : "素材")文件", message: "可选择图片或视频,提交前会先上传到 OSS。")
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 96), spacing: AppMetrics.Spacing.small)], spacing: AppMetrics.Spacing.small) {
ForEach(viewModel.mediaFiles) { file in
ZStack(alignment: .topTrailing) {
MediaLocalPreview(file: file)
Button {
viewModel.removeMediaFile(id: file.id)
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.white, Color.black.opacity(0.5))
}
.padding(AppMetrics.Spacing.xxSmall)
}
}
}
}
}
}
///
private var submitButton: some View {
Button {
Task { await submit() }
} label: {
Text(viewModel.isSubmitting ? "提交中..." : "提交")
.frame(maxWidth: .infinity, minHeight: AppMetrics.ControlSize.primaryButtonHeight)
}
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(.white)
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
.disabled(viewModel.isSubmitting)
}
///
private func loadCover(_ item: PhotosPickerItem?) async {
guard let item else { return }
let files = await AssetPickerLoader.loadMediaFiles(from: [item])
if let file = files.first {
viewModel.setCover(file)
}
}
///
private func loadMedia(_ items: [PhotosPickerItem]) async {
guard !items.isEmpty else { return }
defer { mediaSelection = [] }
let files = await AssetPickerLoader.loadMediaFiles(from: items)
viewModel.addMediaFiles(files)
}
///
private func submit() async {
let success = await globalLoading.withLoading {
await viewModel.submit(
kind: kind,
scenicId: accountContext.currentScenic?.id,
api: assetsAPI,
uploadService: uploadService
)
}
toastCenter.show(success ? "\(kind == .sample ? "样片" : "素材")已提交" : (viewModel.errorMessage ?? "提交失败"))
if success { dismiss() }
}
}
///
private struct MediaDetailMetaRow: View {
let title: String
let value: String
var body: some View {
HStack(spacing: AppMetrics.Spacing.small) {
Text(title)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
Text(value)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textPrimary)
.lineLimit(1)
Spacer(minLength: 0)
}
}
}
///
private struct MediaLocalPreview: View {
let file: MediaLocalUploadFile
var body: some View {
ZStack {
if file.fileType == 2, let image = UIImage(data: file.data) {
Image(uiImage: image)
.resizable()
.scaledToFill()
} else {
Image(systemName: "play.rectangle.fill")
.font(.system(size: 30, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppDesign.primarySoft)
}
}
.frame(height: 96)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
}
}
private extension Int {
///
var auditTitle: String {
switch self {
case 1:
"已通过"
case 2:
"未通过"
default:
"待审核"
}
}
///
var auditColor: Color {
switch self {
case 1:
AppDesign.success
case 2:
.red
default:
AppDesign.warning
}
}
}