Files
suixinkan_ios_new/suixinkan/Features/ScenicPermission/Views/PermissionApplyStatusView.swift
汉秋 26f4d0e671 Migrate from iOS 17 Observation to iOS 16-compatible Combine architecture.
Lower deployment target to iOS 16, replace @Observable with ObservableObject, add navigation and UI compatibility shims, and include login smoke UI tests.

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

190 lines
7.8 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.

//
// PermissionApplyStatusView.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import SwiftUI
///
struct PermissionApplyStatusView: View {
@Environment(\.scenicPermissionAPI) private var scenicPermissionAPI
@StateObject private var viewModel = PermissionApplyStatusViewModel()
@State private var editingPending: RoleApplyPendingResponse?
let applyCode: String?
///
init(applyCode: String? = nil) {
self.applyCode = applyCode
}
var body: some View {
ScrollView {
VStack(spacing: AppMetrics.Spacing.small) {
if let pending = viewModel.pending {
statusCard(pending)
roleCard(pending)
scenicCard(pending)
} else if viewModel.loadFailed {
AppContentUnavailableView(
"申请状态加载失败",
systemImage: "exclamationmark.triangle",
description: Text(viewModel.loadFailureReason ?? "请检查网络后重试")
)
Button("重新加载") {
Task { await viewModel.load(api: scenicPermissionAPI, applyCode: applyCode) }
}
.font(.system(size: AppMetrics.FontSize.footnote, weight: .semibold))
.buttonStyle(.plain)
} else {
AppContentUnavailableView("暂无审核中的申请", systemImage: "doc.text.magnifyingglass")
}
}
.padding(AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA).ignoresSafeArea())
.navigationTitle("权限申请")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if let pending = viewModel.pending, pending.status == 3 {
ToolbarItem(placement: .topBarTrailing) {
Button {
editingPending = pending
} label: {
Label("去编辑", systemImage: "pencil")
}
}
}
}
.appNavigationDestination(item: $editingPending) { pending in
PermissionApplyView(initialPending: pending)
}
.task {
await viewModel.load(api: scenicPermissionAPI, applyCode: applyCode)
}
}
///
private func statusCard(_ pending: RoleApplyPendingResponse) -> some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
HStack {
Label(statusLabel(pending), systemImage: statusIcon(pending.status))
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
.foregroundStyle(statusColor(pending.status))
.padding(.horizontal, AppMetrics.Spacing.small)
.frame(height: 28)
.background(statusColor(pending.status).opacity(0.12), in: Capsule())
Spacer()
}
row("申请编号", pending.code)
row("提交时间", pending.createdAt)
row("审核时间", pending.auditedAt ?? "--")
row("审核人", pending.auditedBy ?? "--")
row("审核备注", pending.auditNote ?? "--")
}
.padding(AppMetrics.Spacing.small)
.background(statusColor(pending.status).opacity(0.08), in: RoundedRectangle(cornerRadius: 12))
.overlay {
RoundedRectangle(cornerRadius: 12).stroke(statusColor(pending.status).opacity(0.35), lineWidth: 1)
}
}
///
private func roleCard(_ pending: RoleApplyPendingResponse) -> some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
Text("新增角色")
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
Text(pending.roleName)
.font(.system(size: AppMetrics.FontSize.footnote, weight: .medium))
.foregroundStyle(AppDesign.primary)
.padding(.horizontal, AppMetrics.Spacing.small)
.padding(.vertical, AppMetrics.Spacing.xSmall)
.background(AppDesign.primarySoft, in: Capsule())
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(AppMetrics.Spacing.small)
.background(.white, in: RoundedRectangle(cornerRadius: 12))
}
///
private func scenicCard(_ pending: RoleApplyPendingResponse) -> some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
HStack {
Text("新增景区")
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
Spacer()
Text("\(pending.scenicList.count)")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
}
if pending.scenicList.isEmpty {
Text("未选择景区")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 90), spacing: AppMetrics.Spacing.xSmall)], alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
ForEach(pending.scenicList) { scenic in
Text(scenic.name)
.font(.system(size: AppMetrics.FontSize.footnote))
.foregroundStyle(AppDesign.primary)
.padding(.horizontal, AppMetrics.Spacing.small)
.padding(.vertical, AppMetrics.Spacing.xSmall)
.background(AppDesign.primarySoft, in: Capsule())
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(AppMetrics.Spacing.small)
.background(.white, in: RoundedRectangle(cornerRadius: 12))
}
private func row(_ title: String, _ value: String?) -> some View {
HStack(spacing: AppMetrics.Spacing.xSmall) {
Text("\(title)")
.foregroundStyle(AppDesign.textSecondary)
Spacer()
Text(nonEmpty(value) ?? "--")
.foregroundStyle(AppDesign.textPrimary)
.multilineTextAlignment(.trailing)
}
.font(.system(size: AppMetrics.FontSize.footnote))
}
private func statusLabel(_ pending: RoleApplyPendingResponse) -> String {
nonEmpty(pending.statusLabel) ?? {
switch pending.status {
case 2: return "已通过"
case 3: return "已驳回"
case 1: return "审核中"
default: return "未知状态"
}
}()
}
private func statusIcon(_ status: Int) -> String {
switch status {
case 2: return "checkmark.circle.fill"
case 3: return "xmark.octagon.fill"
case 1: return "clock.badge.exclamationmark.fill"
default: return "questionmark.circle.fill"
}
}
private func statusColor(_ status: Int) -> Color {
switch status {
case 2: return AppDesign.success
case 3: return Color(hex: 0xDC2626)
case 1: return AppDesign.warning
default: return AppDesign.textSecondary
}
}
///
private func nonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
}