fix: 优化收款码加载态,避免首屏错误占位闪现

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-31 14:07:05 +08:00
parent 61c56c7adb
commit f863004c8d
3 changed files with 66 additions and 9 deletions

View File

@ -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
}
@ -85,11 +91,11 @@ final class PaymentCollectionDetailsViewModel {
func loadPayCode(api: PaymentAPI) async {
guard !loading else { return }
refreshLocalState()
loading = true
notifyStateChange()
refreshLocalState()
defer {
loading = false
hasCompletedPayCodeRequest = true
notifyStateChange()
}

View File

@ -28,6 +28,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
private let qrContainerView = UIView()
private let qrImageView = UIImageView()
private let qrPlaceholderView = UIView()
private let qrLoadingIndicator = UIActivityIndicatorView(style: .medium)
private let qrErrorLabel = UILabel()
private let refreshButton = UIButton(type: .system)
private let setAmountButton = UIButton(type: .system)
@ -127,6 +128,10 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
qrPlaceholderView.layer.cornerRadius = usesBranding ? 10 : 24
qrPlaceholderView.isHidden = true
qrLoadingIndicator.color = AppColor.primary
qrLoadingIndicator.hidesWhenStopped = true
qrLoadingIndicator.accessibilityLabel = "收款码加载中"
qrErrorLabel.text = "未选景区或网络问题"
qrErrorLabel.font = .systemFont(ofSize: 14)
qrErrorLabel.textColor = AppColor.danger
@ -150,6 +155,9 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
trailing: 24
)
}
actionRow.alpha = 0
actionRow.isUserInteractionEnabled = false
actionRow.accessibilityElementsHidden = true
configureLinkButton(setAmountButton, title: "设置金额")
configureLinkButton(saveQRButton, title: "保存二维码")
@ -206,6 +214,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
qrContentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.lg
qrCardView.addSubview(qrContentStack)
qrPlaceholderView.addSubview(qrLoadingIndicator)
qrPlaceholderView.addSubview(qrErrorLabel)
qrPlaceholderView.addSubview(refreshButton)
@ -275,6 +284,9 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
qrPlaceholderView.snp.makeConstraints { make in
make.width.height.equalTo(usesBranding ? brandQRContainerSize : 182)
}
qrLoadingIndicator.snp.makeConstraints { make in
make.center.equalToSuperview()
}
qrErrorLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(56)
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
@ -370,9 +382,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
private func loadData() async {
async let pageConfig: Void = viewModel.loadPayPageConfig(api: paymentAPI)
showLoading()
await viewModel.loadPayCode(api: paymentAPI)
hideLoading()
await pageConfig
}
@ -387,17 +397,33 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
applyBrandConfigIfNeeded()
if let image = viewModel.qrImage {
if viewModel.isPayCodeLoading {
qrImageView.isHidden = true
qrContainerView.isHidden = true
qrPlaceholderView.backgroundColor = viewModel.usesNalatiBranding
? .white
: AppColor.inputBackground
qrPlaceholderView.isHidden = false
qrErrorLabel.isHidden = true
refreshButton.isHidden = true
qrLoadingIndicator.startAnimating()
setActionRowAvailable(false)
} else if let image = viewModel.qrImage {
qrImageView.image = image
qrImageView.isHidden = false
qrContainerView.isHidden = !viewModel.usesNalatiBranding
qrPlaceholderView.isHidden = true
actionRow.isHidden = false
qrLoadingIndicator.stopAnimating()
setActionRowAvailable(true)
} else {
qrImageView.isHidden = true
qrContainerView.isHidden = true
qrPlaceholderView.backgroundColor = AppColor.inputBackground
qrPlaceholderView.isHidden = false
actionRow.isHidden = true
qrErrorLabel.isHidden = false
refreshButton.isHidden = false
qrLoadingIndicator.stopAnimating()
setActionRowAvailable(false)
}
if viewModel.showAmountDialog {
@ -408,6 +434,12 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
}
}
private func setActionRowAvailable(_ isAvailable: Bool) {
actionRow.alpha = isAvailable ? 1 : 0
actionRow.isUserInteractionEnabled = isAvailable
actionRow.accessibilityElementsHidden = !isAvailable
}
private func presentAmountDialogIfNeeded() {
guard amountDialog == nil else {
amountDialog?.apply(amount: viewModel.amount, remark: viewModel.remark)
@ -625,9 +657,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
@objc private func refreshTapped() {
Task {
showLoading()
await viewModel.refresh(api: paymentAPI)
hideLoading()
}
}

View File

@ -69,6 +69,26 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.showAmountDialog)
}
func testPayCodeLoadingStateCoversInitialRenderAndRequest() async {
let payCodeJSON = """
{"code":100000,"msg":"success","data":{"static_pay_url":"https://pay.example.com/static","dynamic_pay_url":"https://pay.example.com/dynamic"}}
""".data(using: .utf8)!
let session = MockURLSession(responses: [payCodeJSON])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
var loadingStates = [viewModel.isPayCodeLoading]
viewModel.onStateChange = {
loadingStates.append(viewModel.isPayCodeLoading)
}
await viewModel.loadPayCode(api: api)
XCTAssertEqual(loadingStates.first, true)
XCTAssertTrue(loadingStates.dropLast().allSatisfy { $0 })
XCTAssertEqual(loadingStates.last, false)
XCTAssertTrue(viewModel.hasPayCode)
}
func testToggleReceiveVoicePersistsInAppStore() {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
viewModel.refreshLocalState()
@ -105,6 +125,7 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
await viewModel.loadPayCode(api: api)
XCTAssertFalse(viewModel.isPayCodeLoading)
XCTAssertFalse(viewModel.hasPayCode)
XCTAssertEqual(session.requests.count, 0)
}