259 lines
8.2 KiB
Swift
259 lines
8.2 KiB
Swift
//
|
|
// PaymentCollectionDetailsViewModel.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
/// 收款详情 ViewModel,管理收款码拉取、设金额、保存二维码与语音开关。
|
|
final class PaymentCollectionDetailsViewModel {
|
|
|
|
private(set) var loading = false
|
|
private(set) var qrImage: UIImage?
|
|
private(set) var scenicName = ""
|
|
private(set) var staffId = ""
|
|
private(set) var isVoiceBroadcastOpen = false
|
|
private(set) var showAmountDialog = false
|
|
private(set) var amount = ""
|
|
private(set) var remark = ""
|
|
private(set) var scenicId = 0
|
|
private(set) var payPageConfig = PayPageConfig.nalatiDefault
|
|
private(set) var brandingRefreshVersion = 0
|
|
|
|
let merchantInfo = PaymentMerchantInfo.defaultInfo
|
|
|
|
var onStateChange: (() -> Void)?
|
|
var onShowMessage: ((String) -> Void)?
|
|
|
|
private var staticPayURL = ""
|
|
private var dynamicPayURL = ""
|
|
private var hasCompletedPayCodeRequest = false
|
|
private let appStore: AppStore
|
|
private let payPageConfigCache: PayPageConfigCaching
|
|
|
|
init(
|
|
appStore: AppStore = .shared,
|
|
payPageConfigCache: PayPageConfigCaching = PayPageConfigCache()
|
|
) {
|
|
self.appStore = appStore
|
|
self.payPageConfigCache = payPageConfigCache
|
|
scenicId = appStore.session.currentScenicId
|
|
scenicName = appStore.session.currentScenicName
|
|
staffId = appStore.session.userId
|
|
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
|
restorePayPageConfig()
|
|
}
|
|
|
|
var hasPayCode: Bool {
|
|
qrImage != nil && !staticPayURL.isEmpty
|
|
}
|
|
|
|
/// 是否应在二维码区域展示加载状态,首次请求开始前也视为加载中,避免短暂闪现错误占位。
|
|
var isPayCodeLoading: Bool {
|
|
loading || !hasCompletedPayCodeRequest
|
|
}
|
|
|
|
var displayScenicName: String {
|
|
scenicName.isEmpty ? "暂无景区" : scenicName
|
|
}
|
|
|
|
/// 是否启用品牌收款页,包含那拉提与赛里木湖。
|
|
var usesBranding: Bool {
|
|
PayPageBrandingPolicy.defaultConfig(scenicId: scenicId) != nil
|
|
}
|
|
|
|
/// 当前景区的随包背景资源名称。
|
|
var brandBackgroundAssetName: String {
|
|
scenicId == PayPageBrandingPolicy.sailimuScenicId
|
|
? "payment_sailimu_background" : "payment_nalati_background"
|
|
}
|
|
|
|
/// 当前景区的随包 Logo 资源名称。
|
|
var brandLogoAssetName: String {
|
|
scenicId == PayPageBrandingPolicy.sailimuScenicId
|
|
? "payment_sailimu_logo" : "payment_nalati_logo"
|
|
}
|
|
|
|
/// 品牌主标题的最大行数,赛里木湖允许两行展示。
|
|
var brandTitleNumberOfLines: Int {
|
|
scenicId == PayPageBrandingPolicy.sailimuScenicId ? 2 : 1
|
|
}
|
|
|
|
var displayPhotographerName: String {
|
|
PaymentAccountDisplayFormatter.photographerName(
|
|
realName: appStore.session.realName
|
|
)
|
|
}
|
|
|
|
var displayStoreNames: String {
|
|
let names = appStore.permissions.rolePermissionList().flatMap(\.store).map(\.name)
|
|
return PaymentAccountDisplayFormatter.storeNames(names)
|
|
}
|
|
|
|
func refreshLocalState() {
|
|
scenicId = appStore.session.currentScenicId
|
|
scenicName = appStore.session.currentScenicName
|
|
staffId = appStore.session.userId
|
|
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
|
restorePayPageConfig()
|
|
notifyStateChange()
|
|
}
|
|
|
|
private func restorePayPageConfig() {
|
|
guard let defaults = PayPageBrandingPolicy.defaultConfig(scenicId: scenicId) else {
|
|
payPageConfig = .nalatiDefault
|
|
return
|
|
}
|
|
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? defaults
|
|
}
|
|
|
|
func loadPayCode(api: PaymentAPI) async {
|
|
guard !loading else { return }
|
|
loading = true
|
|
refreshLocalState()
|
|
defer {
|
|
loading = false
|
|
hasCompletedPayCodeRequest = true
|
|
notifyStateChange()
|
|
}
|
|
|
|
let scenicId = self.scenicId
|
|
guard scenicId > 0 else {
|
|
clearPayCode()
|
|
return
|
|
}
|
|
|
|
do {
|
|
let response = try await api.payCode(scenicId: scenicId)
|
|
staticPayURL = response.staticPayURL
|
|
dynamicPayURL = response.dynamicPayURL
|
|
qrImage = QRCodeGenerator.generateStyledQRCode(content: staticPayURL)
|
|
} catch {
|
|
clearPayCode()
|
|
}
|
|
}
|
|
|
|
func loadPayPageConfig(api: PaymentAPI) async {
|
|
let requestedScenicId = appStore.session.currentScenicId
|
|
guard let defaults = PayPageBrandingPolicy.defaultConfig(scenicId: requestedScenicId) else { return }
|
|
|
|
do {
|
|
let response = try await api.payPageConfig(scenicId: requestedScenicId)
|
|
guard appStore.session.currentScenicId == requestedScenicId,
|
|
scenicId == requestedScenicId else { return }
|
|
let cached = payPageConfigCache.config(scenicId: requestedScenicId) ?? defaults
|
|
let resolved = cached.merging(response)
|
|
payPageConfigCache.save(resolved, scenicId: requestedScenicId)
|
|
payPageConfig = resolved
|
|
brandingRefreshVersion += 1
|
|
notifyStateChange()
|
|
} catch {
|
|
// 品牌配置失败时保留缓存或随包默认值,不影响收款码与页面交互。
|
|
}
|
|
}
|
|
|
|
func refresh(api: PaymentAPI) async {
|
|
async let payCode: Void = loadPayCode(api: api)
|
|
async let pageConfig: Void = loadPayPageConfig(api: api)
|
|
_ = await (payCode, pageConfig)
|
|
}
|
|
|
|
func showSetAmountDialog() {
|
|
guard !staticPayURL.isEmpty else {
|
|
onShowMessage?("请先获取收款码")
|
|
return
|
|
}
|
|
amount = ""
|
|
remark = ""
|
|
showAmountDialog = true
|
|
notifyStateChange()
|
|
}
|
|
|
|
func hideAmountDialog() {
|
|
showAmountDialog = false
|
|
amount = ""
|
|
remark = ""
|
|
notifyStateChange()
|
|
}
|
|
|
|
func updateAmount(_ value: String) {
|
|
guard value.isEmpty || value.matches(PaymentAmountValidator.inputPattern) else { return }
|
|
amount = value
|
|
notifyStateChange()
|
|
}
|
|
|
|
func updateRemark(_ value: String) {
|
|
guard value.count <= 200 else { return }
|
|
remark = value
|
|
notifyStateChange()
|
|
}
|
|
|
|
func confirmAmount() {
|
|
if let message = PaymentAmountValidator.validationMessage(for: amount) {
|
|
onShowMessage?(message)
|
|
return
|
|
}
|
|
guard !dynamicPayURL.isEmpty else {
|
|
onShowMessage?("请先获取收款码")
|
|
return
|
|
}
|
|
|
|
let dynamicURL = dynamicPayURL + "&amount=\(amount)&remark=\(remark)"
|
|
qrImage = QRCodeGenerator.generateStyledQRCode(content: dynamicURL)
|
|
showAmountDialog = false
|
|
onShowMessage?("金额设置成功,二维码已更新")
|
|
notifyStateChange()
|
|
}
|
|
|
|
func saveQRCode() async {
|
|
guard qrImage != nil else {
|
|
onShowMessage?("请先获取收款码")
|
|
return
|
|
}
|
|
let success = await PhotoLibrarySaver.saveImage(qrImage!)
|
|
onShowMessage?(success ? "保存成功" : "保存失败")
|
|
}
|
|
|
|
func toggleReceiveVoice() {
|
|
isVoiceBroadcastOpen.toggle()
|
|
appStore.payment.isOpenReceiveVoice = isVoiceBroadcastOpen
|
|
notifyStateChange()
|
|
}
|
|
|
|
private func clearPayCode() {
|
|
staticPayURL = ""
|
|
dynamicPayURL = ""
|
|
qrImage = nil
|
|
}
|
|
|
|
private func notifyStateChange() {
|
|
onStateChange?()
|
|
}
|
|
}
|
|
|
|
/// 收款金额校验,对齐 Android `PaymentCollectionDetailsViewModel.confirmAmount`。
|
|
enum PaymentAmountValidator {
|
|
static let inputPattern = #"^\d*(\.\d{0,2})?$"#
|
|
private static let confirmPattern = #"^\d+(\.\d{1,2})?$"#
|
|
|
|
static func validationMessage(for amount: String) -> String? {
|
|
if amount.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
return "请输入收款金额"
|
|
}
|
|
guard let value = Double(amount), value > 0 else {
|
|
return "请输入有效的收款金额"
|
|
}
|
|
if !amount.matches(confirmPattern) {
|
|
return "金额最多支持两位小数"
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private extension String {
|
|
func matches(_ pattern: String) -> Bool {
|
|
range(of: pattern, options: .regularExpression) != nil
|
|
}
|
|
}
|