// // 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(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() 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") } }