Refactor home to single collection view and add all-functions customization.

Replace scroll/stack layout with one UICollectionView, fix grid column math and Diffable item IDs so location report and quick actions render, and add the customizable all-functions page with persistence and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 09:57:41 +08:00
parent d8329d19fd
commit 715393e78c
14 changed files with 1222 additions and 209 deletions

View File

@ -0,0 +1,123 @@
//
// AllFunctionsViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// ViewModel
final class AllFunctionsViewModelTests: XCTestCase {
private var defaults: UserDefaults!
private var appStore: AppStore!
private var menuStore: HomeCommonMenuStore!
override func setUp() {
defaults = UserDefaults(suiteName: "AllFunctionsViewModelTests")!
defaults.removePersistentDomain(forName: "AllFunctionsViewModelTests")
appStore = AppStore(defaults: defaults)
menuStore = HomeCommonMenuStore(defaults: defaults)
appStore.userId = "1001"
appStore.accountType = "scenic_user"
appStore.roleCode = AppRoleCode.photographer.rawValue
}
override func tearDown() {
defaults.removePersistentDomain(forName: "AllFunctionsViewModelTests")
super.tearDown()
}
func testLoadFunctionsUsesDefaultFirstFourWhenNoSavedURIs() {
appStore.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
HomePermissionItem(id: "4", name: "设置", uri: "system_settings"),
HomePermissionItem(id: "5", name: "消息", uri: "message_center"),
])
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)
viewModel.loadFunctions()
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
"wallet", "cloud_management", "task_management", "system_settings",
])
XCTAssertEqual(viewModel.moreMenus.map(\.uri), ["message_center"])
XCTAssertEqual(
menuStore.savedCommonURIs(
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
),
["wallet", "cloud_management", "task_management", "system_settings"]
)
}
func testLoadFunctionsUsesSavedURIsOverDefault() {
appStore.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
])
menuStore.saveCommonURIs(
["cloud_management"],
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
)
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)
viewModel.loadFunctions()
XCTAssertEqual(viewModel.commonMenus.map(\.uri), ["cloud_management"])
XCTAssertEqual(Set(viewModel.moreMenus.map(\.uri)), Set(["wallet", "task_management"]))
}
func testAddToCommonMovesMenuAndPersists() {
appStore.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
])
menuStore.saveCommonURIs(
["wallet"],
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
)
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)
viewModel.loadFunctions()
let cloudMenu = viewModel.moreMenus.first { $0.uri == "cloud_management" }!
viewModel.addToCommon(cloudMenu)
XCTAssertEqual(viewModel.commonMenus.map(\.uri), ["wallet", "cloud_management"])
XCTAssertTrue(viewModel.moreMenus.isEmpty)
XCTAssertTrue(viewModel.didCustomize)
XCTAssertEqual(
menuStore.savedCommonURIs(
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
),
["wallet", "cloud_management"]
)
}
func testRemoveFromCommonMovesMenuAndPersists() {
appStore.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
])
menuStore.saveCommonURIs(
["wallet", "cloud_management"],
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
)
let viewModel = AllFunctionsViewModel(appStore: appStore, commonMenuStore: menuStore)
viewModel.loadFunctions()
let walletMenu = viewModel.commonMenus.first { $0.uri == "wallet" }!
viewModel.removeFromCommon(walletMenu)
XCTAssertEqual(viewModel.commonMenus.map(\.uri), ["cloud_management"])
XCTAssertEqual(viewModel.moreMenus.map(\.uri), ["wallet"])
XCTAssertTrue(viewModel.didCustomize)
}
}

View File

