feat: 优化账号注销流程
This commit is contained in:
@@ -12,54 +12,70 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
private let identityName: String
|
||||
private let readOnly: Bool
|
||||
private let onUnresolvedSubmission: (() -> Void)?
|
||||
private let onSubmissionAccepted: () -> Void
|
||||
private let onSubmissionAccepted: (String?) -> Void
|
||||
private var didHandleAcceptedSubmission = false
|
||||
private let scrollView = UIScrollView()
|
||||
private let refreshControl = UIRefreshControl()
|
||||
private let content = UIStackView()
|
||||
private let bottomBar = UIView()
|
||||
private let steps = UIStackView()
|
||||
private let firstStep = Style.label("1 确认资产", size: 13, weight: .semibold)
|
||||
private let secondStep = Style.label("2 手机验证", size: 13, weight: .semibold)
|
||||
private let firstStepNumber = Style.label("1", size: 11, weight: .semibold)
|
||||
private let secondStepNumber = Style.label("2", size: 11, weight: .semibold)
|
||||
private let firstStep = Style.label("确认资产", size: 13, weight: .semibold)
|
||||
private let secondStep = Style.label("手机验证", size: 13, weight: .semibold)
|
||||
private let firstStepView = UIView()
|
||||
private let secondStepView = UIView()
|
||||
private let heading = Style.label(size: 24, weight: .semibold)
|
||||
private let subtitle = Style.label(size: 14, color: AppColor.textSecondary)
|
||||
private let subtitle = Style.label(size: 14, color: Style.textSecondary)
|
||||
private let conditionStack = UIStackView()
|
||||
private let verificationStack = UIStackView()
|
||||
private let walletAmount = Style.label("—", size: 28, weight: .semibold)
|
||||
private let pointsAmount = Style.label("—", size: 28, weight: .semibold)
|
||||
private let walletState = Style.label(size: 12, color: AppColor.textSecondary)
|
||||
private let pointsState = Style.label(size: 12, color: AppColor.textSecondary)
|
||||
private let assetHint = Style.label(size: 13, color: AppColor.textSecondary)
|
||||
private let blockersLabel = Style.label(size: 14, color: AppColor.textSecondary)
|
||||
private let walletState = Style.label(size: 12, color: Style.textSecondary)
|
||||
private let pointsState = Style.label(size: 12, color: Style.textSecondary)
|
||||
private let assetHint = Style.label(size: 13, color: Style.textSecondary)
|
||||
private let blockersLabel = Style.label(size: 14, color: Style.textSecondary)
|
||||
private let riskLabel = Style.label(size: 13, color: AppColor.warning)
|
||||
private let statusLabel = Style.label(size: 13, color: AppColor.textSecondary)
|
||||
private let errorLabel = Style.label(size: 14, color: AppColor.danger)
|
||||
private let statusLabel = Style.label(size: 13, color: Style.textSecondary)
|
||||
private let errorLabel = Style.label(size: 14, color: Style.danger)
|
||||
private let codeField = UITextField()
|
||||
private let reasonField = UITextField()
|
||||
private let reasonField = UITextView()
|
||||
private let reasonTitle = Style.label(size: 16, weight: .semibold)
|
||||
private let reasonPlaceholder = Style.label("请填写注销原因", size: 14, color: Style.textSecondary)
|
||||
private let reasonCount = Style.label("0/50", size: 12, color: Style.textSecondary)
|
||||
private let reasonValidationLabel = Style.label("请填写注销原因", size: 12, color: Style.danger)
|
||||
private let reasonContainer = UIView()
|
||||
private let smsButton = UIButton(type: .system)
|
||||
private let continueButton = Style.button("确认资产并继续", id: "deregister.continue")
|
||||
private let submitButton = Style.button("提交注销申请", id: "deregister.submit")
|
||||
private let backButton = UIButton(type: .system)
|
||||
private let footerHint = Style.label(size: 12, color: AppColor.textSecondary)
|
||||
private var identityCard = UIView()
|
||||
private var blockersCard = UIView()
|
||||
private var errorCard = UIView()
|
||||
private var actionTask: Task<Void, Never>?
|
||||
private var previousPopGestureEnabled: Bool?
|
||||
private var previousViewportHeight: CGFloat = 0
|
||||
private var reasonWasBlurred = false
|
||||
|
||||
/// 注入当前身份及旧接口,不读取任意手机号,也不自动确认资产。
|
||||
init(identityName: String, viewModel: StoreAccountDeregistrationViewModel,
|
||||
api: any StoreAccountDeregistrationServing, readOnly: Bool = false,
|
||||
onUnresolvedSubmission: (() -> Void)? = nil,
|
||||
onSubmissionAccepted: @escaping () -> Void = {
|
||||
NotificationCenter.default.post(name: NotificationName.userDidLogout, object: nil)
|
||||
}) {
|
||||
onSubmissionAccepted: ((String?) -> Void)? = nil) {
|
||||
let frozenIdentityName = identityName
|
||||
self.onSubmissionAccepted = onSubmissionAccepted ?? { coolingUntil in
|
||||
var userInfo = [NotificationUserInfoKey.deregistrationIdentityName: frozenIdentityName]
|
||||
if let coolingUntil { userInfo[NotificationUserInfoKey.deregistrationCoolingUntil] = coolingUntil }
|
||||
NotificationCenter.default.post(
|
||||
name: NotificationName.storeAccountDeregistrationSubmitted,
|
||||
object: nil,
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
self.identityName = identityName
|
||||
self.viewModel = viewModel
|
||||
self.api = api
|
||||
self.readOnly = readOnly
|
||||
self.onUnresolvedSubmission = onUnresolvedSubmission
|
||||
self.onSubmissionAccepted = onSubmissionAccepted
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@@ -71,7 +87,7 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
view.backgroundColor = Style.pageBackground
|
||||
view.addSubview(scrollView)
|
||||
refreshControl.accessibilityIdentifier = "deregister.refresh"
|
||||
refreshControl.accessibilityLabel = "下拉刷新"
|
||||
@@ -83,20 +99,28 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
content.spacing = 16
|
||||
content.addArrangedSubview(steps)
|
||||
steps.axis = .horizontal
|
||||
steps.alignment = .fill
|
||||
steps.distribution = .fillEqually
|
||||
steps.addArrangedSubview(firstStep)
|
||||
steps.addArrangedSubview(secondStep)
|
||||
steps.spacing = 12
|
||||
configureStep(firstStepView, number: firstStepNumber, title: firstStep,
|
||||
identifier: "deregister.step.assets", accessibilityLabel: "第 1 步,确认资产")
|
||||
configureStep(secondStepView, number: secondStepNumber, title: secondStep,
|
||||
identifier: "deregister.step.verification", accessibilityLabel: "第 2 步,手机验证")
|
||||
steps.addArrangedSubview(firstStepView)
|
||||
steps.addArrangedSubview(secondStepView)
|
||||
steps.snp.makeConstraints { $0.height.equalTo(46) }
|
||||
steps.isHidden = readOnly
|
||||
content.addArrangedSubview(Style.stack([heading, subtitle], spacing: 8))
|
||||
let identityText = Style.stack([
|
||||
Style.label("当前门店身份", size: 12, color: AppColor.textSecondary),
|
||||
Style.label("当前门店身份", size: 12, color: Style.textSecondary),
|
||||
Style.label(identityName, size: 17, weight: .semibold)
|
||||
], spacing: 5)
|
||||
let identityRow = UIStackView(arrangedSubviews: [Style.icon("person.crop.circle"), identityText])
|
||||
identityRow.axis = .horizontal
|
||||
identityRow.alignment = .center
|
||||
identityRow.spacing = 12
|
||||
content.addArrangedSubview(Style.card(identityRow))
|
||||
identityCard = Style.card(identityRow)
|
||||
content.addArrangedSubview(identityCard)
|
||||
conditionStack.axis = .vertical
|
||||
conditionStack.spacing = 16
|
||||
verificationStack.axis = .vertical
|
||||
@@ -106,7 +130,7 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
content.addArrangedSubview(conditionStack)
|
||||
content.addArrangedSubview(verificationStack)
|
||||
errorCard = Style.card(errorLabel)
|
||||
errorCard.backgroundColor = AppColor.dangerBackground
|
||||
errorCard.backgroundColor = UIColor(hex: 0xFFF1F1)
|
||||
content.addArrangedSubview(errorCard)
|
||||
content.addArrangedSubview(statusLabel)
|
||||
statusLabel.accessibilityIdentifier = "deregister.status"
|
||||
@@ -114,8 +138,7 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.backgroundColor = .white
|
||||
footerHint.textAlignment = .center
|
||||
let actions = Style.stack([continueButton, submitButton, footerHint], spacing: 10)
|
||||
let actions = Style.stack([continueButton, submitButton], spacing: 10)
|
||||
bottomBar.addSubview(actions)
|
||||
actions.snp.makeConstraints { $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)) }
|
||||
bottomBar.isHidden = readOnly
|
||||
@@ -125,17 +148,45 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
scrollView.alwaysBounceVertical = true
|
||||
}
|
||||
|
||||
private func configureStep(_ container: UIView, number: UILabel, title: UILabel,
|
||||
identifier: String, accessibilityLabel: String) {
|
||||
number.numberOfLines = 1
|
||||
title.numberOfLines = 1
|
||||
number.textAlignment = .center
|
||||
number.layer.cornerRadius = 10
|
||||
number.clipsToBounds = true
|
||||
number.snp.makeConstraints { $0.width.height.equalTo(20) }
|
||||
let row = UIStackView(arrangedSubviews: [number, title])
|
||||
row.axis = .horizontal
|
||||
row.alignment = .center
|
||||
row.spacing = 7
|
||||
title.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
container.layer.cornerRadius = 12
|
||||
container.accessibilityIdentifier = identifier
|
||||
container.isAccessibilityElement = true
|
||||
container.accessibilityLabel = accessibilityLabel
|
||||
container.addSubview(row)
|
||||
row.snp.makeConstraints { $0.center.equalToSuperview() }
|
||||
}
|
||||
|
||||
private func buildConditions() {
|
||||
let columns = UIStackView()
|
||||
columns.axis = .horizontal
|
||||
columns.distribution = .fillEqually
|
||||
columns.distribution = .fill
|
||||
columns.spacing = 16
|
||||
columns.addArrangedSubview(Style.stack([
|
||||
Style.label("现金余额", size: 13, color: AppColor.textSecondary), walletAmount, walletState
|
||||
], spacing: 8))
|
||||
columns.addArrangedSubview(Style.stack([
|
||||
Style.label("积分", size: 13, color: AppColor.textSecondary), pointsAmount, pointsState
|
||||
], spacing: 8))
|
||||
let walletColumn = Style.stack([
|
||||
Style.label("现金余额", size: 13, color: Style.textSecondary), walletAmount, walletState
|
||||
], spacing: 8)
|
||||
columns.addArrangedSubview(walletColumn)
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = Style.border
|
||||
divider.snp.makeConstraints { $0.width.equalTo(1) }
|
||||
columns.addArrangedSubview(divider)
|
||||
let pointsColumn = Style.stack([
|
||||
Style.label("积分", size: 13, color: Style.textSecondary), pointsAmount, pointsState
|
||||
], spacing: 8)
|
||||
columns.addArrangedSubview(pointsColumn)
|
||||
walletColumn.snp.makeConstraints { $0.width.equalTo(pointsColumn) }
|
||||
walletAmount.adjustsFontSizeToFitWidth = true
|
||||
walletAmount.minimumScaleFactor = 0.6
|
||||
walletAmount.numberOfLines = 1
|
||||
@@ -155,23 +206,19 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
]))
|
||||
conditionStack.addArrangedSubview(blockersCard)
|
||||
let notices = Style.stack([
|
||||
Style.label("注销须知", size: 16, weight: .semibold),
|
||||
notice("person.crop.circle", title: "仅注销当前身份", detail: "同手机号的其他身份不受影响。"),
|
||||
notice("clock", title: "7 天冷静期", detail: "提交后可撤销申请,到期由服务端复核。"),
|
||||
notice("exclamationmark.shield", title: "正式完成后不可恢复", detail: "历史订单、财务及审计记录按规则保留。")
|
||||
notice("person.crop.circle", title: "仅注销当前身份"),
|
||||
notice("clock", title: "7 天冷静期"),
|
||||
notice("exclamationmark.shield", title: "正式完成后不可恢复")
|
||||
], spacing: 16)
|
||||
conditionStack.addArrangedSubview(Style.card(notices))
|
||||
conditionStack.addArrangedSubview(Style.card(notices, background: Style.infoBackground))
|
||||
}
|
||||
|
||||
private func notice(_ icon: String, title: String, detail: String) -> UIView {
|
||||
private func notice(_ icon: String, title: String) -> UIView {
|
||||
let image = Style.icon(icon, size: 18)
|
||||
image.snp.makeConstraints { $0.width.equalTo(22) }
|
||||
let row = UIStackView(arrangedSubviews: [image, Style.stack([
|
||||
Style.label(title, size: 14, weight: .medium),
|
||||
Style.label(detail, size: 13, color: AppColor.textSecondary)
|
||||
], spacing: 4)])
|
||||
let row = UIStackView(arrangedSubviews: [image, Style.label(title, size: 14, weight: .medium)])
|
||||
row.axis = .horizontal
|
||||
row.alignment = .top
|
||||
row.alignment = .center
|
||||
row.spacing = 10
|
||||
return row
|
||||
}
|
||||
@@ -182,18 +229,22 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
codeField.textContentType = .oneTimeCode
|
||||
codeField.accessibilityIdentifier = "deregister.code"
|
||||
codeField.accessibilityLabel = "短信验证码"
|
||||
reasonField.placeholder = "请输入注销原因"
|
||||
reasonField.accessibilityIdentifier = "deregister.reason"
|
||||
reasonField.accessibilityLabel = "注销原因"
|
||||
for field in [codeField, reasonField] {
|
||||
field.font = .systemFont(ofSize: 16)
|
||||
field.autocorrectionType = .no
|
||||
// 本页通过keyboardLayoutGuide和滚动区域避让,避免全局键盘库重复抬升整页。
|
||||
field.iq.enableMode = .disabled
|
||||
field.addTarget(self, action: #selector(inputChanged), for: .editingChanged)
|
||||
field.addTarget(self, action: #selector(revealFocusedInput), for: .editingDidBegin)
|
||||
field.snp.makeConstraints { $0.height.equalTo(48) }
|
||||
}
|
||||
reasonField.accessibilityLabel = "注销原因,必填"
|
||||
codeField.font = .systemFont(ofSize: 16)
|
||||
codeField.autocorrectionType = .no
|
||||
codeField.iq.enableMode = .disabled
|
||||
codeField.addTarget(self, action: #selector(inputChanged), for: .editingChanged)
|
||||
codeField.addTarget(self, action: #selector(revealFocusedInput), for: .editingDidBegin)
|
||||
reasonField.font = .systemFont(ofSize: 15)
|
||||
reasonField.textColor = Style.textPrimary
|
||||
reasonField.backgroundColor = .clear
|
||||
reasonField.delegate = self
|
||||
reasonField.autocorrectionType = .no
|
||||
reasonField.iq.enableMode = .disabled
|
||||
reasonField.textContainerInset = .init(top: 12, left: 12, bottom: 24, right: 12)
|
||||
codeField.snp.makeConstraints { $0.height.equalTo(50) }
|
||||
reasonField.snp.makeConstraints { $0.height.equalTo(96) }
|
||||
smsButton.setTitle("获取验证码", for: .normal)
|
||||
smsButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
smsButton.accessibilityIdentifier = "deregister.sms"
|
||||
@@ -203,20 +254,50 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
smsButton.snp.makeConstraints { $0.height.greaterThanOrEqualTo(44) }
|
||||
let codeRow = UIStackView(arrangedSubviews: [codeField, smsButton])
|
||||
codeRow.axis = .horizontal
|
||||
codeRow.alignment = .center
|
||||
codeRow.spacing = 12
|
||||
verificationStack.addArrangedSubview(Style.card(Style.stack([
|
||||
Style.label("短信验证", size: 16, weight: .semibold), codeRow,
|
||||
Style.label("验证码将发送至当前身份绑定的手机号。", size: 13, color: AppColor.textSecondary)
|
||||
], spacing: 8)))
|
||||
verificationStack.addArrangedSubview(Style.card(Style.stack([
|
||||
Style.label("注销原因", size: 16, weight: .semibold), reasonField
|
||||
], spacing: 8)))
|
||||
backButton.setTitle("返回查看资产与须知", for: .normal)
|
||||
backButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
backButton.accessibilityIdentifier = "deregister.back"
|
||||
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
|
||||
backButton.snp.makeConstraints { $0.height.greaterThanOrEqualTo(44) }
|
||||
verificationStack.addArrangedSubview(backButton)
|
||||
codeRow.layoutMargins = .init(top: 0, left: 14, bottom: 0, right: 8)
|
||||
codeRow.isLayoutMarginsRelativeArrangement = true
|
||||
codeRow.layer.cornerRadius = 10
|
||||
codeRow.layer.borderWidth = 1
|
||||
codeRow.layer.borderColor = Style.border.cgColor
|
||||
let smsTitle = Style.label("短信验证码", size: 16, weight: .semibold)
|
||||
reasonContainer.layer.cornerRadius = 10
|
||||
reasonContainer.layer.borderWidth = 1
|
||||
reasonContainer.layer.borderColor = Style.border.cgColor
|
||||
reasonContainer.addSubview(reasonField)
|
||||
reasonContainer.addSubview(reasonPlaceholder)
|
||||
reasonContainer.addSubview(reasonCount)
|
||||
reasonField.snp.makeConstraints { $0.edges.equalToSuperview() }
|
||||
reasonPlaceholder.snp.makeConstraints { $0.top.leading.equalToSuperview().inset(16) }
|
||||
reasonCount.snp.makeConstraints { $0.trailing.bottom.equalToSuperview().inset(12) }
|
||||
let titleText = NSMutableAttributedString(
|
||||
string: "注销原因 ",
|
||||
attributes: [.font: UIFont.systemFont(ofSize: 16, weight: .semibold), .foregroundColor: Style.textPrimary]
|
||||
)
|
||||
titleText.append(NSAttributedString(
|
||||
string: "*",
|
||||
attributes: [.font: UIFont.systemFont(ofSize: 16, weight: .semibold), .foregroundColor: Style.danger]
|
||||
))
|
||||
reasonTitle.attributedText = titleText
|
||||
reasonTitle.accessibilityLabel = "注销原因,必填"
|
||||
reasonTitle.accessibilityIdentifier = "deregister.reason.title"
|
||||
reasonValidationLabel.isHidden = true
|
||||
reasonValidationLabel.accessibilityIdentifier = "deregister.reason.error"
|
||||
let form = Style.stack([smsTitle, codeRow, reasonTitle, reasonContainer, reasonValidationLabel], spacing: 12)
|
||||
form.setCustomSpacing(18, after: codeRow)
|
||||
verificationStack.addArrangedSubview(Style.card(form))
|
||||
let helperIcon = Style.icon("shield", size: 16)
|
||||
helperIcon.tintColor = Style.textSecondary
|
||||
helperIcon.snp.makeConstraints { $0.width.equalTo(22) }
|
||||
let helper = UIStackView(arrangedSubviews: [
|
||||
helperIcon,
|
||||
Style.label("验证码将发送至当前身份绑定的手机号。", size: 13, color: Style.textSecondary)
|
||||
])
|
||||
helper.axis = .horizontal
|
||||
helper.alignment = .center
|
||||
helper.spacing = 8
|
||||
verificationStack.addArrangedSubview(helper)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
@@ -262,7 +343,7 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
}
|
||||
|
||||
@objc private func revealFocusedInput() {
|
||||
guard let field = [codeField, reasonField].first(where: \.isFirstResponder) else { return }
|
||||
guard let field = ([codeField, reasonField] as [UIView]).first(where: \.isFirstResponder) else { return }
|
||||
let rect = field.convert(field.bounds, to: scrollView).insetBy(dx: 0, dy: -12)
|
||||
scrollView.scrollRectToVisible(rect, animated: false)
|
||||
}
|
||||
@@ -273,8 +354,9 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
let unresolved = viewModel.step == .unresolvedRequest && !readOnly
|
||||
heading.text = readOnly ? "当前注销条件" : (verifying ? "验证绑定手机号" : "注销前,请确认")
|
||||
subtitle.text = verifying ? "完成验证后,即可提交注销申请。" : "仅注销此身份,其他身份不受影响。"
|
||||
firstStep.textColor = verifying ? AppColor.textSecondary : AppColor.primary
|
||||
secondStep.textColor = verifying ? AppColor.primary : AppColor.textSecondary
|
||||
applyStepStyle(firstStepView, number: firstStepNumber, title: firstStep, selected: !verifying)
|
||||
applyStepStyle(secondStepView, number: secondStepNumber, title: secondStep, selected: verifying)
|
||||
identityCard.isHidden = verifying || unresolved
|
||||
conditionStack.isHidden = verifying || unresolved
|
||||
verificationStack.isHidden = !verifying
|
||||
continueButton.isHidden = verifying || readOnly
|
||||
@@ -284,10 +366,9 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
submitButton.isEnabled = !busy && viewModel.canContinue && hasVerificationInput
|
||||
smsButton.isEnabled = !busy && viewModel.canContinue
|
||||
codeField.isEnabled = !busy
|
||||
reasonField.isEnabled = !busy
|
||||
backButton.isEnabled = !busy
|
||||
reasonField.isEditable = !busy
|
||||
applyReasonInputStyle()
|
||||
refreshControl.isEnabled = !busy
|
||||
footerHint.text = verifying ? "7 天内可撤销,正式完成后不可恢复" : "请确认资产及注销须知后继续"
|
||||
errorLabel.text = viewModel.errorMessage
|
||||
errorCard.isHidden = viewModel.errorMessage == nil
|
||||
|
||||
@@ -296,13 +377,13 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
pointsAmount.text = "\(value.pointsBalance)"
|
||||
walletState.text = value.walletWaived ? "已确认放弃" : "待确认放弃"
|
||||
pointsState.text = value.pointsWaived ? "已确认放弃" : "待确认放弃"
|
||||
walletState.textColor = value.walletWaived ? AppColor.primary : AppColor.textSecondary
|
||||
pointsState.textColor = value.pointsWaived ? AppColor.primary : AppColor.textSecondary
|
||||
walletState.textColor = value.walletWaived ? Style.primary : Style.textSecondary
|
||||
pointsState.textColor = value.pointsWaived ? Style.primary : Style.textSecondary
|
||||
let assetIssues = value.blockers.filter(\.isAssetConfirmation)
|
||||
let needsAssetHint = readOnly || assetIssues.contains { $0.code == "WAIVER_STALE" }
|
||||
assetHint.isHidden = !needsAssetHint
|
||||
assetHint.text = readOnly ? "以当前查询结果为准,冷静期内不能再次确认资产。"
|
||||
: (assetIssues.contains { $0.code == "WAIVER_STALE" }
|
||||
? "资产已变化,请按最新金额重新确认。"
|
||||
: "正式注销时,将清空已确认放弃的现金和积分。")
|
||||
: "资产已变化,请按最新金额重新确认。"
|
||||
blockersCard.isHidden = value.businessBlockers.isEmpty
|
||||
blockersLabel.text = value.businessBlockers.map { "• \($0.message)\n \($0.guidance)" }.joined(separator: "\n\n")
|
||||
riskLabel.text = value.eligibleAt.map { "业务风险期预计结束:\($0)" }
|
||||
@@ -312,6 +393,7 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
pointsAmount.text = "—"
|
||||
walletState.text = "待查询"
|
||||
pointsState.text = "待查询"
|
||||
assetHint.isHidden = false
|
||||
assetHint.text = busy ? "正在查询资产与注销条件…" : "暂未获取到资产,请刷新重试。"
|
||||
blockersCard.isHidden = true
|
||||
}
|
||||
@@ -325,9 +407,27 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func applyStepStyle(_ container: UIView, number: UILabel, title: UILabel, selected: Bool) {
|
||||
container.backgroundColor = selected ? Style.primary : UIColor(hex: 0xE9EEF5)
|
||||
container.accessibilityTraits = selected ? [.selected] : []
|
||||
number.backgroundColor = selected ? UIColor.white.withAlphaComponent(0.2) : UIColor(hex: 0xDCE3EC)
|
||||
number.textColor = selected ? .white : Style.textSecondary
|
||||
title.textColor = selected ? .white : Style.textSecondary
|
||||
}
|
||||
|
||||
private var hasVerificationInput: Bool {
|
||||
!(codeField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
&& !(reasonField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let code = (codeField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let reason = (reasonField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return !code.isEmpty && !reason.isEmpty && reason.count <= 50
|
||||
}
|
||||
|
||||
private func applyReasonInputStyle() {
|
||||
let missing = (reasonField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let invalid = reasonWasBlurred && missing
|
||||
reasonContainer.layer.borderColor = reasonField.isFirstResponder
|
||||
? Style.primary.cgColor
|
||||
: (invalid ? Style.danger.cgColor : Style.border.cgColor)
|
||||
reasonValidationLabel.isHidden = !invalid
|
||||
}
|
||||
|
||||
private func run(_ operation: @escaping @MainActor () async -> Void) {
|
||||
@@ -344,7 +444,7 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
self.actionTask = nil
|
||||
if !self.readOnly, self.viewModel.submissionAccepted {
|
||||
self.didHandleAcceptedSubmission = true
|
||||
self.onSubmissionAccepted()
|
||||
self.onSubmissionAccepted(self.viewModel.submittedCoolingUntil)
|
||||
return
|
||||
}
|
||||
self.applyViewModel()
|
||||
@@ -364,20 +464,20 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
run { [self] in await self.viewModel.confirmAssetsAndContinue(snapshot: snapshot, api: self.api) }
|
||||
return
|
||||
}
|
||||
let alert = UIAlertController(title: "确认放弃账号资产?", message:
|
||||
"当前身份:\(identityName)\n\n现金余额:¥\(snapshot.walletBalance)\n积分:\(snapshot.pointsBalance)\n\n我确认自愿放弃以上现金余额,并确认自愿放弃以上积分。正式注销时将按规则清零。", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "暂不确认", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确认并继续", style: .destructive) { [weak self] _ in
|
||||
let sheet = StoreAccountDeregistrationConfirmationSheetViewController(
|
||||
title: "确认放弃账号资产",
|
||||
summaries: [
|
||||
.init(icon: "person.crop.circle", title: "当前身份", value: identityName),
|
||||
.init(icon: "banknote", title: "现金余额", value: "¥\(snapshot.walletBalance)"),
|
||||
.init(icon: "star.circle", title: "积分", value: "\(snapshot.pointsBalance)")
|
||||
],
|
||||
notices: [],
|
||||
cancelTitle: "暂不确认", confirmTitle: "确认放弃"
|
||||
) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.run { [self] in await self.viewModel.confirmAssetsAndContinue(snapshot: snapshot, api: self.api) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func backTapped() {
|
||||
view.endEditing(true)
|
||||
viewModel.returnToConditions()
|
||||
applyViewModel()
|
||||
}
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
@objc private func smsTapped() {
|
||||
@@ -390,20 +490,58 @@ final class StoreAccountDeregistrationViewController: BaseViewController {
|
||||
@objc private func submitTapped() {
|
||||
guard !readOnly, viewModel.canContinue, hasVerificationInput else { return }
|
||||
let code = codeField.text ?? ""
|
||||
let reason = reasonField.text ?? ""
|
||||
let reason = (reasonField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
view.endEditing(true)
|
||||
let alert = UIAlertController(title: "提交注销申请?", message:
|
||||
"仅注销“\(identityName)”。提交成功后将退出登录,并进入 7 天冷静期,最终由服务端复核;正式完成不可恢复。\n\n重新登录或选中此身份会自动撤销尚未完成的申请。", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "暂不提交", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确认提交申请", style: .destructive) { [weak self] _ in
|
||||
let sheet = StoreAccountDeregistrationConfirmationSheetViewController(
|
||||
title: "确认提交注销申请?",
|
||||
summaries: [
|
||||
.init(icon: "person.crop.circle", title: "当前身份", value: identityName),
|
||||
.init(icon: "clock", title: "冷静期", value: "7 天"),
|
||||
.init(icon: "text.bubble", title: "注销原因", value: reason)
|
||||
],
|
||||
notices: [
|
||||
("info.circle", "注销后当前身份将无法使用", false),
|
||||
("checkmark.shield", "冷静期内可撤销,重新登录该身份自动撤销申请", false),
|
||||
("exclamationmark.circle.fill", "正式完成后不可恢复,历史记录保留", true)
|
||||
],
|
||||
cancelTitle: "我再想想", confirmTitle: "确认提交"
|
||||
) { [weak self] in
|
||||
self?.submitConfirmedApplication(smsCode: code, reason: reason)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
/// 用户确认最终弹窗后提交;仅明确成功触发一次退出,未知结果仍进入核验流程。
|
||||
/// 用户确认最终弹窗后提交;仅明确成功触发一次退出态结果页,未知结果仍进入核验流程。
|
||||
func submitConfirmedApplication(smsCode: String, reason: String) {
|
||||
guard !readOnly else { return }
|
||||
guard !readOnly, viewModel.validateSubmissionInput(smsCode: smsCode, reason: reason) else {
|
||||
applyViewModel()
|
||||
return
|
||||
}
|
||||
run { [self] in await viewModel.submit(smsCode: smsCode, reason: reason, api: api) }
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreAccountDeregistrationViewController: UITextViewDelegate {
|
||||
/// 注销原因必填;限制长度并同步必填状态、占位和计数展示。
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
if textView.text.count > 50 {
|
||||
textView.text = String(textView.text.prefix(50))
|
||||
}
|
||||
reasonPlaceholder.isHidden = !textView.text.isEmpty
|
||||
reasonCount.text = "\(textView.text.count)/50"
|
||||
if !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
reasonWasBlurred = false
|
||||
}
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
applyReasonInputStyle()
|
||||
revealFocusedInput()
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
reasonWasBlurred = true
|
||||
applyReasonInputStyle()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user