// // FeedbackView.swift // QuickLocation // import UIKit final class FeedbackView: UIView { let navView = BaseNavigationView(title: "问题反馈") let detailTextView = UITextView() let contactTextField = UITextField() let saveBtn = UIButton(type: .custom) private(set) var categoryButtons: [UIButton] = [] var onAddImage: (() -> Void)? var onDeleteImage: ((Int) -> Void)? private let scrollView = UIScrollView() private let contentView = UIView() private let detailPlaceholderLabel = UILabel() private let countLabel = UILabel() private let imagesCollectionView: UICollectionView private var images: [UIImage] = [] override init(frame: CGRect) { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = 8 layout.minimumInteritemSpacing = 8 layout.itemSize = CGSize(width: 100, height: 100) imagesCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) super.init(frame: frame) backgroundColor = UIColor(hexStr: "#F8F8F8") setupUI() updateCategory(selectedIndex: 0) updateDetail(text: "") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { addSubview(navView) navView.layoutChain.top().edgesHorzontal().height(kNaviHeight) saveBtn.setTitle("保存", for: .normal) saveBtn.setTitleColor(.white, for: .normal) saveBtn.titleLabel?.font = FontManager.boboBold(18) saveBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) saveBtn.layer.cornerRadius = 24 saveBtn.clipsToBounds = true addSubview(saveBtn) saveBtn.layoutChain .left(28) .right(28) .bottom(kSafeBottomMargin + 22) .height(56) scrollView.alwaysBounceVertical = false scrollView.keyboardDismissMode = .onDrag scrollView.showsVerticalScrollIndicator = false addSubview(scrollView) scrollView.layoutChain .topToBottomOfView(navView) .edgesHorzontal() .bottomToTopOfView(saveBtn, offset: -12) scrollView.addSubview(contentView) contentView.layoutChain.edges().widthToView(scrollView) let typeTitle = makeSectionTitle(required: true, title: "您想反馈的功能类型") contentView.addSubview(typeTitle) typeTitle.layoutChain.top(14).left(16).right(16) let titles = ["功能问题", "优化建议", "其他"] categoryButtons = titles.enumerated().map { index, title in let button = UIButton(type: .custom) button.tag = index button.setTitle(title, for: .normal) button.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium) button.layer.cornerRadius = 12 button.layer.borderWidth = 1 contentView.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false return button } let firstButton = categoryButtons[0] let secondButton = categoryButtons[1] let thirdButton = categoryButtons[2] NSLayoutConstraint.activate([ firstButton.topAnchor.constraint(equalTo: typeTitle.bottomAnchor, constant: 12), firstButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), firstButton.heightAnchor.constraint(equalToConstant: 34), secondButton.topAnchor.constraint(equalTo: firstButton.topAnchor), secondButton.leadingAnchor.constraint(equalTo: firstButton.trailingAnchor, constant: 16), secondButton.widthAnchor.constraint(equalTo: firstButton.widthAnchor), secondButton.heightAnchor.constraint(equalTo: firstButton.heightAnchor), thirdButton.topAnchor.constraint(equalTo: firstButton.topAnchor), thirdButton.leadingAnchor.constraint(equalTo: secondButton.trailingAnchor, constant: 16), thirdButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), thirdButton.widthAnchor.constraint(equalTo: firstButton.widthAnchor), thirdButton.heightAnchor.constraint(equalTo: firstButton.heightAnchor) ]) let detailTitle = makeSectionTitle(required: true, title: "请补充详细问题或意见") contentView.addSubview(detailTitle) detailTitle.layoutChain .topToBottomOfView(firstButton, offset: 20) .left(16) .right(16) let detailContainer = UIView() detailContainer.backgroundColor = .white detailContainer.layer.cornerRadius = 16 contentView.addSubview(detailContainer) detailContainer.layoutChain .topToBottomOfView(detailTitle, offset: 12) .left(16) .right(16) .height(150) detailTextView.backgroundColor = .clear detailTextView.font = .systemFont(ofSize: 14, weight: .medium) detailTextView.textColor = UIColor(hexStr: "#293445") detailTextView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 28, right: 10) detailTextView.textContainer.lineFragmentPadding = 0 detailTextView.isScrollEnabled = true detailTextView.returnKeyType = .default detailContainer.addSubview(detailTextView) detailTextView.layoutChain.edges() detailPlaceholderLabel.text = "请在这里输入内容" detailPlaceholderLabel.font = .systemFont(ofSize: 14, weight: .medium) detailPlaceholderLabel.textColor = UIColor(hexStr: "#B5B5B5") detailPlaceholderLabel.isUserInteractionEnabled = false detailContainer.addSubview(detailPlaceholderLabel) detailPlaceholderLabel.layoutChain.top(12).left(14).right(14) countLabel.font = .systemFont(ofSize: 13, weight: .medium) countLabel.textColor = UIColor(hexStr: "#767676") countLabel.textAlignment = .right detailContainer.addSubview(countLabel) countLabel.layoutChain.right(14).bottom(10) let imageTitle = makeSectionTitle( required: false, title: "请提供相关问题的图片", detail: "(选填,最多可上传三张)" ) contentView.addSubview(imageTitle) imageTitle.layoutChain .topToBottomOfView(detailContainer, offset: 18) .left(16) .right(16) imagesCollectionView.backgroundColor = .clear imagesCollectionView.isScrollEnabled = false imagesCollectionView.showsHorizontalScrollIndicator = false imagesCollectionView.dataSource = self imagesCollectionView.delegate = self imagesCollectionView.register( FeedbackImageCell.self, forCellWithReuseIdentifier: FeedbackImageCell.reuseIdentifier ) imagesCollectionView.register( FeedbackAddImageCell.self, forCellWithReuseIdentifier: FeedbackAddImageCell.reuseIdentifier ) contentView.addSubview(imagesCollectionView) imagesCollectionView.layoutChain .topToBottomOfView(imageTitle, offset: 12) .left(16) .right(16) .height(100) let contactTitle = makeSectionTitle( required: false, title: "联系方式", detail: "(选填,可以留下您的手机号、微信、邮箱)" ) contentView.addSubview(contactTitle) contactTitle.layoutChain .topToBottomOfView(imagesCollectionView, offset: 18) .left(16) .right(16) contactTextField.backgroundColor = .white contactTextField.layer.cornerRadius = 20 contactTextField.font = .systemFont(ofSize: 14, weight: .medium) contactTextField.textColor = UIColor(hexStr: "#293445") contactTextField.returnKeyType = .done contactTextField.clearButtonMode = .whileEditing contactTextField.attributedPlaceholder = NSAttributedString( string: "请输入您的联系方式", attributes: [ .foregroundColor: UIColor(hexStr: "#B5B5B5"), .font: UIFont.systemFont(ofSize: 14, weight: .medium) ] ) contactTextField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 1)) contactTextField.leftViewMode = .always contactTextField.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 1)) contactTextField.rightViewMode = .unlessEditing contentView.addSubview(contactTextField) contactTextField.layoutChain .topToBottomOfView(contactTitle, offset: 12) .left(16) .right(16) .height(40) .bottom(24) } private func makeSectionTitle(required: Bool, title: String, detail: String? = nil) -> UILabel { let label = UILabel() label.numberOfLines = 0 let text = NSMutableAttributedString() if required { text.append(NSAttributedString( string: "*", attributes: [ .font: UIFont.systemFont(ofSize: 17, weight: .medium), .foregroundColor: UIColor(hexStr: "#F64545") ] )) } text.append(NSAttributedString( string: title, attributes: [ .font: UIFont.systemFont(ofSize: 16, weight: .medium), .foregroundColor: UIColor(hexStr: "#293445") ] )) if let detail { text.append(NSAttributedString( string: detail, attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .medium), .foregroundColor: UIColor(hexStr: "#B5B5B5") ] )) } label.attributedText = text return label } func updateCategory(selectedIndex: Int) { for (index, button) in categoryButtons.enumerated() { let isSelected = index == selectedIndex button.backgroundColor = isSelected ? UIColor(hexStr: "#08B2F5") : .clear button.layer.borderColor = UIColor( hexStr: isSelected ? "#08B2F5" : "#D6D6D6" ).cgColor button.setTitleColor( isSelected ? .white : UIColor(hexStr: "#293445"), for: .normal ) } } func updateDetail(text: String) { detailPlaceholderLabel.isHidden = !text.isEmpty countLabel.text = "\(text.count)/200" } func updateImages(_ images: [UIImage]) { self.images = Array(images.prefix(3)) imagesCollectionView.reloadData() } func setSubmitting(_ submitting: Bool) { saveBtn.isEnabled = !submitting saveBtn.alpha = submitting ? 0.65 : 1 } } extension FeedbackView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath ) -> CGSize { let width = min(100, floor((collectionView.bounds.width - 16) / 3)) return CGSize(width: width, height: 100) } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { images.count + (images.count < 3 ? 1 : 0) } func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { if indexPath.item < images.count { guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: FeedbackImageCell.reuseIdentifier, for: indexPath ) as? FeedbackImageCell else { return UICollectionViewCell() } cell.configure(image: images[indexPath.item]) cell.onDelete = { [weak self, weak cell] in guard let self, let cell, let currentIndexPath = collectionView.indexPath(for: cell) else { return } self.onDeleteImage?(currentIndexPath.item) } return cell } guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: FeedbackAddImageCell.reuseIdentifier, for: indexPath ) as? FeedbackAddImageCell else { return UICollectionViewCell() } return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard indexPath.item == images.count, images.count < 3 else { return } onAddImage?() } } private final class FeedbackImageCell: UICollectionViewCell { static let reuseIdentifier = "FeedbackImageCell" var onDelete: (() -> Void)? private let imageView = UIImageView() private let deleteButton = UIButton(type: .custom) override init(frame: CGRect) { super.init(frame: frame) imageView.contentMode = .scaleAspectFill imageView.layer.cornerRadius = 16 imageView.clipsToBounds = true contentView.addSubview(imageView) imageView.layoutChain.edges() let symbolConfig = UIImage.SymbolConfiguration(pointSize: 10, weight: .bold) deleteButton.setImage(UIImage(systemName: "xmark", withConfiguration: symbolConfig), for: .normal) deleteButton.tintColor = UIColor(hexStr: "#293445") deleteButton.backgroundColor = UIColor.white.withAlphaComponent(0.92) deleteButton.layer.cornerRadius = 11 deleteButton.layer.shadowColor = UIColor.black.withAlphaComponent(0.12).cgColor deleteButton.layer.shadowOpacity = 1 deleteButton.layer.shadowRadius = 3 deleteButton.layer.shadowOffset = CGSize(width: 0, height: 1) deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside) contentView.addSubview(deleteButton) deleteButton.layoutChain.top(6).right(6).width(22).height(22) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() imageView.image = nil onDelete = nil } func configure(image: UIImage) { imageView.image = image } @objc private func deleteTapped() { onDelete?() } } private final class FeedbackAddImageCell: UICollectionViewCell { static let reuseIdentifier = "FeedbackAddImageCell" private let addImageView = UIImageView(image: UIImage(named: "Mine/feedback_add")) override init(frame: CGRect) { super.init(frame: frame) contentView.backgroundColor = .white contentView.layer.cornerRadius = 16 addImageView.contentMode = .scaleAspectFit contentView.addSubview(addImageView) addImageView.layoutChain.centerX().centerY().width(48).height(48) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }