Align iOS home common apps and all-functions page with Android.
Add unified HomeCommonMenu/HomeAllFunctions diagnostics, rebuild all-functions from top-level menuList permissions, and document Home and AllFunctions module logic. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,57 @@
|
||||
//
|
||||
// HomeAllFunctionsBuilder.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/30.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 「全部功能」页菜单快照,包含完整列表及按常用 URI 拆分后的分区。
|
||||
struct HomeAllFunctionsSnapshot: Equatable {
|
||||
let allFunctions: [HomeMenuItem]
|
||||
let commonFunctions: [HomeMenuItem]
|
||||
let moreFunctions: [HomeMenuItem]
|
||||
}
|
||||
|
||||
/// 「全部功能」页菜单构建器,与 Android `AllFunctionsViewModel` 对齐。
|
||||
enum HomeAllFunctionsBuilder {
|
||||
/// 从顶层权限构建全部功能列表,并按常用 URI 精确拆分为常用/更多。
|
||||
static func build(
|
||||
topLevelPermissions: [PermissionItem],
|
||||
commonURIs: [String]
|
||||
) -> HomeAllFunctionsSnapshot {
|
||||
let allFunctions = allMenuItems(from: topLevelPermissions)
|
||||
let commonSet = Set(commonURIs)
|
||||
let commonFunctions = allFunctions.filter { commonSet.contains($0.uri) }
|
||||
let moreFunctions = allFunctions.filter { !commonSet.contains($0.uri) }
|
||||
|
||||
HomeAllFunctionsDiagnostics.log(snapshot: HomeAllFunctionsSnapshot(
|
||||
allFunctions: allFunctions,
|
||||
commonFunctions: commonFunctions,
|
||||
moreFunctions: moreFunctions
|
||||
))
|
||||
|
||||
return HomeAllFunctionsSnapshot(
|
||||
allFunctions: allFunctions,
|
||||
commonFunctions: commonFunctions,
|
||||
moreFunctions: moreFunctions
|
||||
)
|
||||
}
|
||||
|
||||
/// 将顶层权限转为 menuList 白名单内的菜单项,保持 API 返回顺序。
|
||||
private static func allMenuItems(from topLevelPermissions: [PermissionItem]) -> [HomeMenuItem] {
|
||||
topLevelPermissions.compactMap { item -> HomeMenuItem? in
|
||||
let uri = item.uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !uri.isEmpty, HomeCommonMenuStore.androidHomeMenuURIs.contains(uri) else {
|
||||
return nil
|
||||
}
|
||||
let fallbackTitle = item.name.isEmpty ? HomeMenuRouter.title(for: uri) : item.name
|
||||
return HomeMenuItem(
|
||||
title: HomeMenuRouter.displayTitle(for: uri, fallback: fallbackTitle),
|
||||
uri: uri,
|
||||
iconSrc: item.iconSrc
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
//
|
||||
// HomeAllFunctionsDiagnostics.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/30.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
/// 「全部功能」页诊断日志,便于与 Android `AllFunctionsViewModel` 输出对比。
|
||||
enum HomeAllFunctionsDiagnostics {
|
||||
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeAllFunctions")
|
||||
|
||||
/// 记录全部功能页的分区菜单 URI 列表。
|
||||
static func log(snapshot: HomeAllFunctionsSnapshot) {
|
||||
#if DEBUG
|
||||
log(step: "allFunctions", uris: snapshot.allFunctions.map(\.uri))
|
||||
log(step: "commonFunctions", uris: snapshot.commonFunctions.map(\.uri))
|
||||
log(step: "moreFunctions", uris: snapshot.moreFunctions.map(\.uri))
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 记录单步 URI 列表。
|
||||
private static func log(step: String, uris: [String]) {
|
||||
let payload = "count=\(uris.count) uris=\(HomeCommonMenuDiagnostics.formatURIList(uris))"
|
||||
logger.debug("step=\(step, privacy: .public) \(payload, privacy: .public)")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
//
|
||||
// HomeCommonMenuDiagnostics.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/30.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
/// 首页常用应用诊断日志,输出与 Android `HomeCommonMenu` tag 对齐的字段。
|
||||
enum HomeCommonMenuDiagnostics {
|
||||
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeCommonMenu")
|
||||
|
||||
/// 记录常用应用流水线中的单步诊断信息。
|
||||
static func log(step: String, fields: [String: String]) {
|
||||
#if DEBUG
|
||||
let payload = fields.map { "\($0.key)=\($0.value)" }.joined(separator: " ")
|
||||
logger.debug("step=\(step, privacy: .public) \(payload, privacy: .public)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 将 URI 列表格式化为日志可读字符串。
|
||||
static func formatURIList(_ uris: [String]) -> String {
|
||||
"[\(uris.joined(separator: ","))]"
|
||||
}
|
||||
|
||||
/// 将展示项格式化为 title|uri 列表。
|
||||
static func formatDisplayItems(_ items: [(title: String, uri: String)]) -> String {
|
||||
let labels = items.map { "\($0.title)|\($0.uri)" }
|
||||
return "[\(labels.joined(separator: ","))]"
|
||||
}
|
||||
}
|
||||
@ -49,25 +49,26 @@ struct HomeCommonMenuStore {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
/// 按 Android 规则生成默认常用 URI:顶层权限顺序、可用权限过滤、别名去重,超过 4 个取前 4 个。
|
||||
static func defaultCommonURIs(
|
||||
orderedTopLevelURIs: [String],
|
||||
availableURIs: Set<String>
|
||||
) -> [String] {
|
||||
let resolved = orderedTopLevelURIs.compactMap { uri -> String? in
|
||||
/// 按 Android 规则生成默认常用 URI:先取顶层前 4 个 URI,再与 menuList 白名单求交。
|
||||
static func defaultCommonURIs(orderedTopLevelURIs: [String]) -> [String] {
|
||||
let rawTopLevel = orderedTopLevelURIs.compactMap { uri -> String? in
|
||||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
let canonical = HomeMenuRouter.canonicalURI(for: trimmed, availableURIs: availableURIs)
|
||||
return Self.isAndroidHomeMenuURI(canonical) && availableURIs.contains(canonical) ? canonical : nil
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
let deduplicated = unique(resolved)
|
||||
if deduplicated.count > 4 {
|
||||
return Array(deduplicated.prefix(4))
|
||||
}
|
||||
return deduplicated
|
||||
let candidates = rawTopLevel.count > 4 ? Array(rawTopLevel.prefix(4)) : rawTopLevel
|
||||
return candidates.filter { isAndroidHomeMenuURI($0) }
|
||||
}
|
||||
|
||||
/// 读取常用 URI;无角色时返回空,无保存或过滤后为空时写入 Android 对齐默认值。
|
||||
/// 返回顶层权限中属于 Android menuList 的 URI,顺序与 API 一致。
|
||||
static func availableTopLevelMenuURIs(from orderedTopLevelURIs: [String]) -> [String] {
|
||||
orderedTopLevelURIs.compactMap { uri -> String? in
|
||||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty, isAndroidHomeMenuURI(trimmed) else { return nil }
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取常用 URI;无角色时返回空,有 saved 时精确匹配且不回退默认。
|
||||
func load(
|
||||
menuItems: [HomeMenuItem],
|
||||
roleCode: String?,
|
||||
@ -78,55 +79,99 @@ struct HomeCommonMenuStore {
|
||||
guard let roleCode, !roleCode.isEmpty else { return [] }
|
||||
migrateLegacyStorageIfNeeded(roleCode: roleCode, accountScope: accountScope, legacyRoleId: legacyRoleId)
|
||||
|
||||
let availableURIs = Set(menuItems.map(\.uri))
|
||||
let rawTopLevel = topLevelPermissionURIs.compactMap { uri -> String? in
|
||||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
let availableTopLevel = Self.availableTopLevelMenuURIs(from: rawTopLevel)
|
||||
let availableTopLevelSet = Set(availableTopLevel)
|
||||
let key = Self.storageKey(roleCode: roleCode, accountScope: accountScope)
|
||||
let saved = defaults.stringArray(forKey: key)
|
||||
let saved = defaults.stringArray(forKey: key) ?? []
|
||||
let defaultCommon = Self.defaultCommonURIs(orderedTopLevelURIs: rawTopLevel)
|
||||
|
||||
if let saved, !saved.isEmpty {
|
||||
let filtered = Self.unique(saved.compactMap { uri -> String? in
|
||||
let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
|
||||
return Self.isAndroidHomeMenuURI(resolved) && availableURIs.contains(resolved) ? resolved : nil
|
||||
})
|
||||
if !filtered.isEmpty {
|
||||
save(filtered, roleCode: roleCode, accountScope: accountScope)
|
||||
return filtered
|
||||
}
|
||||
HomeCommonMenuDiagnostics.log(
|
||||
step: "context",
|
||||
fields: [
|
||||
"roleCode": roleCode,
|
||||
"accountScope": accountScope ?? "",
|
||||
"storageKey": key
|
||||
]
|
||||
)
|
||||
HomeCommonMenuDiagnostics.log(
|
||||
step: "rawTopLevel",
|
||||
fields: [
|
||||
"count": String(rawTopLevel.count),
|
||||
"uris": HomeCommonMenuDiagnostics.formatURIList(rawTopLevel)
|
||||
]
|
||||
)
|
||||
HomeCommonMenuDiagnostics.log(
|
||||
step: "availableFunctions",
|
||||
fields: [
|
||||
"count": String(availableTopLevel.count),
|
||||
"uris": HomeCommonMenuDiagnostics.formatURIList(availableTopLevel)
|
||||
]
|
||||
)
|
||||
HomeCommonMenuDiagnostics.log(
|
||||
step: "savedCommon",
|
||||
fields: [
|
||||
"uris": HomeCommonMenuDiagnostics.formatURIList(saved)
|
||||
]
|
||||
)
|
||||
HomeCommonMenuDiagnostics.log(
|
||||
step: "defaultCommon",
|
||||
fields: [
|
||||
"uris": HomeCommonMenuDiagnostics.formatURIList(defaultCommon)
|
||||
]
|
||||
)
|
||||
|
||||
let finalCommon: [String]
|
||||
if !saved.isEmpty {
|
||||
finalCommon = saved.filter { availableTopLevelSet.contains($0) }
|
||||
} else {
|
||||
finalCommon = defaultCommon
|
||||
save(finalCommon, roleCode: roleCode, accountScope: accountScope)
|
||||
}
|
||||
|
||||
let defaultURIs = Self.defaultCommonURIs(
|
||||
orderedTopLevelURIs: topLevelPermissionURIs,
|
||||
availableURIs: availableURIs
|
||||
HomeCommonMenuDiagnostics.log(
|
||||
step: "finalCommon",
|
||||
fields: [
|
||||
"count": String(finalCommon.count),
|
||||
"uris": HomeCommonMenuDiagnostics.formatURIList(finalCommon)
|
||||
]
|
||||
)
|
||||
save(defaultURIs, roleCode: roleCode, accountScope: accountScope)
|
||||
return defaultURIs
|
||||
_ = menuItems
|
||||
return finalCommon
|
||||
}
|
||||
|
||||
/// 将指定 URI 加入常用应用,按别名避免重复添加。
|
||||
func add(_ uri: String, current: [String], menuItems: [HomeMenuItem], roleCode: String?, accountScope: String? = nil) -> [String] {
|
||||
/// 将指定 URI 加入常用应用,使用精确 URI 匹配。
|
||||
func add(
|
||||
_ uri: String,
|
||||
current: [String],
|
||||
topLevelPermissionURIs: [String],
|
||||
roleCode: String?,
|
||||
accountScope: String? = nil
|
||||
) -> [String] {
|
||||
guard let roleCode, !roleCode.isEmpty else { return current }
|
||||
let availableURIs = Set(menuItems.map(\.uri))
|
||||
let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
|
||||
guard Self.isAndroidHomeMenuURI(resolved), availableURIs.contains(resolved) else { return current }
|
||||
guard !current.contains(where: { HomeMenuRouter.menuAliasKey(for: $0) == HomeMenuRouter.menuAliasKey(for: resolved) }) else {
|
||||
return current
|
||||
}
|
||||
let next = current + [resolved]
|
||||
let availableTopLevel = Set(Self.availableTopLevelMenuURIs(from: topLevelPermissionURIs))
|
||||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard availableTopLevel.contains(trimmed), !current.contains(trimmed) else { return current }
|
||||
let next = current + [trimmed]
|
||||
save(next, roleCode: roleCode, accountScope: accountScope)
|
||||
return next
|
||||
}
|
||||
|
||||
/// 从常用应用中移除指定 URI,同义 URI 会一起移除。
|
||||
/// 从常用应用中移除指定 URI,使用精确 URI 匹配。
|
||||
func remove(_ uri: String, current: [String], roleCode: String?, accountScope: String? = nil) -> [String] {
|
||||
guard let roleCode, !roleCode.isEmpty else { return current }
|
||||
let aliasKey = HomeMenuRouter.menuAliasKey(for: uri)
|
||||
let next = current.filter { HomeMenuRouter.menuAliasKey(for: $0) != aliasKey }
|
||||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let next = current.filter { $0 != trimmed }
|
||||
save(next, roleCode: roleCode, accountScope: accountScope)
|
||||
return next
|
||||
}
|
||||
|
||||
/// 保存指定角色的常用 URI。
|
||||
func save(_ uris: [String], roleCode: String, accountScope: String? = nil) {
|
||||
defaults.set(Self.unique(uris), forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope))
|
||||
defaults.set(uris, forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope))
|
||||
}
|
||||
|
||||
/// 若账号级 key 无数据,则从旧 roleCode 或 legacy role id key 一次性迁移常用菜单。
|
||||
@ -170,12 +215,4 @@ struct HomeCommonMenuStore {
|
||||
private static func isAndroidHomeMenuURI(_ uri: String) -> Bool {
|
||||
androidHomeMenuURIs.contains(uri)
|
||||
}
|
||||
|
||||
/// 按 URI 别名去重,保留首次出现的 URI。
|
||||
private static func unique(_ uris: [String]) -> [String] {
|
||||
var seen = Set<String>()
|
||||
return uris.filter { uri in
|
||||
seen.insert(HomeMenuRouter.menuAliasKey(for: uri)).inserted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user