Merge branch 'dev_1_2_1' into xh_test

This commit is contained in:
2026-08-03 10:48:41 +08:00
47 changed files with 5386 additions and 2314 deletions
@@ -70,10 +70,10 @@ enum AuthSessionHelper {
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
user.scenicName, user.nickname, user.realName
)
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.userName = firstNonEmpty(user.nickname, user.realName)
AppStore.shared.session.realName = user.realName
AppStore.shared.session.phone = user.phone
AppStore.shared.session.currentScenicId = user.scenicId
AppStore.shared.session.currentScenicName = user.scenicName
@@ -90,10 +90,10 @@ enum AuthSessionHelper {
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
user.storeName, user.scenicName, user.realName
)
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.userName = user.realName
AppStore.shared.session.realName = user.realName
AppStore.shared.session.phone = user.phone
AppStore.shared.session.avatar = user.avatar
AppStore.shared.session.currentScenicId = user.scenicId
@@ -196,7 +196,6 @@ struct V9ScenicUser: Decodable, Equatable {
let userId: Int
let scenicUserId: Int
let ssUserId: Int
let username: String
let realName: String
let nickname: String
let phone: String
@@ -212,7 +211,7 @@ struct V9ScenicUser: Decodable, Equatable {
}
var displayName: String {
scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? username
scenicName.nonEmpty ?? nickname.nonEmpty ?? realName
}
func toAccountSwitchAccount() -> AccountSwitchAccount {
@@ -238,8 +237,6 @@ struct V9ScenicUser: Decodable, Equatable {
case userId = "user_id"
case scenicUserId = "scenic_user_id"
case ssUserId = "ss_user_id"
case username = "user_name"
case legacyUsername = "username"
case realName = "real_name"
case nickname
case phone
@@ -258,9 +255,6 @@ struct V9ScenicUser: Decodable, Equatable {
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
scenicUserId = try container.decodeLossyInt(forKey: .scenicUserId) ?? 0
ssUserId = try container.decodeLossyInt(forKey: .ssUserId) ?? 0
let preferredUsername = try container.decodeLossyString(forKey: .username)
let legacyUsername = try container.decodeLossyString(forKey: .legacyUsername)
username = preferredUsername.nonEmpty ?? legacyUsername
realName = try container.decodeLossyString(forKey: .realName)
nickname = try container.decodeLossyString(forKey: .nickname)
phone = try container.decodeLossyString(forKey: .phone)
@@ -281,8 +275,6 @@ struct V9StoreUser: Decodable, Equatable {
let id: Int
let userId: Int
let storeUserId: Int
let username: String
let userName: String
let realName: String
let phone: String
let avatar: String
@@ -300,14 +292,14 @@ struct V9StoreUser: Decodable, Equatable {
}
var displayName: String {
storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? username
storeName.nonEmpty ?? scenicName.nonEmpty ?? realName
}
func toAccountSwitchAccount() -> AccountSwitchAccount {
AccountSwitchAccount(
accountType: accountType.nonEmpty ?? Self.accountTypeValue,
businessUserId: businessUserId,
title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? "门店账号",
title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? "门店账号",
subtitle: joinAccountSubtitle(scenicName, roleName),
phone: phone,
realName: realName,
@@ -325,8 +317,6 @@ struct V9StoreUser: Decodable, Equatable {
case id
case userId = "user_id"
case storeUserId = "store_user_id"
case username
case userName = "user_name"
case realName = "real_name"
case phone
case avatar
@@ -346,8 +336,6 @@ struct V9StoreUser: Decodable, Equatable {
id = try container.decodeLossyInt(forKey: .id) ?? 0
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
storeUserId = try container.decodeLossyInt(forKey: .storeUserId) ?? 0
username = try container.decodeLossyString(forKey: .username)
userName = try container.decodeLossyString(forKey: .userName)
realName = try container.decodeLossyString(forKey: .realName)
phone = try container.decodeLossyString(forKey: .phone)
avatar = try container.decodeLossyString(forKey: .avatar)
@@ -0,0 +1,94 @@
//
// CommissionRateLogViewModel.swift
// suixinkan
//
import Foundation
/// 获客员分成比例修改记录 ViewModel,负责刷新、分页与重复项合并。
final class CommissionRateLogViewModel {
private enum Constants {
static let pageSize = 10
}
private(set) var items: [CommissionRateLogEntity] = []
private(set) var isRefreshing = false
private(set) var isLoadingMore = false
private(set) var initialLoading = true
private(set) var canLoadMore = false
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
private let saleUserId: Int
private var total = 0
private var lastLoadedPage = 0
private var isLoading = false
init(saleUserId: Int) {
self.saleUserId = saleUserId
}
/// 重新加载第一页修改记录。
func refresh(api: OrderAPI) async {
await load(api: api, page: 1, append: false)
}
/// 在存在下一页时加载更多修改记录。
func loadMore(api: OrderAPI) async {
guard canLoadMore, !isRefreshing, !isLoadingMore else { return }
await load(api: api, page: lastLoadedPage + 1, append: true)
}
private func load(api: OrderAPI, page: Int, append: Bool) async {
guard saleUserId > 0 else {
initialLoading = false
onShowMessage?("获客员信息无效")
notifyStateChange()
return
}
guard !isLoading else { return }
isLoading = true
if append {
isLoadingMore = true
} else {
isRefreshing = true
}
notifyStateChange()
defer {
isLoading = false
initialLoading = false
if append {
isLoadingMore = false
} else {
isRefreshing = false
}
notifyStateChange()
}
do {
let response = try await api.saleUserCommissionRateLogs(
saleUserId: saleUserId,
page: page,
pageSize: Constants.pageSize
)
if append {
let existingIDs = Set(items.map(\.id))
items.append(contentsOf: response.list.filter { !existingIDs.contains($0.id) })
} else {
var seenIDs = Set<Int>()
items = response.list.filter { seenIDs.insert($0.id).inserted }
}
total = max(0, response.total)
lastLoadedPage = page
canLoadMore = items.count < total && !response.list.isEmpty
} catch {
onShowMessage?(error.localizedDescription)
}
}
private func notifyStateChange() {
onStateChange?()
}
}
@@ -16,21 +16,24 @@ enum HomeMenuIconFactory {
private static let imageCache = NSCache<NSString, UIImage>()
/// 优先加载 Assets 资源图;否则加载 SF Symbol,并统一渲染到固定画布。
/// 优先加载 Assets 资源图并归一化画布;SF Symbol 保留系统符号表示与分层渲染。
static func image(named iconName: String) -> UIImage? {
let cacheKey = iconName as NSString
if let cachedImage = imageCache.object(forKey: cacheKey) {
return cachedImage
}
guard let sourceImage = UIImage(named: iconName)
?? UIImage(systemName: iconName, withConfiguration: symbolConfiguration) else {
return nil
let image: UIImage?
if let assetImage = UIImage(named: iconName) {
image = renderOnFixedCanvas(assetImage)
} else {
image = UIImage(systemName: iconName, withConfiguration: symbolConfiguration)
}
let normalizedImage = renderOnFixedCanvas(sourceImage)
imageCache.setObject(normalizedImage, forKey: cacheKey)
return normalizedImage
if let image {
imageCache.setObject(image, forKey: cacheKey)
}
return image
}
private static func renderOnFixedCanvas(_ image: UIImage) -> UIImage {
@@ -20,6 +20,7 @@ enum AMapBootstrap {
defer { lock.unlock() }
guard !isConfigured else { return true }
#if !targetEnvironment(simulator)
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
AMapLocationManager.updatePrivacyAgree(.didAgree)
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
@@ -29,6 +30,7 @@ enum AMapBootstrap {
AMapServices.shared().apiKey = AMapConfig.apiKey
AMapServices.shared().enableHTTPS = true
#endif
isConfigured = true
return true
}
@@ -3,16 +3,28 @@
// suixinkan
//
import CoreLocation
import Foundation
/// 高德逆地理编码封装,对齐 Android `GeocodeSearch`。
/// 地址逆地理编码能力,供平台定位实现注入。
@MainActor
final class LocationGeocoder: NSObject, AMapSearchDelegate {
protocol LocationAddressGeocoding: AnyObject {
/// 将坐标解析为可读地址,失败时返回空字符串。
func reverseGeocode(latitude: Double, longitude: Double) async -> String
}
/// 平台逆地理编码封装;真机使用高德,模拟器使用 Core Location。
@MainActor
final class LocationGeocoder: NSObject, LocationAddressGeocoding {
nonisolated static let shared = LocationGeocoder()
#if targetEnvironment(simulator)
private let geocoder = CLGeocoder()
#else
private var searchAPI: AMapSearchAPI?
private var continuation: CheckedContinuation<String, Never>?
#endif
nonisolated private override init() {
super.init()
@@ -20,6 +32,26 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
/// 将坐标解析为可读地址。
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
#if targetEnvironment(simulator)
let location = CLLocation(latitude: latitude, longitude: longitude)
guard let placemark = try? await geocoder.reverseGeocodeLocation(location).first else {
return ""
}
if let name = placemark.name?.trimmingCharacters(in: .whitespacesAndNewlines),
!name.isEmpty {
return name
}
return [
placemark.administrativeArea,
placemark.locality,
placemark.subLocality,
placemark.thoroughfare,
placemark.subThoroughfare,
]
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.joined()
#else
do {
try AMapBootstrap.requireConfigured()
} catch {
@@ -40,8 +72,10 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
request.radius = 200
api.aMapReGoecodeSearch(request)
}
#endif
}
#if !targetEnvironment(simulator)
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
let address = response?.regeocode?.formattedAddress ?? ""
continuation?.resume(returning: address)
@@ -62,4 +96,9 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
searchAPI = api
return api
}
#endif
}
#if !targetEnvironment(simulator)
extension LocationGeocoder: AMapSearchDelegate {}
#endif
@@ -6,7 +6,165 @@
import CoreLocation
import Foundation
/// 统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
/// Core Location 管理器最小接口,便于模拟器定位实现注入测试替身。
@MainActor
protocol CoreLocationManaging: AnyObject {
var authorizationStatus: CLAuthorizationStatus { get }
var desiredAccuracy: CLLocationAccuracy { get set }
var delegate: CLLocationManagerDelegate? { get set }
/// 请求使用期间定位权限。
func requestWhenInUseAuthorization()
/// 请求一次当前位置。
func requestLocation()
}
extension CLLocationManager: CoreLocationManaging {}
/// Core Location 定位实现,供模拟器运行并支持依赖注入测试。
@MainActor
final class CoreLocationProvider: NSObject, LocationProviding, CLLocationManagerDelegate {
nonisolated static let shared = CoreLocationProvider()
private var appStore: AppStore = .shared
private var manager: any CoreLocationManaging = CLLocationManager()
private var geocoder: any LocationAddressGeocoding = LocationGeocoder.shared
private var permissionContinuation: CheckedContinuation<Void, Error>?
private var locationContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?
nonisolated override init() {
super.init()
}
/// 使用可控依赖创建 Core Location 定位服务。
init(
appStore: AppStore,
manager: any CoreLocationManaging,
geocoder: any LocationAddressGeocoding
) {
self.appStore = appStore
self.manager = manager
self.geocoder = geocoder
super.init()
manager.delegate = self
}
/// 请求坐标及逆地理编码地址。
func requestSnapshot(
desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
) async throws -> HomeLocationSnapshot {
let coordinate = try await requestCoordinate(desiredAccuracy: desiredAccuracy)
let address = await geocoder.reverseGeocode(
latitude: coordinate.latitude,
longitude: coordinate.longitude
)
return HomeLocationSnapshot(
latitude: coordinate.latitude,
longitude: coordinate.longitude,
address: address
)
}
/// 请求一次当前位置坐标。
func requestCoordinate(
desiredAccuracy: CLLocationAccuracy
) async throws -> CLLocationCoordinate2D {
try requirePrivacyAgreement()
try await ensureLocationPermission()
guard locationContinuation == nil else {
throw LocationProviderError.locationFailed
}
manager.delegate = self
manager.desiredAccuracy = desiredAccuracy
return try await withCheckedThrowingContinuation { continuation in
locationContinuation = continuation
manager.requestLocation()
}
}
/// 使用系统服务将坐标解析为地址。
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
guard appStore.session.privacyAgreementAccepted else { return "" }
return await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
}
/// 请求系统定位权限,等待用户完成首次授权。
func ensureLocationPermission() async throws {
manager.delegate = self
switch manager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
return
case .denied, .restricted:
throw LocationProviderError.permissionDenied
case .notDetermined:
guard permissionContinuation == nil else {
throw LocationProviderError.locationFailed
}
try await withCheckedThrowingContinuation { continuation in
permissionContinuation = continuation
manager.requestWhenInUseAuthorization()
}
@unknown default:
throw LocationProviderError.permissionDenied
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
resolvePermission(status: self.manager.authorizationStatus)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let continuation = locationContinuation else { return }
locationContinuation = nil
guard let coordinate = locations.last?.coordinate,
CLLocationCoordinate2DIsValid(coordinate),
coordinate.latitude != 0 || coordinate.longitude != 0 else {
continuation.resume(throwing: LocationProviderError.locationFailed)
return
}
continuation.resume(returning: coordinate)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
guard let continuation = locationContinuation else { return }
locationContinuation = nil
continuation.resume(throwing: LocationProviderError.locationFailed)
}
private func requirePrivacyAgreement() throws {
guard appStore.session.privacyAgreementAccepted else {
throw LocationProviderError.privacyNotAccepted
}
}
private func resolvePermission(status: CLAuthorizationStatus) {
guard let continuation = permissionContinuation else { return }
switch status {
case .authorizedAlways, .authorizedWhenInUse:
permissionContinuation = nil
continuation.resume()
case .denied, .restricted:
permissionContinuation = nil
continuation.resume(throwing: LocationProviderError.permissionDenied)
case .notDetermined:
break
@unknown default:
permissionContinuation = nil
continuation.resume(throwing: LocationProviderError.permissionDenied)
}
}
}
#if targetEnvironment(simulator)
/// 模拟器默认定位服务,使用 Core Location 与系统逆地理编码。
typealias LocationProvider = CoreLocationProvider
#else
/// 真机统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
@MainActor
final class LocationProvider: NSObject, LocationProviding {
@@ -19,7 +177,7 @@ final class LocationProvider: NSObject, LocationProviding {
super.init()
}
@MainActor
/// 请求坐标及高德逆地理编码地址。
func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
try AMapBootstrap.requireConfigured()
try await ensureLocationPermission()
@@ -51,7 +209,7 @@ final class LocationProvider: NSObject, LocationProviding {
}
}
@MainActor
/// 请求一次当前位置坐标。
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
try AMapBootstrap.requireConfigured()
try await ensureLocationPermission()
@@ -78,7 +236,7 @@ final class LocationProvider: NSObject, LocationProviding {
}
}
@MainActor
/// 使用高德服务将坐标解析为地址。
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
}
@@ -110,3 +268,5 @@ final class LocationProvider: NSObject, LocationProviding {
return manager
}
}
#endif
@@ -16,20 +16,25 @@ struct HomeLocationSnapshot: Sendable, Equatable {
/// 定位服务协议,便于 ViewModel 注入与单元测试 mock。
protocol LocationProviding: Sendable {
/// 按指定精度请求当前坐标与地址。
@MainActor
func requestSnapshot(desiredAccuracy: CLLocationAccuracy) async throws -> HomeLocationSnapshot
/// 按指定精度请求当前坐标。
@MainActor
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D
/// 逆地理编码。
@MainActor
func reverseGeocode(latitude: Double, longitude: Double) async -> String
}
extension LocationProviding {
/// 请求当前坐标与地址,默认使用最高精度以保持既有业务行为。
@MainActor
func requestSnapshot() async throws -> HomeLocationSnapshot {
try await requestSnapshot(desiredAccuracy: kCLLocationAccuracyBest)
}
/// 请求当前坐标,默认使用最高精度以保持既有业务行为。
@MainActor
func requestCoordinate() async throws -> CLLocationCoordinate2D {
try await requestCoordinate(desiredAccuracy: kCLLocationAccuracyBest)
}
@@ -293,6 +293,28 @@ final class OrderAPI {
)
}
/// 获取指定获客员的分成比例修改记录。
func saleUserCommissionRateLogs(
saleUserId: Int,
page: Int = 1,
pageSize: Int = 10
) async throws -> CommissionRateLogListResponse {
guard saleUserId > 0 else {
throw APIError.networkFailed("获客员信息无效")
}
return try await client.send(
APIRequest(
method: .get,
path: "/api/yf-handset-app/photog/sale-user/commission-rate/logs",
queryItems: [
URLQueryItem(name: "sale_user_id", value: String(saleUserId)),
URLQueryItem(name: "page", value: String(max(1, page))),
URLQueryItem(name: "page_size", value: String(min(max(1, pageSize), 50))),
]
)
)
}
func shootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse {
try await client.send(
APIRequest(
@@ -875,6 +875,93 @@ struct CooperativeSalerEntity: Decodable, Equatable {
}
}
/// 获客员分成比例修改记录分页响应。
struct CommissionRateLogListResponse: Decodable, Equatable {
let list: [CommissionRateLogEntity]
let total: Int
let page: Int
let pageSize: Int
enum CodingKeys: String, CodingKey {
case list
case total
case page
case pageSize = "page_size"
}
}
/// 单条获客员分成比例修改记录。
struct CommissionRateLogEntity: Decodable, Equatable, Hashable {
let id: Int
let bindingId: Int
let saleUserId: Int
let storeUserId: Int
let beforeRate: Int
let beforeRateLabel: String
let afterRate: Int
let afterRateLabel: String
let operatorType: String
let operatorId: Int
let operatorName: String
let remark: String
let createdAt: String
enum CodingKeys: String, CodingKey {
case id
case bindingId = "binding_id"
case saleUserId = "sale_user_id"
case storeUserId = "store_user_id"
case beforeRate = "before_rate"
case beforeRateLabel = "before_rate_label"
case afterRate = "after_rate"
case afterRateLabel = "after_rate_label"
case operatorType = "operator_type"
case operatorId = "operator_id"
case operatorName = "operator_name"
case remark
case createdAt = "created_at"
}
/// 修改前比例展示文本,优先使用服务端 label。
var displayBeforeRate: String {
let label = beforeRateLabel.trimmingCharacters(in: .whitespacesAndNewlines)
return label.isEmpty ? "\(beforeRate)%" : label
}
/// 修改后比例展示文本,优先使用服务端 label。
var displayAfterRate: String {
let label = afterRateLabel.trimmingCharacters(in: .whitespacesAndNewlines)
return label.isEmpty ? "\(afterRate)%" : label
}
/// 修改时间展示文本。
var metadataLine: String {
formattedCreatedAt
}
/// 去除首尾空白后的备注;空备注不展示。
var displayRemark: String? {
let value = remark.trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
private var formattedCreatedAt: String {
let value = createdAt.trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty else { return "—" }
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "Asia/Shanghai")
for format in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"] {
formatter.dateFormat = format
if let date = formatter.date(from: value) {
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter.string(from: date)
}
}
return value
}
}
struct ReferralOrderListResponse: Decodable {
let total: Int
let list: [ReferralOrderEntity]
@@ -3,6 +3,7 @@
// suixinkan
//
import CoreLocation
import UIKit
/// 主 Tab 中间扫码按钮结果处理,对齐 Android `MainTabViewModel.signIn`。
@@ -69,9 +69,9 @@ enum PayPageBrandingPolicy {
/// 收款页当前账号信息的展示文本格式化规则。
enum PaymentAccountDisplayFormatter {
/// 解析当前登录摄影师名称,昵称为空时使用实名,仍为空时显示占位符。
static func photographerName(userName: String, realName: String) -> String {
userName.trimmedNonEmpty ?? realName.trimmedNonEmpty ?? "-"
/// 解析当前登录摄影师的真实姓名,空值显示占位符。
static func photographerName(realName: String) -> String {
realName.trimmedNonEmpty ?? "-"
}
/// 去除空值与重复项后,合并当前账号的全部店铺名称。
@@ -28,6 +28,7 @@ final class PaymentCollectionDetailsViewModel {
private var staticPayURL = ""
private var dynamicPayURL = ""
private var hasCompletedPayCodeRequest = false
private let appStore: AppStore
private let payPageConfigCache: PayPageConfigCaching
@@ -50,6 +51,11 @@ final class PaymentCollectionDetailsViewModel {
qrImage != nil && !staticPayURL.isEmpty
}
/// 是否应在二维码区域展示加载状态,首次请求开始前也视为加载中,避免短暂闪现错误占位。
var isPayCodeLoading: Bool {
loading || !hasCompletedPayCodeRequest
}
var displayScenicName: String {
scenicName.isEmpty ? "暂无景区" : scenicName
}
@@ -60,7 +66,6 @@ final class PaymentCollectionDetailsViewModel {
var displayPhotographerName: String {
PaymentAccountDisplayFormatter.photographerName(
userName: appStore.session.userName,
realName: appStore.session.realName
)
}
@@ -85,11 +90,11 @@ final class PaymentCollectionDetailsViewModel {
func loadPayCode(api: PaymentAPI) async {
guard !loading else { return }
refreshLocalState()
loading = true
notifyStateChange()
refreshLocalState()
defer {
loading = false
hasCompletedPayCodeRequest = true
notifyStateChange()
}
@@ -17,11 +17,11 @@ final class ProfileViewModel {
var onStateChange: (() -> Void)?
var displayNickname: String {
nonEmpty(userInfo?.nickname) ?? "未设置昵称"
nonEmpty(userInfo?.nickname) ?? AppStore.shared.session.userName.nonEmpty ?? "未设置昵称"
}
var displayRealName: String {
nonEmpty(userInfo?.realName) ?? "--"
nonEmpty(userInfo?.realName) ?? AppStore.shared.session.realName.nonEmpty ?? "--"
}
var displayPhone: String {
@@ -37,6 +37,17 @@ final class ProfileViewModel {
return uid.isEmpty ? "--" : uid
}
/// 本地是否已有可供冷启动首屏展示的基本资料。
var hasCachedBasicInfo: Bool {
[
AppStore.shared.session.userName,
AppStore.shared.session.realName,
AppStore.shared.session.phone,
AppStore.shared.session.avatar,
AppStore.shared.session.userId,
].contains { nonEmpty($0) != nil }
}
var accountDisplayName: String {
let name = AppStore.shared.session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? "--" : name
@@ -112,6 +123,7 @@ final class ProfileViewModel {
if !info.roleName.isEmpty {
AppStore.shared.session.roleName = info.roleName
}
notifyStateChange()
if showPhotographerFields {
async let realName = api.realNameInfo()