修复任务提交与弹窗交互问题
This commit is contained in:
@ -23,8 +23,9 @@ final class CloudStoragePickForTaskViewController: BaseViewController {
|
||||
private let confirmButton = AppButton(title: "确定")
|
||||
private let bottomBar = UIView()
|
||||
|
||||
init(importedFileIDs: Set<Int> = []) {
|
||||
viewModel = CloudStoragePickForTaskViewModel(importedFileIDs: importedFileIDs)
|
||||
/// 初始化云盘多选页,可按云盘文件类型限制选择范围。
|
||||
init(importedFileIDs: Set<Int> = [], allowedFileType: Int? = nil) {
|
||||
viewModel = CloudStoragePickForTaskViewModel(importedFileIDs: importedFileIDs, allowedFileType: allowedFileType)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@ -293,6 +294,7 @@ private final class CloudPickCell: UICollectionViewCell {
|
||||
imageView.image = nil
|
||||
folderIconView.isHidden = true
|
||||
badgeView.isHidden = true
|
||||
contentView.alpha = 1
|
||||
}
|
||||
|
||||
func apply(file: CloudFile, isSelected: Bool, isImported: Bool) {
|
||||
@ -302,6 +304,7 @@ private final class CloudPickCell: UICollectionViewCell {
|
||||
badgeView.isHidden = !isSelected
|
||||
|
||||
if file.isFolder {
|
||||
contentView.alpha = 1
|
||||
imageView.image = nil
|
||||
folderIconView.isHidden = false
|
||||
return
|
||||
|
||||
@ -16,9 +16,11 @@ final class TaskAddViewController: BaseViewController {
|
||||
private let ossUploadService = NetworkServices.shared.ossUploadService
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let formCardView = UIView()
|
||||
private let contentStack = UIStackView()
|
||||
private let mediaSection = TaskAddMediaSectionView()
|
||||
private let prioritySection = TaskPrioritySectionView()
|
||||
private let orderTitleLabel = UILabel()
|
||||
private let orderButton = TaskOrderSelectButton()
|
||||
private let nameField = TaskBorderTextField(placeholder: "请输入任务标题")
|
||||
private let detailsField = TaskBorderTextView(placeholder: "请输入任务描述(必填)")
|
||||
@ -26,6 +28,7 @@ final class TaskAddViewController: BaseViewController {
|
||||
private let bottomBar = UIView()
|
||||
|
||||
private var remarkDialog: TaskFileRemarkDialogView?
|
||||
private var uploadTypeSheet: TaskUploadTypeSheetView?
|
||||
private var pickingMediaType: Int = 2
|
||||
private var detailsPlaceholder = "请输入任务描述(必填)"
|
||||
|
||||
@ -34,37 +37,56 @@ final class TaskAddViewController: BaseViewController {
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
scrollView.backgroundColor = AppColor.pageBackground
|
||||
scrollView.keyboardDismissMode = .interactive
|
||||
|
||||
formCardView.backgroundColor = .white
|
||||
formCardView.layer.cornerRadius = AppRadius.lg
|
||||
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = AppSpacing.sm
|
||||
contentStack.spacing = AppSpacing.md
|
||||
|
||||
orderTitleLabel.text = "关联订单(可选)"
|
||||
orderTitleLabel.font = .app(.bodyMedium)
|
||||
orderTitleLabel.textColor = AppColor.textPrimary
|
||||
|
||||
bottomBar.backgroundColor = .white
|
||||
view.addSubview(scrollView)
|
||||
view.addSubview(bottomBar)
|
||||
scrollView.addSubview(contentStack)
|
||||
scrollView.addSubview(formCardView)
|
||||
formCardView.addSubview(contentStack)
|
||||
|
||||
contentStack.addArrangedSubview(mediaSection)
|
||||
contentStack.addArrangedSubview(prioritySection)
|
||||
contentStack.addArrangedSubview(orderTitleLabel)
|
||||
contentStack.addArrangedSubview(orderButton)
|
||||
contentStack.addArrangedSubview(nameField)
|
||||
contentStack.addArrangedSubview(detailsField)
|
||||
contentStack.setCustomSpacing(AppSpacing.xxs, after: orderTitleLabel)
|
||||
bottomBar.addSubview(saveButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
bottomBar.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
make.height.equalTo(80)
|
||||
}
|
||||
saveButton.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.bottom.equalTo(bottomBar.snp.top)
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
formCardView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppSpacing.md)
|
||||
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.md * 2)
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
@ -91,6 +113,7 @@ final class TaskAddViewController: BaseViewController {
|
||||
nameField.textField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
|
||||
detailsField.textView.delegate = self
|
||||
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
@ -108,11 +131,15 @@ final class TaskAddViewController: BaseViewController {
|
||||
if nameField.textField.text != viewModel.taskName {
|
||||
nameField.textField.text = viewModel.taskName
|
||||
}
|
||||
if detailsField.textView.textColor != AppColor.textTertiary,
|
||||
detailsField.textView.text != viewModel.taskDetails {
|
||||
if viewModel.taskDetails.isEmpty, !detailsField.textView.isFirstResponder {
|
||||
detailsField.textView.text = detailsPlaceholder
|
||||
detailsField.textView.textColor = AppColor.textTertiary
|
||||
} else if detailsField.textView.text != viewModel.taskDetails {
|
||||
detailsField.textView.text = viewModel.taskDetails
|
||||
detailsField.textView.textColor = AppColor.textPrimary
|
||||
}
|
||||
saveButton.isEnabled = !viewModel.isUploading
|
||||
saveButton.isEnabled = true
|
||||
saveButton.alpha = viewModel.isUploading ? 0.6 : 1
|
||||
|
||||
if viewModel.showRemarkDialog, let file = viewModel.remarkTargetFile {
|
||||
showRemarkDialog(file: file)
|
||||
@ -151,15 +178,30 @@ final class TaskAddViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func presentLocalImportSheet() {
|
||||
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
sheet.addAction(UIAlertAction(title: "图片", style: .default) { [weak self] _ in
|
||||
guard uploadTypeSheet == nil else { return }
|
||||
let sheet = TaskUploadTypeSheetView(frame: view.bounds)
|
||||
sheet.onChooseImage = { [weak self] in
|
||||
self?.hideUploadTypeSheet()
|
||||
self?.presentMediaPicker(isImage: true)
|
||||
})
|
||||
sheet.addAction(UIAlertAction(title: "视频", style: .default) { [weak self] _ in
|
||||
}
|
||||
sheet.onChooseVideo = { [weak self] in
|
||||
self?.hideUploadTypeSheet()
|
||||
self?.presentMediaPicker(isImage: false)
|
||||
})
|
||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
sheet.onCancel = { [weak self] in
|
||||
self?.hideUploadTypeSheet()
|
||||
}
|
||||
uploadTypeSheet = sheet
|
||||
view.addSubview(sheet)
|
||||
sheet.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
sheet.show()
|
||||
}
|
||||
|
||||
private func hideUploadTypeSheet() {
|
||||
uploadTypeSheet?.dismiss { [weak self] in
|
||||
self?.uploadTypeSheet?.removeFromSuperview()
|
||||
self?.uploadTypeSheet = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func presentMediaPicker(isImage: Bool) {
|
||||
@ -257,7 +299,6 @@ final class TaskAddViewController: BaseViewController {
|
||||
self?.viewModel.updateUploadProgress(uploadTaskId: uploadTaskId, progress: progress)
|
||||
}
|
||||
}
|
||||
try await taskAPI.registerUploadedFileURL(scenicId: scenicId, fileURL: fileURL, folderId: 0)
|
||||
await MainActor.run {
|
||||
self.viewModel.markUploadSucceeded(
|
||||
uploadTaskId: uploadTaskId,
|
||||
|
||||
@ -17,16 +17,17 @@ final class TaskAddMediaSectionView: UIView {
|
||||
|
||||
private var files: [TaskFileItem] = []
|
||||
private var collectionHeightConstraint: Constraint?
|
||||
private let gridColumns = 3
|
||||
private let gridSpacing: CGFloat = 8
|
||||
|
||||
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 layout = UICollectionViewCompositionalLayout { [weak self] _, environment in
|
||||
let columns = self?.gridColumns ?? 3
|
||||
let spacing = self?.gridSpacing ?? 8
|
||||
let availableWidth = environment.container.effectiveContentSize.width
|
||||
let totalSpacing = spacing * CGFloat(columns - 1)
|
||||
let itemWidth = max(0, (availableWidth - totalSpacing) / CGFloat(columns))
|
||||
let itemHeight = itemWidth / 1.77
|
||||
@ -83,8 +84,7 @@ final class TaskAddMediaSectionView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = AppRadius.md
|
||||
backgroundColor = .clear
|
||||
|
||||
titleLabel.text = "添加图片"
|
||||
titleLabel.font = .app(.bodyMedium)
|
||||
@ -93,11 +93,13 @@ final class TaskAddMediaSectionView: UIView {
|
||||
tipsLabel.text = "(点击图片或视频可添加备注)"
|
||||
tipsLabel.font = .app(.caption)
|
||||
tipsLabel.textColor = AppColor.textPrimary
|
||||
tipsLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
cloudButton.setTitle("云盘导入", for: .normal)
|
||||
cloudButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
cloudButton.titleLabel?.font = .app(.caption)
|
||||
cloudButton.addTarget(self, action: #selector(cloudTapped), for: .touchUpInside)
|
||||
cloudButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
|
||||
collectionView.register(TaskAddMediaCell.self, forCellWithReuseIdentifier: TaskAddMediaCell.reuseIdentifier)
|
||||
collectionView.register(TaskAddAddMediaCell.self, forCellWithReuseIdentifier: TaskAddAddMediaCell.reuseIdentifier)
|
||||
@ -109,20 +111,20 @@ final class TaskAddMediaSectionView: UIView {
|
||||
addSubview(collectionView)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(AppSpacing.md)
|
||||
make.top.leading.equalToSuperview()
|
||||
}
|
||||
tipsLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xxs)
|
||||
make.centerY.equalTo(titleLabel)
|
||||
make.trailing.lessThanOrEqualTo(cloudButton.snp.leading).offset(-AppSpacing.xs)
|
||||
}
|
||||
cloudButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.trailing.equalToSuperview()
|
||||
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)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
collectionHeightConstraint = make.height.equalTo(88).constraint
|
||||
}
|
||||
}
|
||||
@ -138,9 +140,22 @@ final class TaskAddMediaSectionView: UIView {
|
||||
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)
|
||||
updateCollectionHeight(itemCount: files.count + 1)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateCollectionHeight(itemCount: files.count + 1)
|
||||
}
|
||||
|
||||
private func updateCollectionHeight(itemCount: Int) {
|
||||
let availableWidth = collectionView.bounds.width
|
||||
guard availableWidth > 0 else { return }
|
||||
let itemWidth = max(0, (availableWidth - gridSpacing * CGFloat(gridColumns - 1)) / CGFloat(gridColumns))
|
||||
let itemHeight = itemWidth / 1.77
|
||||
let rows = max(1, Int(ceil(Double(itemCount) / Double(gridColumns))))
|
||||
let height = itemHeight * CGFloat(rows) + gridSpacing * CGFloat(rows - 1)
|
||||
collectionHeightConstraint?.update(offset: max(88, height))
|
||||
}
|
||||
|
||||
@objc private func cloudTapped() {
|
||||
@ -161,6 +176,54 @@ extension TaskAddMediaSectionView: UICollectionViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 圆形上传进度视图,对齐 Android 上传素材 cell 的进度表现。
|
||||
final class TaskCircularProgressView: UIView {
|
||||
|
||||
private let trackLayer = CAShapeLayer()
|
||||
private let progressLayer = CAShapeLayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
isHidden = true
|
||||
layer.addSublayer(trackLayer)
|
||||
layer.addSublayer(progressLayer)
|
||||
trackLayer.fillColor = UIColor.clear.cgColor
|
||||
trackLayer.strokeColor = UIColor.white.withAlphaComponent(0.35).cgColor
|
||||
trackLayer.lineWidth = 3
|
||||
progressLayer.fillColor = UIColor.clear.cgColor
|
||||
progressLayer.strokeColor = UIColor.white.cgColor
|
||||
progressLayer.lineCap = .round
|
||||
progressLayer.lineWidth = 3
|
||||
progressLayer.strokeEnd = 0
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let radius = min(bounds.width, bounds.height) / 2 - progressLayer.lineWidth / 2
|
||||
let path = UIBezierPath(
|
||||
arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
|
||||
radius: radius,
|
||||
startAngle: -.pi / 2,
|
||||
endAngle: .pi * 1.5,
|
||||
clockwise: true
|
||||
)
|
||||
trackLayer.frame = bounds
|
||||
progressLayer.frame = bounds
|
||||
trackLayer.path = path.cgPath
|
||||
progressLayer.path = path.cgPath
|
||||
}
|
||||
|
||||
/// 设置 0...1 的上传进度。
|
||||
func setProgress(_ progress: CGFloat) {
|
||||
progressLayer.strokeEnd = min(1, max(0, progress))
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材缩略图 cell。
|
||||
final class TaskAddMediaCell: UICollectionViewCell {
|
||||
|
||||
@ -170,9 +233,11 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
||||
|
||||
private let imageView = UIImageView()
|
||||
private let overlayView = UIView()
|
||||
private let progressRingView = TaskCircularProgressView()
|
||||
private let progressLabel = UILabel()
|
||||
private let deleteButton = UIButton(type: .system)
|
||||
private let playIconView = UIImageView()
|
||||
private let sourceBadgeLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -183,7 +248,7 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
|
||||
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
||||
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
|
||||
overlayView.isHidden = true
|
||||
|
||||
progressLabel.font = .app(.captionMedium)
|
||||
@ -198,22 +263,45 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
||||
playIconView.tintColor = .white
|
||||
playIconView.isHidden = true
|
||||
|
||||
sourceBadgeLabel.font = .app(.caption)
|
||||
sourceBadgeLabel.textColor = .white
|
||||
sourceBadgeLabel.textAlignment = .center
|
||||
sourceBadgeLabel.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
sourceBadgeLabel.layer.cornerRadius = AppRadius.xs
|
||||
sourceBadgeLabel.clipsToBounds = true
|
||||
sourceBadgeLabel.isHidden = true
|
||||
|
||||
contentView.addSubview(imageView)
|
||||
contentView.addSubview(overlayView)
|
||||
contentView.addSubview(sourceBadgeLabel)
|
||||
contentView.addSubview(playIconView)
|
||||
contentView.addSubview(progressRingView)
|
||||
contentView.addSubview(progressLabel)
|
||||
contentView.addSubview(deleteButton)
|
||||
|
||||
imageView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
overlayView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
sourceBadgeLabel.snp.makeConstraints { make in
|
||||
make.leading.bottom.equalToSuperview().inset(AppSpacing.xxs)
|
||||
make.height.equalTo(18)
|
||||
make.width.greaterThanOrEqualTo(34)
|
||||
}
|
||||
playIconView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.width.height.equalTo(24)
|
||||
}
|
||||
progressRingView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.centerY.equalToSuperview().offset(-10)
|
||||
make.width.height.equalTo(28)
|
||||
}
|
||||
progressLabel.snp.makeConstraints { make in make.center.equalToSuperview() }
|
||||
progressLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(progressRingView.snp.bottom).offset(2)
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
deleteButton.snp.makeConstraints { make in
|
||||
make.top.trailing.equalToSuperview().inset(AppSpacing.xxs)
|
||||
make.width.height.equalTo(22)
|
||||
make.width.height.equalTo(20)
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,6 +315,7 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
||||
onDelete = nil
|
||||
imageView.kf.cancelDownloadTask()
|
||||
imageView.image = nil
|
||||
progressRingView.setProgress(0)
|
||||
}
|
||||
|
||||
func apply(file: TaskFileItem) {
|
||||
@ -237,12 +326,18 @@ final class TaskAddMediaCell: UICollectionViewCell {
|
||||
imageView.image = UIImage(systemName: "photo")
|
||||
imageView.tintColor = AppColor.textTertiary
|
||||
}
|
||||
playIconView.isHidden = file.fileType != 1
|
||||
playIconView.isHidden = file.fileType != 1 || file.isUploading
|
||||
deleteButton.isHidden = file.isUploading
|
||||
sourceBadgeLabel.isHidden = file.isUploading
|
||||
sourceBadgeLabel.text = file.sourceBadgeText
|
||||
if file.isUploading {
|
||||
overlayView.isHidden = false
|
||||
progressRingView.isHidden = false
|
||||
progressRingView.setProgress(CGFloat(file.uploadProgress) / 100)
|
||||
progressLabel.text = "\(file.uploadProgress)%"
|
||||
} else {
|
||||
overlayView.isHidden = true
|
||||
progressRingView.isHidden = true
|
||||
progressLabel.text = nil
|
||||
}
|
||||
}
|
||||
@ -302,11 +397,14 @@ final class TaskPrioritySectionView: UIView {
|
||||
private let twoHourButton = UIButton(type: .system)
|
||||
private let customButton = UIButton(type: .system)
|
||||
private let customField = UITextField()
|
||||
private let customConfirmButton = UIButton(type: .system)
|
||||
private let optionRow = UIStackView()
|
||||
private let customRow = UIStackView()
|
||||
private let stackView = UIStackView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = AppRadius.md
|
||||
backgroundColor = .clear
|
||||
|
||||
titleLabel.text = "优先级"
|
||||
titleLabel.font = .app(.bodyMedium)
|
||||
@ -324,32 +422,54 @@ final class TaskPrioritySectionView: UIView {
|
||||
customField.font = .app(.body)
|
||||
customField.textColor = AppColor.textPrimary
|
||||
customField.keyboardType = .numberPad
|
||||
customField.borderStyle = .roundedRect
|
||||
customField.isHidden = true
|
||||
customField.backgroundColor = .white
|
||||
customField.layer.cornerRadius = AppRadius.sm
|
||||
customField.layer.borderWidth = 1
|
||||
customField.layer.borderColor = AppColor.border.cgColor
|
||||
customField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: AppSpacing.sm, height: 1))
|
||||
customField.leftViewMode = .always
|
||||
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
|
||||
customConfirmButton.setTitle("确定", for: .normal)
|
||||
customConfirmButton.setTitleColor(.white, for: .normal)
|
||||
customConfirmButton.titleLabel?.font = .app(.bodyMedium)
|
||||
customConfirmButton.backgroundColor = AppColor.primary
|
||||
customConfirmButton.layer.cornerRadius = AppRadius.xs
|
||||
customConfirmButton.addTarget(self, action: #selector(customConfirmTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(row)
|
||||
addSubview(customField)
|
||||
optionRow.addArrangedSubview(noneButton)
|
||||
optionRow.addArrangedSubview(twoHourButton)
|
||||
optionRow.addArrangedSubview(customButton)
|
||||
optionRow.axis = .horizontal
|
||||
optionRow.spacing = AppSpacing.xs
|
||||
optionRow.distribution = .fillEqually
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(AppSpacing.md)
|
||||
customRow.addArrangedSubview(customField)
|
||||
customRow.addArrangedSubview(customConfirmButton)
|
||||
customRow.axis = .horizontal
|
||||
customRow.spacing = AppSpacing.xs
|
||||
customRow.alignment = .fill
|
||||
customRow.isHidden = true
|
||||
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
stackView.addArrangedSubview(optionRow)
|
||||
stackView.addArrangedSubview(customRow)
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = AppSpacing.xs
|
||||
|
||||
addSubview(stackView)
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
row.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
optionRow.snp.makeConstraints { make in
|
||||
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)
|
||||
make.height.equalTo(46)
|
||||
}
|
||||
customConfirmButton.snp.makeConstraints { make in
|
||||
make.width.equalTo(72)
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,7 +483,7 @@ final class TaskPrioritySectionView: UIView {
|
||||
styleOption(noneButton, selected: urgentHour == 0)
|
||||
styleOption(twoHourButton, selected: urgentHour == 2)
|
||||
styleOption(customButton, selected: isCustom)
|
||||
customField.isHidden = !isCustom
|
||||
customRow.isHidden = !isCustom
|
||||
if isCustom {
|
||||
customField.text = "\(urgentHour)"
|
||||
} else {
|
||||
@ -392,6 +512,9 @@ final class TaskPrioritySectionView: UIView {
|
||||
@objc private func customFieldChanged() {
|
||||
onCustomHourTextChange?(customField.text ?? "")
|
||||
}
|
||||
@objc private func customConfirmTapped() {
|
||||
onCustomHourTextChange?(customField.text ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/// 关联订单选择按钮。
|
||||
@ -443,7 +566,7 @@ final class TaskOrderSelectButton: UIControl {
|
||||
}
|
||||
|
||||
/// 任务素材备注弹窗。
|
||||
final class TaskFileRemarkDialogView: UIView {
|
||||
final class TaskFileRemarkDialogView: KeyboardAvoidingDialogView {
|
||||
|
||||
var onCancel: (() -> Void)?
|
||||
var onConfirm: (() -> Void)?
|
||||
@ -451,31 +574,46 @@ final class TaskFileRemarkDialogView: UIView {
|
||||
private let containerView = UIView()
|
||||
private let titleLabel = UILabel()
|
||||
private let previewView = UIImageView()
|
||||
private let remarkTitleLabel = UILabel()
|
||||
private let textView = UITextView()
|
||||
private let placeholderLabel = UILabel()
|
||||
private let countLabel = UILabel()
|
||||
private let cancelButton = UIButton(type: .system)
|
||||
private let confirmButton = UIButton(type: .system)
|
||||
private let buttonRow = UIStackView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = AppColor.overlayScrim
|
||||
keyboardAvoidingContentView = containerView
|
||||
|
||||
containerView.backgroundColor = .white
|
||||
containerView.layer.cornerRadius = AppRadius.md
|
||||
containerView.layer.cornerRadius = AppRadius.lg
|
||||
|
||||
titleLabel.text = "备注信息"
|
||||
titleLabel.text = "请填写剪辑备注"
|
||||
titleLabel.font = .app(.title)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
|
||||
previewView.contentMode = .scaleAspectFill
|
||||
previewView.clipsToBounds = true
|
||||
previewView.layer.cornerRadius = AppRadius.sm
|
||||
previewView.layer.cornerRadius = AppRadius.lg
|
||||
previewView.backgroundColor = AppColor.inputBackground
|
||||
|
||||
remarkTitleLabel.text = "备注信息"
|
||||
remarkTitleLabel.font = .app(.caption)
|
||||
remarkTitleLabel.textColor = AppColor.textPrimary
|
||||
|
||||
textView.font = .app(.body)
|
||||
textView.textColor = AppColor.textPrimary
|
||||
textView.layer.borderWidth = 1
|
||||
textView.layer.borderColor = AppColor.border.cgColor
|
||||
textView.layer.cornerRadius = AppRadius.sm
|
||||
textView.delegate = self
|
||||
textView.textContainerInset = UIEdgeInsets(top: AppSpacing.sm, left: AppSpacing.xs, bottom: AppSpacing.sm, right: AppSpacing.xs)
|
||||
|
||||
placeholderLabel.text = "请输入备注(最多50字)"
|
||||
placeholderLabel.font = .app(.body)
|
||||
placeholderLabel.textColor = AppColor.textTertiary
|
||||
|
||||
countLabel.font = .app(.caption)
|
||||
countLabel.textColor = AppColor.textTertiary
|
||||
@ -493,16 +631,25 @@ final class TaskFileRemarkDialogView: UIView {
|
||||
confirmButton.layer.cornerRadius = AppRadius.xs
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
|
||||
buttonRow.axis = .horizontal
|
||||
buttonRow.spacing = AppSpacing.xs
|
||||
buttonRow.alignment = .center
|
||||
buttonRow.distribution = .fill
|
||||
buttonRow.addArrangedSubview(cancelButton)
|
||||
buttonRow.addArrangedSubview(confirmButton)
|
||||
|
||||
addSubview(containerView)
|
||||
containerView.addSubview(titleLabel)
|
||||
containerView.addSubview(previewView)
|
||||
containerView.addSubview(textView)
|
||||
containerView.addSubview(remarkTitleLabel)
|
||||
containerView.addSubview(countLabel)
|
||||
containerView.addSubview(cancelButton)
|
||||
containerView.addSubview(confirmButton)
|
||||
containerView.addSubview(textView)
|
||||
textView.addSubview(placeholderLabel)
|
||||
containerView.addSubview(buttonRow)
|
||||
|
||||
containerView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.centerX.equalToSuperview()
|
||||
keyboardAvoidingCenterYConstraint = make.centerY.equalToSuperview().constraint
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.lg)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
@ -511,26 +658,35 @@ final class TaskFileRemarkDialogView: UIView {
|
||||
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)
|
||||
make.height.equalTo(previewView.snp.width)
|
||||
}
|
||||
remarkTitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(previewView.snp.bottom).offset(AppSpacing.md)
|
||||
make.leading.equalToSuperview().offset(AppSpacing.md)
|
||||
}
|
||||
countLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(remarkTitleLabel)
|
||||
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
textView.snp.makeConstraints { make in
|
||||
make.top.equalTo(previewView.snp.bottom).offset(AppSpacing.sm)
|
||||
make.top.equalTo(remarkTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||
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)
|
||||
placeholderLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(AppSpacing.sm)
|
||||
make.leading.equalToSuperview().offset(AppSpacing.sm + 3)
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.sm)
|
||||
}
|
||||
buttonRow.snp.makeConstraints { make in
|
||||
make.top.equalTo(textView.snp.bottom).offset(AppSpacing.md)
|
||||
make.trailing.bottom.equalToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
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)
|
||||
}
|
||||
@ -544,6 +700,7 @@ final class TaskFileRemarkDialogView: UIView {
|
||||
func apply(file: TaskFileItem, text: String) {
|
||||
textView.text = text
|
||||
countLabel.text = "\(text.count)/50"
|
||||
placeholderLabel.isHidden = !text.isEmpty
|
||||
let urlString = file.previewURLString
|
||||
if let url = URL(string: urlString), !urlString.isEmpty {
|
||||
previewView.kf.setImage(with: url)
|
||||
@ -565,6 +722,7 @@ extension TaskFileRemarkDialogView: UITextViewDelegate {
|
||||
if textView.text.count > 50 {
|
||||
textView.text = String(textView.text.prefix(50))
|
||||
}
|
||||
placeholderLabel.isHidden = !textView.text.isEmpty
|
||||
countLabel.text = "\(textView.text.count)/50"
|
||||
}
|
||||
}
|
||||
@ -625,3 +783,177 @@ final class TaskBorderTextView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务上传类型底部弹层,对齐 Android `AddFileBottomModalForTask`。
|
||||
final class TaskUploadTypeSheetView: UIView {
|
||||
|
||||
var onChooseImage: (() -> Void)?
|
||||
var onChooseVideo: (() -> Void)?
|
||||
var onCancel: (() -> Void)?
|
||||
|
||||
private let dimmingView = UIView()
|
||||
private let panelView = UIView()
|
||||
private let titleLabel = UILabel()
|
||||
private let imageItem = TaskUploadTypeItemView(
|
||||
title: "上传图片",
|
||||
image: UIImage(named: "icon_cloud_storage_add_photo") ?? UIImage(systemName: "photo")
|
||||
)
|
||||
private let videoItem = TaskUploadTypeItemView(
|
||||
title: "上传视频",
|
||||
image: UIImage(named: "icon_cloud_storage_add_video") ?? UIImage(systemName: "video")
|
||||
)
|
||||
private let dividerView = UIView()
|
||||
private let cancelButton = UIButton(type: .system)
|
||||
private var panelBottomConstraint: Constraint?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
|
||||
dimmingView.backgroundColor = AppColor.overlayScrim
|
||||
dimmingView.alpha = 0
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(cancelTapped))
|
||||
dimmingView.addGestureRecognizer(tap)
|
||||
|
||||
panelView.backgroundColor = .white
|
||||
panelView.layer.cornerRadius = AppRadius.sheetTop
|
||||
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
panelView.clipsToBounds = true
|
||||
|
||||
titleLabel.text = "选择上传类型"
|
||||
titleLabel.font = .app(.bodyMedium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
imageItem.addTarget(self, action: #selector(imageTapped), for: .touchUpInside)
|
||||
videoItem.addTarget(self, action: #selector(videoTapped), for: .touchUpInside)
|
||||
|
||||
dividerView.backgroundColor = AppColor.border
|
||||
|
||||
cancelButton.setTitle("取消", for: .normal)
|
||||
cancelButton.titleLabel?.font = .app(.bodyMedium)
|
||||
cancelButton.setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
|
||||
cancelButton.backgroundColor = AppColor.inputBackground
|
||||
cancelButton.layer.cornerRadius = AppRadius.lg
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(dimmingView)
|
||||
addSubview(panelView)
|
||||
panelView.addSubview(titleLabel)
|
||||
panelView.addSubview(imageItem)
|
||||
panelView.addSubview(videoItem)
|
||||
panelView.addSubview(dividerView)
|
||||
panelView.addSubview(cancelButton)
|
||||
|
||||
dimmingView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
panelView.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview()
|
||||
panelBottomConstraint = make.bottom.equalToSuperview().offset(260).constraint
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
imageItem.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(58)
|
||||
}
|
||||
videoItem.snp.makeConstraints { make in
|
||||
make.top.equalTo(imageItem.snp.bottom).offset(AppSpacing.sm)
|
||||
make.leading.trailing.equalTo(imageItem)
|
||||
make.height.equalTo(imageItem)
|
||||
}
|
||||
dividerView.snp.makeConstraints { make in
|
||||
make.top.equalTo(videoItem.snp.bottom).offset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(0.5)
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(dividerView.snp.bottom).offset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(48)
|
||||
make.bottom.equalTo(safeAreaLayoutGuide).inset(AppSpacing.md)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 展示底部弹层。
|
||||
func show() {
|
||||
layoutIfNeeded()
|
||||
panelBottomConstraint?.update(offset: 0)
|
||||
UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseOut]) {
|
||||
self.dimmingView.alpha = 1
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭底部弹层。
|
||||
func dismiss(completion: (() -> Void)? = nil) {
|
||||
panelBottomConstraint?.update(offset: panelView.bounds.height)
|
||||
UIView.animate(withDuration: 0.18, delay: 0, options: [.curveEaseIn]) {
|
||||
self.dimmingView.alpha = 0
|
||||
self.layoutIfNeeded()
|
||||
} completion: { _ in
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func imageTapped() {
|
||||
onChooseImage?()
|
||||
}
|
||||
|
||||
@objc private func videoTapped() {
|
||||
onChooseVideo?()
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
onCancel?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传类型弹层内的单个选项。
|
||||
final class TaskUploadTypeItemView: UIControl {
|
||||
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
|
||||
init(title: String, image: UIImage?) {
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = AppRadius.sm
|
||||
layer.borderWidth = 1
|
||||
layer.borderColor = AppColor.inputBackground.cgColor
|
||||
|
||||
iconView.image = image
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.tintColor = AppColor.primary
|
||||
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .app(.bodyMedium)
|
||||
titleLabel.textColor = .black
|
||||
|
||||
addSubview(iconView)
|
||||
addSubview(titleLabel)
|
||||
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(AppSpacing.md)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(32)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(AppSpacing.sm)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user