feat: update app workflows and permissions

This commit is contained in:
2026-07-09 17:34:00 +08:00
parent 43e6133c21
commit 8e356973bd
44 changed files with 2944 additions and 307 deletions

View File

@ -120,9 +120,6 @@ final class CooperationOrderSearchBar: UIView, UITextFieldDelegate {
container.addSubview(icon)
container.addSubview(textField)
snp.makeConstraints { make in
make.height.equalTo(62)
}
container.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalToSuperview()
@ -451,12 +448,18 @@ final class CooperationAcquirerCell: UITableViewCell {
var onCall: (() -> Void)?
private let cardView = UIView()
private let nameLabel = UILabel()
private let avatarContainer = UIView()
private let avatarIconView = UIImageView(image: UIImage(systemName: "person.fill"))
private let nameTitleLabel = UILabel()
private let nameValueLabel = UILabel()
private let editRemarkButton = UIButton(type: .system)
private let phoneLabel = UILabel()
private let phoneTitleLabel = UILabel()
private let phoneValueLabel = UILabel()
private let callButton = UIButton(type: .system)
private let bindTimeRow = CooperationInfoRowView(label: "绑定时间")
private let commissionRow = CooperationInfoRowView(label: "分成比例")
private let bindTimeTitleLabel = UILabel()
private let bindTimeValueLabel = UILabel()
private let commissionTitleLabel = UILabel()
private let commissionValueLabel = UILabel()
private let editCommissionButton = UIButton(type: .system)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -466,70 +469,131 @@ final class CooperationAcquirerCell: UITableViewCell {
contentView.backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = AppRadius.lg
cardView.layer.cornerRadius = AppRadius.md
cardView.layer.shadowColor = UIColor.black.cgColor
cardView.layer.shadowOpacity = 0.04
cardView.layer.shadowOffset = CGSize(width: 0, height: 1)
cardView.layer.shadowRadius = 2
nameLabel.font = .systemFont(ofSize: 16, weight: .bold)
nameLabel.textColor = AppColor.textPrimary
avatarContainer.backgroundColor = AppColor.primary.withAlphaComponent(0.08)
avatarContainer.layer.cornerRadius = AppRadius.sm
avatarContainer.clipsToBounds = true
avatarIconView.tintColor = AppColor.primary
avatarIconView.contentMode = .scaleAspectFit
configureTitleLabel(nameTitleLabel, text: "获客员名称")
configureTitleLabel(phoneTitleLabel, text: "手机号")
configureTitleLabel(bindTimeTitleLabel, text: "绑定时间")
configureTitleLabel(commissionTitleLabel, text: "分成比例")
nameValueLabel.font = .systemFont(ofSize: 15, weight: .bold)
nameValueLabel.textColor = AppColor.textPrimary
nameValueLabel.numberOfLines = 1
nameValueLabel.lineBreakMode = .byTruncatingTail
[phoneValueLabel, bindTimeValueLabel, commissionValueLabel].forEach { label in
label.font = .systemFont(ofSize: 13)
label.textColor = AppColor.textSecondary
label.numberOfLines = 1
label.lineBreakMode = .byTruncatingTail
}
commissionValueLabel.font = .systemFont(ofSize: 13, weight: .medium)
editRemarkButton.setTitle("修改备注", for: .normal)
editRemarkButton.titleLabel?.font = .systemFont(ofSize: 13)
editRemarkButton.setTitleColor(AppColor.primary, for: .normal)
editRemarkButton.contentEdgeInsets = UIEdgeInsets(top: 16, left: 14, bottom: 16, right: 14)
editRemarkButton.addTarget(self, action: #selector(editRemarkTapped), for: .touchUpInside)
phoneLabel.font = .systemFont(ofSize: 13)
phoneLabel.textColor = AppColor.textSecondary
callButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
callButton.tintColor = AppColor.primary
callButton.backgroundColor = AppColor.primaryLight
callButton.layer.cornerRadius = 15
callButton.imageView?.contentMode = .scaleAspectFit
callButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
editCommissionButton.setTitle("修改", for: .normal)
editCommissionButton.setTitle("修改比例", for: .normal)
editCommissionButton.titleLabel?.font = .systemFont(ofSize: 13)
editCommissionButton.setTitleColor(AppColor.primary, for: .normal)
editCommissionButton.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
editCommissionButton.addTarget(self, action: #selector(editCommissionTapped), for: .touchUpInside)
contentView.addSubview(cardView)
cardView.addSubview(nameLabel)
cardView.addSubview(avatarContainer)
avatarContainer.addSubview(avatarIconView)
cardView.addSubview(nameTitleLabel)
cardView.addSubview(nameValueLabel)
cardView.addSubview(editRemarkButton)
cardView.addSubview(phoneLabel)
cardView.addSubview(phoneTitleLabel)
cardView.addSubview(phoneValueLabel)
cardView.addSubview(callButton)
cardView.addSubview(bindTimeRow)
cardView.addSubview(commissionRow)
cardView.addSubview(bindTimeTitleLabel)
cardView.addSubview(bindTimeValueLabel)
cardView.addSubview(commissionTitleLabel)
cardView.addSubview(commissionValueLabel)
cardView.addSubview(editCommissionButton)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: AppSpacing.sm, right: AppSpacing.md))
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 14, bottom: AppSpacing.sm, right: 14))
}
nameLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(14)
avatarContainer.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(14)
make.centerY.equalToSuperview()
make.size.equalTo(44)
}
avatarIconView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(22)
}
nameTitleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(AppSpacing.md)
make.leading.equalTo(avatarContainer.snp.trailing).offset(AppSpacing.sm)
}
nameValueLabel.snp.makeConstraints { make in
make.centerY.equalTo(nameTitleLabel)
make.leading.equalTo(nameTitleLabel.snp.trailing).offset(AppSpacing.sm)
make.trailing.lessThanOrEqualTo(editRemarkButton.snp.leading).offset(-AppSpacing.xs)
}
editRemarkButton.snp.makeConstraints { make in
make.centerY.equalTo(nameLabel)
make.leading.equalTo(nameLabel.snp.trailing).offset(AppSpacing.xs)
make.top.trailing.equalToSuperview()
}
phoneLabel.snp.makeConstraints { make in
make.top.equalTo(nameLabel.snp.bottom).offset(10)
make.leading.equalToSuperview().offset(14)
phoneTitleLabel.snp.makeConstraints { make in
make.top.equalTo(nameTitleLabel.snp.bottom).offset(AppSpacing.xs)
make.leading.equalTo(nameTitleLabel)
}
phoneValueLabel.snp.makeConstraints { make in
make.centerY.equalTo(phoneTitleLabel)
make.leading.equalTo(phoneTitleLabel.snp.trailing).offset(AppSpacing.sm)
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-AppSpacing.xs)
}
callButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(14)
make.centerY.equalTo(phoneLabel)
make.centerY.equalTo(phoneTitleLabel)
make.size.equalTo(30)
}
bindTimeRow.snp.makeConstraints { make in
make.top.equalTo(phoneLabel.snp.bottom).offset(10)
make.leading.trailing.equalToSuperview().inset(14)
bindTimeTitleLabel.snp.makeConstraints { make in
make.top.equalTo(phoneTitleLabel.snp.bottom).offset(AppSpacing.xs)
make.leading.equalTo(nameTitleLabel)
}
commissionRow.snp.makeConstraints { make in
make.top.equalTo(bindTimeRow.snp.bottom).offset(10)
make.leading.equalToSuperview().offset(14)
make.bottom.equalToSuperview().inset(14)
bindTimeValueLabel.snp.makeConstraints { make in
make.centerY.equalTo(bindTimeTitleLabel)
make.leading.equalTo(bindTimeTitleLabel.snp.trailing).offset(AppSpacing.sm)
make.trailing.lessThanOrEqualToSuperview().inset(14)
}
commissionTitleLabel.snp.makeConstraints { make in
make.top.equalTo(bindTimeTitleLabel.snp.bottom).offset(AppSpacing.xs)
make.leading.equalTo(nameTitleLabel)
make.bottom.equalToSuperview().inset(AppSpacing.md)
}
commissionValueLabel.snp.makeConstraints { make in
make.centerY.equalTo(commissionTitleLabel)
make.leading.equalTo(commissionTitleLabel.snp.trailing).offset(AppSpacing.sm)
make.trailing.lessThanOrEqualTo(editCommissionButton.snp.leading).offset(-AppSpacing.xs)
}
editCommissionButton.snp.makeConstraints { make in
make.centerY.equalTo(commissionRow)
make.leading.equalTo(commissionRow.snp.trailing).offset(AppSpacing.xs)
make.centerY.equalTo(commissionTitleLabel)
make.trailing.equalToSuperview().inset(AppSpacing.xs)
}
}
@ -538,11 +602,26 @@ final class CooperationAcquirerCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
onEditRemark = nil
onEditCommission = nil
onCall = nil
}
func configure(with acquirer: CooperativeSalerEntity) {
nameLabel.text = acquirer.displayName.isEmpty ? "" : acquirer.displayName
phoneLabel.text = "手机号 \(CooperationOrderPhoneMask.mask(acquirer.displayPhone))"
bindTimeRow.setValue(acquirer.displayBindTime.isEmpty ? "" : acquirer.displayBindTime)
commissionRow.setValue(acquirer.displayCommissionRate)
nameValueLabel.text = acquirer.displayName.isEmpty ? "" : acquirer.displayName
phoneValueLabel.text = acquirer.displayPhone.isEmpty ? "" : CooperationOrderPhoneMask.mask(acquirer.displayPhone)
bindTimeValueLabel.text = acquirer.displayBindTime.isEmpty ? "" : acquirer.displayBindTime
commissionValueLabel.text = acquirer.displayCommissionRate.isEmpty ? "" : acquirer.displayCommissionRate
}
private func configureTitleLabel(_ label: UILabel, text: String) {
label.text = text
label.font = .systemFont(ofSize: 13)
label.textColor = AppColor.textTertiary
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)
}
@objc private func editRemarkTapped() { onEditRemark?() }
@ -550,6 +629,364 @@ final class CooperationAcquirerCell: UITableViewCell {
@objc private func callTapped() { onCall?() }
}
/// Android `CommissionRateEditDialog`
final class CooperationCommissionRateDialogView: UIView {
var onCommissionRateChange: ((String) -> Void)?
var onSmsCodeChange: ((String) -> Void)?
var onSendSmsCode: (() -> Void)?
var onCancel: (() -> Void)?
var onConfirm: (() -> Void)?
var currentCommissionRate: String { rateField.text }
var currentSmsCode: String { smsCodeField.text }
private let dimView = UIView()
private let cardView = UIView()
private let titleLabel = UILabel()
private let infoBox = UIView()
private let infoLabel = UILabel()
private let contentStack = UIStackView()
private let rateField = CooperationOutlinedTextFieldView(title: "分成比例", placeholder: "请输入0到100", suffix: "%")
private let phoneField = CooperationOutlinedTextFieldView(title: "验证码发送手机号", placeholder: "")
private let smsCodeField = CooperationOutlinedTextFieldView(title: "验证码", placeholder: "请输入验证码")
private let sendCodeButton = UIButton(type: .system)
private let buttonStack = UIStackView()
private let cancelButton = UIButton(type: .system)
private let confirmButton = UIButton(type: .system)
private var cardCenterYConstraint: Constraint?
private var keyboardObservers: [NSObjectProtocol] = []
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
unregisterKeyboardObservers()
}
func apply(commissionRate: String, smsCode: String, phone: String, countdown: Int) {
rateField.text = commissionRate
smsCodeField.text = smsCode
phoneField.text = phone.isEmpty ? "" : phone
let isCountingDown = countdown > 0
sendCodeButton.setTitle(isCountingDown ? "\(countdown)" : "获取验证码", for: .normal)
sendCodeButton.isEnabled = !isCountingDown
sendCodeButton.backgroundColor = AppColor.primary.withAlphaComponent(isCountingDown ? 0.7 : 1)
sendCodeButton.setTitleColor(.white.withAlphaComponent(isCountingDown ? 0.7 : 1), for: .normal)
sendCodeButton.setTitleColor(.white.withAlphaComponent(0.7), for: .disabled)
}
func show(in parent: UIView) {
frame = parent.bounds
autoresizingMask = [.flexibleWidth, .flexibleHeight]
alpha = 0
parent.addSubview(self)
layoutIfNeeded()
registerKeyboardObservers()
rateField.focus()
UIView.animate(withDuration: 0.2) {
self.alpha = 1
}
}
func dismiss() {
endEditing(true)
unregisterKeyboardObservers()
UIView.animate(withDuration: 0.2, animations: {
self.alpha = 0
}, completion: { _ in
self.removeFromSuperview()
})
}
private func setupUI() {
dimView.backgroundColor = AppColor.overlayScrim
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
cardView.backgroundColor = .white
cardView.layer.cornerRadius = AppRadius.xl
cardView.clipsToBounds = true
titleLabel.text = "修改分成比例"
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
titleLabel.textColor = AppColor.textPrimary
infoBox.backgroundColor = AppColor.primary.withAlphaComponent(0.08)
infoBox.layer.cornerRadius = AppRadius.sm
infoLabel.text = "修改分成比例需验证手机号"
infoLabel.font = .systemFont(ofSize: 13)
infoLabel.textColor = AppColor.primary
contentStack.axis = .vertical
contentStack.spacing = AppSpacing.sm
rateField.keyboardType = .numberPad
smsCodeField.keyboardType = .numberPad
phoneField.isReadOnly = true
rateField.onTextChange = { [weak self] text in
self?.onCommissionRateChange?(text)
}
smsCodeField.onTextChange = { [weak self] text in
self?.onSmsCodeChange?(text)
}
sendCodeButton.setTitle("获取验证码", for: .normal)
sendCodeButton.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium)
sendCodeButton.setTitleColor(.white, for: .normal)
sendCodeButton.backgroundColor = AppColor.primary
sendCodeButton.layer.cornerRadius = AppRadius.sm
sendCodeButton.addTarget(self, action: #selector(sendCodeTapped), for: .touchUpInside)
buttonStack.axis = .horizontal
buttonStack.alignment = .center
buttonStack.distribution = .fillEqually
buttonStack.spacing = AppSpacing.sm
cancelButton.setTitle("取消", for: .normal)
cancelButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
cancelButton.setTitleColor(AppColor.textSecondary, for: .normal)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
confirmButton.setTitle("确认修改", for: .normal)
confirmButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
confirmButton.setTitleColor(AppColor.primary, for: .normal)
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
addSubview(dimView)
addSubview(cardView)
cardView.addSubview(titleLabel)
cardView.addSubview(infoBox)
infoBox.addSubview(infoLabel)
cardView.addSubview(contentStack)
contentStack.addArrangedSubview(rateField)
contentStack.addArrangedSubview(phoneField)
contentStack.addArrangedSubview(makeSmsRow())
cardView.addSubview(buttonStack)
buttonStack.addArrangedSubview(cancelButton)
buttonStack.addArrangedSubview(confirmButton)
dimView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
cardView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
cardCenterYConstraint = make.centerY.equalToSuperview().constraint
}
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
infoBox.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
infoLabel.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12))
}
contentStack.snp.makeConstraints { make in
make.top.equalTo(infoBox.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
buttonStack.snp.makeConstraints { make in
make.top.equalTo(contentStack.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.bottom.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(44)
}
}
private func makeSmsRow() -> UIView {
let row = UIStackView(arrangedSubviews: [smsCodeField, sendCodeButton])
row.axis = .horizontal
row.alignment = .fill
row.spacing = 10
sendCodeButton.snp.makeConstraints { make in
make.width.equalTo(104)
make.height.equalTo(46)
}
return row
}
private func registerKeyboardObservers() {
unregisterKeyboardObservers()
let center = NotificationCenter.default
keyboardObservers = [
center.addObserver(
forName: UIResponder.keyboardWillChangeFrameNotification,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleKeyboardNotification(notification)
},
center.addObserver(
forName: UIResponder.keyboardWillHideNotification,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleKeyboardNotification(notification)
}
]
}
private func unregisterKeyboardObservers() {
keyboardObservers.forEach(NotificationCenter.default.removeObserver)
keyboardObservers.removeAll()
}
private func handleKeyboardNotification(_ notification: Notification) {
layoutIfNeeded()
guard let userInfo = notification.userInfo else { return }
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
let curveRaw = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue
?? UInt(UIView.AnimationCurve.easeInOut.rawValue)
let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
let offset = keyboardAvoidanceOffset(from: userInfo)
cardCenterYConstraint?.update(offset: offset)
UIView.animate(withDuration: duration, delay: 0, options: options) {
self.layoutIfNeeded()
}
}
private func keyboardAvoidanceOffset(from userInfo: [AnyHashable: Any]) -> CGFloat {
guard
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
else {
return 0
}
let keyboardFrame = convert(keyboardFrameValue.cgRectValue, from: nil)
guard keyboardFrame.minY < bounds.height else { return 0 }
let cardHeight = cardView.bounds.height
let baseMinY = bounds.midY - cardHeight / 2
let baseMaxY = bounds.midY + cardHeight / 2
let desiredBottom = keyboardFrame.minY - AppSpacing.md
let neededShift = max(0, baseMaxY - desiredBottom)
let maxShift = max(0, baseMinY - AppSpacing.md)
return -min(neededShift, maxShift)
}
@objc private func sendCodeTapped() {
onSendSmsCode?()
}
@objc private func cancelTapped() {
onCancel?()
}
@objc private func confirmTapped() {
onConfirm?()
}
}
///
private final class CooperationOutlinedTextFieldView: UIView {
var onTextChange: ((String) -> Void)?
var text: String {
get { textField.text ?? "" }
set {
if textField.text != newValue {
textField.text = newValue
}
}
}
var keyboardType: UIKeyboardType {
get { textField.keyboardType }
set { textField.keyboardType = newValue }
}
var isReadOnly = false {
didSet {
textField.isUserInteractionEnabled = !isReadOnly
textField.textColor = isReadOnly ? AppColor.textSecondary : AppColor.textPrimary
}
}
private let titleLabel = UILabel()
private let containerView = UIView()
private let textField = UITextField()
private let suffixLabel = UILabel()
private let suffix: String?
init(title: String, placeholder: String, suffix: String? = nil) {
self.suffix = suffix
super.init(frame: .zero)
setupUI(title: title, placeholder: placeholder)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func focus() {
textField.becomeFirstResponder()
}
private func setupUI(title: String, placeholder: String) {
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 12)
titleLabel.textColor = AppColor.textSecondary
titleLabel.backgroundColor = .white
containerView.layer.borderWidth = 1
containerView.layer.borderColor = UIColor(hex: 0xD9DDE4).cgColor
containerView.layer.cornerRadius = AppRadius.sm
textField.placeholder = placeholder
textField.font = .systemFont(ofSize: 15)
textField.textColor = AppColor.textPrimary
textField.borderStyle = .none
textField.backgroundColor = .clear
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
suffixLabel.text = suffix
suffixLabel.font = .systemFont(ofSize: 15)
suffixLabel.textColor = AppColor.textSecondary
suffixLabel.isHidden = suffix == nil
addSubview(containerView)
addSubview(titleLabel)
containerView.addSubview(textField)
containerView.addSubview(suffixLabel)
snp.makeConstraints { make in
make.height.equalTo(56)
}
containerView.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.top.equalToSuperview().offset(6)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(10)
make.top.equalToSuperview()
}
textField.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(AppSpacing.sm)
make.trailing.equalTo(suffixLabel.snp.leading).offset(-AppSpacing.xs)
make.centerY.equalTo(containerView)
}
suffixLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppSpacing.sm)
make.centerY.equalTo(containerView)
}
}
@objc private func textChanged() {
onTextChange?(textField.text ?? "")
}
}
/// -
final class CooperationInfoRowView: UIView {
private let titleLabel = UILabel()