Wire home routing for album_list and album_trailer, add a DEBUG home menu preview in Profile, and expand Assets tests. Co-authored-by: Cursor <cursoragent@cursor.com>
489 lines
18 KiB
Swift
489 lines
18 KiB
Swift
//
|
||
// HomeMenuRouter.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/22.
|
||
//
|
||
|
||
import Foundation
|
||
import os
|
||
|
||
/// 首页菜单路由器,将后端权限 URI 映射为 iOS 内部路由。
|
||
enum HomeMenuRouter {
|
||
private static let titleMap: [String: String] = [
|
||
"basic_info": "基本信息",
|
||
"space_settings": "空间设置",
|
||
"album_list": "相册管理",
|
||
"album_trailer": "相册预览上传",
|
||
"photographer_stats": "数据统计",
|
||
"photographer_orders": "订单管理",
|
||
"/scenic-order-manage": "景区订单",
|
||
"verification_order": "核销订单",
|
||
"wallet": "我的钱包",
|
||
"cloud_management": "相册云盘",
|
||
"cloud_storage_transit": "传输管理",
|
||
"asset_management": "素材管理",
|
||
"material_upload": "上传素材",
|
||
"task_management": "任务管理",
|
||
"task_management_editor": "任务管理",
|
||
"task_create": "发布任务",
|
||
"schedule_management": "日程管理",
|
||
"system_settings": "设置中心",
|
||
"message_center": "消息中心",
|
||
"checkin_points": "打卡点管理",
|
||
"sample_management": "样片管理",
|
||
"sample_upload": "上传样片",
|
||
"live_stream_management": "直播管理",
|
||
"live_album": "直播相册",
|
||
"scenicselection": "景区选择",
|
||
"scenicapplication": "景区申请",
|
||
"permission_apply": "权限申请",
|
||
"permission_apply_status": "权限申请状态",
|
||
"scenic_settlement": "景区结算",
|
||
"scenic_settlement_review": "结算审核",
|
||
"pm": "项目管理",
|
||
"pm_manager": "项目管理",
|
||
"project_edit": "项目编辑",
|
||
"location_report": "位置上报",
|
||
"registration_invitation": "注册邀请",
|
||
"store": "店铺管理",
|
||
"fly": "飞行管理",
|
||
"pilot_cert": "飞手认证",
|
||
"pilot_controller": "飞控",
|
||
"/scenic-queue": "排队管理",
|
||
"queue_management": "排队管理",
|
||
"operating-area": "运营区域",
|
||
"payment_collection": "立即收款",
|
||
"payment_qr": "收款码",
|
||
"payment_code": "收款码",
|
||
"deposit_order_detail": "押金订单详情",
|
||
"deposit_order": "押金订单详情",
|
||
"deposit_order_shooting_info": "押金拍摄信息",
|
||
"withdrawal_audit": "提现审核",
|
||
"location_report_history": "定位上报历史",
|
||
"photographer_invite": "邀请摄影师",
|
||
"invite_record": "邀请记录",
|
||
"more_functions": "更多功能"
|
||
]
|
||
|
||
/// 根据权限 URI 解析跳转目标。
|
||
static func resolve(uri: String, title: String) -> HomeMenuResolvedRoute {
|
||
switch uri {
|
||
case "photographer_orders", "/scenic-order-manage":
|
||
return .orders(.storeOrders)
|
||
case "verification_order":
|
||
return .orders(.verificationOrders)
|
||
case "photographer_stats":
|
||
return .tab(.statistics)
|
||
case "space_settings", "basic_info":
|
||
return .destination(.profileSpace)
|
||
case "system_settings":
|
||
return .destination(.settings)
|
||
case "scenicselection":
|
||
return .destination(.scenicSelection)
|
||
case "permission_apply":
|
||
return .destination(.permissionApply)
|
||
case "permission_apply_status":
|
||
return .destination(.permissionApplyStatus)
|
||
case "scenicapplication":
|
||
return .destination(.scenicApplication)
|
||
case "wallet":
|
||
return .destination(.wallet)
|
||
case "payment_collection", "payment_qr", "payment_code":
|
||
return .destination(.paymentCollection)
|
||
case "task_management", "task_management_editor":
|
||
return .destination(.taskManagement)
|
||
case "task_create":
|
||
return .destination(.taskCreate)
|
||
case "cloud_management":
|
||
return .destination(.cloudStorage)
|
||
case "cloud_storage_transit":
|
||
return .destination(.cloudStorageTransit)
|
||
case "asset_management":
|
||
return .destination(.materialLibrary)
|
||
case "material_upload":
|
||
return .destination(.materialUpload)
|
||
case "album_list":
|
||
return .destination(.albumList)
|
||
case "album_trailer":
|
||
return .destination(.albumTrailer)
|
||
case "store", "more_functions":
|
||
return .destination(.moreFunctions)
|
||
case "fly", "pilot_controller":
|
||
return .unsupported(
|
||
uri: uri,
|
||
title: title.isEmpty ? self.title(for: uri) : title,
|
||
reason: "DJI/飞控模块已明确不纳入 iOS 迁移和后续开发范围。"
|
||
)
|
||
case "message_center",
|
||
"/scenic-queue",
|
||
"queue_management",
|
||
"deposit_order_detail",
|
||
"deposit_order",
|
||
"deposit_order_shooting_info",
|
||
"withdrawal_audit",
|
||
"schedule_management",
|
||
"checkin_points",
|
||
"pm",
|
||
"pm_manager",
|
||
"project_edit",
|
||
"sample_management",
|
||
"sample_upload",
|
||
"live_stream_management",
|
||
"live_album",
|
||
"scenic_settlement",
|
||
"scenic_settlement_review",
|
||
"operating-area",
|
||
"location_report_history",
|
||
"location_report",
|
||
"registration_invitation",
|
||
"photographer_invite",
|
||
"invite_record",
|
||
"pilot_cert":
|
||
let resolvedTitle = title.isEmpty ? self.title(for: uri) : title
|
||
return .destination(.modulePlaceholder(uri: uri, title: resolvedTitle))
|
||
default:
|
||
return .placeholder(uri: uri, title: title.isEmpty ? self.title(for: uri) : title)
|
||
}
|
||
}
|
||
|
||
/// 返回指定 URI 的默认标题。
|
||
static func title(for uri: String) -> String {
|
||
titleMap[uri] ?? uri
|
||
}
|
||
|
||
#if DEBUG
|
||
/// 返回调试页使用的全部已知首页菜单,不读取权限,便于预览迁移页面。
|
||
static func debugAllMenuItems() -> [HomeMenuItem] {
|
||
let preferredOrder = [
|
||
"space_settings",
|
||
"basic_info",
|
||
"album_list",
|
||
"album_trailer",
|
||
"wallet",
|
||
"payment_collection",
|
||
"payment_qr",
|
||
"payment_code",
|
||
"cloud_management",
|
||
"cloud_storage_transit",
|
||
"asset_management",
|
||
"material_upload",
|
||
"task_management",
|
||
"task_management_editor",
|
||
"task_create",
|
||
"schedule_management",
|
||
"system_settings",
|
||
"message_center",
|
||
"checkin_points",
|
||
"sample_management",
|
||
"sample_upload",
|
||
"live_stream_management",
|
||
"live_album",
|
||
"scenicselection",
|
||
"scenicapplication",
|
||
"permission_apply",
|
||
"permission_apply_status",
|
||
"scenic_settlement",
|
||
"scenic_settlement_review",
|
||
"verification_order",
|
||
"photographer_orders",
|
||
"/scenic-order-manage",
|
||
"photographer_stats",
|
||
"pm",
|
||
"pm_manager",
|
||
"project_edit",
|
||
"location_report",
|
||
"registration_invitation",
|
||
"photographer_invite",
|
||
"invite_record",
|
||
"store",
|
||
"fly",
|
||
"pilot_cert",
|
||
"pilot_controller",
|
||
"/scenic-queue",
|
||
"queue_management",
|
||
"operating-area",
|
||
"deposit_order_detail",
|
||
"deposit_order",
|
||
"deposit_order_shooting_info",
|
||
"withdrawal_audit",
|
||
"location_report_history",
|
||
"more_functions"
|
||
]
|
||
let orderedURIs = preferredOrder + titleMap.keys.sorted().filter { !preferredOrder.contains($0) }
|
||
return orderedURIs.map { uri in
|
||
HomeMenuItem(title: title(for: uri), uri: uri, iconSrc: nil)
|
||
}
|
||
}
|
||
#endif
|
||
|
||
/// 在可用 URI 集合中选取规范形式。
|
||
static func canonicalURI(for uri: String, availableURIs: Set<String>) -> String {
|
||
if availableURIs.contains(uri) {
|
||
return uri
|
||
}
|
||
switch uri {
|
||
case "task_management":
|
||
return availableURIs.contains("task_management_editor") ? "task_management_editor" : uri
|
||
case "task_management_editor":
|
||
return availableURIs.contains("task_management") ? "task_management" : uri
|
||
case "registration_invitation":
|
||
return availableURIs.contains("photographer_invite") ? "photographer_invite" : uri
|
||
case "photographer_invite":
|
||
return availableURIs.contains("registration_invitation") ? "registration_invitation" : uri
|
||
case "pm":
|
||
return availableURIs.contains("pm_manager") ? "pm_manager" : uri
|
||
case "pm_manager":
|
||
return availableURIs.contains("pm") ? "pm" : uri
|
||
case "payment_code":
|
||
return availableURIs.contains("payment_qr") ? "payment_qr" : uri
|
||
default:
|
||
return uri
|
||
}
|
||
}
|
||
|
||
/// 返回同义 URI 的去重键。
|
||
static func menuAliasKey(for uri: String) -> String {
|
||
switch uri {
|
||
case "task_management", "task_management_editor":
|
||
return "task_management"
|
||
case "registration_invitation", "photographer_invite":
|
||
return "registration_invitation"
|
||
case "pm", "pm_manager", "project_edit":
|
||
return "pm"
|
||
case "payment_collection", "payment_qr", "payment_code":
|
||
return "payment_collection"
|
||
case "deposit_order_detail", "deposit_order", "deposit_order_shooting_info":
|
||
return "deposit_order"
|
||
case "photographer_orders", "/scenic-order-manage":
|
||
return "photographer_orders"
|
||
case "/scenic-queue", "queue_management":
|
||
return "queue_management"
|
||
default:
|
||
return uri
|
||
}
|
||
}
|
||
|
||
/// 返回首页展示标题,部分同义入口统一文案。
|
||
static func displayTitle(for uri: String, fallback: String) -> String {
|
||
switch uri {
|
||
case "registration_invitation", "photographer_invite":
|
||
return "注册邀请"
|
||
case "location_report":
|
||
return "位置上报"
|
||
case "pm", "pm_manager":
|
||
return "项目管理"
|
||
case "space_settings":
|
||
return "空间设置"
|
||
case "task_management", "task_management_editor":
|
||
return "任务管理"
|
||
default:
|
||
return fallback
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 首页权限路由审计器,用于识别权限里哪些 URI 尚未映射。
|
||
enum HomePermissionRouteAuditor {
|
||
/// 扫描当前角色权限并分类统计可路由、已知不支持和未知 URI。
|
||
static func audit(permissions: [RolePermissionResponse], currentRoleId: Int?) -> HomePermissionRouteAudit {
|
||
let source: RolePermissionResponse?
|
||
if let currentRoleId {
|
||
source = permissions.first { $0.role.id == currentRoleId }
|
||
} else {
|
||
source = permissions.first
|
||
}
|
||
|
||
let entries = uniquePermissionItems(from: source?.role.permission ?? []).map { item in
|
||
let title = item.name.isEmpty ? HomeMenuRouter.title(for: item.uri) : item.name
|
||
return HomePermissionRouteAuditEntry(
|
||
uri: item.uri,
|
||
title: title,
|
||
route: HomeMenuRouter.resolve(uri: item.uri, title: title)
|
||
)
|
||
}
|
||
|
||
return HomePermissionRouteAudit(
|
||
routable: entries.filter { entry in
|
||
switch entry.route {
|
||
case .tab, .orders, .destination:
|
||
return true
|
||
case .unsupported, .placeholder:
|
||
return false
|
||
}
|
||
},
|
||
unsupported: entries.filter { entry in
|
||
if case .unsupported = entry.route { return true }
|
||
return false
|
||
},
|
||
unknown: entries.filter { entry in
|
||
if case .placeholder = entry.route { return true }
|
||
return false
|
||
}
|
||
)
|
||
}
|
||
|
||
/// 生成 Markdown 审计报告。
|
||
static func markdownReport(audit: HomePermissionRouteAudit, generatedAt: Date = Date()) -> String {
|
||
var lines = [
|
||
"# Home Permission Route Audit",
|
||
"",
|
||
"Generated: \(reportDateFormatter.string(from: generatedAt))",
|
||
"",
|
||
"Routable: \(audit.routable.count)",
|
||
"Unsupported: \(audit.unsupported.count)",
|
||
"Unknown: \(audit.unknown.count)",
|
||
""
|
||
]
|
||
|
||
if audit.unknown.isEmpty {
|
||
lines.append("No unknown permission routes.")
|
||
} else {
|
||
lines.append("## Unknown Routes")
|
||
lines.append("")
|
||
lines.append("| URI | Title |")
|
||
lines.append("| --- | --- |")
|
||
lines.append(contentsOf: audit.unknown.map { "| \(escape($0.uri)) | \(escape($0.title)) |" })
|
||
lines.append("")
|
||
lines.append("Action: map these URIs in `HomeMenuRouter`, confirm unsupported scope, or remove them before release.")
|
||
}
|
||
|
||
if !audit.unsupported.isEmpty {
|
||
lines.append("")
|
||
lines.append("## Known Unsupported Routes")
|
||
lines.append("")
|
||
lines.append("| URI | Title | Reason |")
|
||
lines.append("| --- | --- | --- |")
|
||
lines.append(contentsOf: audit.unsupported.map { entry in
|
||
let reason: String
|
||
if case let .unsupported(_, _, value) = entry.route {
|
||
reason = value
|
||
} else {
|
||
reason = ""
|
||
}
|
||
return "| \(escape(entry.uri)) | \(escape(entry.title)) | \(escape(reason)) |"
|
||
})
|
||
}
|
||
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
private static var reportDateFormatter: ISO8601DateFormatter {
|
||
let formatter = ISO8601DateFormatter()
|
||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||
return formatter
|
||
}
|
||
|
||
/// 从权限树提取非空 URI,并按同义 URI 去重。
|
||
private static func uniquePermissionItems(from items: [PermissionItem]) -> [PermissionItem] {
|
||
var seen = Set<String>()
|
||
return flatten(items).filter { item in
|
||
guard !item.uri.isEmpty else { return false }
|
||
return seen.insert(HomeMenuRouter.menuAliasKey(for: item.uri)).inserted
|
||
}
|
||
}
|
||
|
||
/// 递归展开权限树。
|
||
private static func flatten(_ items: [PermissionItem]) -> [PermissionItem] {
|
||
items.flatMap { item in
|
||
[item] + flatten(item.children)
|
||
}
|
||
}
|
||
|
||
/// 转义 Markdown 表格中的特殊字符。
|
||
private static func escape(_ value: String) -> String {
|
||
value
|
||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||
.replacingOccurrences(of: "|", with: "\\|")
|
||
.replacingOccurrences(of: "\n", with: " ")
|
||
}
|
||
}
|
||
|
||
/// 首页未知路由诊断工具,记录运行时无法识别的权限 URI。
|
||
enum HomeRouteDiagnostics {
|
||
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeRouting")
|
||
private static let defaultsKey = "home.routing.unknown.records"
|
||
private static let dateFormatter: ISO8601DateFormatter = {
|
||
let formatter = ISO8601DateFormatter()
|
||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||
return formatter
|
||
}()
|
||
|
||
/// 记录一个未知首页 URI。
|
||
static func recordUnknown(uri: String, title: String) {
|
||
logger.warning("Unknown home menu uri: \(uri, privacy: .public), title: \(title, privacy: .public)")
|
||
var records = unknownRoutes()
|
||
let now = Date()
|
||
if let index = records.firstIndex(where: { $0.uri == uri }) {
|
||
let current = records[index]
|
||
records[index] = UnknownHomeRouteRecord(
|
||
uri: current.uri,
|
||
title: title.isEmpty ? current.title : title,
|
||
firstSeenAt: current.firstSeenAt,
|
||
lastSeenAt: now,
|
||
count: current.count + 1
|
||
)
|
||
} else {
|
||
records.append(UnknownHomeRouteRecord(uri: uri, title: title, firstSeenAt: now, lastSeenAt: now, count: 1))
|
||
}
|
||
save(records)
|
||
}
|
||
|
||
/// 读取所有未知首页路由记录。
|
||
static func unknownRoutes() -> [UnknownHomeRouteRecord] {
|
||
guard let data = UserDefaults.standard.data(forKey: defaultsKey),
|
||
let records = try? JSONDecoder().decode([UnknownHomeRouteRecord].self, from: data)
|
||
else { return [] }
|
||
return records
|
||
}
|
||
|
||
/// 生成未知首页路由 Markdown 报告。
|
||
static func unknownRouteReport(generatedAt: Date = Date()) -> String {
|
||
let records = unknownRoutes().sorted { lhs, rhs in
|
||
if lhs.count != rhs.count {
|
||
return lhs.count > rhs.count
|
||
}
|
||
return lhs.lastSeenAt > rhs.lastSeenAt
|
||
}
|
||
var lines = [
|
||
"# Home Route Diagnostics",
|
||
"",
|
||
"Generated: \(dateFormatter.string(from: generatedAt))",
|
||
""
|
||
]
|
||
|
||
guard !records.isEmpty else {
|
||
lines.append("No unknown home routes recorded.")
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
lines.append("| URI | Title | Count | First Seen | Last Seen |")
|
||
lines.append("| --- | --- | ---: | --- | --- |")
|
||
lines.append(contentsOf: records.map { record in
|
||
"| \(escape(record.uri)) | \(escape(record.title)) | \(record.count) | \(dateFormatter.string(from: record.firstSeenAt)) | \(dateFormatter.string(from: record.lastSeenAt)) |"
|
||
})
|
||
lines.append("")
|
||
lines.append("Action: each URI above must be mapped in `HomeMenuRouter`, confirmed as unsupported scope, or explicitly accepted before release.")
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
/// 清空未知首页路由记录。
|
||
static func resetUnknownRoutes() {
|
||
UserDefaults.standard.removeObject(forKey: defaultsKey)
|
||
}
|
||
|
||
/// 保存未知首页路由记录。
|
||
private static func save(_ records: [UnknownHomeRouteRecord]) {
|
||
guard let data = try? JSONEncoder().encode(records) else { return }
|
||
UserDefaults.standard.set(data, forKey: defaultsKey)
|
||
}
|
||
|
||
/// 转义 Markdown 表格中的特殊字符。
|
||
private static func escape(_ value: String) -> String {
|
||
value
|
||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||
.replacingOccurrences(of: "|", with: "\\|")
|
||
.replacingOccurrences(of: "\n", with: " ")
|
||
}
|
||
}
|