同步个人空间配置页面
This commit is contained in:
@ -35,8 +35,10 @@ enum HomeRouteHandler {
|
||||
selectTab(.orders, from: viewController)
|
||||
case "photographer_stats":
|
||||
selectTab(.statistics, from: viewController)
|
||||
case "system_settings", "space_settings":
|
||||
case "system_settings":
|
||||
selectTab(.profile, from: viewController)
|
||||
case "space_settings":
|
||||
viewController.navigationController?.pushViewController(ProfileSpaceSettingsViewController(), animated: true)
|
||||
case "pilot_cert":
|
||||
let controller = RealNameAuthViewController()
|
||||
viewController.navigationController?.pushViewController(controller, animated: true)
|
||||
|
||||
@ -21,6 +21,50 @@ final class ProfileAPI {
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取摄影师在当前景区的个人空间配置。
|
||||
func profileInfo(scenicId: Int) async throws -> PhotographerProfileInfoResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/profile/info",
|
||||
queryItems: [URLQueryItem(name: "scenic_id", value: String(scenicId))]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 更新摄影师个人空间配置。
|
||||
func updateProfileInfo(_ request: UpdatePhotographerProfileInfoRequest) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/profile/info-update",
|
||||
body: request
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 添加摄影师日程。
|
||||
func addSchedule(_ request: AddPhotographerScheduleRequest) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/schedule/add",
|
||||
body: request
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 删除摄影师日程。
|
||||
func deleteSchedule(id: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/schedule/delete",
|
||||
body: PhotographerScheduleDeleteRequest(id: id)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取当前登录账号可切换的景区账号和门店账号。
|
||||
func switchableAccounts() async throws -> V9AuthResponse {
|
||||
try await client.send(
|
||||
|
||||
@ -62,6 +62,241 @@ struct UpdateInfoRequest: Encodable {
|
||||
let avatar: String?
|
||||
}
|
||||
|
||||
/// 摄影师个人空间配置响应,对齐 Android `ProfileInfoResponse`。
|
||||
struct PhotographerProfileInfoResponse: Decodable, Equatable {
|
||||
let id: Int
|
||||
let scenicId: Int
|
||||
let photogUid: Int
|
||||
let realName: String
|
||||
let nickname: String
|
||||
let avatar: String
|
||||
let scenicCertification: [String]
|
||||
let description: String
|
||||
let attrLabel: [String]
|
||||
let shootLabel: [String]
|
||||
let cameraDevice: [String]
|
||||
let businessStartTime: String
|
||||
let businessEndTime: String
|
||||
let holidayStartTime: String
|
||||
let holidayEndTime: String
|
||||
let businessRange: [Int]
|
||||
let acceptOrderStatus: Int
|
||||
let schedule: [String: [PhotographerSchedule]]
|
||||
let virtualPhone: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case scenicId = "scenic_id"
|
||||
case photogUid = "photog_uid"
|
||||
case realName = "real_name"
|
||||
case nickname
|
||||
case avatar
|
||||
case scenicCertification = "scenic_certification"
|
||||
case description
|
||||
case attrLabel = "attr_label"
|
||||
case shootLabel = "shoot_label"
|
||||
case cameraDevice = "camera_device"
|
||||
case businessStartTime = "business_start_time"
|
||||
case businessEndTime = "business_end_time"
|
||||
case holidayStartTime = "holiday_start_time"
|
||||
case holidayEndTime = "holiday_end_time"
|
||||
case businessRange = "business_range"
|
||||
case acceptOrderStatus = "accept_order_status"
|
||||
case schedule
|
||||
case virtualPhone = "virtual_phone"
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
scenicId: Int = 0,
|
||||
photogUid: Int = 0,
|
||||
realName: String = "",
|
||||
nickname: String = "",
|
||||
avatar: String = "",
|
||||
scenicCertification: [String] = [],
|
||||
description: String = "",
|
||||
attrLabel: [String] = [],
|
||||
shootLabel: [String] = [],
|
||||
cameraDevice: [String] = [],
|
||||
businessStartTime: String = "",
|
||||
businessEndTime: String = "",
|
||||
holidayStartTime: String = "",
|
||||
holidayEndTime: String = "",
|
||||
businessRange: [Int] = [],
|
||||
acceptOrderStatus: Int = 0,
|
||||
schedule: [String: [PhotographerSchedule]] = [:],
|
||||
virtualPhone: String = ""
|
||||
) {
|
||||
self.id = id
|
||||
self.scenicId = scenicId
|
||||
self.photogUid = photogUid
|
||||
self.realName = realName
|
||||
self.nickname = nickname
|
||||
self.avatar = avatar
|
||||
self.scenicCertification = scenicCertification
|
||||
self.description = description
|
||||
self.attrLabel = attrLabel
|
||||
self.shootLabel = shootLabel
|
||||
self.cameraDevice = cameraDevice
|
||||
self.businessStartTime = businessStartTime
|
||||
self.businessEndTime = businessEndTime
|
||||
self.holidayStartTime = holidayStartTime
|
||||
self.holidayEndTime = holidayEndTime
|
||||
self.businessRange = businessRange
|
||||
self.acceptOrderStatus = acceptOrderStatus
|
||||
self.schedule = schedule
|
||||
self.virtualPhone = virtualPhone
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
scenicId = try container.decodeLossyInt(forKey: .scenicId) ?? 0
|
||||
photogUid = try container.decodeLossyInt(forKey: .photogUid) ?? 0
|
||||
realName = try container.decodeLossyString(forKey: .realName)
|
||||
nickname = try container.decodeLossyString(forKey: .nickname)
|
||||
avatar = try container.decodeLossyString(forKey: .avatar)
|
||||
scenicCertification = try container.decodeLossyStringArray(forKey: .scenicCertification)
|
||||
description = try container.decodeLossyString(forKey: .description)
|
||||
attrLabel = try container.decodeLossyStringArray(forKey: .attrLabel)
|
||||
shootLabel = try container.decodeLossyStringArray(forKey: .shootLabel)
|
||||
cameraDevice = try container.decodeLossyStringArray(forKey: .cameraDevice)
|
||||
businessStartTime = try container.decodeLossyString(forKey: .businessStartTime)
|
||||
businessEndTime = try container.decodeLossyString(forKey: .businessEndTime)
|
||||
holidayStartTime = try container.decodeLossyString(forKey: .holidayStartTime)
|
||||
holidayEndTime = try container.decodeLossyString(forKey: .holidayEndTime)
|
||||
businessRange = try container.decodeLossyIntArray(forKey: .businessRange)
|
||||
acceptOrderStatus = try container.decodeLossyInt(forKey: .acceptOrderStatus) ?? 0
|
||||
schedule = try container.decodeIfPresent([String: [PhotographerSchedule]].self, forKey: .schedule) ?? [:]
|
||||
virtualPhone = try container.decodeLossyString(forKey: .virtualPhone)
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄影师日程实体,对齐 Android `ScheduleEntity`。
|
||||
struct PhotographerSchedule: Decodable, Equatable, Hashable, Sendable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let remark: String
|
||||
let startTime: String
|
||||
let endTime: String
|
||||
let scheduleDate: String
|
||||
let orderNumber: String?
|
||||
let userNickname: String?
|
||||
let userPhone: String?
|
||||
let orderStatus: Int?
|
||||
let orderStatusName: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case remark
|
||||
case startTime = "start_time"
|
||||
case endTime = "end_time"
|
||||
case scheduleDate = "schedule_date"
|
||||
case orderNumber = "order_number"
|
||||
case userNickname = "user_nickname"
|
||||
case userPhone = "user_phone"
|
||||
case orderStatus = "order_status"
|
||||
case orderStatusName = "order_status_name"
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
name: String = "",
|
||||
remark: String = "",
|
||||
startTime: String = "",
|
||||
endTime: String = "",
|
||||
scheduleDate: String = "",
|
||||
orderNumber: String? = nil,
|
||||
userNickname: String? = nil,
|
||||
userPhone: String? = nil,
|
||||
orderStatus: Int? = nil,
|
||||
orderStatusName: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.remark = remark
|
||||
self.startTime = startTime
|
||||
self.endTime = endTime
|
||||
self.scheduleDate = scheduleDate
|
||||
self.orderNumber = orderNumber
|
||||
self.userNickname = userNickname
|
||||
self.userPhone = userPhone
|
||||
self.orderStatus = orderStatus
|
||||
self.orderStatusName = orderStatusName
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
name = try container.decodeLossyString(forKey: .name)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
startTime = try container.decodeLossyString(forKey: .startTime)
|
||||
endTime = try container.decodeLossyString(forKey: .endTime)
|
||||
scheduleDate = try container.decodeLossyString(forKey: .scheduleDate)
|
||||
orderNumber = try container.decodeIfPresent(String.self, forKey: .orderNumber)
|
||||
userNickname = try container.decodeIfPresent(String.self, forKey: .userNickname)
|
||||
userPhone = try container.decodeIfPresent(String.self, forKey: .userPhone)
|
||||
orderStatus = try container.decodeLossyInt(forKey: .orderStatus)
|
||||
orderStatusName = try container.decodeIfPresent(String.self, forKey: .orderStatusName)
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄影师个人空间配置更新请求,对齐 Android `UpdateProfileInfoRequest`。
|
||||
struct UpdatePhotographerProfileInfoRequest: Encodable, Equatable {
|
||||
let scenicId: String
|
||||
let description: String
|
||||
let cameraDevice: [String]
|
||||
let attrLabel: [String]
|
||||
let shootLabel: [String]
|
||||
let businessStartTime: String
|
||||
let businessEndTime: String
|
||||
let holidayStartTime: String
|
||||
let holidayEndTime: String
|
||||
let businessRange: [Int]
|
||||
let acceptOrderStatus: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case description
|
||||
case cameraDevice = "camera_device"
|
||||
case attrLabel = "attr_label"
|
||||
case shootLabel = "shoot_label"
|
||||
case businessStartTime = "business_start_time"
|
||||
case businessEndTime = "business_end_time"
|
||||
case holidayStartTime = "holiday_start_time"
|
||||
case holidayEndTime = "holiday_end_time"
|
||||
case businessRange = "business_range"
|
||||
case acceptOrderStatus = "accept_order_status"
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄影师新增日程请求,对齐 Android `AddScheduleRequest`。
|
||||
struct AddPhotographerScheduleRequest: Encodable, Equatable {
|
||||
var scenicId: String
|
||||
var name: String
|
||||
var remark: String
|
||||
var startTime: String
|
||||
var endTime: String
|
||||
var scheduleDate: String
|
||||
var orderNumber: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case name
|
||||
case remark
|
||||
case startTime = "start_time"
|
||||
case endTime = "end_time"
|
||||
case scheduleDate = "schedule_date"
|
||||
case orderNumber = "order_number"
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄影师删除日程请求,对齐 Android `ScheduleDeleteRequest`。
|
||||
struct PhotographerScheduleDeleteRequest: Encodable, Equatable {
|
||||
let id: Int
|
||||
}
|
||||
|
||||
/// 实名认证响应实体,包裹当前用户的认证详情。
|
||||
struct RealNameInfoResponse: Decodable, Equatable {
|
||||
let realNameInfo: RealNameInfo?
|
||||
@ -350,4 +585,31 @@ private extension KeyedDecodingContainer {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeLossyStringArray(forKey key: Key) throws -> [String] {
|
||||
if let values = try? decodeIfPresent([String].self, forKey: key) {
|
||||
return values
|
||||
}
|
||||
if let values = try? decodeIfPresent([Int].self, forKey: key) {
|
||||
return values.map(String.init)
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? [] : [text]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
func decodeLossyIntArray(forKey key: Key) throws -> [Int] {
|
||||
if let values = try? decodeIfPresent([Int].self, forKey: key) {
|
||||
return values
|
||||
}
|
||||
if let values = try? decodeIfPresent([String].self, forKey: key) {
|
||||
return values.compactMap { Int($0.trimmingCharacters(in: .whitespacesAndNewlines)) }
|
||||
}
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return [value]
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,546 @@
|
||||
//
|
||||
// ProfileSpaceSettingsViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 个人空间配置接口协议,便于页面和测试注入真实或模拟 API。
|
||||
@MainActor
|
||||
protocol ProfileSpaceSettingsAPI {
|
||||
/// 拉取当前景区的个人空间配置。
|
||||
func profileInfo(scenicId: Int) async throws -> PhotographerProfileInfoResponse
|
||||
|
||||
/// 更新当前景区的个人空间配置。
|
||||
func updateProfileInfo(_ request: UpdatePhotographerProfileInfoRequest) async throws
|
||||
|
||||
/// 更新账号基础昵称或头像。
|
||||
func updateUserInfo(nickname: String?, password: String?, avatar: String?) async throws
|
||||
|
||||
/// 添加个人日程。
|
||||
func addSchedule(_ request: AddPhotographerScheduleRequest) async throws
|
||||
|
||||
/// 删除个人日程。
|
||||
func deleteSchedule(id: Int) async throws
|
||||
}
|
||||
|
||||
extension ProfileAPI: ProfileSpaceSettingsAPI {}
|
||||
|
||||
/// 个人空间配置校验错误。
|
||||
enum ProfileSpaceSettingsValidationError: LocalizedError, Equatable {
|
||||
case emptyTime(String)
|
||||
case invalidTimeRange(String)
|
||||
case emptyLabel
|
||||
case duplicateLabel
|
||||
case labelTooLong
|
||||
case labelLimitReached
|
||||
case emptyDevice
|
||||
case emptyScheduleName
|
||||
case emptyScheduleTime
|
||||
case invalidScheduleTime
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .emptyTime(let title):
|
||||
"请输入\(title)"
|
||||
case .invalidTimeRange(let title):
|
||||
"\(title)开始时间不能大于结束时间"
|
||||
case .emptyLabel:
|
||||
"标签不能为空"
|
||||
case .duplicateLabel:
|
||||
"标签已存在"
|
||||
case .labelTooLong:
|
||||
"标签最多20个字符"
|
||||
case .labelLimitReached:
|
||||
"最多8个标签"
|
||||
case .emptyDevice:
|
||||
"设备名称不能为空"
|
||||
case .emptyScheduleName:
|
||||
"请输入日程名称"
|
||||
case .emptyScheduleTime:
|
||||
"请选择日程时间"
|
||||
case .invalidScheduleTime:
|
||||
"结束时间必须晚于开始时间"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 个人空间配置 ViewModel,对齐 Android `ProfileInfoViewModel` 的状态和校验。
|
||||
final class ProfileSpaceSettingsViewModel {
|
||||
|
||||
private(set) var avatarURL = ""
|
||||
private(set) var realName = ""
|
||||
private(set) var nickname = ""
|
||||
private(set) var editingNickname = ""
|
||||
private(set) var introduction = ""
|
||||
private(set) var scenicCertification: [String] = []
|
||||
private(set) var attrLabels: [String] = []
|
||||
private(set) var shootLabels: [String] = []
|
||||
private(set) var businessStartTime: String?
|
||||
private(set) var businessEndTime: String?
|
||||
private(set) var holidayStartTime: String?
|
||||
private(set) var holidayEndTime: String?
|
||||
private(set) var cameraDevices: [String] = []
|
||||
private(set) var acceptOrderStatus = 0
|
||||
private(set) var scheduleMap: [String: [PhotographerSchedule]] = [:]
|
||||
private(set) var selectedDate: Date
|
||||
private(set) var isEditingProfile = false
|
||||
private(set) var isLoading = false
|
||||
private(set) var hasChanges = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
private var originalSnapshot = Snapshot()
|
||||
private let appStore: AppStore
|
||||
private let calendar: Calendar
|
||||
private let nowProvider: () -> Date
|
||||
|
||||
init(
|
||||
appStore: AppStore = .shared,
|
||||
calendar: Calendar = Calendar(identifier: .gregorian),
|
||||
nowProvider: @escaping () -> Date = Date.init
|
||||
) {
|
||||
self.appStore = appStore
|
||||
self.calendar = calendar
|
||||
self.nowProvider = nowProvider
|
||||
selectedDate = calendar.startOfDay(for: nowProvider())
|
||||
}
|
||||
|
||||
/// 当前景区 ID。
|
||||
var scenicId: Int {
|
||||
appStore.currentScenicId
|
||||
}
|
||||
|
||||
/// 选中日期对应日程。
|
||||
var selectedDateSchedules: [PhotographerSchedule] {
|
||||
scheduleMap[Self.dateKey(from: selectedDate, calendar: calendar)] ?? []
|
||||
}
|
||||
|
||||
/// 从今天起连续 7 天。
|
||||
var weekDates: [Date] {
|
||||
let start = calendar.startOfDay(for: nowProvider())
|
||||
return (0..<7).compactMap { calendar.date(byAdding: .day, value: $0, to: start) }
|
||||
}
|
||||
|
||||
/// 拉取个人空间配置。
|
||||
func load(api: any ProfileSpaceSettingsAPI) async {
|
||||
guard scenicId > 0 else {
|
||||
onShowMessage?("请先选择景区")
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
defer { setLoading(false) }
|
||||
do {
|
||||
let response = try await api.profileInfo(scenicId: scenicId)
|
||||
apply(response)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换昵称和简介编辑状态,退出编辑时按 Android 逻辑单独保存昵称。
|
||||
func toggleProfileEditing(api: any ProfileSpaceSettingsAPI) async {
|
||||
if isEditingProfile {
|
||||
let newNickname = editingNickname.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let currentNickname = nickname.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !newNickname.isEmpty, newNickname != currentNickname else {
|
||||
isEditingProfile = false
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
defer { setLoading(false) }
|
||||
do {
|
||||
try await api.updateUserInfo(nickname: newNickname, password: nil, avatar: nil)
|
||||
nickname = newNickname
|
||||
appStore.userName = newNickname
|
||||
isEditingProfile = false
|
||||
onShowMessage?("修改成功")
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
return
|
||||
}
|
||||
editingNickname = nickname
|
||||
isEditingProfile = true
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 更新头像 URL。
|
||||
func updateAvatarURL(_ url: String) {
|
||||
avatarURL = url
|
||||
appStore.avatar = url
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 更新编辑昵称。
|
||||
func updateEditingNickname(_ value: String) {
|
||||
editingNickname = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 更新简介。
|
||||
func updateIntroduction(_ value: String) {
|
||||
introduction = value
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 添加个人标签。
|
||||
func addAttrLabel(_ value: String) {
|
||||
do {
|
||||
attrLabels = try adding(label: value, to: attrLabels)
|
||||
updateHasChanges()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除个人标签。
|
||||
func removeAttrLabel(_ label: String) {
|
||||
attrLabels.removeAll { $0 == label }
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 添加拍摄说明标签。
|
||||
func addShootLabel(_ value: String) {
|
||||
do {
|
||||
shootLabels = try adding(label: value, to: shootLabels)
|
||||
updateHasChanges()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除拍摄说明标签。
|
||||
func removeShootLabel(_ label: String) {
|
||||
shootLabels.removeAll { $0 == label }
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 更新工作日开始时间。
|
||||
func updateBusinessStartTime(_ value: String) {
|
||||
businessStartTime = value
|
||||
if let end = businessEndTime, !Self.isStart(value, before: end) {
|
||||
onShowMessage?("开始时间不能大于结束时间")
|
||||
businessStartTime = nil
|
||||
}
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 更新工作日结束时间。
|
||||
func updateBusinessEndTime(_ value: String) {
|
||||
businessEndTime = value
|
||||
if let start = businessStartTime, !Self.isStart(start, before: value) {
|
||||
onShowMessage?("结束时间不能小于开始时间")
|
||||
businessEndTime = nil
|
||||
}
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 更新节假日开始时间。
|
||||
func updateHolidayStartTime(_ value: String) {
|
||||
holidayStartTime = value
|
||||
if let end = holidayEndTime, !Self.isStart(value, before: end) {
|
||||
onShowMessage?("开始时间不能大于结束时间")
|
||||
holidayStartTime = nil
|
||||
}
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 更新节假日结束时间。
|
||||
func updateHolidayEndTime(_ value: String) {
|
||||
holidayEndTime = value
|
||||
if let start = holidayStartTime, !Self.isStart(start, before: value) {
|
||||
onShowMessage?("结束时间不能小于开始时间")
|
||||
holidayEndTime = nil
|
||||
}
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 添加相机设备。
|
||||
func addDevice(_ value: String) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else {
|
||||
onShowMessage?(ProfileSpaceSettingsValidationError.emptyDevice.localizedDescription)
|
||||
return
|
||||
}
|
||||
cameraDevices.append(text)
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 删除相机设备。
|
||||
func removeDevice(_ value: String) {
|
||||
cameraDevices.removeAll { $0 == value }
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 更新接单状态。
|
||||
func updateAcceptOrderStatus(_ status: Int) {
|
||||
acceptOrderStatus = status
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
/// 选择日程日期。
|
||||
func selectDate(_ date: Date) {
|
||||
selectedDate = calendar.startOfDay(for: date)
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 保存个人空间配置。
|
||||
func save(api: any ProfileSpaceSettingsAPI) async {
|
||||
do {
|
||||
let request = try makeSaveRequest()
|
||||
setLoading(true)
|
||||
defer { setLoading(false) }
|
||||
try await api.updateProfileInfo(request)
|
||||
originalSnapshot = currentSnapshot()
|
||||
hasChanges = false
|
||||
onShowMessage?("保存成功")
|
||||
notifyStateChange()
|
||||
await load(api: api)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建个人空间保存请求。
|
||||
func makeSaveRequest() throws -> UpdatePhotographerProfileInfoRequest {
|
||||
guard let businessStartTime else { throw ProfileSpaceSettingsValidationError.emptyTime("工作日开始时间") }
|
||||
guard let businessEndTime else { throw ProfileSpaceSettingsValidationError.emptyTime("工作日结束时间") }
|
||||
guard let holidayStartTime else { throw ProfileSpaceSettingsValidationError.emptyTime("节假日开始时间") }
|
||||
guard let holidayEndTime else { throw ProfileSpaceSettingsValidationError.emptyTime("节假日结束时间") }
|
||||
guard Self.isStart(businessStartTime, before: businessEndTime) else {
|
||||
throw ProfileSpaceSettingsValidationError.invalidTimeRange("工作日")
|
||||
}
|
||||
guard Self.isStart(holidayStartTime, before: holidayEndTime) else {
|
||||
throw ProfileSpaceSettingsValidationError.invalidTimeRange("节假日")
|
||||
}
|
||||
return UpdatePhotographerProfileInfoRequest(
|
||||
scenicId: String(scenicId),
|
||||
description: introduction,
|
||||
cameraDevice: cameraDevices,
|
||||
attrLabel: attrLabels,
|
||||
shootLabel: shootLabels,
|
||||
businessStartTime: Self.fullTimeString(businessStartTime),
|
||||
businessEndTime: Self.fullTimeString(businessEndTime),
|
||||
holidayStartTime: Self.fullTimeString(holidayStartTime),
|
||||
holidayEndTime: Self.fullTimeString(holidayEndTime),
|
||||
businessRange: [],
|
||||
acceptOrderStatus: acceptOrderStatus
|
||||
)
|
||||
}
|
||||
|
||||
/// 构建新增日程请求。
|
||||
func makeAddScheduleRequest(
|
||||
name: String,
|
||||
remark: String,
|
||||
startTime: String?,
|
||||
endTime: String?,
|
||||
scheduleDate: Date,
|
||||
orderNumber: String?
|
||||
) throws -> AddPhotographerScheduleRequest {
|
||||
let scheduleName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !scheduleName.isEmpty else { throw ProfileSpaceSettingsValidationError.emptyScheduleName }
|
||||
guard let startTime, let endTime else { throw ProfileSpaceSettingsValidationError.emptyScheduleTime }
|
||||
guard Self.isStart(startTime, before: endTime) else { throw ProfileSpaceSettingsValidationError.invalidScheduleTime }
|
||||
let order = orderNumber?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return AddPhotographerScheduleRequest(
|
||||
scenicId: String(scenicId),
|
||||
name: scheduleName,
|
||||
remark: remark,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
scheduleDate: Self.dateKey(from: scheduleDate, calendar: calendar),
|
||||
orderNumber: order?.isEmpty == true ? nil : order
|
||||
)
|
||||
}
|
||||
|
||||
/// 添加日程并刷新个人空间配置。
|
||||
func addSchedule(
|
||||
name: String,
|
||||
remark: String,
|
||||
startTime: String?,
|
||||
endTime: String?,
|
||||
scheduleDate: Date,
|
||||
orderNumber: String?,
|
||||
api: any ProfileSpaceSettingsAPI
|
||||
) async -> Bool {
|
||||
do {
|
||||
let request = try makeAddScheduleRequest(
|
||||
name: name,
|
||||
remark: remark,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
scheduleDate: scheduleDate,
|
||||
orderNumber: orderNumber
|
||||
)
|
||||
setLoading(true)
|
||||
defer { setLoading(false) }
|
||||
try await api.addSchedule(request)
|
||||
onShowMessage?("添加成功")
|
||||
await load(api: api)
|
||||
return true
|
||||
} catch is CancellationError {
|
||||
return false
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除日程并刷新个人空间配置。
|
||||
func deleteSchedule(id: Int, api: any ProfileSpaceSettingsAPI) async {
|
||||
setLoading(true)
|
||||
defer { setLoading(false) }
|
||||
do {
|
||||
try await api.deleteSchedule(id: id)
|
||||
onShowMessage?("删除成功")
|
||||
await load(api: api)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(_ response: PhotographerProfileInfoResponse) {
|
||||
realName = response.realName
|
||||
nickname = response.nickname
|
||||
editingNickname = response.nickname
|
||||
if !response.avatar.isEmpty {
|
||||
avatarURL = response.avatar
|
||||
appStore.avatar = response.avatar
|
||||
}
|
||||
scenicCertification = response.scenicCertification
|
||||
introduction = response.description
|
||||
attrLabels = response.attrLabel
|
||||
shootLabels = response.shootLabel
|
||||
cameraDevices = response.cameraDevice
|
||||
businessStartTime = Self.shortTimeString(response.businessStartTime)
|
||||
businessEndTime = Self.shortTimeString(response.businessEndTime)
|
||||
holidayStartTime = Self.shortTimeString(response.holidayStartTime)
|
||||
holidayEndTime = Self.shortTimeString(response.holidayEndTime)
|
||||
acceptOrderStatus = response.acceptOrderStatus
|
||||
scheduleMap = response.schedule
|
||||
appStore.userName = response.nickname
|
||||
appStore.realName = response.realName
|
||||
originalSnapshot = currentSnapshot()
|
||||
hasChanges = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func adding(label rawValue: String, to labels: [String]) throws -> [String] {
|
||||
let label = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !label.isEmpty else { throw ProfileSpaceSettingsValidationError.emptyLabel }
|
||||
guard label.count <= 20 else { throw ProfileSpaceSettingsValidationError.labelTooLong }
|
||||
guard labels.count < 8 else { throw ProfileSpaceSettingsValidationError.labelLimitReached }
|
||||
guard !labels.contains(label) else { throw ProfileSpaceSettingsValidationError.duplicateLabel }
|
||||
return labels + [label]
|
||||
}
|
||||
|
||||
private func setLoading(_ loading: Bool) {
|
||||
isLoading = loading
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func updateHasChanges() {
|
||||
hasChanges = currentSnapshot() != originalSnapshot
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
private func currentSnapshot() -> Snapshot {
|
||||
Snapshot(
|
||||
introduction: introduction,
|
||||
attrLabels: attrLabels,
|
||||
shootLabels: shootLabels,
|
||||
businessStartTime: businessStartTime,
|
||||
businessEndTime: businessEndTime,
|
||||
holidayStartTime: holidayStartTime,
|
||||
holidayEndTime: holidayEndTime,
|
||||
cameraDevices: cameraDevices,
|
||||
acceptOrderStatus: acceptOrderStatus
|
||||
)
|
||||
}
|
||||
|
||||
private static func shortTimeString(_ value: String) -> String? {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty, text != "00:00:00" else { return nil }
|
||||
if text.count >= 5 {
|
||||
return String(text.prefix(5))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func fullTimeString(_ value: String) -> String {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.count == 5 ? "\(text):00" : text
|
||||
}
|
||||
|
||||
static func isStart(_ start: String, before end: String) -> Bool {
|
||||
minutes(from: start) < minutes(from: end)
|
||||
}
|
||||
|
||||
static func minutes(from value: String) -> Int {
|
||||
let pieces = value.split(separator: ":").compactMap { Int($0) }
|
||||
guard pieces.count >= 2 else { return 0 }
|
||||
return pieces[0] * 60 + pieces[1]
|
||||
}
|
||||
|
||||
static func dateKey(from date: Date, calendar: Calendar) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = calendar
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.timeZone = calendar.timeZone
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
/// 将日期展示为 Android 风格的 yyyy年M月d日。
|
||||
static func displayDate(_ date: Date, calendar: Calendar = Calendar(identifier: .gregorian)) -> String {
|
||||
let components = calendar.dateComponents([.year, .month, .day], from: date)
|
||||
return "\(components.year ?? 0)年\(components.month ?? 0)月\(components.day ?? 0)日"
|
||||
}
|
||||
|
||||
/// 将日期展示为日数字。
|
||||
static func displayDay(_ date: Date, calendar: Calendar = Calendar(identifier: .gregorian)) -> String {
|
||||
String(calendar.component(.day, from: date))
|
||||
}
|
||||
|
||||
/// 将日期展示为中文星期。
|
||||
static func displayWeekday(_ date: Date, calendar: Calendar = Calendar(identifier: .gregorian)) -> String {
|
||||
switch calendar.component(.weekday, from: date) {
|
||||
case 1: return "日"
|
||||
case 2: return "一"
|
||||
case 3: return "二"
|
||||
case 4: return "三"
|
||||
case 5: return "四"
|
||||
case 6: return "五"
|
||||
case 7: return "六"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Snapshot: Equatable {
|
||||
var introduction = ""
|
||||
var attrLabels: [String] = []
|
||||
var shootLabels: [String] = []
|
||||
var businessStartTime: String?
|
||||
var businessEndTime: String?
|
||||
var holidayStartTime: String?
|
||||
var holidayEndTime: String?
|
||||
var cameraDevices: [String] = []
|
||||
var acceptOrderStatus = 0
|
||||
}
|
||||
1091
suixinkan/UI/Profile/ProfileSpaceSettingsViewController.swift
Normal file
1091
suixinkan/UI/Profile/ProfileSpaceSettingsViewController.swift
Normal file
File diff suppressed because it is too large
Load Diff
334
suixinkanTests/ProfileSpaceSettingsViewModelTests.swift
Normal file
334
suixinkanTests/ProfileSpaceSettingsViewModelTests.swift
Normal file
@ -0,0 +1,334 @@
|
||||
//
|
||||
// ProfileSpaceSettingsViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 个人空间配置 ViewModel 与首页路由测试。
|
||||
final class ProfileSpaceSettingsViewModelTests: XCTestCase {
|
||||
|
||||
private var defaults: UserDefaults!
|
||||
private var appStore: AppStore!
|
||||
private var calendar: Calendar!
|
||||
private var fixedNow: Date!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
defaults = UserDefaults(suiteName: "ProfileSpaceSettingsViewModelTests")!
|
||||
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
appStore.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.logout()
|
||||
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testProfileInfoDecodeSupportsEmptyStringsArraysAndScheduleMap() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": "9",
|
||||
"scenic_id": "11",
|
||||
"photog_uid": "",
|
||||
"real_name": "",
|
||||
"nickname": "小林",
|
||||
"avatar": "",
|
||||
"scenic_certification": ["西湖", ""],
|
||||
"description": "",
|
||||
"attr_label": ["航拍", 8],
|
||||
"shoot_label": "",
|
||||
"camera_device": ["Sony A7"],
|
||||
"business_start_time": "09:00:00",
|
||||
"business_end_time": "18:00:00",
|
||||
"holiday_start_time": "",
|
||||
"holiday_end_time": "20:00:00",
|
||||
"business_range": ["1", 2],
|
||||
"accept_order_status": "1",
|
||||
"virtual_phone": "",
|
||||
"schedule": {
|
||||
"2026-07-08": [{
|
||||
"id": "22",
|
||||
"name": "旅拍",
|
||||
"remark": "",
|
||||
"start_time": "10:00",
|
||||
"end_time": "11:00",
|
||||
"schedule_date": "2026-07-08",
|
||||
"order_number": "NO1",
|
||||
"user_nickname": "游客",
|
||||
"user_phone": "13800000000",
|
||||
"order_status": "2",
|
||||
"order_status_name": "待拍摄"
|
||||
}]
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let response = try JSONDecoder().decode(PhotographerProfileInfoResponse.self, from: json)
|
||||
|
||||
XCTAssertEqual(response.id, 9)
|
||||
XCTAssertEqual(response.photogUid, 0)
|
||||
XCTAssertEqual(response.attrLabel, ["航拍", "8"])
|
||||
XCTAssertEqual(response.shootLabel, [])
|
||||
XCTAssertEqual(response.businessRange, [1, 2])
|
||||
XCTAssertEqual(response.schedule["2026-07-08"]?.first?.id, 22)
|
||||
XCTAssertEqual(response.schedule["2026-07-08"]?.first?.userPhone, "13800000000")
|
||||
}
|
||||
|
||||
func testLabelValidationCoversEmptyTooLongDuplicateAndLimit() {
|
||||
let viewModel = makeViewModel()
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
|
||||
viewModel.addAttrLabel(" ")
|
||||
XCTAssertEqual(messages.last, "标签不能为空")
|
||||
|
||||
viewModel.addAttrLabel(String(repeating: "a", count: 21))
|
||||
XCTAssertEqual(messages.last, "标签最多20个字符")
|
||||
|
||||
viewModel.addAttrLabel("航拍")
|
||||
viewModel.addAttrLabel("航拍")
|
||||
XCTAssertEqual(viewModel.attrLabels, ["航拍"])
|
||||
XCTAssertEqual(messages.last, "标签已存在")
|
||||
|
||||
for index in 1...7 {
|
||||
viewModel.addAttrLabel("标签\(index)")
|
||||
}
|
||||
viewModel.addAttrLabel("第九个")
|
||||
XCTAssertEqual(viewModel.attrLabels.count, 8)
|
||||
XCTAssertEqual(messages.last, "最多8个标签")
|
||||
}
|
||||
|
||||
func testBusinessTimeValidationRejectsInvalidRanges() throws {
|
||||
let viewModel = makeViewModel()
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
|
||||
viewModel.updateBusinessEndTime("10:00")
|
||||
viewModel.updateBusinessStartTime("11:00")
|
||||
XCTAssertNil(viewModel.businessStartTime)
|
||||
XCTAssertEqual(messages.last, "开始时间不能大于结束时间")
|
||||
|
||||
viewModel.updateHolidayStartTime("13:00")
|
||||
viewModel.updateHolidayEndTime("12:00")
|
||||
XCTAssertNil(viewModel.holidayEndTime)
|
||||
XCTAssertEqual(messages.last, "结束时间不能小于开始时间")
|
||||
|
||||
viewModel.updateBusinessStartTime("09:00")
|
||||
viewModel.updateBusinessEndTime("18:00")
|
||||
viewModel.updateHolidayStartTime("08:00")
|
||||
viewModel.updateHolidayEndTime("20:00")
|
||||
|
||||
viewModel.updateBusinessEndTime("08:00")
|
||||
XCTAssertThrowsError(try viewModel.makeSaveRequest())
|
||||
}
|
||||
|
||||
func testHasChangesAndSaveRequestFieldsMatchAndroid() async throws {
|
||||
let api = FakeProfileSpaceSettingsAPI(response: .loaded())
|
||||
let viewModel = makeViewModel()
|
||||
|
||||
await viewModel.load(api: api)
|
||||
XCTAssertFalse(viewModel.hasChanges)
|
||||
|
||||
viewModel.updateIntroduction("更新后的简介")
|
||||
viewModel.addDevice("Nikon Z8")
|
||||
viewModel.updateAcceptOrderStatus(2)
|
||||
XCTAssertTrue(viewModel.hasChanges)
|
||||
|
||||
await viewModel.save(api: api)
|
||||
|
||||
let request = try XCTUnwrap(api.lastUpdateRequest)
|
||||
XCTAssertEqual(request.scenicId, "11")
|
||||
XCTAssertEqual(request.description, "更新后的简介")
|
||||
XCTAssertEqual(request.cameraDevice, ["Sony A7", "Nikon Z8"])
|
||||
XCTAssertEqual(request.attrLabel, ["航拍"])
|
||||
XCTAssertEqual(request.shootLabel, ["亲子"])
|
||||
XCTAssertEqual(request.businessStartTime, "09:00:00")
|
||||
XCTAssertEqual(request.businessEndTime, "18:00:00")
|
||||
XCTAssertEqual(request.holidayStartTime, "10:00:00")
|
||||
XCTAssertEqual(request.holidayEndTime, "20:00:00")
|
||||
XCTAssertEqual(request.businessRange, [])
|
||||
XCTAssertEqual(request.acceptOrderStatus, 2)
|
||||
XCTAssertFalse(viewModel.hasChanges)
|
||||
}
|
||||
|
||||
func testAddScheduleValidationAndDeleteId() async throws {
|
||||
let api = FakeProfileSpaceSettingsAPI(response: .loaded())
|
||||
let viewModel = makeViewModel()
|
||||
let scheduleDate = try XCTUnwrap(calendar.date(from: DateComponents(year: 2026, month: 7, day: 9)))
|
||||
|
||||
XCTAssertThrowsError(try viewModel.makeAddScheduleRequest(
|
||||
name: "",
|
||||
remark: "内容",
|
||||
startTime: "10:00",
|
||||
endTime: "11:00",
|
||||
scheduleDate: scheduleDate,
|
||||
orderNumber: nil
|
||||
)) { error in
|
||||
XCTAssertEqual(error as? ProfileSpaceSettingsValidationError, .emptyScheduleName)
|
||||
}
|
||||
|
||||
XCTAssertThrowsError(try viewModel.makeAddScheduleRequest(
|
||||
name: "日程",
|
||||
remark: "内容",
|
||||
startTime: "12:00",
|
||||
endTime: "11:00",
|
||||
scheduleDate: scheduleDate,
|
||||
orderNumber: nil
|
||||
)) { error in
|
||||
XCTAssertEqual(error as? ProfileSpaceSettingsValidationError, .invalidScheduleTime)
|
||||
}
|
||||
|
||||
let didAdd = await viewModel.addSchedule(
|
||||
name: " 日程 ",
|
||||
remark: "内容",
|
||||
startTime: "10:00",
|
||||
endTime: "11:00",
|
||||
scheduleDate: scheduleDate,
|
||||
orderNumber: " ",
|
||||
api: api
|
||||
)
|
||||
|
||||
XCTAssertTrue(didAdd)
|
||||
XCTAssertEqual(api.lastAddScheduleRequest?.scenicId, "11")
|
||||
XCTAssertEqual(api.lastAddScheduleRequest?.name, "日程")
|
||||
XCTAssertEqual(api.lastAddScheduleRequest?.scheduleDate, "2026-07-09")
|
||||
XCTAssertNil(api.lastAddScheduleRequest?.orderNumber)
|
||||
|
||||
await viewModel.deleteSchedule(id: 33, api: api)
|
||||
XCTAssertEqual(api.lastDeletedScheduleId, 33)
|
||||
}
|
||||
|
||||
func testSpaceSettingsRoutePushesProfileSpaceSettingsPage() {
|
||||
AppStore.shared.currentScenicId = 11
|
||||
let root = UIViewController()
|
||||
let navigationController = UINavigationController(rootViewController: root)
|
||||
|
||||
HomeRouteHandler.open(uri: "space_settings", from: root, storeItem: nil)
|
||||
|
||||
XCTAssertTrue(navigationController.topViewController is ProfileSpaceSettingsViewController)
|
||||
}
|
||||
|
||||
func testSystemSettingsRouteStillSelectsProfileTab() {
|
||||
AppStore.shared.currentScenicId = 11
|
||||
let tabBarController = MainTabBarController()
|
||||
tabBarController.loadViewIfNeeded()
|
||||
guard
|
||||
let navigationController = tabBarController.selectedViewController as? UINavigationController,
|
||||
let root = navigationController.viewControllers.first
|
||||
else {
|
||||
XCTFail("首页 Tab 未正确初始化")
|
||||
return
|
||||
}
|
||||
|
||||
HomeRouteHandler.open(uri: "system_settings", from: root, storeItem: nil)
|
||||
|
||||
XCTAssertEqual(tabBarController.selectedIndex, 4)
|
||||
}
|
||||
|
||||
private func makeViewModel() -> ProfileSpaceSettingsViewModel {
|
||||
ProfileSpaceSettingsViewModel(
|
||||
appStore: appStore,
|
||||
calendar: calendar,
|
||||
nowProvider: { self.fixedNow }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class FakeProfileSpaceSettingsAPI: ProfileSpaceSettingsAPI {
|
||||
var response: PhotographerProfileInfoResponse
|
||||
var lastUpdateRequest: UpdatePhotographerProfileInfoRequest?
|
||||
var lastAddScheduleRequest: AddPhotographerScheduleRequest?
|
||||
var lastDeletedScheduleId: Int?
|
||||
|
||||
init(response: PhotographerProfileInfoResponse) {
|
||||
self.response = response
|
||||
}
|
||||
|
||||
func profileInfo(scenicId: Int) async throws -> PhotographerProfileInfoResponse {
|
||||
response
|
||||
}
|
||||
|
||||
func updateProfileInfo(_ request: UpdatePhotographerProfileInfoRequest) async throws {
|
||||
lastUpdateRequest = request
|
||||
response = PhotographerProfileInfoResponse(
|
||||
id: response.id,
|
||||
scenicId: Int(request.scenicId) ?? 0,
|
||||
photogUid: response.photogUid,
|
||||
realName: response.realName,
|
||||
nickname: response.nickname,
|
||||
avatar: response.avatar,
|
||||
scenicCertification: response.scenicCertification,
|
||||
description: request.description,
|
||||
attrLabel: request.attrLabel,
|
||||
shootLabel: request.shootLabel,
|
||||
cameraDevice: request.cameraDevice,
|
||||
businessStartTime: request.businessStartTime,
|
||||
businessEndTime: request.businessEndTime,
|
||||
holidayStartTime: request.holidayStartTime,
|
||||
holidayEndTime: request.holidayEndTime,
|
||||
businessRange: request.businessRange,
|
||||
acceptOrderStatus: request.acceptOrderStatus,
|
||||
schedule: response.schedule,
|
||||
virtualPhone: response.virtualPhone
|
||||
)
|
||||
}
|
||||
|
||||
func updateUserInfo(nickname: String?, password: String?, avatar: String?) async throws {}
|
||||
|
||||
func addSchedule(_ request: AddPhotographerScheduleRequest) async throws {
|
||||
lastAddScheduleRequest = request
|
||||
}
|
||||
|
||||
func deleteSchedule(id: Int) async throws {
|
||||
lastDeletedScheduleId = id
|
||||
}
|
||||
}
|
||||
|
||||
private extension PhotographerProfileInfoResponse {
|
||||
static func loaded() -> PhotographerProfileInfoResponse {
|
||||
PhotographerProfileInfoResponse(
|
||||
id: 1,
|
||||
scenicId: 11,
|
||||
photogUid: 100,
|
||||
realName: "林摄影",
|
||||
nickname: "小林",
|
||||
avatar: "https://example.com/a.jpg",
|
||||
scenicCertification: ["西湖"],
|
||||
description: "简介",
|
||||
attrLabel: ["航拍"],
|
||||
shootLabel: ["亲子"],
|
||||
cameraDevice: ["Sony A7"],
|
||||
businessStartTime: "09:00:00",
|
||||
businessEndTime: "18:00:00",
|
||||
holidayStartTime: "10:00:00",
|
||||
holidayEndTime: "20:00:00",
|
||||
businessRange: [],
|
||||
acceptOrderStatus: 1,
|
||||
schedule: [
|
||||
"2026-07-08": [
|
||||
PhotographerSchedule(
|
||||
id: 22,
|
||||
name: "旅拍",
|
||||
remark: "西湖",
|
||||
startTime: "10:00",
|
||||
endTime: "11:00",
|
||||
scheduleDate: "2026-07-08",
|
||||
userNickname: "游客",
|
||||
userPhone: "13800000000"
|
||||
),
|
||||
],
|
||||
],
|
||||
virtualPhone: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user