Wire deposit orders, historical shooting, and multi-travel uploads into Orders, and replace home placeholders for queue management, message center, scenic settlement, and withdrawal audit. Co-authored-by: Cursor <cursoragent@cursor.com>
597 lines
24 KiB
Swift
597 lines
24 KiB
Swift
//
|
||
// DepositOrderViews.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// 押金订单入口页,支持订单号查询、列表分页、核销和退款。
|
||
struct DepositOrderEntryView: View {
|
||
@Environment(AccountContext.self) private var accountContext
|
||
@Environment(OrdersAPI.self) private var ordersAPI
|
||
@Environment(RouterPath.self) private var router
|
||
@Environment(ToastCenter.self) private var toastCenter
|
||
@Environment(\.globalLoading) private var globalLoading
|
||
|
||
@State private var viewModel = DepositOrderListViewModel()
|
||
@State private var orderNumber = ""
|
||
@State private var pendingWriteOff: DepositOrderListItem?
|
||
@State private var pendingRefund: DepositOrderListItem?
|
||
@State private var refundReason = ""
|
||
|
||
var body: some View {
|
||
List {
|
||
Section("订单查询") {
|
||
TextField("请输入押金订单号", text: $orderNumber)
|
||
.textInputAutocapitalization(.never)
|
||
.autocorrectionDisabled(true)
|
||
|
||
Button("查看订单详情") {
|
||
navigateToDetail(orderNumber)
|
||
}
|
||
.disabled(orderNumber.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||
}
|
||
|
||
Section {
|
||
if viewModel.orders.isEmpty {
|
||
ContentUnavailableView("暂无押金订单", systemImage: "doc.text.magnifyingglass")
|
||
.frame(maxWidth: .infinity, minHeight: 180)
|
||
} else {
|
||
ForEach(viewModel.orders) { item in
|
||
DepositOrderRow(
|
||
item: item,
|
||
isOperating: viewModel.operatingOrderNumber == item.orderNumber,
|
||
onDetail: { navigateToDetail(item.orderNumber) },
|
||
onWriteOff: { pendingWriteOff = item },
|
||
onRefund: { pendingRefund = item }
|
||
)
|
||
}
|
||
|
||
if viewModel.hasMore {
|
||
Button(viewModel.loadingMore ? "加载中..." : "加载更多") {
|
||
Task { await viewModel.reload(api: ordersAPI, scenicId: accountContext.currentScenic?.id, reset: false) }
|
||
}
|
||
.frame(maxWidth: .infinity, alignment: .center)
|
||
.disabled(viewModel.loadingMore)
|
||
}
|
||
}
|
||
} header: {
|
||
HStack {
|
||
Text("押金订单列表")
|
||
Spacer()
|
||
Text("\(viewModel.orders.count)/\(viewModel.total)")
|
||
}
|
||
}
|
||
}
|
||
.listStyle(.insetGrouped)
|
||
.scrollContentBackground(.hidden)
|
||
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
|
||
.navigationTitle("押金订单")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.refreshable { await reload(showLoading: false) }
|
||
.task { await reload(showLoading: true) }
|
||
.confirmationDialog("确认核销该押金订单?", isPresented: writeOffBinding, titleVisibility: .visible) {
|
||
Button("确认核销") {
|
||
guard let pendingWriteOff else { return }
|
||
Task { await writeOff(pendingWriteOff) }
|
||
}
|
||
Button("取消", role: .cancel) {}
|
||
} message: {
|
||
Text(pendingWriteOff?.orderNumber ?? "")
|
||
}
|
||
.alert("申请退款", isPresented: refundBinding) {
|
||
TextField("请输入退款原因", text: $refundReason)
|
||
Button("提交") {
|
||
guard let pendingRefund else { return }
|
||
Task { await refund(pendingRefund) }
|
||
}
|
||
Button("取消", role: .cancel) {
|
||
pendingRefund = nil
|
||
refundReason = ""
|
||
}
|
||
} message: {
|
||
Text("请填写退款原因后提交。")
|
||
}
|
||
.alert("提示", isPresented: errorBinding) {
|
||
Button("知道了", role: .cancel) {}
|
||
} message: {
|
||
Text(viewModel.errorMessage ?? "")
|
||
}
|
||
}
|
||
|
||
private var writeOffBinding: Binding<Bool> {
|
||
Binding(
|
||
get: { pendingWriteOff != nil },
|
||
set: { if !$0 { pendingWriteOff = nil } }
|
||
)
|
||
}
|
||
|
||
private var refundBinding: Binding<Bool> {
|
||
Binding(
|
||
get: { pendingRefund != nil },
|
||
set: { if !$0 { pendingRefund = nil; refundReason = "" } }
|
||
)
|
||
}
|
||
|
||
private var errorBinding: Binding<Bool> {
|
||
Binding(
|
||
get: { viewModel.errorMessage != nil },
|
||
set: { if !$0 { viewModel.clearError() } }
|
||
)
|
||
}
|
||
|
||
/// 跳转到押金订单详情页。
|
||
private func navigateToDetail(_ rawOrderNumber: String) {
|
||
let text = rawOrderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !text.isEmpty else { return }
|
||
router.navigate(to: .orders(.depositDetail(orderNumber: text)))
|
||
}
|
||
|
||
/// 刷新押金订单列表。
|
||
private func reload(showLoading: Bool) async {
|
||
if showLoading {
|
||
await globalLoading.withLoading {
|
||
await viewModel.reload(api: ordersAPI, scenicId: accountContext.currentScenic?.id, reset: true)
|
||
}
|
||
} else {
|
||
await viewModel.reload(api: ordersAPI, scenicId: accountContext.currentScenic?.id, reset: true)
|
||
}
|
||
}
|
||
|
||
/// 核销押金订单并提示结果。
|
||
private func writeOff(_ item: DepositOrderListItem) async {
|
||
pendingWriteOff = nil
|
||
let success = await viewModel.writeOff(api: ordersAPI, scenicId: accountContext.currentScenic?.id, orderNumber: item.orderNumber)
|
||
if success {
|
||
toastCenter.show("核销成功")
|
||
}
|
||
}
|
||
|
||
/// 提交押金订单退款并提示结果。
|
||
private func refund(_ item: DepositOrderListItem) async {
|
||
let reason = refundReason
|
||
pendingRefund = nil
|
||
refundReason = ""
|
||
let success = await viewModel.refund(api: ordersAPI, scenicId: accountContext.currentScenic?.id, orderNumber: item.orderNumber, reason: reason)
|
||
if success {
|
||
toastCenter.show("退款申请已提交")
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 押金订单列表行,展示订单状态和操作按钮。
|
||
private struct DepositOrderRow: View {
|
||
let item: DepositOrderListItem
|
||
let isOperating: Bool
|
||
let onDetail: () -> Void
|
||
let onWriteOff: () -> Void
|
||
let onRefund: () -> Void
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
|
||
HStack {
|
||
Text(item.orderNumber.isEmpty ? "--" : item.orderNumber)
|
||
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
|
||
.foregroundStyle(AppDesign.textPrimary)
|
||
Spacer()
|
||
Text(item.statusName.isEmpty ? "状态\(item.status)" : item.statusName)
|
||
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
|
||
.foregroundStyle(statusColor)
|
||
.padding(.horizontal, AppMetrics.Spacing.small)
|
||
.frame(height: 24)
|
||
.background(statusColor.opacity(0.12), in: Capsule())
|
||
}
|
||
|
||
HStack {
|
||
Text("¥\(item.amount.isEmpty ? "0.00" : item.amount)")
|
||
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
|
||
.foregroundStyle(AppDesign.primary)
|
||
Spacer()
|
||
Text(item.createdAt.isEmpty ? "--" : item.createdAt)
|
||
.font(.system(size: AppMetrics.FontSize.caption))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
|
||
if !item.userPhone.isEmpty {
|
||
Text("手机号:\(item.userPhone)")
|
||
.font(.system(size: AppMetrics.FontSize.caption))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
|
||
HStack(spacing: AppMetrics.Spacing.small) {
|
||
rowButton("详情", tint: AppDesign.primary, action: onDetail)
|
||
rowButton(isOperating ? "处理中..." : "核销", tint: Color(hex: 0x22C55E), disabled: isOperating, action: onWriteOff)
|
||
rowButton(isOperating ? "处理中..." : "退款", tint: Color(hex: 0xF59E0B), disabled: isOperating, action: onRefund)
|
||
}
|
||
}
|
||
.padding(.vertical, AppMetrics.Spacing.xSmall)
|
||
}
|
||
|
||
private var statusColor: Color {
|
||
switch item.status {
|
||
case 20:
|
||
return AppDesign.primary
|
||
case 30:
|
||
return Color(hex: 0x22C55E)
|
||
case 50:
|
||
return Color(hex: 0xEF4444)
|
||
default:
|
||
return Color(hex: 0xF59E0B)
|
||
}
|
||
}
|
||
|
||
/// 构建列表行操作按钮。
|
||
private func rowButton(_ title: String, tint: Color, disabled: Bool = false, action: @escaping () -> Void) -> some View {
|
||
Button(action: action) {
|
||
Text(title)
|
||
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
|
||
.foregroundStyle(disabled ? AppDesign.textSecondary : tint)
|
||
.frame(maxWidth: .infinity)
|
||
.frame(height: 34)
|
||
.background((disabled ? Color(hex: 0xEEF2F7) : tint.opacity(0.12)), in: RoundedRectangle(cornerRadius: 8))
|
||
}
|
||
.buttonStyle(.plain)
|
||
.disabled(disabled)
|
||
}
|
||
}
|
||
|
||
/// 押金订单详情页,展示订单基础信息和拍摄点列表。
|
||
struct DepositOrderDetailView: View {
|
||
@Environment(AccountContext.self) private var accountContext
|
||
@Environment(OrdersAPI.self) private var ordersAPI
|
||
@Environment(RouterPath.self) private var router
|
||
@Environment(\.globalLoading) private var globalLoading
|
||
|
||
let orderNumber: String
|
||
@State private var viewModel = DepositOrderDetailViewModel()
|
||
|
||
var body: some View {
|
||
List {
|
||
if let detail = viewModel.detail {
|
||
Section("订单信息") {
|
||
OrderDetailValueRow(title: "订单号", value: detail.orderNumber)
|
||
OrderDetailValueRow(title: "订单状态", value: detail.orderStatusName)
|
||
OrderDetailValueRow(title: "订单类型", value: detail.orderTypeLabel)
|
||
OrderDetailValueRow(title: "项目名称", value: detail.projectName)
|
||
OrderDetailValueRow(title: "订单金额", value: "¥\(detail.actualPayAmount.isEmpty ? "0.00" : detail.actualPayAmount)")
|
||
OrderDetailValueRow(title: "退款金额", value: "¥\(detail.actualRefundAmount.isEmpty ? "0" : detail.actualRefundAmount)")
|
||
OrderDetailValueRow(title: "手机号", value: detail.phone)
|
||
OrderDetailValueRow(title: "下单时间", value: detail.createdAt)
|
||
OrderDetailValueRow(title: "付款时间", value: detail.payTime)
|
||
OrderDetailValueRow(title: "完成时间", value: detail.completeTime)
|
||
}
|
||
|
||
if let projectInfo = detail.multiTravel?.projectInfo {
|
||
Section("项目配置") {
|
||
OrderDetailValueRow(title: "打卡点数", value: "\(projectInfo.settleSpotNum)")
|
||
OrderDetailValueRow(title: "单点底片", value: "\(projectInfo.singleSpotMaterialNum)")
|
||
OrderDetailValueRow(title: "单点精修", value: "\(projectInfo.singleSpotPhotoNum)")
|
||
OrderDetailValueRow(title: "单点视频", value: "\(projectInfo.singleSpotVideoNum)")
|
||
}
|
||
}
|
||
|
||
Section("拍摄详情") {
|
||
let shootingList = detail.multiTravel?.shootingList ?? []
|
||
if shootingList.isEmpty {
|
||
ContentUnavailableView("暂无拍摄记录", systemImage: "photo.stack")
|
||
} else {
|
||
ForEach(shootingList) { item in
|
||
Button {
|
||
router.navigate(
|
||
to: .orders(
|
||
.depositShootingInfo(
|
||
orderNumber: detail.orderNumber,
|
||
scenicSpotId: item.scenicSpotId,
|
||
photogUid: item.photogUid
|
||
)
|
||
)
|
||
)
|
||
} label: {
|
||
DepositShootingPointRow(item: item)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
Section {
|
||
ContentUnavailableView("暂无订单详情", systemImage: "doc.text.magnifyingglass")
|
||
.frame(maxWidth: .infinity, minHeight: 220)
|
||
}
|
||
}
|
||
}
|
||
.listStyle(.insetGrouped)
|
||
.scrollContentBackground(.hidden)
|
||
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
|
||
.navigationTitle("押金订单详情")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.task {
|
||
await globalLoading.withLoading {
|
||
await viewModel.load(api: ordersAPI, storeId: accountContext.currentStore?.id, orderNumber: orderNumber)
|
||
}
|
||
}
|
||
.alert("提示", isPresented: errorBinding) {
|
||
Button("知道了", role: .cancel) {}
|
||
} message: {
|
||
Text(viewModel.errorMessage ?? "")
|
||
}
|
||
}
|
||
|
||
private var errorBinding: Binding<Bool> {
|
||
Binding(
|
||
get: { viewModel.errorMessage != nil },
|
||
set: { if !$0 { viewModel.clearError() } }
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 押金订单拍摄点行。
|
||
private struct DepositShootingPointRow: View {
|
||
let item: StoreOrderShootingListItem
|
||
|
||
var body: some View {
|
||
HStack {
|
||
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
|
||
Text(title)
|
||
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
|
||
.foregroundStyle(AppDesign.textPrimary)
|
||
Text(item.photogName.isEmpty ? "摄影师未分配" : item.photogName)
|
||
.font(.system(size: AppMetrics.FontSize.caption))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
Spacer()
|
||
VStack(alignment: .trailing, spacing: AppMetrics.Spacing.xSmall) {
|
||
Text(item.status == 2 ? "已拍摄" : "待拍摄")
|
||
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
|
||
.foregroundStyle(item.status == 2 ? Color(hex: 0x22C55E) : AppDesign.primary)
|
||
Text(String(format: "评分 %.1f", max(item.startAvg, item.start)))
|
||
.font(.system(size: AppMetrics.FontSize.caption))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
Image(systemName: "chevron.right")
|
||
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
}
|
||
|
||
private var title: String {
|
||
let text = item.scenicSpotName.isEmpty ? item.staffName : item.scenicSpotName
|
||
return text.isEmpty ? "未命名打卡点" : text
|
||
}
|
||
}
|
||
|
||
/// 押金拍摄信息页,展示评价、底片和成片。
|
||
struct DepositOrderShootingInfoView: View {
|
||
@Environment(AccountContext.self) private var accountContext
|
||
@Environment(OrdersAPI.self) private var ordersAPI
|
||
@Environment(\.globalLoading) private var globalLoading
|
||
|
||
let orderNumber: String
|
||
let scenicSpotId: Int
|
||
let photogUid: Int
|
||
|
||
@State private var viewModel = DepositOrderShootingInfoViewModel()
|
||
@State private var selectedTab: DepositShootingMediaTab = .negative
|
||
|
||
var body: some View {
|
||
List {
|
||
if let detail = viewModel.detail {
|
||
Section("拍摄点") {
|
||
OrderDetailValueRow(title: "名称", value: detail.scenicSpotName)
|
||
OrderDetailValueRow(title: "订单类型", value: detail.orderTypeName)
|
||
}
|
||
|
||
Section("评分与评价") {
|
||
if let comment = detail.orderComment {
|
||
RatingRow(title: "拍摄满意度", score: comment.starShooting)
|
||
RatingRow(title: "修图满意度", score: comment.starRetouching)
|
||
RatingRow(title: "景区满意度", score: comment.starScenery)
|
||
RatingRow(title: "服务满意度", score: comment.starService)
|
||
RatingRow(title: "相机满意度", score: comment.starCamera)
|
||
Text(comment.content.isEmpty ? "暂无评价内容" : comment.content)
|
||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
} else {
|
||
ContentUnavailableView("暂无评价信息", systemImage: "star")
|
||
}
|
||
}
|
||
|
||
Section {
|
||
Picker("媒体类型", selection: $selectedTab) {
|
||
Text("底片").tag(DepositShootingMediaTab.negative)
|
||
Text("成片").tag(DepositShootingMediaTab.complete)
|
||
}
|
||
.pickerStyle(.segmented)
|
||
|
||
OrderMediaGrid(files: selectedTab == .negative ? detail.materialList : detail.completeList)
|
||
} header: {
|
||
Text("媒体文件")
|
||
}
|
||
} else {
|
||
Section {
|
||
ContentUnavailableView("暂无拍摄信息", systemImage: "photo.stack")
|
||
.frame(maxWidth: .infinity, minHeight: 220)
|
||
}
|
||
}
|
||
}
|
||
.listStyle(.insetGrouped)
|
||
.scrollContentBackground(.hidden)
|
||
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
|
||
.navigationTitle("拍摄信息")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.task {
|
||
await globalLoading.withLoading {
|
||
await viewModel.load(
|
||
api: ordersAPI,
|
||
storeId: accountContext.currentStore?.id,
|
||
orderNumber: orderNumber,
|
||
scenicSpotId: scenicSpotId,
|
||
photogUid: photogUid
|
||
)
|
||
}
|
||
}
|
||
.alert("提示", isPresented: errorBinding) {
|
||
Button("知道了", role: .cancel) {}
|
||
} message: {
|
||
Text(viewModel.errorMessage ?? "")
|
||
}
|
||
}
|
||
|
||
private var errorBinding: Binding<Bool> {
|
||
Binding(
|
||
get: { viewModel.errorMessage != nil },
|
||
set: { if !$0 { viewModel.clearError() } }
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 历史拍摄信息页,展示多点位拍摄媒体。
|
||
struct HistoricalShootingInfoView: View {
|
||
@Environment(OrdersAPI.self) private var ordersAPI
|
||
@Environment(\.globalLoading) private var globalLoading
|
||
|
||
let orderNumber: String
|
||
@State private var viewModel = HistoricalShootingInfoViewModel()
|
||
|
||
var body: some View {
|
||
List {
|
||
Section("订单") {
|
||
OrderDetailValueRow(title: "订单号", value: orderNumber)
|
||
OrderDetailValueRow(title: "项目名称", value: viewModel.projectName)
|
||
OrderDetailValueRow(title: "项目类型", value: viewModel.projectTypeName)
|
||
}
|
||
|
||
Section("历史拍摄") {
|
||
if viewModel.spots.isEmpty {
|
||
ContentUnavailableView("暂无历史拍摄信息", systemImage: "photo.on.rectangle")
|
||
.frame(maxWidth: .infinity, minHeight: 220)
|
||
} else {
|
||
ForEach(viewModel.spots) { spot in
|
||
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
|
||
HStack {
|
||
Label(spot.scenicSpotName.isEmpty ? "未命名打卡点" : spot.scenicSpotName, systemImage: "mappin.and.ellipse")
|
||
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
|
||
.foregroundStyle(AppDesign.primary)
|
||
Spacer()
|
||
Text("摄影师:\(spot.photographerDisplayName)")
|
||
.font(.system(size: AppMetrics.FontSize.caption))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
OrderMediaGrid(files: spot.files)
|
||
}
|
||
.padding(.vertical, AppMetrics.Spacing.xSmall)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.listStyle(.insetGrouped)
|
||
.scrollContentBackground(.hidden)
|
||
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
|
||
.navigationTitle("历史拍摄")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.task {
|
||
await globalLoading.withLoading {
|
||
await viewModel.load(api: ordersAPI, orderNumber: orderNumber)
|
||
}
|
||
}
|
||
.alert("提示", isPresented: errorBinding) {
|
||
Button("知道了", role: .cancel) {}
|
||
} message: {
|
||
Text(viewModel.errorMessage ?? "")
|
||
}
|
||
}
|
||
|
||
private var errorBinding: Binding<Bool> {
|
||
Binding(
|
||
get: { viewModel.errorMessage != nil },
|
||
set: { if !$0 { viewModel.clearError() } }
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 拍摄媒体类型。
|
||
private enum DepositShootingMediaTab: Hashable {
|
||
case negative
|
||
case complete
|
||
}
|
||
|
||
/// 通用订单详情键值行。
|
||
private struct OrderDetailValueRow: View {
|
||
let title: String
|
||
let value: String
|
||
|
||
var body: some View {
|
||
HStack(alignment: .top) {
|
||
Text(title)
|
||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
Spacer(minLength: AppMetrics.Spacing.medium)
|
||
Text(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "--" : value)
|
||
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .medium))
|
||
.foregroundStyle(AppDesign.textPrimary)
|
||
.multilineTextAlignment(.trailing)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 评分展示行。
|
||
private struct RatingRow: View {
|
||
let title: String
|
||
let score: Int
|
||
|
||
var body: some View {
|
||
HStack {
|
||
Text(title)
|
||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
Spacer()
|
||
Text(String(repeating: "★", count: max(score, 0)))
|
||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||
.foregroundStyle(Color(hex: 0xF59E0B))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 订单媒体网格,统一展示图片和视频缩略图。
|
||
private struct OrderMediaGrid: View {
|
||
let files: [OrderMediaFile]
|
||
|
||
private let columns = [
|
||
GridItem(.flexible(), spacing: AppMetrics.Spacing.small),
|
||
GridItem(.flexible(), spacing: AppMetrics.Spacing.small),
|
||
GridItem(.flexible(), spacing: AppMetrics.Spacing.small)
|
||
]
|
||
|
||
var body: some View {
|
||
if files.isEmpty {
|
||
ContentUnavailableView("暂无媒体文件", systemImage: "photo")
|
||
.frame(maxWidth: .infinity, minHeight: 140)
|
||
} else {
|
||
LazyVGrid(columns: columns, spacing: AppMetrics.Spacing.small) {
|
||
ForEach(files) { file in
|
||
Link(destination: URL(string: file.fileUrl) ?? APIEnvironment.production.baseURL) {
|
||
ZStack(alignment: .bottomTrailing) {
|
||
RemoteImage(urlString: file.previewURLString) {
|
||
RoundedRectangle(cornerRadius: 8)
|
||
.fill(AppDesign.primarySoft)
|
||
}
|
||
.frame(height: 88)
|
||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||
|
||
if file.isVideo {
|
||
Image(systemName: "play.circle.fill")
|
||
.font(.system(size: 18, weight: .semibold))
|
||
.foregroundStyle(.white)
|
||
.shadow(radius: 2)
|
||
.padding(AppMetrics.Spacing.xSmall)
|
||
}
|
||
}
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|