@ -0,0 +1,53 @@
//
// HomeCollectionModelsTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// Collection snapshot
final class HomeCollectionModelsTests: XCTestCase {
func testMakeSnapshotIncludesDistinctWorkBlockItems() {
let snapshot = HomeCollectionLayoutBuilder.makeSnapshot(
showWorkBlock: true,
showStore: false,
storeItem: nil,
commonMenus: []
)
XCTAssertEqual(snapshot.numberOfSections, 4)
XCTAssertEqual(snapshot.numberOfItems(inSection: .workStatus), 1)
XCTAssertEqual(snapshot.numberOfItems(inSection: .locationReport), 1)
XCTAssertEqual(snapshot.numberOfItems(inSection: .quickActions), 1)
XCTAssertEqual(snapshot.itemIdentifiers(inSection: .workStatus), [.workStatus])
XCTAssertEqual(snapshot.itemIdentifiers(inSection: .locationReport), [.locationReport])
XCTAssertEqual(snapshot.itemIdentifiers(inSection: .quickActions), [.quickActions])
}
func testMakeSnapshotOmitsWorkBlockForMinimalTopRole() {
let snapshot = HomeCollectionLayoutBuilder.makeSnapshot(
showWorkBlock: false,
showStore: false,
storeItem: nil,
commonMenus: []
)
XCTAssertEqual(snapshot.sectionIdentifiers, [.commonApps])
XCTAssertEqual(snapshot.itemIdentifiers(inSection: .commonApps).count, 1)
}
func testMakeSnapshotIncludesStoreSectionWhenAvailable() {
let store = StoreItem(id: 1, name: "测试门店", address: "测试地址")
let snapshot = HomeCollectionLayoutBuilder.makeSnapshot(
showWorkBlock: true,
showStore: true,
storeItem: store,
commonMenus: []
)
XCTAssertTrue(snapshot.sectionIdentifiers.contains(.store))
XCTAssertEqual(snapshot.itemIdentifiers(inSection: .store), [.store])
}
}

View File

@ -47,4 +47,15 @@ final class HomeCommonMenuStoreTests: XCTestCase {
)
XCTAssertEqual(common.map(\.uri), ["cloud_management"])
}
func testSplitMenusSeparatesCommonAndMore() {
let menus = [
HomeMenuItem(uri: "wallet", title: "钱包", iconName: "wallet.pass"),
HomeMenuItem(uri: "cloud_management", title: "云盘", iconName: "icloud"),
HomeMenuItem(uri: "task_management", title: "任务", iconName: "checklist"),
]
let split = HomeCommonMenuStore.splitMenus(from: menus, commonURIs: ["wallet", "task_management"])
XCTAssertEqual(split.common.map(\.uri), ["wallet", "task_management"])
XCTAssertEqual(split.more.map(\.uri), ["cloud_management"])
}
}

View File

@ -11,9 +11,10 @@ import XCTest
final class HomeViewModelTests: XCTestCase {
private var appStore: AppStore!
private var defaults: UserDefaults!
override func setUp() {
let defaults = UserDefaults(suiteName: "HomeViewModelTests")!
defaults = UserDefaults(suiteName: "HomeViewModelTests")!
defaults.removePersistentDomain(forName: "HomeViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.userId = "2001"
@ -50,6 +51,31 @@ final class HomeViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.commonMenus.map(\.uri), ["wallet"])
}
func testRebuildCommonMenusPersistsDefaultURIsOnFirstLoad() {
let menuStore = HomeCommonMenuStore(defaults: defaults)
appStore.savePermissionItems([
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
HomePermissionItem(id: "4", name: "设置", uri: "system_settings"),
HomePermissionItem(id: "5", name: "消息", uri: "message_center"),
])
let viewModel = HomeViewModel(appStore: appStore, commonMenuStore: menuStore)
viewModel.rebuildCommonMenus()
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
"wallet", "cloud_management", "task_management", "system_settings",
])
XCTAssertEqual(
menuStore.savedCommonURIs(
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
),
["wallet", "cloud_management", "task_management", "system_settings"]
)
}
func testEmptyRolePermissionShowsPermissionDialog() async throws {
let responseData = try TestJSON.envelope(data: [RolePermissionResponse]())
let session = MockURLSession(responses: [responseData])