新增订单长尾流程,并迁移排队、消息、结算与审核模块
将定金订单、历史拍摄与多行程上传接入 Orders,并以真实页面替换首页排队管理、消息中心、景区结算与提现审核占位入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
302
suixinkan/Features/MessageCenter/Views/MessageCenterViews.swift
Normal file
302
suixinkan/Features/MessageCenter/Views/MessageCenterViews.swift
Normal file
@ -0,0 +1,302 @@
|
||||
//
|
||||
// MessageCenterViews.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 消息中心页面,展示消息列表、筛选、已读和删除入口。
|
||||
struct MessageCenterView: View {
|
||||
@Environment(MessageCenterAPI.self) private var messageAPI
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@State private var viewModel = MessageCenterViewModel()
|
||||
@State private var selectedMessage: MessageItem?
|
||||
@State private var processingMessageId: String?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
content
|
||||
}
|
||||
.background(Color(hex: 0xF5F7FA).ignoresSafeArea())
|
||||
.navigationTitle("消息中心")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("全部已读") {
|
||||
Task { await markAllAsRead() }
|
||||
}
|
||||
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
|
||||
.disabled(viewModel.unreadCount == 0 || viewModel.messages.isEmpty)
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedMessage) { item in
|
||||
MessageDetailView(item: item) { deleteItem in
|
||||
await deleteMessage(deleteItem)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if viewModel.messages.isEmpty {
|
||||
await viewModel.reloadFirstPage(api: messageAPI)
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.reloadFirstPage(api: messageAPI)
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
|
||||
HStack(spacing: AppMetrics.Spacing.medium) {
|
||||
messageMetric(title: "消息总数", value: "\(viewModel.messages.count)", color: AppDesign.primary)
|
||||
messageMetric(title: "未读消息", value: "\(viewModel.unreadCount)", color: AppDesign.warning)
|
||||
}
|
||||
|
||||
Picker("消息筛选", selection: Binding(
|
||||
get: { viewModel.selectedFilter },
|
||||
set: { filter in
|
||||
Task { await viewModel.selectFilter(filter, api: messageAPI) }
|
||||
}
|
||||
)) {
|
||||
ForEach(MessageFilter.allCases) { filter in
|
||||
Text(filter.title).tag(filter)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Text(viewModel.filterDescription)
|
||||
.font(.system(size: AppMetrics.FontSize.caption))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
}
|
||||
.padding(AppMetrics.Spacing.pageHorizontal)
|
||||
.background(.white)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if viewModel.isLoading && viewModel.messages.isEmpty {
|
||||
ProgressView("加载中...")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if viewModel.loadFailed {
|
||||
ContentUnavailableView {
|
||||
Label("消息加载失败", systemImage: "exclamationmark.triangle")
|
||||
} description: {
|
||||
Text(viewModel.loadFailureReason ?? "请稍后重试")
|
||||
} actions: {
|
||||
Button("重新加载") {
|
||||
Task { await viewModel.reloadFirstPage(api: messageAPI) }
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if viewModel.messages.isEmpty {
|
||||
ContentUnavailableView("暂无消息", systemImage: "bell", description: Text("暂无可查看的系统消息"))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: AppMetrics.Spacing.medium) {
|
||||
ForEach(viewModel.messages) { item in
|
||||
Button {
|
||||
Task { await openMessage(item) }
|
||||
} label: {
|
||||
MessageRow(item: item, processing: processingMessageId == item.id)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onAppear {
|
||||
if item.id == viewModel.messages.last?.id {
|
||||
Task { await viewModel.loadMore(api: messageAPI) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoadingMore {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, AppMetrics.Spacing.medium)
|
||||
}
|
||||
}
|
||||
.padding(AppMetrics.Spacing.pageHorizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func messageMetric(title: String, value: String, color: Color) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.system(size: AppMetrics.FontSize.caption))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
Text(value)
|
||||
.font(.system(size: 24, weight: .bold))
|
||||
.foregroundStyle(color)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(AppMetrics.Spacing.medium)
|
||||
.background(color.opacity(0.08), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
|
||||
}
|
||||
|
||||
private func openMessage(_ item: MessageItem) async {
|
||||
processingMessageId = item.id
|
||||
defer { processingMessageId = nil }
|
||||
do {
|
||||
try await viewModel.markAsRead(api: messageAPI, item: item)
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
selectedMessage = viewModel.messages.first(where: { $0.id == item.id }) ?? item
|
||||
}
|
||||
|
||||
private func markAllAsRead() async {
|
||||
do {
|
||||
try await viewModel.markAllAsRead(api: messageAPI)
|
||||
toastCenter.show("已全部标记为已读")
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func deleteMessage(_ item: MessageItem) async -> Bool {
|
||||
processingMessageId = item.id
|
||||
defer { processingMessageId = nil }
|
||||
do {
|
||||
try await viewModel.deleteMessage(api: messageAPI, item: item)
|
||||
toastCenter.show("消息已删除")
|
||||
return true
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MessageRow: View {
|
||||
let item: MessageItem
|
||||
let processing: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: AppMetrics.Spacing.medium) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(item.type.color.opacity(0.12))
|
||||
Image(systemName: item.type.iconName)
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundStyle(item.type.color)
|
||||
}
|
||||
.frame(width: 48, height: 48)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(item.title)
|
||||
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
|
||||
.foregroundStyle(AppDesign.textPrimary)
|
||||
.lineLimit(1)
|
||||
if !item.isRead {
|
||||
Circle()
|
||||
.fill(Color(hex: 0xEF4444))
|
||||
.frame(width: 7, height: 7)
|
||||
}
|
||||
Spacer()
|
||||
Text(item.type.title)
|
||||
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
|
||||
.foregroundStyle(item.type.color)
|
||||
}
|
||||
|
||||
Text(item.detail.isEmpty ? "暂无消息内容" : item.detail)
|
||||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(item.time)
|
||||
.font(.system(size: AppMetrics.FontSize.caption))
|
||||
.foregroundStyle(Color(hex: 0x9CA3AF))
|
||||
}
|
||||
}
|
||||
.padding(AppMetrics.Spacing.medium)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card)
|
||||
.stroke(Color(hex: item.isRead ? 0xECEFF3 : 0xBFD7FF), lineWidth: 1)
|
||||
}
|
||||
.opacity(processing ? 0.6 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
private struct MessageDetailView: View {
|
||||
let item: MessageItem
|
||||
let onDelete: (MessageItem) async -> Bool
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var showDeleteConfirm = false
|
||||
@State private var deleting = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(spacing: AppMetrics.Spacing.large) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(item.type.color.opacity(0.12))
|
||||
Image(systemName: item.type.iconName)
|
||||
.font(.system(size: 38, weight: .bold))
|
||||
.foregroundStyle(item.type.color)
|
||||
}
|
||||
.frame(width: 82, height: 82)
|
||||
|
||||
VStack(spacing: AppMetrics.Spacing.small) {
|
||||
Text(item.title)
|
||||
.font(.system(size: AppMetrics.FontSize.title2, weight: .bold))
|
||||
.foregroundStyle(AppDesign.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(item.time)
|
||||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
}
|
||||
|
||||
Text(item.detail.isEmpty ? "暂无消息内容" : item.detail)
|
||||
.font(.system(size: AppMetrics.FontSize.body))
|
||||
.foregroundStyle(AppDesign.textPrimary)
|
||||
.lineSpacing(6)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(AppMetrics.Spacing.large)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
|
||||
}
|
||||
.padding(AppMetrics.Spacing.pageHorizontal)
|
||||
}
|
||||
.background(Color(hex: 0xF5F7FA).ignoresSafeArea())
|
||||
.navigationTitle("消息详情")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("关闭") { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button(role: .destructive) {
|
||||
showDeleteConfirm = true
|
||||
} label: {
|
||||
if deleting {
|
||||
ProgressView()
|
||||
} else {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
.disabled(deleting || item.msgId == nil)
|
||||
}
|
||||
}
|
||||
.alert("确认删除", isPresented: $showDeleteConfirm) {
|
||||
Button("取消", role: .cancel) {}
|
||||
Button("删除", role: .destructive) {
|
||||
Task {
|
||||
deleting = true
|
||||
let success = await onDelete(item)
|
||||
deleting = false
|
||||
if success { dismiss() }
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("删除后不可恢复,是否继续?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user