模块化 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

@ -20,14 +20,14 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
defaults = UserDefaults(suiteName: "ProfileSpaceSettingsViewModelTests")!
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.currentScenicId = 11
appStore.session.currentScenicId = 11
calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
fixedNow = calendar.date(from: DateComponents(year: 2026, month: 7, day: 8))!
}
override func tearDown() {
AppStore.shared.currentScenicId = 0
AppStore.shared.session.currentScenicId = 0
appStore.logout()
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
super.tearDown()
@ -170,7 +170,8 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
viewModel.updateHolidayEndTime("20:00")
viewModel.updateBusinessEndTime("08:00")
XCTAssertThrowsError(try viewModel.makeSaveRequest())
XCTAssertEqual(viewModel.businessEndTime, "18:00")
XCTAssertNoThrow(try viewModel.makeSaveRequest())
}
func testHasChangesAndSaveRequestFieldsMatchAndroid() async throws {
@ -250,7 +251,7 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
}
func testSpaceSettingsRoutePushesProfileSpaceSettingsPage() {
AppStore.shared.currentScenicId = 11
AppStore.shared.session.currentScenicId = 11
let root = UIViewController()
let navigationController = UINavigationController(rootViewController: root)
@ -259,8 +260,26 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
XCTAssertTrue(navigationController.topViewController is ProfileSpaceSettingsViewController)
}
func testCertificationExpansionAndAvatarUploadRefresh() async {
let response = PhotographerProfileInfoResponse.loaded(certifications: ["1", "2", "3", "4"])
let api = FakeProfileSpaceSettingsAPI(response: response)
let viewModel = makeViewModel()
await viewModel.load(api: api)
XCTAssertFalse(viewModel.isCertificationExpanded)
viewModel.toggleCertificationExpanded()
XCTAssertTrue(viewModel.isCertificationExpanded)
let avatarData = Data([1, 2, 3])
await viewModel.uploadAvatar(data: avatarData, fileName: "avatar.jpg", api: api)
XCTAssertEqual(api.uploadedAvatarData, avatarData)
XCTAssertEqual(api.uploadedAvatarFileName, "avatar.jpg")
XCTAssertEqual(viewModel.avatarURL, "https://example.com/new-avatar.jpg")
}
func testSystemSettingsRoutePushesSettingPage() {
AppStore.shared.currentScenicId = 11
AppStore.shared.session.currentScenicId = 11
let tabBarController = MainTabBarController()
tabBarController.loadViewIfNeeded()
guard
@ -292,6 +311,8 @@ private final class FakeProfileSpaceSettingsAPI: ProfileSpaceSettingsAPI {
var lastUpdateRequest: UpdatePhotographerProfileInfoRequest?
var lastAddScheduleRequest: AddPhotographerScheduleRequest?
var lastDeletedScheduleId: Int?
var uploadedAvatarData: Data?
var uploadedAvatarFileName: String?
init(response: PhotographerProfileInfoResponse) {
self.response = response
@ -335,10 +356,40 @@ private final class FakeProfileSpaceSettingsAPI: ProfileSpaceSettingsAPI {
func deleteSchedule(id: Int) async throws {
lastDeletedScheduleId = id
}
func uploadAvatar(data: Data, fileName: String) async throws {
uploadedAvatarData = data
uploadedAvatarFileName = fileName
response = PhotographerProfileInfoResponse(
id: response.id,
scenicId: response.scenicId,
photogUid: response.photogUid,
realName: response.realName,
nickname: response.nickname,
avatar: "https://example.com/new-avatar.jpg",
scenicCertification: response.scenicCertification,
description: response.description,
attrLabel: response.attrLabel,
shootLabel: response.shootLabel,
cameraDevice: response.cameraDevice,
businessStartTime: response.businessStartTime,
businessEndTime: response.businessEndTime,
holidayStartTime: response.holidayStartTime,
holidayEndTime: response.holidayEndTime,
businessRange: response.businessRange,
acceptOrderStatus: response.acceptOrderStatus,
schedule: response.schedule,
virtualPhone: response.virtualPhone
)
}
}
private extension PhotographerProfileInfoResponse {
static func loaded() -> PhotographerProfileInfoResponse {
loaded(certifications: ["西湖"])
}
static func loaded(certifications: [String]) -> PhotographerProfileInfoResponse {
PhotographerProfileInfoResponse(
id: 1,
scenicId: 11,
@ -346,7 +397,7 @@ private extension PhotographerProfileInfoResponse {
realName: "林摄影",
nickname: "小林",
avatar: "https://example.com/a.jpg",
scenicCertification: ["西湖"],
scenicCertification: certifications,
description: "简介",
attrLabel: ["航拍"],
shootLabel: ["亲子"],