feat: 优化账号注销流程

This commit is contained in:
2026-08-31 15:56:16 +08:00
parent e529bb5942
commit 4444d328df
16 changed files with 982 additions and 208 deletions
@@ -18,17 +18,19 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
private let stack = UIStackView()
private let bottomBar = UIView()
private let titleLabel = Style.label(size: 24, weight: .semibold)
private let stateIcon = Style.icon("clock", size: 36)
private let stateIcon = Style.icon("lock", size: 36)
private let stateIconBadge = UIView()
private let deadlineLabel = Style.label(size: 19, weight: .semibold)
private let warningLabel = Style.label(size: 13, color: AppColor.textSecondary)
private let infoLabel = Style.label("重新登录该身份自动撤销申请", size: 13, color: Style.textSecondary)
private var deadlineCard = UIView()
private var infoCard = UIView()
private let messageLabel = UILabel()
private let retryButton = UIButton(type: .system)
private let conditionsButton = UIButton(type: .system)
private let cancelButton = UIButton(type: .system)
private let logoutButton = UIButton(type: .system)
private var queryTask: Task<Void, Never>?
private var needsForegroundRefresh = false
private var previousPopGestureEnabled: Bool?
/// 显式注入会话与 API;身份构造失败时仍展示错误,不自动退出或重新登录。
init(identity: StoreAccountDeregistrationIdentity?, api: (any StoreAccountDeregistrationServing)?,
@@ -53,10 +55,11 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
override func setupNavigationBar() {
title = "注销账号"
navigationItem.hidesBackButton = true
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackground
view.backgroundColor = Style.pageBackground
view.addSubview(scrollView)
scrollView.alwaysBounceVertical = true
refreshControl.accessibilityIdentifier = "deregister.access.refresh"
@@ -66,47 +69,55 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
scrollView.refreshControl = refreshControl
scrollView.addSubview(stack)
stack.axis = .vertical
stack.spacing = 20
stateIcon.snp.makeConstraints { $0.height.equalTo(64) }
stack.spacing = 18
stateIconBadge.backgroundColor = Style.infoBackground
stateIconBadge.layer.cornerRadius = 44
stateIconBadge.addSubview(stateIcon)
stateIconBadge.snp.makeConstraints { $0.width.height.equalTo(88) }
stateIcon.snp.makeConstraints { $0.center.equalToSuperview(); $0.width.height.equalTo(44) }
titleLabel.textAlignment = .center
messageLabel.numberOfLines = 0
messageLabel.font = .systemFont(ofSize: 15)
messageLabel.textColor = AppColor.textSecondary
messageLabel.textColor = Style.textSecondary
messageLabel.textAlignment = .center
let hero = Style.stack([stateIcon, titleLabel, messageLabel], spacing: 12)
let iconRow = UIView()
iconRow.addSubview(stateIconBadge)
stateIconBadge.snp.makeConstraints { $0.top.bottom.centerX.equalToSuperview() }
let hero = Style.stack([iconRow, titleLabel, messageLabel], spacing: 12)
stack.addArrangedSubview(hero)
let identity = Style.stack([
Style.label("当前门店身份", size: 12, color: AppColor.textSecondary),
Style.label(self.identity?.displayName ?? "当前身份", size: 17, weight: .semibold)
], spacing: 8)
stack.addArrangedSubview(Style.card(identity))
deadlineLabel.accessibilityIdentifier = "deregister.access.deadline"
deadlineCard = Style.card(Style.stack([
Style.label("冷静期截止时间", size: 13, color: AppColor.textSecondary),
deadlineLabel,
Style.label("以服务端时间为准,到期后仍需复核。", size: 12, color: AppColor.textSecondary)
], spacing: 10))
let deadlineIcon = Style.icon("calendar", size: 22)
deadlineIcon.snp.makeConstraints { $0.width.equalTo(28) }
let deadlineText = Style.stack([
Style.label("冷静期截止时间", size: 13, color: Style.textSecondary), deadlineLabel
], spacing: 8)
let deadlineRow = UIStackView(arrangedSubviews: [deadlineIcon, deadlineText])
deadlineRow.axis = .horizontal
deadlineRow.alignment = .center
deadlineRow.spacing = 12
deadlineCard = Style.card(deadlineRow)
stack.addArrangedSubview(deadlineCard)
warningLabel.text = "重新登录或选中此身份,会自动撤销尚未完成的注销申请。其他身份不受影响。"
stack.addArrangedSubview(warningLabel)
conditionsButton.setTitle("查看注销条件", for: .normal)
conditionsButton.titleLabel?.font = .systemFont(ofSize: 14)
conditionsButton.snp.makeConstraints { $0.height.greaterThanOrEqualTo(44) }
stack.addArrangedSubview(conditionsButton)
let infoIcon = Style.icon("info.circle", size: 17)
infoIcon.snp.makeConstraints { $0.width.equalTo(22) }
let infoRow = UIStackView(arrangedSubviews: [infoIcon, infoLabel])
infoRow.axis = .horizontal
infoRow.alignment = .center
infoRow.spacing = 8
infoCard = Style.card(infoRow, background: Style.infoBackground)
stack.addArrangedSubview(infoCard)
view.addSubview(bottomBar)
bottomBar.backgroundColor = .white
Style.configure(retryButton, title: "重新查询状态")
Style.configure(logoutButton, title: "退出登录")
Style.configure(cancelButton, title: "撤销注销申请", primary: false)
let actions = Style.stack([retryButton, logoutButton, cancelButton], spacing: 4)
let actions = Style.stack([retryButton, logoutButton, cancelButton], spacing: 12)
bottomBar.addSubview(actions)
actions.snp.makeConstraints { $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)) }
for button in [retryButton, logoutButton, cancelButton] {
button.snp.makeConstraints { $0.height.greaterThanOrEqualTo(50) }
}
retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
conditionsButton.addTarget(self, action: #selector(conditionsTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
logoutButton.addTarget(self, action: #selector(logoutTapped), for: .touchUpInside)
messageLabel.accessibilityIdentifier = "deregister.access.message"
@@ -114,7 +125,6 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
retryButton.accessibilityIdentifier = "deregister.access.retry"
cancelButton.accessibilityIdentifier = "deregister.access.cancel"
logoutButton.accessibilityIdentifier = "deregister.access.logout"
conditionsButton.accessibilityIdentifier = "deregister.access.conditions"
}
override func setupConstraints() {
@@ -138,6 +148,20 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
if hasInitialResult { applyViewModel() } else { retryTapped() }
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
previousPopGestureEnabled = navigationController?.interactivePopGestureRecognizer?.isEnabled
navigationItem.hidesBackButton = true
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let previousPopGestureEnabled {
navigationController?.interactivePopGestureRecognizer?.isEnabled = previousPopGestureEnabled
}
}
/// 在途旧查询不能覆盖刚收到的受限信号;之后由用户主动重新查询。
func recordRestriction() {
viewModel.recordRestriction()
@@ -164,15 +188,13 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
retryButton.isEnabled = !busy && api != nil
retryButton.isHidden = cooling
refreshControl.isEnabled = !busy && api != nil
conditionsButton.isEnabled = !busy && api != nil
conditionsButton.isHidden = ![.unresolved, .cooling].contains(viewModel.decision)
cancelButton.isHidden = !cooling
cancelButton.isEnabled = !busy && api != nil
logoutButton.isEnabled = !busy
Style.configure(logoutButton, title: "退出登录", primary: cooling)
deadlineCard.isHidden = !cooling
warningLabel.isHidden = !cooling
deadlineLabel.text = viewModel.status?.coolingUntil
infoCard.isHidden = !cooling
deadlineLabel.text = viewModel.status?.coolingUntil ?? "—"
guard identity != nil, api != nil else {
titleLabel.text = "暂时无法查询"
messageLabel.text = "当前身份信息不完整,请联系管理员。"
@@ -190,9 +212,8 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
symbol = "checkmark.circle"
case .cooling:
titleLabel.text = "注销申请已提交"
messageLabel.text = (viewModel.status?.remainingSeconds ?? 0) > 0
? "当前处于冷静期,期间暂停普通业务。\n你仍可以撤销申请,恢复使用。"
: "冷静期已结束,正在等待服务端复核。\n最终结果请刷新后查看。"
messageLabel.text = "当前身份:\(identity?.displayName ?? "当前身份")"
symbol = "lock"
case .unresolved:
titleLabel.text = "注销状态待确认"
messageLabel.text = "暂时未获取到明确结果,请刷新重试。\n请勿重复提交;如持续出现,请联系管理员。"
@@ -208,6 +229,8 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
}
stateIcon.image = UIImage(systemName: symbol,
withConfiguration: UIImage.SymbolConfiguration(pointSize: 36, weight: .medium))
navigationItem.hidesBackButton = true
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
@objc private func cancelTapped() {
@@ -263,20 +286,9 @@ final class StoreAccountDeregistrationAccessViewController: BaseViewController {
}
}
@objc private func conditionsTapped() {
guard let identity, let api, identity.matches(session: session),
[.unresolved, .cooling].contains(viewModel.decision) else { return }
let controller = StoreAccountDeregistrationViewController(
identityName: identity.displayName,
viewModel: StoreAccountDeregistrationViewModel(storeUserID: Int(identity.userID) ?? 0), api: api,
readOnly: true
)
navigationController?.pushViewController(controller, animated: true)
}
@objc private func logoutTapped() {
let alert = UIAlertController(title: "退出登录?", message:
"退出本身不会撤销申请,但会清除本机登录凭证。再次登录或选中此身份可能自动撤销尚未完成的申请。", preferredStyle: .alert)
"退出本身不会撤销申请,但会清除本机登录凭证。再次登录或选中此身份会自动撤销尚未完成的申请。", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "继续保留查询", style: .cancel))
alert.addAction(UIAlertAction(title: "退出登录", style: .destructive) { _ in
NotificationCenter.default.post(name: NotificationName.userDidLogout, object: nil)