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

@ -167,7 +167,7 @@ final class AllFunctionsViewController: BaseViewController {
private static func makeLayout() -> UICollectionViewCompositionalLayout {
UICollectionViewCompositionalLayout { sectionIndex, environment in
let spacing: CGFloat = 12
let spacing: CGFloat = 15
let columns = 3
let horizontalInset = AppSpacing.md
let availableWidth = environment.container.effectiveContentSize.width - horizontalInset * 2

View File

@ -28,8 +28,7 @@ enum HomeCollectionItem: Hashable {
enum HomeCollectionLayoutBuilder {
private static let menuColumns = 3
private static let menuSpacing: CGFloat = 15
private static let menuAspectRatio: CGFloat = 1.3
private static let menuSpacing = AppSpacing.sm
private static let sectionSpacing = AppSpacing.sm
/// Compositional Layout section
@ -42,7 +41,7 @@ enum HomeCollectionLayoutBuilder {
case .locationReport:
return fullWidthSection(height: 132)
case .quickActions:
return fullWidthSection(height: 72)
return quickActionsSection(environment: environment)
case .store:
return fullWidthSection(height: 96)
case .commonApps:
@ -120,19 +119,16 @@ enum HomeCollectionLayoutBuilder {
}
private static func menuGridSection(environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
let availableWidth = environment.container.effectiveContentSize.width - horizontalInset * 2
let totalSpacing = menuSpacing * CGFloat(menuColumns - 1)
let itemWidth = max(0, (availableWidth - totalSpacing) / CGFloat(menuColumns))
let itemHeight = itemWidth / menuAspectRatio
let itemWidth = menuItemWidth(environment: environment)
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute(itemWidth),
heightDimension: .absolute(itemHeight)
heightDimension: .absolute(itemWidth)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(itemHeight)
heightDimension: .absolute(itemWidth)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
@ -162,4 +158,14 @@ enum HomeCollectionLayoutBuilder {
section.boundarySupplementaryItems = [header]
return section
}
private static func quickActionsSection(environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
fullWidthSection(height: menuItemWidth(environment: environment))
}
private static func menuItemWidth(environment: NSCollectionLayoutEnvironment) -> CGFloat {
let availableWidth = environment.container.effectiveContentSize.width - horizontalInset * 2
let totalSpacing = menuSpacing * CGFloat(menuColumns - 1)
return max(0, (availableWidth - totalSpacing) / CGFloat(menuColumns))
}
}

View File

@ -207,8 +207,8 @@ final class HomeViewController: BaseViewController {
)
}
cell.cardView.onSubmitTask = { [weak self] in
self?.navigationController?.pushViewController(TaskAddViewController(), animated: true)
}
self?.navigationController?.pushViewController(TaskAddViewController(), animated: true)
}
cell.cardView.onToggleOnline = { [weak self] in
self?.viewModel.showOnlineStatusSwitchDialog()
}
@ -301,10 +301,17 @@ final class HomeViewController: BaseViewController {
storeItem: viewModel.storeItem,
commonMenus: viewModel.commonMenus
)
dataSource.apply(snapshot, animatingDifferences: false)
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
self?.refreshStatusVisibleCells()
}
presentDialogsIfNeeded()
}
private func refreshStatusVisibleCells() {
refreshWorkStatusVisibleCell()
refreshQuickActionsVisibleCell()
}
private func refreshWorkStatusVisibleCell() {
guard !viewModel.isMinimalTopRole,
let sectionIndex = currentSections.firstIndex(of: .workStatus) else {
@ -321,6 +328,18 @@ final class HomeViewController: BaseViewController {
)
}
private func refreshQuickActionsVisibleCell() {
guard !viewModel.isMinimalTopRole,
let sectionIndex = currentSections.firstIndex(of: .quickActions) else {
return
}
let indexPath = IndexPath(item: 0, section: sectionIndex)
guard let cell = collectionView.cellForItem(at: indexPath) as? HomeQuickActionsCell else {
return
}
cell.apply(isOnline: viewModel.isOnline)
}
private func presentDialogsIfNeeded() {
if viewModel.showPermissionDialog {
presentDialog(.permission)

View File

@ -20,6 +20,7 @@ final class AllFunctionMenuCell: UICollectionViewCell {
var onActionTap: (() -> Void)?
private let cardView = UIView()
private let contentStackView = UIStackView()
private let iconView = UIImageView()
private let titleLabel = UILabel()
private let actionButton = UIButton(type: .system)
@ -67,30 +68,33 @@ final class AllFunctionMenuCell: UICollectionViewCell {
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
titleLabel.font = .app(.caption)
titleLabel.textColor = AppColor.textPrimary
contentStackView.axis = .vertical
contentStackView.alignment = .center
contentStackView.spacing = AppSpacing.sm
contentStackView.isUserInteractionEnabled = false
titleLabel.font = .app(.body)
titleLabel.textColor = AppColor.textSecondary
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 2
actionButton.addTarget(self, action: #selector(actionTapped), for: .touchUpInside)
contentView.addSubview(cardView)
cardView.addSubview(iconView)
cardView.addSubview(titleLabel)
cardView.addSubview(contentStackView)
cardView.addSubview(actionButton)
contentStackView.addArrangedSubview(iconView)
contentStackView.addArrangedSubview(titleLabel)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
iconView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(AppSpacing.sm)
make.centerX.equalToSuperview()
make.width.height.equalTo(32)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconView.snp.bottom).offset(AppSpacing.xs)
contentStackView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(AppSpacing.xxs)
make.bottom.lessThanOrEqualToSuperview().inset(AppSpacing.xs)
}
iconView.snp.makeConstraints { make in
make.width.height.equalTo(26)
}
actionButton.snp.makeConstraints { make in
make.top.trailing.equalToSuperview().inset(AppSpacing.xxs)

View File

@ -112,6 +112,8 @@ final class HomeMenuCell: UICollectionViewCell {
static let reuseIdentifier = "HomeMenuCell"
private let cardView = UIView()
private let contentStackView = UIStackView()
private let iconContainerView = UIView()
private let iconView = UIImageView()
private let titleLabel = UILabel()
@ -127,40 +129,54 @@ final class HomeMenuCell: UICollectionViewCell {
func apply(menu: HomeMenuItem) {
iconView.image = UIImage(named: menu.iconName) ?? UIImage(systemName: menu.iconName)
iconView.accessibilityIdentifier = menu.iconName
titleLabel.text = menu.title
}
private func setupUI() {
contentView.backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = AppRadius.md
cardView.backgroundColor = AppColor.cardBackground
cardView.layer.cornerRadius = AppRadius.lg
cardView.layer.borderWidth = 1
cardView.layer.borderColor = AppColor.cardOutline.cgColor
cardView.clipsToBounds = true
iconContainerView.backgroundColor = AppColor.iconBackground
iconContainerView.layer.cornerRadius = 20
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
contentStackView.axis = .vertical
contentStackView.alignment = .center
contentStackView.spacing = AppSpacing.xs
contentStackView.isUserInteractionEnabled = false
titleLabel.font = .app(.body)
titleLabel.textColor = AppColor.textSecondary
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 2
contentView.addSubview(cardView)
cardView.addSubview(iconView)
cardView.addSubview(titleLabel)
cardView.addSubview(contentStackView)
iconContainerView.addSubview(iconView)
contentStackView.addArrangedSubview(iconContainerView)
contentStackView.addArrangedSubview(titleLabel)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
iconView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(AppSpacing.sm)
make.centerX.equalToSuperview()
make.width.height.equalTo(24)
contentStackView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(AppSpacing.xs)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconView.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.xxs)
make.bottom.lessThanOrEqualToSuperview().inset(AppSpacing.xs)
iconContainerView.snp.makeConstraints { make in
make.width.height.equalTo(40)
}
iconView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(24)
}
}
}

View File

@ -11,6 +11,7 @@ final class HomeLocationReportCardView: UIView {
var onReportTap: (() -> Void)?
private let contentStackView = UIStackView()
private let titleStackView = UIStackView()
private let titleIconView = UIImageView(image: UIImage(systemName: "arrow.up.square.fill"))
private let titleLabel = UILabel()
@ -22,9 +23,15 @@ final class HomeLocationReportCardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = AppColor.cardBackground
layer.cornerRadius = AppRadius.md
layer.cornerRadius = AppRadius.lg
layer.borderWidth = 1
layer.borderColor = AppColor.cardOutline.cgColor
clipsToBounds = true
contentStackView.axis = .vertical
contentStackView.alignment = .fill
contentStackView.spacing = AppSpacing.xxs
titleStackView.axis = .horizontal
titleStackView.alignment = .center
titleStackView.spacing = AppSpacing.xs
@ -33,14 +40,14 @@ final class HomeLocationReportCardView: UIView {
titleIconView.contentMode = .scaleAspectFit
titleLabel.text = "立即上报"
titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
titleLabel.font = .systemFont(ofSize: 24, weight: .bold)
titleLabel.textColor = AppColor.textPrimary
addressLabel.font = .systemFont(ofSize: 15, weight: .regular)
addressLabel.font = .app(.subtitle)
addressLabel.textColor = AppColor.textTertiary
addressLabel.numberOfLines = 2
reportGradientLayer.colors = [AppColor.primary.cgColor, UIColor(hex: 0x5CA8FF).cgColor]
reportGradientLayer.colors = [AppColor.primary.cgColor, AppColor.primaryGradientEnd.cgColor]
reportGradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
reportGradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
reportButtonContainer.layer.insertSublayer(reportGradientLayer, at: 0)
@ -55,33 +62,34 @@ final class HomeLocationReportCardView: UIView {
forImageIn: .normal
)
reportButton.imageView?.contentMode = .scaleAspectFit
reportButton.addTarget(self, action: #selector(reportTapped), for: .touchUpInside)
reportButton.isExclusiveTouch = true
reportButton.addTarget(self, action: #selector(reportTouchDown), for: .touchDown)
reportButton.addTarget(self, action: #selector(reportTouchUpInside), for: .touchUpInside)
reportButton.addTarget(self, action: #selector(reportTouchEnded), for: [.touchUpOutside, .touchCancel, .touchDragExit])
reportButton.accessibilityLabel = "上报位置"
titleStackView.addArrangedSubview(titleIconView)
titleStackView.addArrangedSubview(titleLabel)
addSubview(titleStackView)
addSubview(addressLabel)
contentStackView.addArrangedSubview(titleStackView)
contentStackView.addArrangedSubview(addressLabel)
addSubview(contentStackView)
addSubview(reportButtonContainer)
reportButtonContainer.addSubview(reportButton)
titleIconView.snp.makeConstraints { make in
make.width.height.equalTo(25)
}
titleStackView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(15)
make.top.equalToSuperview().offset(15)
make.trailing.lessThanOrEqualTo(reportButtonContainer.snp.leading).offset(-AppSpacing.sm)
}
addressLabel.snp.makeConstraints { make in
make.top.equalTo(titleStackView.snp.bottom).offset(AppSpacing.xxs)
make.leading.equalTo(titleStackView)
make.trailing.lessThanOrEqualTo(reportButtonContainer.snp.leading).offset(-AppSpacing.sm)
contentStackView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(AppSpacing.md)
make.centerY.equalTo(reportButtonContainer)
make.trailing.equalTo(reportButtonContainer.snp.leading).offset(-AppSpacing.sm)
make.top.greaterThanOrEqualToSuperview().offset(AppSpacing.md)
make.bottom.lessThanOrEqualToSuperview().offset(-AppSpacing.md)
}
reportButtonContainer.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-15)
make.trailing.equalToSuperview().offset(-AppSpacing.md)
make.centerY.equalToSuperview()
make.width.height.equalTo(92)
make.width.height.equalTo(96)
}
reportButton.snp.makeConstraints { make in
make.edges.equalToSuperview()
@ -103,10 +111,81 @@ final class HomeLocationReportCardView: UIView {
addressLabel.text = address
reportButton.isEnabled = !isReporting
reportButton.alpha = 1
if isReporting {
resetReportButtonScale()
}
reportGradientLayer.colors = isReporting
? [UIColor(hex: 0xC9CED6).cgColor, UIColor(hex: 0xD9DDE4).cgColor]
: [AppColor.primary.cgColor, UIColor(hex: 0x5CA8FF).cgColor]
? [AppColor.controlDisabledStart.cgColor, AppColor.controlDisabledEnd.cgColor]
: [AppColor.primary.cgColor, AppColor.primaryGradientEnd.cgColor]
}
@objc private func reportTapped() { onReportTap?() }
@objc private func reportTouchDown() {
animateReportButtonScale(isPressed: true)
playCenteredRipple()
}
@objc private func reportTouchUpInside() {
animateReportButtonScale(isPressed: false)
onReportTap?()
}
@objc private func reportTouchEnded() {
animateReportButtonScale(isPressed: false)
}
private func playCenteredRipple() {
reportButtonContainer.layer.sublayers?
.filter { $0.name == "HomeLocationReportRippleLayer" }
.forEach { $0.removeFromSuperlayer() }
let diameter = max(reportButtonContainer.bounds.width, reportButtonContainer.bounds.height) * 1.45
let rippleLayer = CALayer()
rippleLayer.name = "HomeLocationReportRippleLayer"
rippleLayer.backgroundColor = UIColor.white.withAlphaComponent(0.28).cgColor
rippleLayer.bounds = CGRect(x: 0, y: 0, width: diameter, height: diameter)
rippleLayer.cornerRadius = diameter / 2
rippleLayer.position = CGPoint(x: reportButtonContainer.bounds.midX, y: reportButtonContainer.bounds.midY)
rippleLayer.transform = CATransform3DMakeScale(0.18, 0.18, 1)
rippleLayer.opacity = 0.32
reportButtonContainer.layer.insertSublayer(rippleLayer, below: reportButton.layer)
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 0.18
scaleAnimation.toValue = 1
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0.32
opacityAnimation.toValue = 0
let group = CAAnimationGroup()
group.animations = [scaleAnimation, opacityAnimation]
group.duration = 0.55
group.timingFunction = CAMediaTimingFunction(name: .easeOut)
group.isRemovedOnCompletion = false
group.fillMode = .forwards
CATransaction.begin()
CATransaction.setCompletionBlock { [weak rippleLayer] in
rippleLayer?.removeFromSuperlayer()
}
rippleLayer.add(group, forKey: "centeredRipple")
CATransaction.commit()
}
private func animateReportButtonScale(isPressed: Bool) {
UIView.animate(
withDuration: isPressed ? 0.12 : 0.18,
delay: 0,
options: [.beginFromCurrentState, .curveEaseOut]
) {
self.reportButtonContainer.transform = isPressed
? CGAffineTransform(scaleX: 0.96, y: 0.96)
: .identity
}
}
private func resetReportButtonScale() {
reportButtonContainer.layer.removeAllAnimations()
reportButtonContainer.transform = .identity
}
}

View File

@ -23,7 +23,6 @@ final class HomeQuickActionsView: UIView {
addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(72)
}
}
@ -34,19 +33,36 @@ final class HomeQuickActionsView: UIView {
func apply(isOnline: Bool) {
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
stackView.addArrangedSubview(makeActionButton(title: "立即收款", icon: "yensign.circle", action: #selector(collectPaymentTapped)))
stackView.addArrangedSubview(makeActionButton(title: "提交任务", icon: "doc.text", action: #selector(submitTaskTapped)))
stackView.addArrangedSubview(makeActionButton(title: "立即收款", icon: "qrcode", action: #selector(collectPaymentTapped)))
stackView.addArrangedSubview(makeActionButton(title: "提交任务", icon: "checklist.checked", action: #selector(submitTaskTapped)))
stackView.addArrangedSubview(makeActionButton(
title: isOnline ? "在线" : "离线",
icon: isOnline ? "dot.radiowaves.left.and.right" : "moon",
action: #selector(toggleOnlineTapped)
icon: isOnline ? "wifi" : "wifi.slash",
action: #selector(toggleOnlineTapped),
backgroundColor: isOnline ? AppColor.online : AppColor.cardBackground,
iconBackgroundColor: isOnline ? UIColor.white.withAlphaComponent(0.18) : AppColor.inputBackground,
iconColor: isOnline ? AppColor.successBackground : AppColor.textTabInactive,
titleColor: isOnline ? AppColor.successBackground : AppColor.textTabInactive,
showsBorder: !isOnline
))
}
private func makeActionButton(title: String, icon: String, action: Selector) -> UIView {
private func makeActionButton(
title: String,
icon: String,
action: Selector,
backgroundColor: UIColor = AppColor.cardBackground,
iconBackgroundColor: UIColor = AppColor.iconBackground,
iconColor: UIColor = AppColor.primary,
titleColor: UIColor = AppColor.textSecondary,
showsBorder: Bool = true
) -> UIView {
let container = UIView()
container.backgroundColor = AppColor.cardBackground
container.layer.cornerRadius = AppRadius.md
container.backgroundColor = backgroundColor
container.layer.cornerRadius = AppRadius.lg
container.layer.borderWidth = showsBorder ? 1 : 0
container.layer.borderColor = AppColor.cardOutline.cgColor
container.accessibilityLabel = title
let button = UIButton(type: .system)
button.addTarget(self, action: action, for: .touchUpInside)
@ -55,26 +71,39 @@ final class HomeQuickActionsView: UIView {
make.edges.equalToSuperview()
}
let iconContainer = UIView()
iconContainer.backgroundColor = iconBackgroundColor
iconContainer.layer.cornerRadius = 18
let imageView = UIImageView(image: UIImage(systemName: icon))
imageView.tintColor = AppColor.primary
imageView.accessibilityIdentifier = icon
imageView.tintColor = iconColor
imageView.contentMode = .scaleAspectFit
let label = UILabel()
label.text = title
label.font = .app(.bodyMedium)
label.textColor = AppColor.textPrimary
label.font = .app(.body)
label.textColor = titleColor
label.textAlignment = .center
label.numberOfLines = 2
let column = UIStackView(arrangedSubviews: [imageView, label])
iconContainer.addSubview(imageView)
let column = UIStackView(arrangedSubviews: [iconContainer, label])
column.axis = .vertical
column.alignment = .center
column.spacing = 6
column.spacing = AppSpacing.xs
column.isUserInteractionEnabled = false
container.addSubview(column)
column.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(AppSpacing.xxs)
}
iconContainer.snp.makeConstraints { make in
make.width.height.equalTo(36)
}
imageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(22)
}
return container

View File

@ -15,14 +15,16 @@ final class HomeWorkStatusCardView: UIView {
private let contentStackView = UIStackView()
private let onlineButton = UIButton(type: .system)
private let countdownStackView = UIStackView()
private let countdownIconView = UIImageView(image: UIImage(systemName: "clock"))
private let countdownIconView = UIImageView(image: UIImage(systemName: "clock.fill"))
private let countdownLabel = UILabel()
private let reminderButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = AppColor.cardBackground
layer.cornerRadius = AppRadius.md
layer.cornerRadius = AppRadius.lg
layer.borderWidth = 1
layer.borderColor = AppColor.cardOutline.cgColor
clipsToBounds = true
contentStackView.axis = .horizontal
@ -31,8 +33,8 @@ final class HomeWorkStatusCardView: UIView {
contentStackView.spacing = AppSpacing.sm
onlineButton.titleLabel?.font = .app(.captionMedium)
onlineButton.layer.cornerRadius = AppRadius.xs
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
onlineButton.layer.cornerRadius = AppRadius.sm
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14)
onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside)
countdownStackView.axis = .horizontal
@ -42,16 +44,17 @@ final class HomeWorkStatusCardView: UIView {
countdownIconView.contentMode = .scaleAspectFit
countdownIconView.setContentCompressionResistancePriority(.required, for: .horizontal)
countdownLabel.font = .app(.subtitle)
countdownLabel.font = .systemFont(ofSize: 18, weight: .semibold)
countdownLabel.textColor = AppColor.primary
countdownLabel.adjustsFontSizeToFitWidth = true
countdownLabel.minimumScaleFactor = 0.85
countdownLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
reminderButton.titleLabel?.font = .app(.subtitle)
reminderButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
reminderButton.setTitleColor(AppColor.primary, for: .normal)
reminderButton.tintColor = AppColor.primary
reminderButton.setImage(UIImage(systemName: "bell.fill"), for: .normal)
let reminderIconConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
reminderButton.setImage(UIImage(systemName: "bell.fill", withConfiguration: reminderIconConfig), for: .normal)
reminderButton.semanticContentAttribute = .forceLeftToRight
reminderButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 6)
reminderButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: -6)
@ -67,10 +70,10 @@ final class HomeWorkStatusCardView: UIView {
addSubview(contentStackView)
contentStackView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(15)
make.edges.equalToSuperview().inset(AppSpacing.md)
}
countdownIconView.snp.makeConstraints { make in
make.width.height.equalTo(15)
make.width.height.equalTo(18)
}
snp.makeConstraints { make in
make.height.equalTo(84)