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

@ -18,9 +18,9 @@ enum AuthSessionHelper {
let token = response.token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !token.isEmpty else { return }
AppStore.shared.saveToken(token)
AppStore.shared.lastLoginUsername = username
AppStore.shared.privacyAgreementAccepted = privacyAgreementAccepted
AppStore.shared.session.saveToken(token)
AppStore.shared.session.lastLoginUsername = username
AppStore.shared.session.privacyAgreementAccepted = privacyAgreementAccepted
applyAccountSession(from: selectedAccount, in: response)
if privacyAgreementAccepted {
@ -39,14 +39,14 @@ enum AuthSessionHelper {
let token = response.token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !token.isEmpty else { return }
AppStore.shared.saveToken(token)
AppStore.shared.session.saveToken(token)
applyAccountSession(from: account, in: response)
NotificationCenter.default.post(name: NotificationName.accountDidSwitch, object: nil)
}
/// v9 AppStore
static func applyAccountSession(from account: AccountSwitchAccount, in response: V9AuthResponse) {
AppStore.shared.applyAccount(account)
AppStore.shared.session.applyAccount(account)
if let scenicUser = response.scenicUsers.first(where: { matches($0, account: account) }) {
applyScenicUser(scenicUser)
@ -67,43 +67,43 @@ enum AuthSessionHelper {
}
private static func applyScenicUser(_ user: V9ScenicUser) {
AppStore.shared.userId = String(user.businessUserId)
AppStore.shared.accountType = V9ScenicUser.accountTypeValue
AppStore.shared.accountDisplayName = firstNonEmpty(
AppStore.shared.session.userId = String(user.businessUserId)
AppStore.shared.session.accountType = .scenicUser
AppStore.shared.session.accountDisplayName = firstNonEmpty(
user.scenicName, user.nickname, user.realName, user.username
)
AppStore.shared.userName = firstNonEmpty(user.nickname, user.username, user.realName)
AppStore.shared.realName = firstNonEmpty(user.realName, user.nickname, user.username)
AppStore.shared.phone = user.phone
AppStore.shared.currentScenicId = user.scenicId
AppStore.shared.currentScenicName = user.scenicName
AppStore.shared.currentStoreId = 0
AppStore.shared.session.userName = firstNonEmpty(user.nickname, user.username, user.realName)
AppStore.shared.session.realName = firstNonEmpty(user.realName, user.nickname, user.username)
AppStore.shared.session.phone = user.phone
AppStore.shared.session.currentScenicId = user.scenicId
AppStore.shared.session.currentScenicName = user.scenicName
AppStore.shared.session.currentStoreId = 0
if !user.appRoleCode.isEmpty {
AppStore.shared.roleCode = user.appRoleCode
AppStore.shared.session.roleCode = user.appRoleCode
}
if !user.appRoleName.isEmpty {
AppStore.shared.roleName = user.appRoleName
AppStore.shared.session.roleName = user.appRoleName
}
}
private static func applyStoreUser(_ user: V9StoreUser) {
AppStore.shared.userId = String(user.businessUserId)
AppStore.shared.accountType = V9StoreUser.accountTypeValue
AppStore.shared.accountDisplayName = firstNonEmpty(
AppStore.shared.session.userId = String(user.businessUserId)
AppStore.shared.session.accountType = .storeUser
AppStore.shared.session.accountDisplayName = firstNonEmpty(
user.storeName, user.scenicName, user.realName, user.userName, user.username
)
AppStore.shared.userName = firstNonEmpty(user.userName, user.username, user.realName)
AppStore.shared.realName = firstNonEmpty(user.realName, user.userName, user.username)
AppStore.shared.phone = user.phone
AppStore.shared.avatar = user.avatar
AppStore.shared.currentScenicId = user.scenicId
AppStore.shared.currentScenicName = user.scenicName
AppStore.shared.currentStoreId = user.storeId
AppStore.shared.session.userName = firstNonEmpty(user.userName, user.username, user.realName)
AppStore.shared.session.realName = firstNonEmpty(user.realName, user.userName, user.username)
AppStore.shared.session.phone = user.phone
AppStore.shared.session.avatar = user.avatar
AppStore.shared.session.currentScenicId = user.scenicId
AppStore.shared.session.currentScenicName = user.scenicName
AppStore.shared.session.currentStoreId = user.storeId
if !user.appRoleCode.isEmpty {
AppStore.shared.roleCode = user.appRoleCode
AppStore.shared.session.roleCode = user.appRoleCode
}
if !user.appRoleName.isEmpty {
AppStore.shared.roleName = user.appRoleName
AppStore.shared.session.roleName = user.appRoleName
}
}

View File

@ -14,7 +14,7 @@ final class CloudTransferManager {
api: NetworkServices.shared.cloudDriveAPI,
downloader: CloudURLSessionDownloader(),
photoSaver: CloudPhotoLibrarySaver(),
scenicIdProvider: { AppStore.shared.currentScenicId }
scenicIdProvider: { AppStore.shared.session.currentScenicId }
)
private(set) var uploadTasks: [CloudTransferTask] = []

View File

@ -33,7 +33,7 @@ final class CooperationAcquirerViewModel {
}
func loadAcquirers(api: OrderAPI, initial: Bool = false) async {
let storeId = appStore.currentStoreId
let storeId = appStore.session.currentStoreId
let storeIdParam = storeId > 0 ? String(storeId) : nil
do {
let response = try await api.cooperativeSalers(storeId: storeIdParam, pageSize: "20")

View File

@ -24,8 +24,8 @@ final class HomeLocationStateStore {
init(store: AppStore = .shared) {
self.store = store
reminderMinutes = store.locationReminderMinutes
isOnline = store.onlineStatus
reminderMinutes = store.location.reminderMinutes
isOnline = store.location.onlineStatus
}
deinit {
@ -44,18 +44,18 @@ final class HomeLocationStateStore {
/// 线
func restoreStateIfNeeded() {
reminderMinutes = store.locationReminderMinutes
guard store.onlineStatus else {
reminderMinutes = store.location.reminderMinutes
guard store.location.onlineStatus else {
isOnline = false
stopCountdown()
notifyChange()
return
}
let lastTime = store.lastLocationReportTime
let lastTime = store.location.lastReportTime
guard lastTime > 0 else {
isOnline = false
store.onlineStatus = false
store.clearLastLocationReportTime()
store.location.onlineStatus = false
store.location.clearLastReportTime()
stopCountdown()
notifyChange()
return
@ -65,7 +65,7 @@ final class HomeLocationStateStore {
let elapsed = now - lastTime
if elapsed >= Self.onlineDurationMillis {
updateOnlineStatus(false)
store.clearLastLocationReportTime()
store.location.clearLastReportTime()
notifyChange()
return
}
@ -81,10 +81,10 @@ final class HomeLocationStateStore {
/// 线
func updateOnlineStatus(_ online: Bool) {
isOnline = online
store.onlineStatus = online
store.location.onlineStatus = online
if !online {
stopCountdown()
store.clearLastLocationReportTime()
store.location.clearLastReportTime()
}
notifyChange()
}
@ -92,16 +92,16 @@ final class HomeLocationStateStore {
///
func updateReminderMinutes(_ minutes: Int) {
reminderMinutes = minutes
store.locationReminderMinutes = minutes
store.location.reminderMinutes = minutes
notifyChange()
}
///
func startLocationReport(at timestamp: Int64 = Int64(Date().timeIntervalSince1970 * 1000)) {
anchorTimestamp = timestamp
store.lastLocationReportTime = timestamp
store.location.lastReportTime = timestamp
isOnline = true
store.onlineStatus = true
store.location.onlineStatus = true
elapsedSeconds = 0
nextReportCountdownSeconds = Self.onlineDurationMillis / 1000
startCountdown(anchorTime: timestamp)
@ -156,7 +156,7 @@ final class HomeLocationStateStore {
if remaining <= 0 {
self.nextReportCountdownSeconds = 0
self.updateOnlineStatus(false)
self.store.clearLastLocationReportTime()
self.store.location.clearLastReportTime()
return
}
self.nextReportCountdownSeconds = max(0, remaining / 1000)

View File

@ -10,10 +10,10 @@ enum HomeMenuCatalog {
/// Android `Constants.menuList`
static let allItems: [HomeMenuItem] = [
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "person.crop.square.fill"),
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
HomeMenuItem(uri: "wallet", title: "我的钱包", iconName: "creditcard.fill"),
HomeMenuItem(uri: "cloud_management", title: "相册云盘", iconName: "icloud.fill"),
HomeMenuItem(uri: "asset_management", title: "素材管理", iconName: "photo.on.rectangle.angled"),
HomeMenuItem(uri: "asset_management", title: "素材管理", iconName: "home_menu_material"),
HomeMenuItem(uri: "task_management", title: "任务管理", iconName: "checklist"),
HomeMenuItem(uri: "task_management_editor", title: "任务管理", iconName: "checklist"),
HomeMenuItem(uri: "schedule_management", title: "日程管理", iconName: "calendar"),

View File

@ -21,7 +21,7 @@ enum HomeRouteHandler {
return
}
if uri != "pilot_controller", uri != "wallet", AppStore.shared.currentScenicId <= 0 {
if uri != "pilot_controller", uri != "wallet", AppStore.shared.session.currentScenicId <= 0 {
showToast("请先选择景区", from: viewController)
return
}
@ -87,7 +87,7 @@ enum HomeRouteHandler {
@MainActor
private static func handleOperatingArea(from viewController: UIViewController, storeItem: StoreItem?) {
guard let role = AppStore.shared.currentAppRole else {
guard let role = AppStore.shared.session.currentAppRole else {
showToast("当前账号暂不支持运营区域", from: viewController)
return
}
@ -99,7 +99,7 @@ enum HomeRouteHandler {
}
showDeveloping(from: viewController)
case .scenicAdmin:
guard AppStore.shared.currentScenicId > 0 else {
guard AppStore.shared.session.currentScenicId > 0 else {
showToast("请先选择景区", from: viewController)
return
}
@ -110,7 +110,7 @@ enum HomeRouteHandler {
}
private static func hasCooperationOrderPermission() -> Bool {
let permissions = AppStore.shared.permissionItems()
let permissions = AppStore.shared.permissions.permissionItems()
return permissions.contains { $0.uri == "cooperation_order" }
|| permissions.contains(where: { containsCooperationOrder(in: $0) })
}

View File

@ -27,10 +27,10 @@ final class AllFunctionsViewModel {
///
func loadFunctions() {
let permissions = appStore.permissionItems()
let permissions = appStore.permissions.permissionItems()
let allMenus = HomeMenuCatalog.visibleMenus(from: permissions)
let accountScope = appStore.accountCachePrefix
let roleCode = appStore.roleCode
let accountScope = appStore.session.accountCachePrefix
let roleCode = appStore.session.roleCode
let savedURIs = commonMenuStore.savedCommonURIs(accountScope: accountScope, roleCode: roleCode)
let defaultURIs = HomeCommonMenuStore.defaultCommonURIs(from: permissions)
let commonURIs = savedURIs.isEmpty ? defaultURIs : savedURIs
@ -71,8 +71,8 @@ final class AllFunctionsViewModel {
private func persistCommonMenus() {
commonMenuStore.saveCommonURIs(
commonMenus.map(\.uri),
accountScope: appStore.accountCachePrefix,
roleCode: appStore.roleCode
accountScope: appStore.session.accountCachePrefix,
roleCode: appStore.session.roleCode
)
}

View File

@ -94,7 +94,7 @@ final class HomeViewModel {
/// role-permission
func loadPermissions(api: HomeAPI, force: Bool = false) async {
if !force, !appStore.permissionItems().isEmpty {
if !force, !appStore.permissions.permissionItems().isEmpty {
rebuildCommonMenus()
await loadStoreListIfNeeded(api: api)
notifyStateChange()
@ -104,21 +104,21 @@ final class HomeViewModel {
do {
let rolePermissionList = try await api.rolePermissions()
if rolePermissionList.isEmpty {
appStore.savePermissionItems([])
appStore.permissions.savePermissionItems([])
showPermissionDialog = true
rebuildCommonMenus()
notifyStateChange()
return
}
appStore.saveRolePermissionList(rolePermissionList)
let matched = appStore.matchRolePermissionItem(in: rolePermissionList) ?? rolePermissionList[0]
appStore.permissions.saveRolePermissionList(rolePermissionList)
let matched = appStore.permissions.matchRolePermissionItem(in: rolePermissionList) ?? rolePermissionList[0]
if !matched.role.name.isEmpty {
appStore.roleName = matched.role.name
appStore.session.roleName = matched.role.name
}
appStore.saveRoleScenicList(matched.scenic)
appStore.permissions.saveRoleScenicList(matched.scenic)
let permissions = matched.role.permission.map(HomePermissionItem.init)
appStore.savePermissionItems(permissions)
appStore.permissions.savePermissionItems(permissions)
showPermissionDialog = false
refreshLocalDisplayState()
rebuildCommonMenus()
@ -127,7 +127,7 @@ final class HomeViewModel {
} catch is CancellationError {
return
} catch {
appStore.savePermissionItems([])
appStore.permissions.savePermissionItems([])
showPermissionDialog = true
rebuildCommonMenus()
onShowMessage?(error.localizedDescription)
@ -142,7 +142,7 @@ final class HomeViewModel {
notifyStateChange()
return
}
guard appStore.currentScenicId > 0 else {
guard appStore.session.currentScenicId > 0 else {
storeItem = nil
notifyStateChange()
return
@ -150,13 +150,13 @@ final class HomeViewModel {
do {
let response = try await api.storeList()
let scenicId = appStore.currentScenicId
let savedStoreId = appStore.currentStoreId
let scenicId = appStore.session.currentScenicId
let savedStoreId = appStore.session.currentStoreId
let savedMatch = savedStoreId > 0 ? response.list.first { $0.id == savedStoreId } : nil
let matched = savedMatch ?? response.list.first { $0.scenicId == scenicId }
storeItem = matched
if let matched {
appStore.currentStoreId = matched.id
appStore.session.currentStoreId = matched.id
}
notifyStateChange()
} catch is CancellationError {
@ -173,7 +173,7 @@ final class HomeViewModel {
showScenicDialog = false
return
}
let hasScenic = appStore.currentScenicId > 0
let hasScenic = appStore.session.currentScenicId > 0
showScenicDialog = !hasScenic
if showScenicDialog || dismissedLocationTimeoutDialog {
showLocationTimeoutDialog = false
@ -196,7 +196,7 @@ final class HomeViewModel {
notifyStateChange()
return
}
let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines)
let staffId = appStore.session.userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !staffId.isEmpty else {
showLocationTimeoutDialog = true
notifyStateChange()
@ -225,25 +225,25 @@ final class HomeViewModel {
/// Android `initCurrentScenicName`
func refreshLocalDisplayState() {
let scenicId = appStore.currentScenicId
let scenicName = appStore.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
let scenicId = appStore.session.currentScenicId
let scenicName = appStore.session.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
if scenicId > 0, !scenicName.isEmpty {
currentScenicName = scenicName
} else {
currentScenicName = ""
appStore.currentScenicId = 0
appStore.currentScenicName = ""
appStore.session.currentScenicId = 0
appStore.session.currentScenicName = ""
}
currentAppRole = appStore.currentAppRole
currentAppRole = appStore.session.currentAppRole
isMinimalTopRole = currentAppRole?.usesHomeMinimalTopLayout ?? false
}
///
func rebuildCommonMenus() {
let permissions = appStore.permissionItems()
let permissions = appStore.permissions.permissionItems()
let allMenus = HomeMenuCatalog.visibleMenus(from: permissions)
let accountScope = appStore.accountCachePrefix
let roleCode = appStore.roleCode
let accountScope = appStore.session.accountCachePrefix
let roleCode = appStore.session.roleCode
let savedURIs = commonMenuStore.savedCommonURIs(accountScope: accountScope, roleCode: roleCode)
if savedURIs.isEmpty, !permissions.isEmpty {

View File

@ -21,7 +21,7 @@ final class PermissionApplyStatusViewModel {
init(applyCode: String, appStore: AppStore = .shared) {
self.applyCode = applyCode
self.appStore = appStore
rolePermissionList = appStore.rolePermissionList()
rolePermissionList = appStore.permissions.rolePermissionList()
}
func loadPendingData(api: HomeAPI) async {

View File

@ -45,7 +45,7 @@ final class PermissionApplyViewModel {
}
func loadRolesFromLocal() {
let rolePermissionList = appStore.rolePermissionList()
let rolePermissionList = appStore.permissions.rolePermissionList()
var seen = Set<Int>()
availableRoles = rolePermissionList.compactMap { item in
guard !seen.contains(item.role.id) else { return nil }
@ -158,7 +158,7 @@ final class PermissionApplyViewModel {
///
func existingScenics(for roleId: Int) -> [ScenicInfo] {
appStore.rolePermissionList()
appStore.permissions.rolePermissionList()
.filter { $0.role.id == roleId }
.flatMap(\.scenic)
}

View File

@ -43,7 +43,7 @@ final class ScenicSelectionViewModel {
searchQuery: searchQuery,
currentLat: currentLat,
currentLng: currentLng,
selectedScenicId: appStore.currentScenicId
selectedScenicId: appStore.session.currentScenicId
)
notifyStateChange()
}
@ -73,8 +73,8 @@ final class ScenicSelectionViewModel {
}
func selectSpot(_ spot: ScenicSpotDisplayItem) {
appStore.currentScenicId = spot.id
appStore.currentScenicName = spot.name
appStore.session.currentScenicId = spot.id
appStore.session.currentScenicName = spot.name
NotificationCenter.default.post(
name: NotificationName.scenicDidChange,
object: nil,
@ -109,11 +109,11 @@ final class ScenicSelectionViewModel {
}
private func resolvedScenicList() -> [ScenicInfo] {
let cached = appStore.roleScenicList()
let cached = appStore.permissions.roleScenicList()
if !cached.isEmpty { return cached }
let rolePermissionList = appStore.rolePermissionList()
let rolePermissionList = appStore.permissions.rolePermissionList()
guard !rolePermissionList.isEmpty else { return [] }
return appStore.matchRolePermissionItem(in: rolePermissionList)?.scenic ?? []
return appStore.permissions.matchRolePermissionItem(in: rolePermissionList)?.scenic ?? []
}
private func notifyStateChange() {

View File

@ -34,7 +34,7 @@ final class LiveManageViewModel {
private let pageSize = 10
/// ViewModel
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }) {
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }) {
self.currentScenicIdProvider = currentScenicIdProvider
}
@ -282,7 +282,7 @@ final class LiveAlbumListViewModel {
private let pageSize = 10
/// ViewModel
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }) {
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }) {
self.currentScenicIdProvider = currentScenicIdProvider
}
@ -419,7 +419,7 @@ final class LiveAlbumAddViewModel {
private let currentScenicIdProvider: () -> Int
/// ViewModel
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }) {
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }) {
self.currentScenicIdProvider = currentScenicIdProvider
}

View File

@ -14,7 +14,7 @@ enum AMapBootstrap {
/// App SDK
@discardableResult
static func configureIfNeeded(appStore: AppStore = .shared) -> Bool {
guard appStore.privacyAgreementAccepted else { return false }
guard appStore.session.privacyAgreementAccepted else { return false }
lock.lock()
defer { lock.unlock() }

View File

@ -44,7 +44,7 @@ final class LocationReportService {
let remaining = Int((Self.operationIntervalMillis - (now - lastOnlineStatusSwitchTime)) / 1000)
throw LocationReportServiceError.tooFrequent(remainingSeconds: remaining)
}
guard appStore.currentScenicId > 0 else {
guard appStore.session.currentScenicId > 0 else {
throw LocationReportServiceError.missingScenic
}
@ -112,10 +112,10 @@ final class LocationReportService {
}
}
guard appStore.currentScenicId > 0 else {
guard appStore.session.currentScenicId > 0 else {
throw LocationReportServiceError.missingScenic
}
let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines)
let staffId = appStore.session.userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !staffId.isEmpty else {
throw LocationReportServiceError.missingStaffId
}
@ -153,7 +153,7 @@ final class LocationReportService {
longitude: snapshot.longitude,
address: snapshot.address,
type: type,
scenicId: String(appStore.currentScenicId)
scenicId: String(appStore.session.currentScenicId)
)
lastLocationReportTime = currentTimestampMillis()

View File

@ -34,7 +34,7 @@ final class LocationReportHistoryViewModel {
///
func loadReportList(api: HomeAPI, refresh: Bool = false) async {
guard !isLoading || refresh else { return }
let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines)
let staffId = appStore.session.userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !staffId.isEmpty else {
onShowMessage?("获取用户ID失败")
return

View File

@ -62,13 +62,6 @@ enum MaterialMediaType: Int, CaseIterable, Sendable, Equatable {
}
}
/// 1 2
var cloudFileType: Int {
switch self {
case .image: 2
case .video: 1
}
}
}
/// Android `MaterialItemEntity`

View File

@ -141,7 +141,12 @@ final class MaterialListViewModel {
)
totalCount = response.total
orderInfo = response.order ?? MaterialOrderInfo()
items = reset ? response.list : items + response.list
if reset {
items = response.list
} else {
let existingIDs = Set(items.map(\.id))
items.append(contentsOf: response.list.filter { !existingIDs.contains($0.id) })
}
canLoadMore = items.count < totalCount
} catch is CancellationError {
return
@ -244,6 +249,7 @@ final class MaterialFormViewModel {
private(set) var coverImage: MaterialUploadedMediaItem?
private(set) var tagList: [MaterialTagItem] = []
private(set) var tagInput = ""
private(set) var pendingDeleteTag: MaterialTagItem?
private(set) var uploadDialogState: MaterialUploadDialogState?
private(set) var isSubmitting = false
private(set) var isLoadingDetail = false
@ -259,7 +265,7 @@ final class MaterialFormViewModel {
/// ViewModel
init(
mode: Mode = .create,
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }
) {
self.mode = mode
self.currentScenicIdProvider = currentScenicIdProvider
@ -295,7 +301,7 @@ final class MaterialFormViewModel {
///
func updateTagInput(_ text: String) {
tagInput = text
tagInput = String(text.prefix(15))
notifyStateChange()
}
@ -327,9 +333,23 @@ final class MaterialFormViewModel {
}
}
///
func deleteTag(_ tag: MaterialTagItem) {
///
func requestDeleteTag(_ tag: MaterialTagItem) {
pendingDeleteTag = tag
notifyStateChange()
}
///
func cancelDeleteTag() {
pendingDeleteTag = nil
notifyStateChange()
}
///
func confirmDeleteTag() {
guard let tag = pendingDeleteTag else { return }
tagList.removeAll { $0.id == tag.id }
pendingDeleteTag = nil
notifyStateChange()
}
@ -354,48 +374,6 @@ final class MaterialFormViewModel {
notifyStateChange()
}
/// 使 URL
func addCloudMediaFiles(_ files: [CloudFile]) {
guard !files.isEmpty else { return }
let remaining = mediaType.maxCount - uploadedMediaList.count
guard remaining > 0 else {
onShowMessage?(mediaType == .video ? "视频只能上传1个" : "图片最多上传9张")
return
}
var seenCloudIDs = importedCloudFileIDs
var accepted: [MaterialUploadedMediaItem] = []
for file in files where accepted.count < remaining {
let fileURL = file.fileUrl.trimmingCharacters(in: .whitespacesAndNewlines)
guard file.type == mediaType.cloudFileType, !fileURL.isEmpty, !seenCloudIDs.contains(file.id) else {
continue
}
seenCloudIDs.insert(file.id)
accepted.append(
MaterialUploadedMediaItem(
id: "cloud_\(file.id)",
data: Data(),
thumbnailData: nil,
previewURL: file.coverUrl.isEmpty ? fileURL : file.coverUrl,
fileName: file.name.isEmpty ? "cloud_file_\(file.id)" : file.name,
size: file.fileSize,
width: 0,
height: 0,
ossUrl: fileURL,
isUploading: false,
uploadProgress: 100
)
)
}
guard !accepted.isEmpty else {
onShowMessage?(mediaType == .video ? "请选择视频素材" : "请选择图片素材")
return
}
uploadedMediaList.append(contentsOf: accepted)
notifyStateChange()
}
///
func setCoverImage(_ item: MaterialUploadedMediaItem, uploader: any MaterialOSSUploading) async {
coverImage = item
@ -418,16 +396,6 @@ final class MaterialFormViewModel {
notifyStateChange()
}
/// ID
var importedCloudFileIDs: Set<Int> {
Set(
uploadedMediaList.compactMap { item in
guard item.id.hasPrefix("cloud_") else { return nil }
return Int(item.id.dropFirst("cloud_".count))
}
)
}
///
func submit(api: any MaterialManagementServing) async {
guard !materialName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {

View File

@ -40,7 +40,7 @@ final class MainTabScanHandler {
case "check_in":
await performCheckIn(scanResult: scanResult)
case "order_verify":
if AppStore.shared.currentAppRole == .storeAdmin {
if AppStore.shared.session.currentAppRole == .storeAdmin {
await performStoreVerify(parsed: parsed)
} else {
await performPhotographerVerify(jsonBody: parsed.rawJSON)
@ -92,7 +92,7 @@ final class MainTabScanHandler {
onShowMessage?("请扫描正确的二维码")
return
}
json["scenic_id"] = String(AppStore.shared.currentScenicId)
json["scenic_id"] = String(AppStore.shared.session.currentScenicId)
guard let body = serializeJSON(json) else { return }
do {
let response = try await api.orderCheckIn(jsonBody: body)
@ -116,7 +116,7 @@ final class MainTabScanHandler {
onShowMessage?("请扫描正确的核销码")
return
}
let storeId = AppStore.shared.currentStoreId
let storeId = AppStore.shared.session.currentStoreId
guard storeId > 0 else {
onShowMessage?("店铺信息异常,请先选择店铺")
return

View File

@ -102,7 +102,7 @@ final class DepositOrderListViewModel {
onShowMessage?("核销码内容不完整")
return
}
let storeId = AppStore.shared.currentStoreId
let storeId = AppStore.shared.session.currentStoreId
guard storeId > 0 else {
onShowMessage?("店铺信息异常,请先选择店铺")
return
@ -125,7 +125,7 @@ final class DepositOrderListViewModel {
refundReason: String,
api: OrderAPI
) async {
let storeId = AppStore.shared.currentStoreId
let storeId = AppStore.shared.session.currentStoreId
guard storeId > 0 else {
onShowMessage?("店铺信息异常")
return
@ -166,7 +166,7 @@ final class DepositOrderListViewModel {
let phone = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
let isPhoneSearch = !phone.isEmpty && Self.isValidPhone(phone)
let storeId = AppStore.shared.currentStoreId
let storeId = AppStore.shared.session.currentStoreId
if storeId <= 0 {
finishLoading()
orderList = []

View File

@ -122,7 +122,7 @@ final class OrderListViewModel {
}()
var isScenicAdminReadOnly: Bool {
AppStore.shared.currentAppRole == .scenicAdmin
AppStore.shared.session.currentAppRole == .scenicAdmin
}
var listAPIPath: String {
@ -359,7 +359,7 @@ final class OrderListViewModel {
isLoadingOrderSourcePeople = false
notifyStateChange()
}
let storeId = AppStore.shared.currentStoreId
let storeId = AppStore.shared.session.currentStoreId
let storeIdParam = storeId > 0 ? String(storeId) : nil
do {
let response = try await api.cooperativeSalers(storeId: storeIdParam)
@ -402,7 +402,7 @@ final class OrderListViewModel {
}
private func loadOrderList(api: OrderAPI, append: Bool) async {
let scenicId = AppStore.shared.currentScenicId
let scenicId = AppStore.shared.session.currentScenicId
guard scenicId > 0 else {
finishLoading()
onShowMessage?("当前景区信息缺失")
@ -479,7 +479,7 @@ final class OrderListViewModel {
var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
json["scenic_id"] = String(AppStore.shared.currentScenicId)
json["scenic_id"] = String(AppStore.shared.session.currentScenicId)
guard let updated = try? JSONSerialization.data(withJSONObject: json),
let string = String(data: updated, encoding: .utf8) else {
return nil

View File

@ -40,9 +40,9 @@ final class PaymentCollectionDetailsViewModel {
}
func refreshLocalState() {
scenicName = appStore.currentScenicName
staffId = appStore.userId
isVoiceBroadcastOpen = appStore.isOpenReceiveVoice
scenicName = appStore.session.currentScenicName
staffId = appStore.session.userId
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
notifyStateChange()
}
@ -56,7 +56,7 @@ final class PaymentCollectionDetailsViewModel {
notifyStateChange()
}
let scenicId = appStore.currentScenicId
let scenicId = appStore.session.currentScenicId
guard scenicId > 0 else {
clearPayCode()
return
@ -134,7 +134,7 @@ final class PaymentCollectionDetailsViewModel {
func toggleReceiveVoice() {
isVoiceBroadcastOpen.toggle()
appStore.isOpenReceiveVoice = isVoiceBroadcastOpen
appStore.payment.isOpenReceiveVoice = isVoiceBroadcastOpen
notifyStateChange()
}

View File

@ -28,7 +28,7 @@ final class PaymentCollectionRecordViewModel {
notifyStateChange()
}
let scenicId = appStore.currentScenicId
let scenicId = appStore.session.currentScenicId
guard scenicId > 0 else {
groups = []
return

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,

View File

@ -36,7 +36,7 @@ final class PunchPointListViewModel {
private let currentScenicIdProvider: () -> Int
/// ViewModel
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }) {
init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }) {
self.currentScenicIdProvider = currentScenicIdProvider
}
@ -223,7 +223,7 @@ final class PunchPointFormViewModel {
/// ViewModel
init(
mode: Mode = .create,
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId },
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId },
locationProvider: any LocationProviding = LocationProvider.shared
) {
self.mode = mode

View File

@ -222,8 +222,8 @@ final class UploadSampleViewModel {
/// ViewModel
init(
scenicListProvider: @escaping () -> [ScenicInfo] = { AppStore.shared.roleScenicList() },
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }
scenicListProvider: @escaping () -> [ScenicInfo] = { AppStore.shared.permissions.roleScenicList() },
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }
) {
self.scenicListProvider = scenicListProvider
self.currentScenicIdProvider = currentScenicIdProvider

View File

@ -84,7 +84,7 @@ final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynth
func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async {
guard let api else { return }
guard !appStore.scenicQueueUseOfflineTTS || supportsOfflineTTS else {
guard !appStore.scenicQueue.useOfflineTTS || supportsOfflineTTS else {
nativeReady = false
return
}

View File

@ -39,7 +39,7 @@ final class ScenicQueueSettingChangeLogViewModel {
}
private func load(api: ScenicQueueAPIProtocol, page: Int, append: Bool) async {
guard appStore.currentScenicId > 0 else {
guard appStore.session.currentScenicId > 0 else {
onShowMessage?("请先选择景区")
return
}
@ -53,10 +53,10 @@ final class ScenicQueueSettingChangeLogViewModel {
if append { isLoadingMore = false } else { isRefreshing = false }
notifyStateChange()
}
let spotId = Int64(appStore.scenicQueuePunchSpotId)
let spotId = Int64(appStore.scenicQueue.punchSpotId)
do {
let data = try await api.settingChangeLog(
scenicId: Int64(appStore.currentScenicId),
scenicId: Int64(appStore.session.currentScenicId),
scenicSpotId: spotId,
page: page,
pageSize: Constants.pageSize

View File

@ -61,9 +61,9 @@ final class ScenicQueueSettingsViewModel {
init(appStore: AppStore = .shared, ttsManager: ScenicQueueTTSManaging = AliyunNLSTTSManager.shared) {
self.appStore = appStore
self.ttsManager = ttsManager
let snapshot = appStore.scenicQueueSettingsSnapshot() ?? ScenicQueueSettingsSnapshot()
selectedPunchSpotId = appStore.scenicQueuePunchSpotId.nonEmptyTrimmed
selectedPunchSpotLabel = appStore.scenicQueuePunchSpotName
let snapshot = appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()
selectedPunchSpotId = appStore.scenicQueue.punchSpotId.nonEmptyTrimmed
selectedPunchSpotLabel = appStore.scenicQueue.punchSpotName
shootMinute = snapshot.shootMinute.clamped(to: Limits.shootMinute)
shootSecond = snapshot.shootSecond.clamped(to: Limits.shootSecond)
firstAheadCount = snapshot.firstAheadCount.clamped(to: Limits.ahead)
@ -84,8 +84,8 @@ final class ScenicQueueSettingsViewModel {
businessOpen = snapshot.businessOpen
businessStartTime = Self.normalizedTime(snapshot.businessStartTime, fallback: "10:00")
businessEndTime = Self.normalizedTime(snapshot.businessEndTime, fallback: "20:00")
customTTSText = appStore.scenicQueueRemark
presetVoices = appStore.scenicQueuePresetVoices()
customTTSText = appStore.scenicQueue.remark
presetVoices = appStore.scenicQueue.presetVoices()
}
var selectedPunchSpotDisplayText: String {
@ -105,7 +105,7 @@ final class ScenicQueueSettingsViewModel {
/// 使线
var useOfflineTTS: Bool {
appStore.scenicQueueUseOfflineTTS
appStore.scenicQueue.useOfflineTTS
}
func openSettingChangeLog() {
@ -114,12 +114,12 @@ final class ScenicQueueSettingsViewModel {
///
func loadPunchSpotOptions(api: ScenicQueueAPIProtocol) async {
guard appStore.currentScenicId > 0 else {
guard appStore.session.currentScenicId > 0 else {
onShowMessage?("请先选择景区")
return
}
do {
let response = try await api.scenicSpotListAll(scenicId: String(appStore.currentScenicId))
let response = try await api.scenicSpotListAll(scenicId: String(appStore.session.currentScenicId))
punchSpotOptions = response.list.map {
ScenicQueuePunchSpotOption(id: String($0.id), label: $0.name)
}
@ -178,17 +178,17 @@ final class ScenicQueueSettingsViewModel {
/// 线
func setUseOfflineTTS(_ enabled: Bool) {
guard enabled else {
appStore.scenicQueueUseOfflineTTS = false
appStore.scenicQueue.useOfflineTTS = false
notifyStateChange()
return
}
guard ttsManager.supportsOfflineTTS else {
appStore.scenicQueueUseOfflineTTS = false
appStore.scenicQueue.useOfflineTTS = false
onShowMessage?("离线语音资源未安装")
notifyStateChange()
return
}
appStore.scenicQueueUseOfflineTTS = true
appStore.scenicQueue.useOfflineTTS = true
notifyStateChange()
}
@ -234,7 +234,7 @@ final class ScenicQueueSettingsViewModel {
///
func saveCurrentTextAsPresetVoiceLocal() {
guard appStore.currentScenicId > 0, selectedPunchSpotId?.nonEmptyTrimmed != nil else {
guard appStore.session.currentScenicId > 0, selectedPunchSpotId?.nonEmptyTrimmed != nil else {
onShowMessage?("请先选择打卡点")
return
}
@ -252,7 +252,7 @@ final class ScenicQueueSettingsViewModel {
return
}
presetVoices.append(text)
appStore.saveScenicQueuePresetVoices(presetVoices)
appStore.scenicQueue.savePresetVoices(presetVoices)
onShowMessage?("已保存到本地")
notifyStateChange()
}
@ -260,7 +260,7 @@ final class ScenicQueueSettingsViewModel {
func deletePresetVoice(at index: Int) {
guard presetVoices.indices.contains(index) else { return }
let removed = presetVoices.remove(at: index)
appStore.saveScenicQueuePresetVoices(presetVoices)
appStore.scenicQueue.savePresetVoices(presetVoices)
if ttsLoopAnchorText == removed {
ttsManager.stopCustomTextLoop()
ttsLoopAnchorText = nil
@ -332,11 +332,11 @@ final class ScenicQueueSettingsViewModel {
)
do {
try await api.saveSetting(request)
appStore.scenicQueuePunchSpotId = String(ids.scenicSpotId)
appStore.scenicQueuePunchSpotName = selectedPunchSpotLabel
appStore.scenicQueueRemark = remark
appStore.saveScenicQueuePresetVoices(presetVoices)
appStore.saveScenicQueueSettingsSnapshot(currentSnapshot())
appStore.scenicQueue.punchSpotId = String(ids.scenicSpotId)
appStore.scenicQueue.punchSpotName = selectedPunchSpotLabel
appStore.scenicQueue.remark = remark
appStore.scenicQueue.savePresetVoices(presetVoices)
appStore.scenicQueue.saveSettingsSnapshot(currentSnapshot())
_ = try? await api.stats(scenicId: ids.scenicId, scenicSpotId: ids.scenicSpotId)
onShowMessage?("保存成功")
onNavigateBack?()
@ -346,7 +346,7 @@ final class ScenicQueueSettingsViewModel {
}
private func fetchQueueSettingAfterPunchSpotSelected(_ spotId: String, api: ScenicQueueAPIProtocol) async {
guard let scenicId = Int64("\(appStore.currentScenicId)"),
guard let scenicId = Int64("\(appStore.session.currentScenicId)"),
let scenicSpotId = Int64(spotId),
scenicId > 0,
scenicSpotId > 0 else { return }
@ -480,7 +480,7 @@ final class ScenicQueueSettingsViewModel {
}
private func currentScenicAndSpotIdsForSelected() -> (scenicId: Int64, scenicSpotId: Int64)? {
guard let scenicId = Int64("\(appStore.currentScenicId)"), scenicId > 0,
guard let scenicId = Int64("\(appStore.session.currentScenicId)"), scenicId > 0,
let spotIdText = selectedPunchSpotId?.trimmingCharacters(in: .whitespacesAndNewlines),
let scenicSpotId = Int64(spotIdText), scenicSpotId > 0 else { return nil }
return (scenicId, scenicSpotId)

View File

@ -97,8 +97,8 @@ final class ScenicQueueViewModel {
/// WebSocket
func startQueueStatsPolling(api: ScenicQueueAPIProtocol) {
guard queueGatePassed,
let scenicId = Int64("\(appStore.currentScenicId)"),
let scenicSpotId = Int64(appStore.scenicQueuePunchSpotId),
let scenicId = Int64("\(appStore.session.currentScenicId)"),
let scenicSpotId = Int64(appStore.scenicQueue.punchSpotId),
scenicId > 0,
scenicSpotId > 0 else { return }
Task {
@ -186,16 +186,16 @@ final class ScenicQueueViewModel {
///
func confirmUserMark(api: ScenicQueueAPIProtocol, markAsFreelancePhotog: Bool, queueBanDays: Int) async {
guard let pendingMark else { return }
guard appStore.currentScenicId > 0 else {
guard appStore.session.currentScenicId > 0 else {
onShowMessage?("请先选择景区")
return
}
let request = ScenicQueueUserMarkRequest(
uid: pendingMark.uid,
scenicId: Int64(appStore.currentScenicId),
scenicId: Int64(appStore.session.currentScenicId),
markAsFreelancePhotog: markAsFreelancePhotog ? 1 : 0,
queueBanDays: markAsFreelancePhotog ? min(max(queueBanDays, 0), 999) : nil,
operatorId: Int(appStore.userId)
operatorId: Int(appStore.session.userId)
)
do {
try await api.userMark(request)
@ -270,7 +270,7 @@ final class ScenicQueueViewModel {
func confirmRequeueTicket(api: ScenicQueueAPIProtocol) async {
guard let pending = pendingRequeue else { return }
dismissRequeueDialog()
guard let operatorId = Int(appStore.userId), operatorId > 0 else {
guard let operatorId = Int(appStore.session.userId), operatorId > 0 else {
onShowMessage?("请先登录")
return
}
@ -395,18 +395,18 @@ final class ScenicQueueViewModel {
}
private func refreshSelectedPunchSpotLine() {
selectedPunchSpotLine = appStore.hasScenicQueuePunchSpotSaved()
? appStore.scenicQueuePunchSpotName.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "--"
selectedPunchSpotLine = appStore.scenicQueue.hasPunchSpotSaved()
? appStore.scenicQueue.punchSpotName.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "--"
: "--"
refreshShootingCallConfig()
}
private func refreshQueueGateLocally() {
queueGatePassed = appStore.hasScenicQueuePunchSpotSaved()
queueGatePassed = appStore.scenicQueue.hasPunchSpotSaved()
}
private func refreshShootingCallConfig() {
let snapshot = appStore.scenicQueueSettingsSnapshot() ?? ScenicQueueSettingsSnapshot()
let snapshot = appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()
showStartShootingButton = snapshot.showStartShootingButton
quickCallButtonEnabled = snapshot.quickCallButtonEnabled
prepareCallButtonEnabled = snapshot.prepareCallButtonEnabled
@ -419,18 +419,18 @@ final class ScenicQueueViewModel {
guard data.exists, let setting = data.setting,
setting.scenicId == nil || setting.scenicId == ids.scenicId,
setting.scenicSpotId == nil || setting.scenicSpotId == ids.scenicSpotId else { return }
let local = appStore.scenicQueueSettingsSnapshot() ?? ScenicQueueSettingsSnapshot()
appStore.saveScenicQueueSettingsSnapshot(setting.toSnapshot(fallback: local))
appStore.scenicQueueRemark = setting.remark ?? ""
let local = appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()
appStore.scenicQueue.saveSettingsSnapshot(setting.toSnapshot(fallback: local))
appStore.scenicQueue.remark = setting.remark ?? ""
if let voices = setting.voiceBroadcasts {
appStore.saveScenicQueuePresetVoices(
appStore.scenicQueue.savePresetVoices(
voices
.sorted { ($0.sortOrder ?? Int.max) < ($1.sortOrder ?? Int.max) }
.compactMap { $0.content?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty }
)
}
if let name = setting.scenicSpotName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty {
appStore.scenicQueuePunchSpotName = name
appStore.scenicQueue.punchSpotName = name
selectedPunchSpotLine = name
}
refreshShootingCallConfig()
@ -501,7 +501,7 @@ final class ScenicQueueViewModel {
private func handleSocketMessage(_ message: ScenicQueueSocketMessage) {
guard let params = message.data?.params,
let changedSpotId = params.scenicSpotId,
let currentSpotId = Int64(appStore.scenicQueuePunchSpotId),
let currentSpotId = Int64(appStore.scenicQueue.punchSpotId),
changedSpotId == currentSpotId else { return }
switch message.data?.action {
case ScenicQueueSocketAction.queueUpdated.rawValue:
@ -516,7 +516,7 @@ final class ScenicQueueViewModel {
private func handleRemoteTicketCalled(_ params: ScenicQueueSocketParams) {
guard let recordId = params.recordId, recordId > 0 else { return }
if let myUid = Int64(appStore.userId), let operatorUid = params.operatorUid, myUid == operatorUid {
if let myUid = Int64(appStore.session.userId), let operatorUid = params.operatorUid, myUid == operatorUid {
return
}
let eventId = params.eventId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
@ -614,7 +614,7 @@ final class ScenicQueueViewModel {
}
private func resolveShootingTiming() -> (totalSeconds: Int, intervalSeconds: Int, readableThresholdSeconds: Int) {
let snapshot = appStore.scenicQueueSettingsSnapshot() ?? ScenicQueueSettingsSnapshot()
let snapshot = appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()
let total = max(0, snapshot.shootMinute * 60 + snapshot.shootSecond)
let interval = (40...60).contains(snapshot.broadcastIntervalSec) ? snapshot.broadcastIntervalSec : 50
let readable = (10...30).contains(snapshot.countdownThresholdSec) ? snapshot.countdownThresholdSec : 15
@ -627,7 +627,7 @@ final class ScenicQueueViewModel {
}
private func speakFollowingTickets(after queueNo: String) async {
let count = (appStore.scenicQueueSettingsSnapshot() ?? ScenicQueueSettingsSnapshot()).autoCallAheadCount.clamped(to: 0...5)
let count = (appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()).autoCallAheadCount.clamped(to: 0...5)
guard count > 0, let index = currentQueue.firstIndex(where: { $0.queueNo == queueNo }) else { return }
let nextNos = currentQueue.dropFirst(index + 1).prefix(count).map(\.queueNo).filter { !$0.isEmpty }
guard !nextNos.isEmpty else { return }
@ -639,9 +639,9 @@ final class ScenicQueueViewModel {
}
private func currentScenicAndSpotIds() -> (scenicId: Int64, scenicSpotId: Int64)? {
let scenicId = Int64(appStore.currentScenicId)
let scenicId = Int64(appStore.session.currentScenicId)
guard scenicId > 0,
let scenicSpotId = Int64(appStore.scenicQueuePunchSpotId),
let scenicSpotId = Int64(appStore.scenicQueue.punchSpotId),
scenicSpotId > 0 else { return nil }
return (scenicId, scenicSpotId)
}

View File

@ -213,9 +213,9 @@ final class StatisticsViewModel {
let storeId = try resolveStoreId()
return try await api.storeSummary(storeId: storeId, range: range)
case .scenicAdmin:
return try await api.scenicAdminSummary(scenicId: AppStore.shared.currentScenicId, range: range)
return try await api.scenicAdminSummary(scenicId: AppStore.shared.session.currentScenicId, range: range)
default:
return try await api.photographerSummary(scenicId: AppStore.shared.currentScenicId, range: range)
return try await api.photographerSummary(scenicId: AppStore.shared.session.currentScenicId, range: range)
}
}
@ -231,14 +231,14 @@ final class StatisticsViewModel {
)
case .scenicAdmin:
return try await api.scenicAdminDaily(
scenicId: AppStore.shared.currentScenicId,
scenicId: AppStore.shared.session.currentScenicId,
startTime: startTime,
endTime: endTime,
page: currentPage
)
default:
return try await api.photographerDaily(
scenicId: AppStore.shared.currentScenicId,
scenicId: AppStore.shared.session.currentScenicId,
startTime: startTime,
endTime: endTime,
page: currentPage
@ -247,11 +247,11 @@ final class StatisticsViewModel {
}
private var currentRole: AppRoleCode {
AppStore.shared.currentAppRole ?? .photographer
AppStore.shared.session.currentAppRole ?? .photographer
}
private func resolveStoreId() throws -> Int {
let storeId = AppStore.shared.currentStoreId
let storeId = AppStore.shared.session.currentStoreId
guard storeId > 0 else {
throw StatisticsFlowError.missingStore
}

View File

@ -262,7 +262,7 @@ final class TaskAddViewModel {
)
}
return AddTaskRequest(
scenicId: appStore.currentScenicId,
scenicId: appStore.session.currentScenicId,
name: name ?? taskName,
orderNumber: selectedOrderNumber,
remark: taskDetails,

View File

@ -32,7 +32,7 @@ final class TaskOrderSelectViewModel {
notifyStateChange()
}
let scenicId = appStore.currentScenicId
let scenicId = appStore.session.currentScenicId
guard scenicId > 0 else {
orders = []
onShowMessage?("请先选择景区")

View File

@ -151,10 +151,10 @@ struct TravelAlbumOTGStorageContext: Equatable, Sendable {
/// 使
static var current: TravelAlbumOTGStorageContext {
TravelAlbumOTGStorageContext(
accountCachePrefix: AppStore.shared.accountCachePrefix,
userId: AppStore.shared.userId,
scenicId: AppStore.shared.currentScenicId,
storeId: AppStore.shared.currentStoreId
accountCachePrefix: AppStore.shared.session.accountCachePrefix,
userId: AppStore.shared.session.userId,
scenicId: AppStore.shared.session.currentScenicId,
storeId: AppStore.shared.session.currentStoreId
)
}
}

View File

@ -201,7 +201,7 @@ final class TravelAlbumCameraImportViewModel {
status: .pending,
progress: 0,
albumId: albumId,
userId: appStore.userId
userId: appStore.session.userId
)
storage.upsert(record, albumId: albumId)
existingPhotoIds.insert(object.id)

View File

@ -44,7 +44,7 @@ final class TravelAlbumEntryViewModel {
private let dateProvider: () -> Date
init(
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId },
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId },
dateProvider: @escaping () -> Date = Date.init
) {
self.currentScenicIdProvider = currentScenicIdProvider

View File

@ -383,7 +383,7 @@ final class WiredCameraTransferViewModel {
fileSizeBytes: Int64(item.data.count),
status: .pending,
albumId: albumId,
userId: appStore.userId
userId: appStore.session.userId
)
storage.upsert(record, albumId: albumId)
importedPhotoIds.append(record.id)
@ -553,7 +553,7 @@ final class WiredCameraTransferViewModel {
updateRecord(id: id, status: .uploading, progress: max(record.progress, 1), error: nil)
let material = try await uploader.upload(
record: record,
scenicId: appStore.currentScenicId
scenicId: appStore.session.currentScenicId
) { [weak self] progress in
self?.updateRecord(id: id, status: .uploading, progress: progress, error: nil)
}
@ -596,7 +596,7 @@ final class WiredCameraTransferViewModel {
progress: progress,
errorMessage: error,
albumId: albumId,
userId: appStore.userId,
userId: appStore.session.userId,
remoteUrl: remoteUrl ?? item.remoteUrl
)
}

View File

@ -99,7 +99,7 @@ final class WalletViewModel {
/// ViewModel
init(
staffIdProvider: @escaping () -> Int? = {
Int(AppStore.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines))
Int(AppStore.shared.session.userId.trimmingCharacters(in: .whitespacesAndNewlines))
},
calendar: Calendar = Calendar(identifier: .gregorian)
) {
@ -480,7 +480,7 @@ final class PointsRedemptionViewModel {
/// ViewModel
init(staffIdProvider: @escaping () -> Int? = {
Int(AppStore.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines))
Int(AppStore.shared.session.userId.trimmingCharacters(in: .whitespacesAndNewlines))
}) {
self.staffIdProvider = staffIdProvider
}

View File

@ -1057,7 +1057,7 @@ final class WildReportRiskMapViewModel {
init() {
self.markers = []
self.region = Self.region(for: [])
let scenicName = AppStore.shared.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
let scenicName = AppStore.shared.session.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
self.scenicAreaName = scenicName.isEmpty ? "当前景区" : scenicName
self.selectedMarkerID = nil
}
@ -1203,7 +1203,7 @@ final class WildReportRiskMapViewModel {
scenicName: String?,
selectCurrentLocation: Bool
) async {
let resolvedScenicId = scenicId ?? AppStore.shared.currentScenicId
let resolvedScenicId = scenicId ?? AppStore.shared.session.currentScenicId
guard resolvedScenicId >= 1 else {
errorMessage = "请先选择景区后再查看风险地图"
notifyStateChange()
@ -1244,7 +1244,7 @@ final class WildReportRiskMapViewModel {
))
scenicAreaName = response.scenicName.nonEmpty
?? scenicName?.nonEmpty
?? AppStore.shared.currentScenicName.nonEmpty
?? AppStore.shared.session.currentScenicName.nonEmpty
?? scenicAreaName
greenMarkerTip = response.greenMarkerTip
legendItems = response.legend
@ -1376,7 +1376,7 @@ final class WildReportRiskMapViewModel {
for marker: WildReportMapMarker,
scenicId: Int?
) -> WildReportMarkerDetailRequest? {
let resolvedScenicId = scenicId ?? AppStore.shared.currentScenicId
let resolvedScenicId = scenicId ?? AppStore.shared.session.currentScenicId
guard resolvedScenicId >= 1 else { return nil }
let parts = marker.id.split(separator: "-", maxSplits: 1).map(String.init)
guard parts.count == 2, let id = Int(parts[1]), id >= 1 else { return nil }