添加提交任务流程并抽取资料编辑页

This commit is contained in:
2026-07-07 10:12:20 +08:00
parent af450fb4cc
commit ef1d3b4af5
23 changed files with 3186 additions and 170 deletions

View File

@ -0,0 +1,627 @@
//
// TaskAddViews.swift
// suixinkan
//
import Kingfisher
import SnapKit
import UIKit
///
final class TaskAddMediaSectionView: UIView {
var onCloudImport: (() -> Void)?
var onLocalImport: (() -> Void)?
var onFileTap: ((TaskFileItem) -> Void)?
var onDeleteFile: ((TaskFileItem) -> Void)?
private var files: [TaskFileItem] = []
private var collectionHeightConstraint: Constraint?
private enum Section { case main }
private enum Item: Hashable { case add; case file(String) }
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewCompositionalLayout { _, environment in
let columns = 3
let spacing: CGFloat = 8
let inset = AppSpacing.md * 2
let availableWidth = environment.container.effectiveContentSize.width - inset
let totalSpacing = spacing * CGFloat(columns - 1)
let itemWidth = max(0, (availableWidth - totalSpacing) / CGFloat(columns))
let itemHeight = itemWidth / 1.77
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute(itemWidth),
heightDimension: .absolute(itemHeight)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(itemHeight)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
repeatingSubitem: item,
count: columns
)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
return section
}
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .clear
view.isScrollEnabled = false
return view
}()
private lazy var dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
[weak self] collectionView, indexPath, item in
switch item {
case .add:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TaskAddAddMediaCell.reuseIdentifier,
for: indexPath
) as! TaskAddAddMediaCell
return cell
case .file(let id):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TaskAddMediaCell.reuseIdentifier,
for: indexPath
) as! TaskAddMediaCell
if let file = self?.files.first(where: { $0.id == id }) {
cell.apply(file: file)
cell.onDelete = { self?.onDeleteFile?(file) }
}
return cell
}
}
private let titleLabel = UILabel()
private let tipsLabel = UILabel()
private let cloudButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = AppRadius.md
titleLabel.text = "添加图片"
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
tipsLabel.text = "(点击图片或视频可添加备注)"
tipsLabel.font = .app(.caption)
tipsLabel.textColor = AppColor.textPrimary
cloudButton.setTitle("云盘导入", for: .normal)
cloudButton.setTitleColor(AppColor.primary, for: .normal)
cloudButton.titleLabel?.font = .app(.caption)
cloudButton.addTarget(self, action: #selector(cloudTapped), for: .touchUpInside)
collectionView.register(TaskAddMediaCell.self, forCellWithReuseIdentifier: TaskAddMediaCell.reuseIdentifier)
collectionView.register(TaskAddAddMediaCell.self, forCellWithReuseIdentifier: TaskAddAddMediaCell.reuseIdentifier)
collectionView.delegate = self
addSubview(titleLabel)
addSubview(tipsLabel)
addSubview(cloudButton)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(AppSpacing.md)
}
tipsLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xxs)
make.centerY.equalTo(titleLabel)
}
cloudButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalTo(titleLabel)
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.bottom.equalToSuperview().inset(AppSpacing.md)
collectionHeightConstraint = make.height.equalTo(88).constraint
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(files: [TaskFileItem]) {
self.files = files
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(files.map { Item.file($0.id) } + [.add])
dataSource.apply(snapshot, animatingDifferences: false)
collectionView.layoutIfNeeded()
let height = max(88, collectionView.collectionViewLayout.collectionViewContentSize.height)
collectionHeightConstraint?.update(offset: height)
}
@objc private func cloudTapped() {
onCloudImport?()
}
}
extension TaskAddMediaSectionView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .add:
onLocalImport?()
case .file(let id):
guard let file = files.first(where: { $0.id == id }) else { return }
onFileTap?(file)
}
}
}
/// cell
final class TaskAddMediaCell: UICollectionViewCell {
static let reuseIdentifier = "TaskAddMediaCell"
var onDelete: (() -> Void)?
private let imageView = UIImageView()
private let overlayView = UIView()
private let progressLabel = UILabel()
private let deleteButton = UIButton(type: .system)
private let playIconView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = AppColor.inputBackground
contentView.layer.cornerRadius = AppRadius.sm
contentView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
overlayView.isHidden = true
progressLabel.font = .app(.captionMedium)
progressLabel.textColor = .white
progressLabel.textAlignment = .center
deleteButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
deleteButton.tintColor = .white
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
playIconView.image = UIImage(systemName: "play.circle.fill")
playIconView.tintColor = .white
playIconView.isHidden = true
contentView.addSubview(imageView)
contentView.addSubview(overlayView)
contentView.addSubview(playIconView)
contentView.addSubview(progressLabel)
contentView.addSubview(deleteButton)
imageView.snp.makeConstraints { make in make.edges.equalToSuperview() }
overlayView.snp.makeConstraints { make in make.edges.equalToSuperview() }
playIconView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(28)
}
progressLabel.snp.makeConstraints { make in make.center.equalToSuperview() }
deleteButton.snp.makeConstraints { make in
make.top.trailing.equalToSuperview().inset(AppSpacing.xxs)
make.width.height.equalTo(22)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
onDelete = nil
imageView.kf.cancelDownloadTask()
imageView.image = nil
}
func apply(file: TaskFileItem) {
let urlString = file.previewURLString
if let url = URL(string: urlString), !urlString.isEmpty {
imageView.kf.setImage(with: url)
} else {
imageView.image = UIImage(systemName: "photo")
imageView.tintColor = AppColor.textTertiary
}
playIconView.isHidden = file.fileType != 1
if file.isUploading {
overlayView.isHidden = false
progressLabel.text = "\(file.uploadProgress)%"
} else {
overlayView.isHidden = true
progressLabel.text = nil
}
}
@objc private func deleteTapped() {
onDelete?()
}
}
/// cell
final class TaskAddAddMediaCell: UICollectionViewCell {
static let reuseIdentifier = "TaskAddAddMediaCell"
private let iconView = UIImageView()
private let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = AppColor.inputBackground
contentView.layer.cornerRadius = AppRadius.sm
iconView.image = UIImage(systemName: "plus")
iconView.tintColor = AppColor.textTabInactive
titleLabel.text = "本地导入"
titleLabel.font = .app(.caption)
titleLabel.textColor = AppColor.textTabInactive
contentView.addSubview(iconView)
contentView.addSubview(titleLabel)
iconView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(-8)
make.width.height.equalTo(24)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconView.snp.bottom).offset(2)
make.centerX.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
final class TaskPrioritySectionView: UIView {
var onUrgentHourChange: ((Int) -> Void)?
var onCustomHourTextChange: ((String) -> Void)?
private let titleLabel = UILabel()
private let noneButton = UIButton(type: .system)
private let twoHourButton = UIButton(type: .system)
private let customButton = UIButton(type: .system)
private let customField = UITextField()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = AppRadius.md
titleLabel.text = "优先级"
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
configureOptionButton(noneButton, title: "不加急")
configureOptionButton(twoHourButton, title: "加急(2小时)")
configureOptionButton(customButton, title: "自定义")
noneButton.addTarget(self, action: #selector(noneTapped), for: .touchUpInside)
twoHourButton.addTarget(self, action: #selector(twoHourTapped), for: .touchUpInside)
customButton.addTarget(self, action: #selector(customTapped), for: .touchUpInside)
customField.placeholder = "请输入小时数"
customField.font = .app(.body)
customField.textColor = AppColor.textPrimary
customField.keyboardType = .numberPad
customField.borderStyle = .roundedRect
customField.isHidden = true
customField.addTarget(self, action: #selector(customFieldChanged), for: .editingChanged)
let row = UIStackView(arrangedSubviews: [noneButton, twoHourButton, customButton])
row.axis = .horizontal
row.spacing = AppSpacing.xs
row.distribution = .fillEqually
addSubview(titleLabel)
addSubview(row)
addSubview(customField)
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(AppSpacing.md)
}
row.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(42)
}
customField.snp.makeConstraints { make in
make.top.equalTo(row.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.bottom.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(40)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(urgentHour: Int) {
let isCustom = urgentHour > 0 && urgentHour != 2
styleOption(noneButton, selected: urgentHour == 0)
styleOption(twoHourButton, selected: urgentHour == 2)
styleOption(customButton, selected: isCustom)
customField.isHidden = !isCustom
if isCustom {
customField.text = "\(urgentHour)"
} else {
customField.text = ""
}
}
private func configureOptionButton(_ button: UIButton, title: String) {
button.setTitle(title, for: .normal)
button.titleLabel?.font = .app(.caption)
button.layer.cornerRadius = AppRadius.xs
button.clipsToBounds = true
}
private func styleOption(_ button: UIButton, selected: Bool) {
button.backgroundColor = selected ? AppColor.primary : AppColor.inputBackground
button.setTitleColor(selected ? .white : AppColor.textSecondary, for: .normal)
}
@objc private func noneTapped() { onUrgentHourChange?(0) }
@objc private func twoHourTapped() { onUrgentHourChange?(2) }
@objc private func customTapped() {
onUrgentHourChange?(1)
onCustomHourTextChange?("1")
}
@objc private func customFieldChanged() {
onCustomHourTextChange?(customField.text ?? "")
}
}
///
final class TaskOrderSelectButton: UIControl {
private let titleLabel = UILabel()
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = AppColor.border.cgColor
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textTabInactive
titleLabel.text = "关联订单(可选)"
arrowView.tintColor = AppColor.textTabInactive
addSubview(titleLabel)
addSubview(arrowView)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(AppSpacing.sm)
make.centerY.equalToSuperview()
}
arrowView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppSpacing.sm)
make.centerY.equalToSuperview()
}
snp.makeConstraints { make in make.height.equalTo(44) }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(orderNumber: String?) {
if let orderNumber, !orderNumber.isEmpty {
titleLabel.text = orderNumber
titleLabel.textColor = AppColor.textPrimary
} else {
titleLabel.text = "关联订单(可选)"
titleLabel.textColor = AppColor.textTabInactive
}
}
}
///
final class TaskFileRemarkDialogView: UIView {
var onCancel: (() -> Void)?
var onConfirm: (() -> Void)?
private let containerView = UIView()
private let titleLabel = UILabel()
private let previewView = UIImageView()
private let textView = UITextView()
private let countLabel = UILabel()
private let cancelButton = UIButton(type: .system)
private let confirmButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = AppColor.overlayScrim
containerView.backgroundColor = .white
containerView.layer.cornerRadius = AppRadius.md
titleLabel.text = "备注信息"
titleLabel.font = .app(.title)
previewView.contentMode = .scaleAspectFill
previewView.clipsToBounds = true
previewView.layer.cornerRadius = AppRadius.sm
previewView.backgroundColor = AppColor.inputBackground
textView.font = .app(.body)
textView.layer.borderWidth = 1
textView.layer.borderColor = AppColor.border.cgColor
textView.layer.cornerRadius = AppRadius.sm
textView.delegate = self
countLabel.font = .app(.caption)
countLabel.textColor = AppColor.textTertiary
countLabel.text = "0/50"
cancelButton.setTitle("取消", for: .normal)
cancelButton.setTitleColor(AppColor.textSecondary, for: .normal)
cancelButton.backgroundColor = AppColor.inputBackground
cancelButton.layer.cornerRadius = AppRadius.xs
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
confirmButton.setTitle("确定", for: .normal)
confirmButton.setTitleColor(.white, for: .normal)
confirmButton.backgroundColor = AppColor.primary
confirmButton.layer.cornerRadius = AppRadius.xs
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
addSubview(containerView)
containerView.addSubview(titleLabel)
containerView.addSubview(previewView)
containerView.addSubview(textView)
containerView.addSubview(countLabel)
containerView.addSubview(cancelButton)
containerView.addSubview(confirmButton)
containerView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(AppSpacing.lg)
}
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(AppSpacing.md)
}
previewView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(160)
}
textView.snp.makeConstraints { make in
make.top.equalTo(previewView.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(80)
}
countLabel.snp.makeConstraints { make in
make.top.equalTo(textView.snp.bottom).offset(AppSpacing.xxs)
make.trailing.equalTo(textView)
}
cancelButton.snp.makeConstraints { make in
make.top.equalTo(countLabel.snp.bottom).offset(AppSpacing.md)
make.leading.bottom.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(36)
make.width.equalTo(72)
}
confirmButton.snp.makeConstraints { make in
make.centerY.equalTo(cancelButton)
make.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(36)
make.width.equalTo(72)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(file: TaskFileItem, text: String) {
textView.text = text
countLabel.text = "\(text.count)/50"
let urlString = file.previewURLString
if let url = URL(string: urlString), !urlString.isEmpty {
previewView.kf.setImage(with: url)
} else {
previewView.image = UIImage(systemName: "photo")
previewView.tintColor = AppColor.textTertiary
}
}
@objc private func cancelTapped() { onCancel?() }
@objc private func confirmTapped() { onConfirm?() }
///
var inputText: String { textView.text ?? "" }
}
extension TaskFileRemarkDialogView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
if textView.text.count > 50 {
textView.text = String(textView.text.prefix(50))
}
countLabel.text = "\(textView.text.count)/50"
}
}
///
final class TaskBorderTextField: UIView {
let textField = UITextField()
init(placeholder: String) {
super.init(frame: .zero)
backgroundColor = .white
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = AppColor.border.cgColor
textField.placeholder = placeholder
textField.font = .app(.body)
textField.textColor = AppColor.textPrimary
addSubview(textField)
textField.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12))
}
snp.makeConstraints { make in make.height.equalTo(44) }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
final class TaskBorderTextView: UIView {
let textView = UITextView()
init(placeholder: String) {
super.init(frame: .zero)
backgroundColor = .white
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = AppColor.border.cgColor
textView.font = .app(.body)
textView.textColor = AppColor.textPrimary
textView.text = placeholder
textView.textColor = AppColor.textTertiary
addSubview(textView)
textView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppSpacing.xs)
}
snp.makeConstraints { make in make.height.equalTo(80) }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}