265 lines
12 KiB
Swift
265 lines
12 KiB
Swift
import SnapKit
|
|
import UIKit
|
|
|
|
/// 注销页面共用的轻量视觉组件,仅负责颜色、字号和布局,不包含业务状态。
|
|
@MainActor
|
|
enum StoreAccountDeregistrationStyle {
|
|
/// 注销流程独立视觉令牌,与设计稿保持一致且不修改全局主题。
|
|
static let primary = UIColor(hex: 0x1677FF)
|
|
static let pageBackground = UIColor(hex: 0xF5F7FA)
|
|
static let textPrimary = UIColor(hex: 0x172033)
|
|
static let textSecondary = UIColor(hex: 0x7A8496)
|
|
static let border = UIColor(hex: 0xE7ECF3)
|
|
static let infoBackground = UIColor(hex: 0xEEF5FF)
|
|
static let danger = UIColor(hex: 0xE5484D)
|
|
|
|
/// 创建支持多行的系统字体标签。
|
|
static func label(_ text: String = "", size: CGFloat = 15, weight: UIFont.Weight = .regular,
|
|
color: UIColor? = nil) -> UILabel {
|
|
let label = UILabel()
|
|
label.text = text
|
|
label.font = .systemFont(ofSize: size, weight: weight)
|
|
label.textColor = color ?? textPrimary
|
|
label.numberOfLines = 0
|
|
return label
|
|
}
|
|
|
|
/// 创建统一间距的纵向内容组。
|
|
static func stack(_ views: [UIView], spacing: CGFloat = 12) -> UIStackView {
|
|
let stack = UIStackView(arrangedSubviews: views)
|
|
stack.axis = .vertical
|
|
stack.spacing = spacing
|
|
return stack
|
|
}
|
|
|
|
/// 白色圆角卡片,内容由页面提供。
|
|
static func card(_ content: UIView, background: UIColor = .white) -> UIView {
|
|
let card = UIView()
|
|
card.backgroundColor = background
|
|
card.layer.cornerRadius = 16
|
|
card.layer.borderWidth = background == .white ? 0.5 : 0
|
|
card.layer.borderColor = border.cgColor
|
|
card.addSubview(content)
|
|
content.snp.makeConstraints { $0.edges.equalToSuperview().inset(16) }
|
|
return card
|
|
}
|
|
|
|
/// 统一主次按钮;禁用态及加载期间不依赖系统默认的灰底样式。
|
|
static func button(_ title: String, id: String, primary: Bool = true) -> UIButton {
|
|
let button = UIButton(type: .system)
|
|
button.accessibilityIdentifier = id
|
|
configure(button, title: title, primary: primary)
|
|
button.snp.makeConstraints { $0.height.greaterThanOrEqualTo(50) }
|
|
return button
|
|
}
|
|
|
|
/// 更新按钮角色和文案,保留清晰的主次关系。
|
|
static func configure(_ button: UIButton, title: String, primary: Bool = true) {
|
|
var config = primary ? UIButton.Configuration.filled() : .plain()
|
|
config.title = title
|
|
config.baseBackgroundColor = StoreAccountDeregistrationStyle.primary
|
|
config.baseForegroundColor = primary ? .white : StoreAccountDeregistrationStyle.primary
|
|
config.background.cornerRadius = 12
|
|
if !primary {
|
|
config.background.strokeColor = StoreAccountDeregistrationStyle.primary
|
|
config.background.strokeWidth = 1
|
|
}
|
|
config.contentInsets = .init(top: 14, leading: 16, bottom: 14, trailing: 16)
|
|
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
|
var attributes = attributes
|
|
attributes.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
|
|
return attributes
|
|
}
|
|
button.configuration = config
|
|
button.configurationUpdateHandler = { button in
|
|
let enabled = button.isEnabled
|
|
var updated = button.configuration
|
|
updated?.background.backgroundColorTransformer = UIConfigurationColorTransformer { _ in
|
|
primary ? (enabled ? StoreAccountDeregistrationStyle.primary : StoreAccountDeregistrationStyle.primary.withAlphaComponent(0.12)) : .clear
|
|
}
|
|
updated?.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
|
var attributes = attributes
|
|
attributes.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
|
|
attributes.foregroundColor = enabled ? (primary ? .white : StoreAccountDeregistrationStyle.primary) : StoreAccountDeregistrationStyle.textSecondary
|
|
return attributes
|
|
}
|
|
button.configuration = updated
|
|
}
|
|
}
|
|
|
|
/// 以统一的SF Symbol展示提示图标,不加载额外图片资源。
|
|
static func icon(_ name: String, size: CGFloat = 22) -> UIImageView {
|
|
let image = UIImageView(image: UIImage(systemName: name,
|
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: size, weight: .medium)))
|
|
image.tintColor = primary
|
|
image.contentMode = .scaleAspectFit
|
|
image.setContentHuggingPriority(.required, for: .horizontal)
|
|
return image
|
|
}
|
|
|
|
/// 带浅色圆形底的状态图标,用于冷静期和身份信息强调。
|
|
static func iconBadge(_ name: String, iconSize: CGFloat = 30, diameter: CGFloat = 88) -> UIView {
|
|
let container = UIView()
|
|
container.backgroundColor = infoBackground
|
|
container.layer.cornerRadius = diameter / 2
|
|
let image = icon(name, size: iconSize)
|
|
container.addSubview(image)
|
|
container.snp.makeConstraints { $0.width.height.equalTo(diameter) }
|
|
image.snp.makeConstraints { $0.center.equalToSuperview(); $0.width.height.equalTo(iconSize + 8) }
|
|
return container
|
|
}
|
|
|
|
/// 为危险确认操作应用红色实心按钮样式。
|
|
static func configureDestructive(_ button: UIButton, title: String) {
|
|
var config = UIButton.Configuration.filled()
|
|
config.title = title
|
|
config.baseBackgroundColor = danger
|
|
config.baseForegroundColor = .white
|
|
config.background.cornerRadius = 12
|
|
config.contentInsets = .init(top: 13, leading: 16, bottom: 13, trailing: 16)
|
|
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
|
var attributes = attributes
|
|
attributes.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
return attributes
|
|
}
|
|
button.configuration = config
|
|
}
|
|
}
|
|
|
|
/// 注销资产及最终提交共用的高保真底部确认弹层。
|
|
@MainActor
|
|
final class StoreAccountDeregistrationConfirmationSheetViewController: UIViewController {
|
|
/// 弹层展示的一行摘要数据。
|
|
struct Summary {
|
|
let icon: String
|
|
let title: String
|
|
let value: String
|
|
}
|
|
|
|
private typealias Style = StoreAccountDeregistrationStyle
|
|
private let sheetTitle: String
|
|
private let summaries: [Summary]
|
|
private let notices: [(icon: String, text: String, danger: Bool)]
|
|
private let cancelTitle: String
|
|
private let confirmTitle: String
|
|
private let onConfirm: () -> Void
|
|
private let cancelButton = UIButton(type: .system)
|
|
private let confirmButton = UIButton(type: .system)
|
|
private let contentStack = UIStackView()
|
|
|
|
/// 创建只在用户明确确认后回调的弹层;下拉或取消不会触发业务请求。
|
|
init(title: String, summaries: [Summary], notices: [(String, String, Bool)],
|
|
cancelTitle: String, confirmTitle: String, onConfirm: @escaping () -> Void) {
|
|
sheetTitle = title
|
|
self.summaries = summaries
|
|
self.notices = notices
|
|
self.cancelTitle = cancelTitle
|
|
self.confirmTitle = confirmTitle
|
|
self.onConfirm = onConfirm
|
|
super.init(nibName: nil, bundle: nil)
|
|
modalPresentationStyle = .pageSheet
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = .white
|
|
let titleLabel = Style.label(sheetTitle, size: 20, weight: .semibold)
|
|
titleLabel.textAlignment = .center
|
|
let summaryStack = Style.stack([], spacing: 0)
|
|
for (index, summary) in summaries.enumerated() {
|
|
let icon = Style.icon(summary.icon, size: 18)
|
|
icon.snp.makeConstraints { $0.width.equalTo(24) }
|
|
let title = Style.label(summary.title, size: 14, color: Style.textSecondary)
|
|
let value = Style.label(summary.value, size: 15, weight: .semibold)
|
|
value.textAlignment = .right
|
|
let row = UIStackView(arrangedSubviews: [icon, title, UIView(), value])
|
|
row.axis = .horizontal
|
|
row.alignment = .center
|
|
row.spacing = 8
|
|
row.snp.makeConstraints { $0.height.greaterThanOrEqualTo(48) }
|
|
summaryStack.addArrangedSubview(row)
|
|
if index < summaries.count - 1 {
|
|
let divider = UIView()
|
|
divider.backgroundColor = Style.border
|
|
divider.snp.makeConstraints { $0.height.equalTo(0.5) }
|
|
summaryStack.addArrangedSubview(divider)
|
|
}
|
|
}
|
|
let summaryCard = Style.card(summaryStack)
|
|
summaryCard.layer.cornerRadius = 12
|
|
|
|
let noticeStack = Style.stack([], spacing: 12)
|
|
for notice in notices {
|
|
let icon = Style.icon(notice.icon, size: 16)
|
|
icon.tintColor = notice.danger ? Style.danger : Style.primary
|
|
icon.snp.makeConstraints { $0.width.equalTo(22) }
|
|
let text = Style.label(notice.text, size: 13,
|
|
color: notice.danger ? Style.danger : Style.textSecondary)
|
|
let row = UIStackView(arrangedSubviews: [icon, text])
|
|
row.axis = .horizontal
|
|
row.alignment = .top
|
|
row.spacing = 8
|
|
noticeStack.addArrangedSubview(row)
|
|
}
|
|
|
|
Style.configure(cancelButton, title: cancelTitle, primary: false)
|
|
Style.configureDestructive(confirmButton, title: confirmTitle)
|
|
cancelButton.accessibilityIdentifier = "deregister.sheet.cancel"
|
|
confirmButton.accessibilityIdentifier = "deregister.sheet.confirm"
|
|
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
|
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
|
let actions = UIStackView(arrangedSubviews: [cancelButton, confirmButton])
|
|
actions.axis = .horizontal
|
|
actions.distribution = .fillEqually
|
|
actions.spacing = 12
|
|
actions.snp.makeConstraints { $0.height.equalTo(50) }
|
|
|
|
contentStack.axis = .vertical
|
|
contentStack.spacing = 16
|
|
contentStack.addArrangedSubview(titleLabel)
|
|
contentStack.addArrangedSubview(summaryCard)
|
|
if !notices.isEmpty { contentStack.addArrangedSubview(noticeStack) }
|
|
contentStack.addArrangedSubview(actions)
|
|
view.addSubview(contentStack)
|
|
contentStack.snp.makeConstraints {
|
|
$0.top.equalTo(view.safeAreaLayoutGuide).offset(28)
|
|
$0.leading.trailing.equalToSuperview().inset(16)
|
|
$0.bottom.lessThanOrEqualTo(view.safeAreaLayoutGuide).inset(12)
|
|
}
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
guard let sheet = sheetPresentationController else { return }
|
|
view.layoutIfNeeded()
|
|
let sheetWidth = presentingViewController?.view.bounds.width ?? view.bounds.width
|
|
let contentWidth = max(0, sheetWidth - 32)
|
|
let contentHeight = contentStack.systemLayoutSizeFitting(
|
|
CGSize(width: contentWidth, height: UIView.layoutFittingCompressedSize.height),
|
|
withHorizontalFittingPriority: .required,
|
|
verticalFittingPriority: .fittingSizeLevel
|
|
).height
|
|
// 自定义 detent 会由系统计入底部安全区,这里只计算内容和视觉间距,避免重复留白。
|
|
let preferredHeight = ceil(28 + contentHeight + 12)
|
|
let identifier = UISheetPresentationController.Detent.Identifier("accountDeregistrationConfirmation")
|
|
sheet.detents = [.custom(identifier: identifier) { context in
|
|
min(preferredHeight, context.maximumDetentValue)
|
|
}]
|
|
sheet.selectedDetentIdentifier = identifier
|
|
sheet.prefersGrabberVisible = true
|
|
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
|
|
sheet.preferredCornerRadius = 22
|
|
preferredContentSize.height = preferredHeight
|
|
}
|
|
|
|
@objc private func cancelTapped() { dismiss(animated: true) }
|
|
|
|
@objc private func confirmTapped() {
|
|
confirmButton.isEnabled = false
|
|
dismiss(animated: true) { [onConfirm] in onConfirm() }
|
|
}
|
|
}
|