Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,503 @@
//
// 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 "schedule_management":
return .destination(.scheduleManagement)
case "pm", "project_edit":
return .destination(.projectManagement)
case "pm_manager":
return .destination(.pmProjectManagement)
case "registration_invitation", "photographer_invite":
return .destination(.photographerInvite)
case "invite_record":
return .destination(.inviteRecord)
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 "sample_management":
return .destination(.sampleLibrary)
case "sample_upload":
return .destination(.sampleUpload)
case "album_list":
return .destination(.albumList)
case "album_trailer":
return .destination(.albumTrailer)
case "checkin_points":
return .destination(.punchPointList)
case "location_report":
return .destination(.locationReport)
case "location_report_history":
return .destination(.locationReportHistory)
case "deposit_order_detail", "deposit_order", "deposit_order_shooting_info":
return .destination(.depositOrders)
case "withdrawal_audit":
return .destination(.withdrawalAudit)
case "scenic_settlement":
return .destination(.scenicSettlement)
case "scenic_settlement_review":
return .destination(.scenicSettlementReview)
case "message_center":
return .destination(.messageCenter)
case "/scenic-queue", "queue_management":
return .destination(.queueManagement)
case "live_stream_management":
return .destination(.liveManagement)
case "live_album":
return .destination(.liveAlbum)
case "operating-area":
return .destination(.operatingArea)
case "pilot_cert":
return .destination(.pilotCertification)
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 迁移和后续开发范围。"
)
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("project_edit") ? "project_edit" : uri
case "project_edit":
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", "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", "project_edit":
return "项目管理"
case "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: " ")
}
}