402 lines
14 KiB
Swift
402 lines
14 KiB
Swift
//
|
||
// 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()
|
||
}
|
||
}
|