模块化 AppStore 并完善素材管理与个人空间设置。

将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 11:01:08 +08:00
parent ceca780ab3
commit 005349f8e6
128 changed files with 2953 additions and 1123 deletions

View File

@ -24,8 +24,8 @@ final class HomeLocationStateStore {
init(store: AppStore = .shared) {
self.store = store
reminderMinutes = store.locationReminderMinutes
isOnline = store.onlineStatus
reminderMinutes = store.location.reminderMinutes
isOnline = store.location.onlineStatus
}
deinit {
@ -44,18 +44,18 @@ final class HomeLocationStateStore {
/// 线
func restoreStateIfNeeded() {
reminderMinutes = store.locationReminderMinutes
guard store.onlineStatus else {
reminderMinutes = store.location.reminderMinutes
guard store.location.onlineStatus else {
isOnline = false
stopCountdown()
notifyChange()
return
}
let lastTime = store.lastLocationReportTime
let lastTime = store.location.lastReportTime
guard lastTime > 0 else {
isOnline = false
store.onlineStatus = false
store.clearLastLocationReportTime()
store.location.onlineStatus = false
store.location.clearLastReportTime()
stopCountdown()
notifyChange()
return
@ -65,7 +65,7 @@ final class HomeLocationStateStore {
let elapsed = now - lastTime
if elapsed >= Self.onlineDurationMillis {
updateOnlineStatus(false)
store.clearLastLocationReportTime()
store.location.clearLastReportTime()
notifyChange()
return
}
@ -81,10 +81,10 @@ final class HomeLocationStateStore {
/// 线
func updateOnlineStatus(_ online: Bool) {
isOnline = online
store.onlineStatus = online
store.location.onlineStatus = online
if !online {
stopCountdown()
store.clearLastLocationReportTime()
store.location.clearLastReportTime()
}
notifyChange()
}
@ -92,16 +92,16 @@ final class HomeLocationStateStore {
///
func updateReminderMinutes(_ minutes: Int) {
reminderMinutes = minutes
store.locationReminderMinutes = minutes
store.location.reminderMinutes = minutes
notifyChange()
}
///
func startLocationReport(at timestamp: Int64 = Int64(Date().timeIntervalSince1970 * 1000)) {
anchorTimestamp = timestamp
store.lastLocationReportTime = timestamp
store.location.lastReportTime = timestamp
isOnline = true
store.onlineStatus = true
store.location.onlineStatus = true
elapsedSeconds = 0
nextReportCountdownSeconds = Self.onlineDurationMillis / 1000
startCountdown(anchorTime: timestamp)
@ -156,7 +156,7 @@ final class HomeLocationStateStore {
if remaining <= 0 {
self.nextReportCountdownSeconds = 0
self.updateOnlineStatus(false)
self.store.clearLastLocationReportTime()
self.store.location.clearLastReportTime()
return
}
self.nextReportCountdownSeconds = max(0, remaining / 1000)

View File

@ -10,10 +10,10 @@ enum HomeMenuCatalog {
/// Android `Constants.menuList`
static let allItems: [HomeMenuItem] = [
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "person.crop.square.fill"),
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
HomeMenuItem(uri: "wallet", title: "我的钱包", iconName: "creditcard.fill"),
HomeMenuItem(uri: "cloud_management", title: "相册云盘", iconName: "icloud.fill"),
HomeMenuItem(uri: "asset_management", title: "素材管理", iconName: "photo.on.rectangle.angled"),
HomeMenuItem(uri: "asset_management", title: "素材管理", iconName: "home_menu_material"),
HomeMenuItem(uri: "task_management", title: "任务管理", iconName: "checklist"),
HomeMenuItem(uri: "task_management_editor", title: "任务管理", iconName: "checklist"),
HomeMenuItem(uri: "schedule_management", title: "日程管理", iconName: "calendar"),

View File

@ -21,7 +21,7 @@ enum HomeRouteHandler {
return
}
if uri != "pilot_controller", uri != "wallet", AppStore.shared.currentScenicId <= 0 {
if uri != "pilot_controller", uri != "wallet", AppStore.shared.session.currentScenicId <= 0 {
showToast("请先选择景区", from: viewController)
return
}
@ -87,7 +87,7 @@ enum HomeRouteHandler {
@MainActor
private static func handleOperatingArea(from viewController: UIViewController, storeItem: StoreItem?) {
guard let role = AppStore.shared.currentAppRole else {
guard let role = AppStore.shared.session.currentAppRole else {
showToast("当前账号暂不支持运营区域", from: viewController)
return
}
@ -99,7 +99,7 @@ enum HomeRouteHandler {
}
showDeveloping(from: viewController)
case .scenicAdmin:
guard AppStore.shared.currentScenicId > 0 else {
guard AppStore.shared.session.currentScenicId > 0 else {
showToast("请先选择景区", from: viewController)
return
}
@ -110,7 +110,7 @@ enum HomeRouteHandler {
}
private static func hasCooperationOrderPermission() -> Bool {
let permissions = AppStore.shared.permissionItems()
let permissions = AppStore.shared.permissions.permissionItems()
return permissions.contains { $0.uri == "cooperation_order" }
|| permissions.contains(where: { containsCooperationOrder(in: $0) })
}

View File

@ -27,10 +27,10 @@ final class AllFunctionsViewModel {
///
func loadFunctions() {
let permissions = appStore.permissionItems()
let permissions = appStore.permissions.permissionItems()
let allMenus = HomeMenuCatalog.visibleMenus(from: permissions)
let accountScope = appStore.accountCachePrefix
let roleCode = appStore.roleCode
let accountScope = appStore.session.accountCachePrefix
let roleCode = appStore.session.roleCode
let savedURIs = commonMenuStore.savedCommonURIs(accountScope: accountScope, roleCode: roleCode)
let defaultURIs = HomeCommonMenuStore.defaultCommonURIs(from: permissions)
let commonURIs = savedURIs.isEmpty ? defaultURIs : savedURIs
@ -71,8 +71,8 @@ final class AllFunctionsViewModel {
private func persistCommonMenus() {
commonMenuStore.saveCommonURIs(
commonMenus.map(\.uri),
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
)
}

View File

@ -94,7 +94,7 @@ final class HomeViewModel {
/// role-permission
func loadPermissions(api: HomeAPI, force: Bool = false) async {
if !force, !appStore.permissionItems().isEmpty {
if !force, !appStore.permissions.permissionItems().isEmpty {
rebuildCommonMenus()
await loadStoreListIfNeeded(api: api)
notifyStateChange()
@ -104,21 +104,21 @@ final class HomeViewModel {
do {
let rolePermissionList = try await api.rolePermissions()
if rolePermissionList.isEmpty {
appStore.savePermissionItems([])
appStore.permissions.savePermissionItems([])
showPermissionDialog = true
rebuildCommonMenus()
notifyStateChange()
return
}
appStore.saveRolePermissionList(rolePermissionList)
let matched = appStore.matchRolePermissionItem(in: rolePermissionList) ?? rolePermissionList[0]
appStore.permissions.saveRolePermissionList(rolePermissionList)
let matched = appStore.permissions.matchRolePermissionItem(in: rolePermissionList) ?? rolePermissionList[0]
if !matched.role.name.isEmpty {
appStore.roleName = matched.role.name
appStore.session.roleName = matched.role.name
}
appStore.saveRoleScenicList(matched.scenic)
appStore.permissions.saveRoleScenicList(matched.scenic)
let permissions = matched.role.permission.map(HomePermissionItem.init)
appStore.savePermissionItems(permissions)
appStore.permissions.savePermissionItems(permissions)
showPermissionDialog = false
refreshLocalDisplayState()
rebuildCommonMenus()
@ -127,7 +127,7 @@ final class HomeViewModel {
} catch is CancellationError {
return
} catch {
appStore.savePermissionItems([])
appStore.permissions.savePermissionItems([])
showPermissionDialog = true
rebuildCommonMenus()
onShowMessage?(error.localizedDescription)
@ -142,7 +142,7 @@ final class HomeViewModel {
notifyStateChange()
return
}
guard appStore.currentScenicId > 0 else {
guard appStore.session.currentScenicId > 0 else {
storeItem = nil
notifyStateChange()
return
@ -150,13 +150,13 @@ final class HomeViewModel {
do {
let response = try await api.storeList()
let scenicId = appStore.currentScenicId
let savedStoreId = appStore.currentStoreId
let scenicId = appStore.session.currentScenicId
let savedStoreId = appStore.session.currentStoreId
let savedMatch = savedStoreId > 0 ? response.list.first { $0.id == savedStoreId } : nil
let matched = savedMatch ?? response.list.first { $0.scenicId == scenicId }
storeItem = matched
if let matched {
appStore.currentStoreId = matched.id
appStore.session.currentStoreId = matched.id
}
notifyStateChange()
} catch is CancellationError {
@ -173,7 +173,7 @@ final class HomeViewModel {
showScenicDialog = false
return
}
let hasScenic = appStore.currentScenicId > 0
let hasScenic = appStore.session.currentScenicId > 0
showScenicDialog = !hasScenic
if showScenicDialog || dismissedLocationTimeoutDialog {
showLocationTimeoutDialog = false
@ -196,7 +196,7 @@ final class HomeViewModel {
notifyStateChange()
return
}
let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines)
let staffId = appStore.session.userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !staffId.isEmpty else {
showLocationTimeoutDialog = true
notifyStateChange()
@ -225,25 +225,25 @@ final class HomeViewModel {
/// Android `initCurrentScenicName`
func refreshLocalDisplayState() {
let scenicId = appStore.currentScenicId
let scenicName = appStore.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
let scenicId = appStore.session.currentScenicId
let scenicName = appStore.session.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
if scenicId > 0, !scenicName.isEmpty {
currentScenicName = scenicName
} else {
currentScenicName = ""
appStore.currentScenicId = 0
appStore.currentScenicName = ""
appStore.session.currentScenicId = 0
appStore.session.currentScenicName = ""
}
currentAppRole = appStore.currentAppRole
currentAppRole = appStore.session.currentAppRole
isMinimalTopRole = currentAppRole?.usesHomeMinimalTopLayout ?? false
}
///
func rebuildCommonMenus() {
let permissions = appStore.permissionItems()
let permissions = appStore.permissions.permissionItems()
let allMenus = HomeMenuCatalog.visibleMenus(from: permissions)
let accountScope = appStore.accountCachePrefix
let roleCode = appStore.roleCode
let accountScope = appStore.session.accountCachePrefix
let roleCode = appStore.session.roleCode
let savedURIs = commonMenuStore.savedCommonURIs(accountScope: accountScope, roleCode: roleCode)
if savedURIs.isEmpty, !permissions.isEmpty {

View File

@ -21,7 +21,7 @@ final class PermissionApplyStatusViewModel {
init(applyCode: String, appStore: AppStore = .shared) {
self.applyCode = applyCode
self.appStore = appStore
rolePermissionList = appStore.rolePermissionList()
rolePermissionList = appStore.permissions.rolePermissionList()
}
func loadPendingData(api: HomeAPI) async {

View File

@ -45,7 +45,7 @@ final class PermissionApplyViewModel {
}
func loadRolesFromLocal() {
let rolePermissionList = appStore.rolePermissionList()
let rolePermissionList = appStore.permissions.rolePermissionList()
var seen = Set<Int>()
availableRoles = rolePermissionList.compactMap { item in
guard !seen.contains(item.role.id) else { return nil }
@ -158,7 +158,7 @@ final class PermissionApplyViewModel {
///
func existingScenics(for roleId: Int) -> [ScenicInfo] {
appStore.rolePermissionList()
appStore.permissions.rolePermissionList()
.filter { $0.role.id == roleId }
.flatMap(\.scenic)
}

View File

@ -43,7 +43,7 @@ final class ScenicSelectionViewModel {
searchQuery: searchQuery,
currentLat: currentLat,
currentLng: currentLng,
selectedScenicId: appStore.currentScenicId
selectedScenicId: appStore.session.currentScenicId
)
notifyStateChange()
}
@ -73,8 +73,8 @@ final class ScenicSelectionViewModel {
}
func selectSpot(_ spot: ScenicSpotDisplayItem) {
appStore.currentScenicId = spot.id
appStore.currentScenicName = spot.name
appStore.session.currentScenicId = spot.id
appStore.session.currentScenicName = spot.name
NotificationCenter.default.post(
name: NotificationName.scenicDidChange,
object: nil,
@ -109,11 +109,11 @@ final class ScenicSelectionViewModel {
}
private func resolvedScenicList() -> [ScenicInfo] {
let cached = appStore.roleScenicList()
let cached = appStore.permissions.roleScenicList()
if !cached.isEmpty { return cached }
let rolePermissionList = appStore.rolePermissionList()
let rolePermissionList = appStore.permissions.rolePermissionList()
guard !rolePermissionList.isEmpty else { return [] }
return appStore.matchRolePermissionItem(in: rolePermissionList)?.scenic ?? []
return appStore.permissions.matchRolePermissionItem(in: rolePermissionList)?.scenic ?? []
}
private func notifyStateChange() {