// // ChatModePickSheet.swift // Crush // // Created by Leon on 2025/8/17. // import UIKit /// 对话模型选择回调 typealias ChatModelSelectionCallback = (AIChatModel) -> Void /// 对话模型选择 class ChatModePickSheet: EGPopBaseView { var closeButton: EPIconTertiaryButton! var titleLabel: UILabel! // 滚动视图 var scrollView: UIScrollView! // 模型卡片视图数组 var modelCardViews: [ChatModelCardView] = [] var stayTunedLabel: CLLabel! // 数据源 private var chatModels: [AIChatModel] = [] private var currentSelectedModelCode: String? var cardHeight = 200.0//168.0 // 选择回调 var selectionCallback: ChatModelSelectionCallback? required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } init(currentSelectedModelCode: String? = nil) { self.currentSelectedModelCode = currentSelectedModelCode super.init(direction: .bottom) contentView.backgroundColor = .c.csbn // 初始高度,后续会根据数据动态调整 contentLength = 200 + UIWindow.safeAreaBottom setupViews() loadChatModels() } private func setupViews() { closeButton = { let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) contentView.addSubview(v) v.snp.makeConstraints { make in make.top.equalToSuperview().offset(20) make.size.equalTo(v.bgImageSize()) make.trailing.equalToSuperview().offset(-16) } return v }() titleLabel = { let v = UILabel() v.font = .t.ttm v.textColor = .text v.textAlignment = .center contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(24) make.trailing.equalToSuperview().offset(-24) make.top.equalToSuperview().offset(32) } return v }() scrollView = { let v = UIScrollView() v.showsVerticalScrollIndicator = false v.showsHorizontalScrollIndicator = false v.delegate = self contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview() make.trailing.equalToSuperview() make.top.equalTo(titleLabel.snp.bottom).offset(24) make.height.equalTo(cardHeight) // 每个卡片的高度 } return v }() stayTunedLabel = { let v = CLLabel() v.font = .t.tbs v.textColor = .c.ctsn contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(24) make.top.equalTo(scrollView.snp.bottom).offset(16) } return v }() titleLabel.text = "Dialog Model" stayTunedLabel.text = "Stay tuned for more models" } private func loadChatModels() { guard let models = AppDictManager.shared.chatModels, models.count > 0 else { // 如果没有数据,尝试加载 AppDictManager.shared.loadChatModelDict { [weak self] success in if success { self?.loadChatModels() } } return } chatModels = models setupModelCards() } private func setupModelCards() { // 清除之前的卡片 modelCardViews.forEach { $0.removeFromSuperview() } modelCardViews.removeAll() // 创建新的卡片 for (index, model) in chatModels.enumerated() { let cardView = ChatModelCardView() cardView.configure(with: model, isSelected: model.code == currentSelectedModelCode) cardView.tag = index cardView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(modelCardTapped(_:)))) scrollView.addSubview(cardView) modelCardViews.append(cardView) cardView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.width.equalTo(scrollView.snp.width) make.height.equalTo(cardHeight) // 每个卡片固定高度 if index == 0 { make.top.equalToSuperview() } else { make.top.equalTo(modelCardViews[index - 1].snp.bottom).offset(12) } if index == chatModels.count - 1 { make.bottom.equalToSuperview() } } } // 动态调整contentLength updateContentLength() // 延迟执行滚动操作,确保视图布局完成 // DispatchQueue.main.async { [weak self] in // self?.scrollToSelectedModel() // } } private func updateContentLength() { // 基础高度:标题 + 间距 + 底部标签 let baseHeight: CGFloat = 32 + 24 + 16 + 20 + UIWindow.safeAreaBottom// titleLabel top + spacing + stayTunedLabel + bottom margin // 卡片高度:每个卡片200 + 间距12(除了最后一个) let cardHeight: CGFloat = cardHeight let cardSpacing: CGFloat = 12 let totalCardHeight = CGFloat(chatModels.count) * cardHeight + CGFloat(max(0, chatModels.count - 1)) * cardSpacing // 滚动视图高度:根据卡片数量调整,最多显示2个卡片的高度 let maxVisibleHeight = min(totalCardHeight, cardHeight * 2 + cardSpacing) // 更新scrollView高度约束 scrollView.snp.updateConstraints { make in make.height.equalTo(maxVisibleHeight) } // 更新contentLength contentLength = baseHeight + maxVisibleHeight + UIWindow.safeAreaBottom // 更新布局 layoutIfNeeded() } private func scrollToSelectedModel() { guard let selectedCode = currentSelectedModelCode, let selectedIndex = chatModels.firstIndex(where: { $0.code == selectedCode }), selectedIndex < chatModels.count else { return } // 确保scrollView已经有正确的frame guard scrollView.frame.height > 0 else { // 如果frame还没有设置,再次延迟执行 DispatchQueue.main.async { [weak self] in self?.scrollToSelectedModel() } return } let offsetY = CGFloat(selectedIndex) * (cardHeight + 12) // 卡片高度 + 间距 scrollView.setContentOffset(CGPoint(x: 0, y: offsetY), animated: false) } @objc private func modelCardTapped(_ gesture: UITapGestureRecognizer) { guard let cardView = gesture.view as? ChatModelCardView, let index = modelCardViews.firstIndex(of: cardView), index < chatModels.count else { return } let selectedModel = chatModels[index] // 更新选中状态 modelCardViews.forEach { $0.setSelected(false) } cardView.setSelected(true) // 调用回调 selectionCallback?(selectedModel) // 关闭弹窗 dismiss() } } // MARK: - UIScrollViewDelegate extension ChatModePickSheet: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { // 垂直滚动不需要页面指示器逻辑 } } // MARK: - ChatModelCardView class ChatModelCardView: UIView { private var block: UIView! private var modelName: UILabel! private var modelDesc: UILabel! private var selectMark: UIImageView! private var innerBlock: UIView! private var queryButton: EPIconTertiaryButton! private var priceStackView: UIStackView! private var model: AIChatModel? override init(frame: CGRect) { super.init(frame: frame) setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { block = { let v = UIView() v.backgroundColor = .c.csen v.cornerRadius = 16 addSubview(v) v.snp.makeConstraints { make in // make.edges.equalToSuperview() make.leading.equalToSuperview().offset(24) make.trailing.equalToSuperview().offset(-24) make.top.bottom.equalToSuperview() } return v }() selectMark = { let v = UIImageView() v.image = UIImage(named: "checkmark_tick") v.isHidden = true block.addSubview(v) v.snp.makeConstraints { make in make.size.equalTo(CGSize(width: 20, height: 20)) make.top.equalToSuperview().offset(18) make.trailing.equalToSuperview().offset(-16) } return v }() modelName = { let v = CLLabel() v.font = .t.tts v.setContentCompressionResistancePriority(UILayoutPriority(749), for: .horizontal) block.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.top.equalToSuperview().offset(16) make.trailing.lessThanOrEqualTo(selectMark.snp.leading).offset(-8) } return v }() queryButton = { let v = EPIconTertiaryButton(radius: .round, iconSize: .xs10, iconCode: .question) v.addTarget(self, action: #selector(tapQueryButton), for: .touchUpInside) block.addSubview(v) v.snp.makeConstraints { make in make.leading.equalTo(modelName.snp.trailing).offset(4) make.centerY.equalTo(modelName) make.trailing.lessThanOrEqualToSuperview().offset(-16) } return v }() modelDesc = { let v = CLLabel() v.font = .t.tbs v.textColor = .c.ctsn block.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.trailing.equalToSuperview().offset(-16) make.top.equalTo(modelName.snp.bottom).offset(4) } return v }() innerBlock = { let v = UIView() v.backgroundColor = .c.csdn v.cornerRadius = 12 block.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.trailing.equalToSuperview().offset(-16) //make.top.equalTo(modelDesc.snp.bottom).offset(12) make.top.equalToSuperview().offset(76) make.height.equalTo(108) // 76 //make.bottom.equalToSuperview().offset(-16) } return v }() priceStackView = { let v = UIStackView() v.axis = .vertical v.spacing = 12 v.alignment = .leading innerBlock.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.trailing.equalToSuperview().offset(-12) make.top.equalToSuperview().offset(12) make.bottom.equalToSuperview().offset(-12) } return v }() } func configure(with model: AIChatModel, isSelected: Bool = false) { self.model = model modelName.text = model.name ?? "" modelDesc.text = model.description ?? "" // 清除之前的价格标签 priceStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } // 添加价格信息 if let price = model.textPrice { let coin = Coin(cents: price) let priceLabel = CLIconLabel() priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond") priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/Text Message" priceStackView.addArrangedSubview(priceLabel) } if let price = model.voicePrice { let coin = Coin(cents: price) let priceLabel = CLIconLabel() priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond") priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/Send or play voice" priceStackView.addArrangedSubview(priceLabel) } if let price = model.voiceChatPrice { let coin = Coin(cents: price) let priceLabel = CLIconLabel() priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond") priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/ 1 min Voice call" priceStackView.addArrangedSubview(priceLabel) } setSelected(isSelected) } func setSelected(_ selected: Bool) { selectMark.isHidden = !selected // block.layer.borderWidth = selected ? 2 : 0 // block.layer.borderColor = selected ? UIColor.c.cpn.cgColor : UIColor.clear.cgColor } @objc func tapQueryButton(){ let content = "*文本消息价格是指与角色进行文本消息对话的价格,含发送文本,发送图片,发送礼物;按条计算\n\n*发送语音消息价格是指与角色发送语音或者播放角色的语音的价格;按次计算\n\n*语音通话消息价格是指与角色进行语音电话对话的价格;按分钟计算" let alert = Alert(title: "Tips", text: content) let action1 = AlertAction(title: "Got it", actionStyle: .confirm) { } alert.addAction(action1) alert.show() } }