完善首页权限申请流程与队列播报体验。

对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 13:46:45 +08:00
parent c083f1d4b3
commit efb3925257
50 changed files with 2585 additions and 455 deletions

View File

@ -46,6 +46,8 @@ final class PermissionApplyViewModelTests: XCTestCase {
XCTAssertEqual(existing?.isDisabled, true)
XCTAssertEqual(existing?.isSelected, true)
XCTAssertEqual(fresh?.isDisabled, false)
if let existing { viewModel.toggleScenic(existing) }
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
}
func testSubmitValidationRequiresRoleAndScenic() async {
@ -55,21 +57,24 @@ final class PermissionApplyViewModelTests: XCTestCase {
viewModel.onShowMessage = { message = $0 }
await viewModel.submit(api: api)
XCTAssertEqual(message, "请选择角色")
XCTAssertEqual(message, "请选择景区和角色")
XCTAssertFalse(viewModel.canSubmit)
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
await viewModel.submit(api: api)
XCTAssertEqual(message, "至少选择一个景区")
XCTAssertEqual(message, "选择景区和角色")
XCTAssertFalse(viewModel.canSubmit)
}
func testSubmitSuccessReturnsApplyCode() async throws {
func testSubmitSuccessUsesAndroidPayloadAndCompletion() async throws {
let scenicJSON = """
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":""}]}}
""".data(using: .utf8)!
let submitJSON = """
{"code":100000,"msg":"success","data":{"code":"AP999"}}
""".data(using: .utf8)!
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON, submitJSON])))
let session = MockURLSession(responses: [scenicJSON, submitJSON])
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PermissionApplyViewModel(appStore: appStore)
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
await viewModel.loadAvailableScenics(api: api)
@ -77,10 +82,138 @@ final class PermissionApplyViewModelTests: XCTestCase {
viewModel.toggleScenic(scenic)
}
var applyCode: String?
viewModel.onSubmitSuccess = { applyCode = $0 }
var didSubmit = false
viewModel.onSubmitSuccess = { didSubmit = true }
XCTAssertTrue(viewModel.canSubmit)
await viewModel.submit(api: api)
XCTAssertEqual(applyCode, "AP999")
XCTAssertTrue(didSubmit)
XCTAssertFalse(viewModel.isSubmitting)
XCTAssertEqual(session.requests.last?.url?.path, "/api/yf-handset-app/photog/role-apply/submit")
let body = try XCTUnwrap(session.requests.last?.httpBody)
let object = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
XCTAssertEqual(object["role_id"] as? Int, 10)
XCTAssertEqual(object["scenic_id"] as? [Int], [101])
}
func testSubmitFailureRestoresEnabledButtonAndKeepsSelection() async throws {
let scenicJSON = """
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":""}]}}
""".data(using: .utf8)!
let failureJSON = #"{"code":100001,"msg":"failed","data":null}"#.data(using: .utf8)!
let api = HomeAPI(
client: APIClient(
environment: .testing,
session: MockURLSession(responses: [scenicJSON, failureJSON])
)
)
let viewModel = PermissionApplyViewModel(appStore: appStore)
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
await viewModel.loadAvailableScenics(api: api)
viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first))
var message: String?
viewModel.onShowMessage = { message = $0 }
await viewModel.submit(api: api)
XCTAssertEqual(message, "提交失败,请稍后重试")
XCTAssertFalse(viewModel.isSubmitting)
XCTAssertTrue(viewModel.canSubmit)
XCTAssertEqual(viewModel.selectedScenicIds, [101])
}
func testSwitchingRoleClearsScenicSelectionAndLoadedOptions() async throws {
let scenicJSON = """
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":""}]}}
""".data(using: .utf8)!
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON])))
let viewModel = PermissionApplyViewModel(appStore: appStore)
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
await viewModel.loadAvailableScenics(api: api)
viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first))
viewModel.selectRole(RoleOption(id: 11, name: "门店管理员", description: "", isSelected: true))
XCTAssertEqual(viewModel.selectedRoleId, 11)
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
XCTAssertTrue(viewModel.availableScenics.isEmpty)
}
func testAvailableRolesAreDeduplicatedByRoleID() {
appStore.permissions.saveRolePermissionList([
RolePermissionResponse(
role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"),
scenic: [ScenicInfo(id: 100, name: "景区A", status: 1)]
),
RolePermissionResponse(
role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"),
scenic: [ScenicInfo(id: 101, name: "景区B", status: 1)]
),
])
let viewModel = PermissionApplyViewModel(appStore: appStore)
XCTAssertEqual(viewModel.availableRoles.map(\.id), [10])
XCTAssertEqual(viewModel.rolePermissionList.count, 2)
}
func testScenicSearchAndDeletionConfirmation() async throws {
let scenicJSON = """
{"code":100000,"msg":"success","data":{"total":3,"list":[{"id":100,"name":""},{"id":101,"name":"西"},{"id":102,"name":""}]}}
""".data(using: .utf8)!
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON])))
let viewModel = PermissionApplyViewModel(appStore: appStore)
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
await viewModel.loadAvailableScenics(api: api)
viewModel.updateScenicSearchQuery("西湖")
XCTAssertEqual(viewModel.filteredScenics.map(\.id), [101])
let scenic = try XCTUnwrap(viewModel.filteredScenics.first)
viewModel.toggleScenic(scenic)
XCTAssertTrue(viewModel.canSubmit)
viewModel.removeSelectedScenic(id: scenic.id, name: scenic.name)
XCTAssertEqual(viewModel.pendingDeletion, .scenic(id: 101, name: "西湖景区"))
viewModel.cancelDeletion()
XCTAssertEqual(viewModel.selectedScenicIds, [101])
viewModel.removeSelectedScenic(id: scenic.id, name: scenic.name)
viewModel.confirmDeletion()
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
XCTAssertFalse(viewModel.canSubmit)
}
func testRoleDeletionRequiresConfirmationAndClearsScenics() async throws {
let scenicJSON = """
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":""}]}}
""".data(using: .utf8)!
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON])))
let viewModel = PermissionApplyViewModel(appStore: appStore)
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
await viewModel.loadAvailableScenics(api: api)
viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first))
viewModel.clearRoleSelection()
XCTAssertEqual(viewModel.pendingDeletion, .role(name: "摄影师"))
XCTAssertEqual(viewModel.selectedRoleId, 10)
viewModel.confirmDeletion()
XCTAssertNil(viewModel.selectedRoleId)
XCTAssertTrue(viewModel.selectedScenicIds.isEmpty)
XCTAssertTrue(viewModel.availableScenics.isEmpty)
}
func testExistingRoleListAndScenicDialogState() {
let viewModel = PermissionApplyViewModel(appStore: appStore)
XCTAssertEqual(viewModel.rolePermissionList.count, 1)
XCTAssertEqual(viewModel.availableRoles.first?.description, "拍摄")
let scenics = viewModel.rolePermissionList[0].scenic
viewModel.showScenicList(roleName: "摄影师", scenics: scenics)
XCTAssertTrue(viewModel.showScenicListDialog)
XCTAssertEqual(viewModel.dialogRoleName, "摄影师")
XCTAssertEqual(viewModel.dialogScenicList.map(\.id), [100])
viewModel.hideScenicList()
XCTAssertFalse(viewModel.showScenicListDialog)
}
}