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

@ -65,6 +65,17 @@ final class ProfileAPI {
)
}
/// Android multipart
func uploadAvatar(data: Data, fileName: String) async throws {
let _: EmptyPayload = try await client.sendMultipart(
path: "/api/yf-handset-app/photog/userinfo-update-avatar",
fileName: fileName,
mimeType: "image/jpeg",
data: data,
timeoutInterval: 60
)
}
///
func switchableAccounts() async throws -> V9AuthResponse {
try await client.send(

View File

@ -72,9 +72,9 @@ final class ProfileEditViewModel {
try await api.updateUserInfo(nickname: nextNickname)
}
AppStore.shared.userName = nextNickname
AppStore.shared.session.userName = nextNickname
if let uploadedAvatarURL {
AppStore.shared.avatar = uploadedAvatarURL
AppStore.shared.session.avatar = uploadedAvatarURL
}
clearPendingAvatar()

View File

@ -22,6 +22,9 @@ protocol ProfileSpaceSettingsAPI {
///
func deleteSchedule(id: Int) async throws
///
func uploadAvatar(data: Data, fileName: String) async throws
}
extension ProfileAPI: ProfileSpaceSettingsAPI {}
@ -85,6 +88,7 @@ final class ProfileSpaceSettingsViewModel {
private(set) var scheduleMap: [String: [PhotographerSchedule]] = [:]
private(set) var selectedDate: Date
private(set) var isEditingProfile = false
private(set) var isCertificationExpanded = false
private(set) var isLoading = false
private(set) var hasChanges = false
@ -109,7 +113,7 @@ final class ProfileSpaceSettingsViewModel {
/// ID
var scenicId: Int {
appStore.currentScenicId
appStore.session.currentScenicId
}
///
@ -156,7 +160,7 @@ final class ProfileSpaceSettingsViewModel {
do {
try await api.updateUserInfo(nickname: newNickname, password: nil, avatar: nil)
nickname = newNickname
appStore.userName = newNickname
appStore.session.userName = newNickname
isEditingProfile = false
onShowMessage?("修改成功")
notifyStateChange()
@ -175,10 +179,35 @@ final class ProfileSpaceSettingsViewModel {
/// URL
func updateAvatarURL(_ url: String) {
avatarURL = url
appStore.avatar = url
appStore.session.avatar = url
notifyStateChange()
}
///
func toggleCertificationExpanded() {
isCertificationExpanded.toggle()
notifyStateChange()
}
///
func uploadAvatar(
data: Data,
fileName: String,
api: any ProfileSpaceSettingsAPI
) async {
setLoading(true)
defer { setLoading(false) }
do {
try await api.uploadAvatar(data: data, fileName: fileName)
onShowMessage?("上传成功")
await load(api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription)
}
}
///
func updateEditingNickname(_ value: String) {
editingNickname = value
@ -225,41 +254,41 @@ final class ProfileSpaceSettingsViewModel {
///
func updateBusinessStartTime(_ value: String) {
businessStartTime = value
if let end = businessEndTime, !Self.isStart(value, before: end) {
onShowMessage?("开始时间不能大于结束时间")
businessStartTime = nil
return
}
businessStartTime = value
updateHasChanges()
}
///
func updateBusinessEndTime(_ value: String) {
businessEndTime = value
if let start = businessStartTime, !Self.isStart(start, before: value) {
onShowMessage?("结束时间不能小于开始时间")
businessEndTime = nil
return
}
businessEndTime = value
updateHasChanges()
}
///
func updateHolidayStartTime(_ value: String) {
holidayStartTime = value
if let end = holidayEndTime, !Self.isStart(value, before: end) {
onShowMessage?("开始时间不能大于结束时间")
holidayStartTime = nil
return
}
holidayStartTime = value
updateHasChanges()
}
///
func updateHolidayEndTime(_ value: String) {
holidayEndTime = value
if let start = holidayStartTime, !Self.isStart(start, before: value) {
onShowMessage?("结束时间不能小于开始时间")
holidayEndTime = nil
return
}
holidayEndTime = value
updateHasChanges()
}
@ -417,7 +446,7 @@ final class ProfileSpaceSettingsViewModel {
editingNickname = response.nickname
if !response.avatar.isEmpty {
avatarURL = response.avatar
appStore.avatar = response.avatar
appStore.session.avatar = response.avatar
}
scenicCertification = response.scenicCertification
introduction = response.description
@ -430,8 +459,8 @@ final class ProfileSpaceSettingsViewModel {
holidayEndTime = Self.shortTimeString(response.holidayEndTime)
acceptOrderStatus = response.acceptOrderStatus
scheduleMap = response.schedule
appStore.userName = response.nickname
appStore.realName = response.realName
appStore.session.userName = response.nickname
appStore.session.realName = response.realName
originalSnapshot = currentSnapshot()
hasChanges = false
notifyStateChange()

View File

@ -25,42 +25,42 @@ final class ProfileViewModel {
}
var displayPhone: String {
nonEmpty(userInfo?.phone) ?? AppStore.shared.phone.nonEmpty ?? "--"
nonEmpty(userInfo?.phone) ?? AppStore.shared.session.phone.nonEmpty ?? "--"
}
var displayAvatarURL: String {
nonEmpty(userInfo?.avatar) ?? AppStore.shared.avatar.nonEmpty ?? ""
nonEmpty(userInfo?.avatar) ?? AppStore.shared.session.avatar.nonEmpty ?? ""
}
var displayUID: String {
let uid = AppStore.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
let uid = AppStore.shared.session.userId.trimmingCharacters(in: .whitespacesAndNewlines)
return uid.isEmpty ? "--" : uid
}
var accountDisplayName: String {
let name = AppStore.shared.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines)
let name = AppStore.shared.session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? "--" : name
}
///
var accountTypeLabel: String {
let type = AppStore.shared.accountType.trimmingCharacters(in: .whitespacesAndNewlines)
if type == V9StoreUser.accountTypeValue {
return "门店账号"
switch AppStore.shared.session.accountType {
case .storeUser:
"门店账号"
case .scenicUser:
"景区账号"
case .unknown:
"--"
}
if type == V9ScenicUser.accountTypeValue {
return "景区账号"
}
return "--"
}
///
var isStoreAccount: Bool {
AppStore.shared.accountType == V9StoreUser.accountTypeValue
AppStore.shared.session.accountType == .storeUser
}
var currentScenicName: String {
let name = AppStore.shared.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
let name = AppStore.shared.session.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? "--" : name
}
@ -69,7 +69,7 @@ final class ProfileViewModel {
}
var showPhotographerFields: Bool {
if AppStore.shared.isPhotographerRole {
if AppStore.shared.session.isPhotographerRole {
return true
}
if let roleName = nonEmpty(userInfo?.roleName) {
@ -108,9 +108,9 @@ final class ProfileViewModel {
let info = try await api.userInfo()
userInfo = info
AppStore.shared.applyUserInfo(info)
AppStore.shared.session.applyUserInfo(info)
if !info.roleName.isEmpty {
AppStore.shared.roleName = info.roleName
AppStore.shared.session.roleName = info.roleName
}
if showPhotographerFields {
@ -128,8 +128,8 @@ final class ProfileViewModel {
/// `AppStore`
func applyLocalProfileUpdate(from store: AppStore = .shared) {
let previous = userInfo ?? UserInfoResponse()
let nextNickname = store.userName.trimmingCharacters(in: .whitespacesAndNewlines)
let nextAvatar = store.avatar.trimmingCharacters(in: .whitespacesAndNewlines)
let nextNickname = store.session.userName.trimmingCharacters(in: .whitespacesAndNewlines)
let nextAvatar = store.session.avatar.trimmingCharacters(in: .whitespacesAndNewlines)
userInfo = UserInfoResponse(
avatar: nextAvatar.isEmpty ? previous.avatar : nextAvatar,
realName: previous.realName,