diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatButtleCollectionCell.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatButtleCollectionCell.swift index 2fe613f..e9c659e 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatButtleCollectionCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatButtleCollectionCell.swift @@ -6,9 +6,286 @@ // import UIKit +import SnapKit -class ChatButtleCollectionCell: UICollectionViewCell { +struct SubButtleRow: RowModel { + let subItems: [ChatButtleRow] + var cellReuseID: String { "ChatButtleCollectionCell" } + func cellHeight(tableWidth: CGFloat) -> CGFloat { + let maxDisplayCount = 4 + let displayCount = min(subItems.count, maxDisplayCount) + return CGFloat(displayCount) * 58 // 每行40高度 + } +} + +// MARK: - 数据模型 +struct ChatButtleItem { + let id: String + let name: String + let price: Int? + enum Kind { case `default`, vip, premium } + enum Status { case selected, available, locked, gettable } + let kind: Kind + let status: Status +} + +// MARK: - RowModel +struct ChatButtleRow: RowModel { + let id: String + let name: String + let price: Int? + enum Kind { case `default`, vip, premium } + enum Status { case selected, available, locked, gettable } + let kind: Kind + let status: Status + let items: [ChatButtleRow] = [] + var cellReuseID: String { "ChatButtleCollectionCell" } + func cellHeight(tableWidth: CGFloat) -> CGFloat { + let columns: CGFloat = 3 + let spacing: CGFloat = 8 + let horizontalPadding: CGFloat = 40 // container 左右各 20 + let availableWidth = tableWidth - horizontalPadding + _ = (availableWidth - spacing * (columns - 1)) / columns // width 未用到,但保留用于后续等比计算 + let itemHeight: CGFloat = 120 + let rows = ceil(CGFloat(items.count) / columns) + let displayRows = min(rows, 4.5) + let total = displayRows * itemHeight + (displayRows - 1) * spacing + 5 + return total + } +} + +// MARK: - 容器 Cell +class ChatButtleCollectionCell: UITableViewCell, CellConfigurable { + private var items: [ChatButtleRow] = [] + private var selectedId: String? + private let widthItem: CGFloat = ((UIScreen.width * 0.84 - 60.0) / 3.0 - 1.0) + private var heightConstraint: Constraint? + + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.minimumInteritemSpacing = 10 + layout.minimumLineSpacing = 10 + layout.itemSize = CGSizeMake(widthItem, widthItem * 110.0 / 87.0) + layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) + layout.scrollDirection = .vertical + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.delegate = self + cv.dataSource = self + cv.showsVerticalScrollIndicator = false + cv.register(ChatButtleCard.self, forCellWithReuseIdentifier: "ChatButtleCard") + cv.layer.cornerRadius = 15 + cv.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + return cv + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + backgroundColor = .clear + selectionStyle = .none + contentView.addSubview(collectionView) + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.bottom.equalToSuperview().inset(2.5) + heightConstraint = make.height.equalTo(0).constraint + } + } + + func configure(with row: RowModel) { + guard let r = row as? SubButtleRow else { return } + items = r.subItems + if let first = items.first(where: { $0.status == .selected || $0.status == .available }) { + selectedId = first.id + } + collectionView.reloadData() + updateHeight() + } + + private func updateHeight() { + let columns: CGFloat = 3 + let spacing: CGFloat = 10 + let availableWidth = UIScreen.width * 0.84 - 60.0 + _ = (availableWidth - spacing * (columns - 1)) / columns + let itemHeight: CGFloat = widthItem * 110.0 / 87.0 + let rows = ceil(CGFloat(items.count) / columns) + let displayRows = min(rows, 4.5) + let total = displayRows * itemHeight + (displayRows - 1) * spacing + 5 + heightConstraint?.update(offset: total) + layoutIfNeeded() + } + + override func layoutSubviews() { + super.layoutSubviews() + updateHeight() + } +} + +// MARK: - CollectionView +extension ChatButtleCollectionCell: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { items.count } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ChatButtleCard", for: indexPath) as! ChatButtleCard + let item = items[indexPath.item] + cell.configure(with: item, selected: item.id == selectedId) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let item = items[indexPath.item] + guard item.status == .available || item.status == .gettable else { return } + selectedId = item.id + collectionView.reloadData() + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let columns: CGFloat = 3 + let spacing: CGFloat = 10 + let availableWidth = UIScreen.width * 0.84 - 60.0 + let itemWidth = (availableWidth - spacing * (columns - 1)) / columns + return CGSize(width: itemWidth, height: widthItem * 110.0 / 87.0) + } +} + +// MARK: - 卡片 +//class ChatButtleCard: UICollectionViewCell { +// private lazy var container: UIView = { +// let v = UIView() +// v.backgroundColor = UIColor(hex: "#1A1A2E") +// v.layer.cornerRadius = 12 +// return v +// }() +// private lazy var bubble: UIView = { +// let v = UIView() +// v.backgroundColor = .white +// v.layer.cornerRadius = 20 +// return v +// }() +// private lazy var hiLabel: UILabel = { +// let l = UILabel() +// l.text = "hi" +// l.textColor = .black +// l.font = UIFont.systemFont(ofSize: 14, weight: .medium) +// return l +// }() +// private lazy var title: UILabel = { +// let l = UILabel() +// l.textColor = .white +// l.font = UIFont.systemFont(ofSize: 12, weight: .medium) +// l.textAlignment = .center +// return l +// }() +// private lazy var vipBadge: UILabel = { +// let l = UILabel() +// l.text = "VIP" +// l.textColor = .black +// l.font = UIFont.systemFont(ofSize: 10, weight: .bold) +// l.backgroundColor = UIColor(hex: "#FFD700") +// l.textAlignment = .center +// l.layer.cornerRadius = 8 +// l.clipsToBounds = true +// l.isHidden = true +// return l +// }() +// private lazy var lockIcon: UIImageView = { +// let v = UIImageView(image: UIImage(systemName: "lock.fill")) +// v.tintColor = UIColor(hex: "#999999") +// v.isHidden = true +// return v +// }() +// private lazy var getButton: UIButton = { +// let b = UIButton(type: .custom) +// b.setTitle("Get", for: .normal) +// b.setTitleColor(.white, for: .normal) +// b.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold) +// b.backgroundColor = UIColor(hex: "#8B5CF6") +// b.layer.cornerRadius = 8 +// b.isHidden = true +// return b +// }() +// private lazy var highlight: UIView = { +// let v = UIView() +// v.layer.borderWidth = 2 +// v.layer.borderColor = UIColor(hex: "#8B5CF6").cgColor +// v.layer.cornerRadius = 12 +// v.isHidden = true +// return v +// }() +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// setup() +// } +// required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } +// +// private func setup() { +// contentView.addSubview(container) +// container.addSubview(bubble) +// container.addSubview(hiLabel) +// container.addSubview(title) +// container.addSubview(vipBadge) +// container.addSubview(lockIcon) +// container.addSubview(getButton) +// container.addSubview(highlight) +// +// container.snp.makeConstraints { make in make.edges.equalToSuperview() } +// bubble.snp.makeConstraints { make in +// make.centerX.equalToSuperview() +// make.centerY.equalToSuperview().offset(-10) +// make.width.height.equalTo(40) +// } +// hiLabel.snp.makeConstraints { make in make.center.equalTo(bubble) } +// title.snp.makeConstraints { make in +// make.centerX.equalToSuperview() +// make.bottom.equalToSuperview().offset(-12) +// make.left.right.equalToSuperview().inset(6) +// } +// vipBadge.snp.makeConstraints { make in +// make.top.left.equalToSuperview().offset(8) +// make.width.equalTo(24) +// make.height.equalTo(16) +// } +// lockIcon.snp.makeConstraints { make in +// make.top.right.equalToSuperview().inset(8) +// make.width.height.equalTo(12) +// } +// getButton.snp.makeConstraints { make in +// make.centerX.equalToSuperview() +// make.bottom.equalToSuperview().offset(-8) +// make.width.equalTo(60) +// make.height.equalTo(24) +// } +// highlight.snp.makeConstraints { make in make.edges.equalToSuperview() } +// } +// +// func configure(with item: ChatButtleItem, selected: Bool) { +// title.text = item.name +// vipBadge.isHidden = item.kind != .vip +// lockIcon.isHidden = item.status != .locked +// getButton.isHidden = item.status != .gettable +// highlight.isHidden = !selected && item.status != .gettable +// if item.kind == .premium { title.textColor = UIColor(hex: "#0066FF") } else if item.kind == .vip { title.textColor = UIColor(hex: "#FF8C00") } else { title.textColor = .white } +// title.isHidden = item.status == .gettable +// } +//} + +// +// ChatButtleCollectionCell.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/10/30. +// + +class ChatButtleCard: UICollectionViewCell { + lazy var statusImgView: UIImageView = { let imgView = UIImageView() imgView.backgroundColor = .blue @@ -43,6 +320,7 @@ class ChatButtleCollectionCell: UICollectionViewCell { lab.text = "hi" lab.textColor = UIColor(hex: "#333333") lab.font = UIFont.systemFont(ofSize: 14) + lab.textAlignment = .center return lab }() @@ -56,6 +334,7 @@ class ChatButtleCollectionCell: UICollectionViewCell { lab.font = UIFont.boldSystemFont(ofSize: 12) lab.text = "Default" lab.textColor = .white + lab.textAlignment = .center return lab }() @@ -72,7 +351,7 @@ class ChatButtleCollectionCell: UICollectionViewCell { let stackView = UIStackView(arrangedSubviews: [stoneImgView, stoneLa]) stackView.axis = .horizontal stackView.spacing = 5 - stackView.alignment = .center + stackView.alignment = .fill stackView.distribution = .fill return stackView }() @@ -92,7 +371,19 @@ class ChatButtleCollectionCell: UICollectionViewCell { } + func configure(with item: ChatButtleRow, selected: Bool) { + nameLab.text = item.name + vipImgView.isHidden = item.kind != .vip + lockImgView.isHidden = item.status != .locked + getBtn.isHidden = item.status != .gettable +// highlight.isHidden = !selected && item.status != .gettable + if item.kind == .premium { nameLab.textColor = UIColor(hex: "#0066FF") } else if item.kind == .vip { nameLab.textColor = UIColor(hex: "#FF8C00") } else { nameLab.textColor = .white } + nameLab.isHidden = item.status == .gettable + } + func setupViews() { + self.backgroundColor = .gray + self.cornerRadius = 10 contentView.addSubview(vipImgView) contentView.addSubview(statusImgView) contentView.addSubview(stoneStackView) @@ -122,7 +413,7 @@ class ChatButtleCollectionCell: UICollectionViewCell { } hiLab.snp.makeConstraints { make in - make.centerX.centerX.equalToSuperview() + make.centerX.centerY.equalToSuperview() } lockImgView.snp.makeConstraints { make in diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift index 7239bc4..4658530 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift @@ -16,18 +16,20 @@ struct ImageRow: RowModel { let showArrow: Bool let showSwitch: Bool let subItems: [ImageRow]? // 子项列表 + let buttleItems: [ChatButtleRow]? var isExpanded: Bool = false // 展开状态 var cellReuseID: String { "ChatSwipeCell" } func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 } - init(icon: String, title: String, showAvatar: Bool = false, showArrow: Bool = false, showSwitch: Bool = false, subItems: [ImageRow]? = nil) { + init(icon: String, title: String, showAvatar: Bool = false, showArrow: Bool = false, showSwitch: Bool = false, subItems: [ImageRow]? = nil, buttleItems: [ChatButtleRow]? = nil) { self.icon = icon self.title = title self.showAvatar = showAvatar self.showArrow = showArrow self.showSwitch = showSwitch self.subItems = subItems + self.buttleItems = buttleItems } } @@ -92,7 +94,7 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable { updateContainerHeight(50) } - private func updateArrowState(isExpanded: Bool) { + func updateArrowState(isExpanded: Bool) { UIView.animate(withDuration: 0.3) { [weak self] in self?.arrowImgView.transform = isExpanded ? CGAffineTransform(rotationAngle: .pi / 2.0) : .identity } diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift index c031428..a1e3431 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift @@ -35,15 +35,29 @@ class ChatSettingSwipeView: CLContainer { // ImageRow(icon: "role_exchange_mode", title: "Sub Item 10", showAvatar: false, showArrow: false, showSwitch: false) ]) - let buttleRow = ImageRow(icon: "role_chat_buttle", title: "Chat buttle", showAvatar: false, showArrow: true, showSwitch: false, subItems: [ - + let buttleRow = ImageRow(icon: "role_chat_buttle", title: "Chat buttle", showAvatar: false, showArrow: true, showSwitch: false, buttleItems: [ + ChatButtleRow(id: "1", name: "Default", price: nil, kind: .default, status: .selected), + ChatButtleRow(id: "2", name: "Name1", price: nil, kind: .vip, status: .available), + ChatButtleRow(id: "3", name: "Name2", price: nil, kind: .premium, status: .available), + // Row 2 + ChatButtleRow(id: "4", name: "Name3", price: nil, kind: .vip, status: .locked), + ChatButtleRow(id: "5", name: "Name4", price: nil, kind: .vip, status: .locked), + ChatButtleRow(id: "6", name: "Name5", price: nil, kind: .vip, status: .locked), + // Row 3 + ChatButtleRow(id: "7", name: "Name6", price: 20, kind: .premium, status: .locked), + ChatButtleRow(id: "8", name: "Name7", price: 20, kind: .premium, status: .locked), + ChatButtleRow(id: "9", name: "Name8", price: 20, kind: .premium, status: .gettable), + // Row 4 +// ChatButtleRow(id: "10", name: "Name9", price: 20, kind: .premium, status: .locked), +// ChatButtleRow(id: "11", name: "Name10", price: 20, kind: .premium, status: .locked), +// ChatButtleRow(id: "12", name: "Name11", price: 20, kind: .premium, status: .locked) ]) rows = [ [modelRow, ImageRow(icon: "role_text_mode", title: "Short Text Mode", showAvatar: false, showArrow: false, showSwitch: true)], [ImageRow(icon: "role_voice", title: "Voice actor", showAvatar: true, showArrow: true, showSwitch: false), ImageRow(icon: "role_talk", title: "Play dialogue only", showAvatar: false, showArrow: false, showSwitch: true)], [TokenRow(count: "2500")], - [FontRow(count: "20", icon: "role_font", title: "Font size"), ImageRow(icon: "role_chat_mode", title: "Chat Mode", showAvatar: false, showArrow: true, showSwitch: false), ImageRow(icon: "role_chat_buttle", title: "Chat buttle", showAvatar: false, showArrow: true, showSwitch: false)], + [FontRow(count: "20", icon: "role_font", title: "Font size"), ImageRow(icon: "role_chat_mode", title: "Chat Mode", showAvatar: false, showArrow: true, showSwitch: false), buttleRow], [BackgroundRow(count: 50)], [HistoryRow(time: "", icon: "", title: "", itemCount: 30)] ] @@ -96,6 +110,7 @@ class ChatSettingSwipeView: CLContainer { tableView.register(ChatBackgroundCell.self, forCellReuseIdentifier: "ChatBackgroundCell") tableView.register(ChatHistoryCell.self, forCellReuseIdentifier: "ChatHistoryCell") tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell") + tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell") tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) return tableView }() @@ -176,42 +191,70 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let imageRow = rows[indexPath.section][indexPath.row] as? ImageRow, - imageRow.showArrow, - let subItems = imageRow.subItems, - !subItems.isEmpty else { + imageRow.showArrow else { return } + // 统一处理:优先判断 Chat buttle(使用集合视图) + if imageRow.title == "Chat buttle", let subItems = imageRow.buttleItems, !subItems.isEmpty { + tableView.deselectRow(at: indexPath, animated: true) + let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false + expandedStates[indexPath.section, default: [:]][indexPath.row] = !isExpanded + if !isExpanded { + // 展开:插入 ChatButtleRow + let subItemsRow = SubButtleRow(subItems: subItems) + rows[indexPath.section].insert(subItemsRow, at: indexPath.row + 1) + + let insertIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) + tableView.insertRows(at: [insertIndexPath], with: .fade) + if let cell = tableView.cellForRow(at: indexPath) as? ChatSwipeCell { + cell.updateArrowState(isExpanded: true) + } + } else { + // 折叠 + if indexPath.row + 1 < rows[indexPath.section].count, + rows[indexPath.section][indexPath.row + 1] is SubButtleRow { + rows[indexPath.section].remove(at: indexPath.row + 1) + + let deleteIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) + tableView.deleteRows(at: [deleteIndexPath], with: .fade) + if let cell = tableView.cellForRow(at: indexPath) as? ChatSwipeCell { + cell.updateArrowState(isExpanded: false) + } + } + } return } - // 切换展开状态 - let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false - expandedStates[indexPath.section, default: [:]][indexPath.row] = !isExpanded - - // 插入或删除SubItemsContainerCell - if !isExpanded { - // 展开:插入SubItemsContainerCell - let subItemsRow = SubItemsRow(subItems: subItems) - rows[indexPath.section].insert(subItemsRow, at: indexPath.row + 1) + if let subItems = imageRow.subItems, !subItems.isEmpty { + // 切换展开状态 + let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false + expandedStates[indexPath.section, default: [:]][indexPath.row] = !isExpanded - let insertIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) - tableView.beginUpdates() - tableView.insertRows(at: [insertIndexPath], with: .fade) - tableView.endUpdates() - - // 更新箭头状态 - tableView.reloadRows(at: [indexPath], with: .none) - } else { - // 折叠:删除SubItemsContainerCell - if indexPath.row + 1 < rows[indexPath.section].count, - rows[indexPath.section][indexPath.row + 1] is SubItemsRow { - rows[indexPath.section].remove(at: indexPath.row + 1) + // 插入或删除SubItemsContainerCell + if !isExpanded { + // 展开:插入SubItemsContainerCell + let subItemsRow = SubItemsRow(subItems: subItems) + rows[indexPath.section].insert(subItemsRow, at: indexPath.row + 1) - let deleteIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) + let insertIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) tableView.beginUpdates() - tableView.deleteRows(at: [deleteIndexPath], with: .fade) + tableView.insertRows(at: [insertIndexPath], with: .fade) tableView.endUpdates() // 更新箭头状态 tableView.reloadRows(at: [indexPath], with: .none) + } else { + // 折叠:删除SubItemsContainerCell + if indexPath.row + 1 < rows[indexPath.section].count, + rows[indexPath.section][indexPath.row + 1] is SubItemsRow { + rows[indexPath.section].remove(at: indexPath.row + 1) + + let deleteIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) + tableView.beginUpdates() + tableView.deleteRows(at: [deleteIndexPath], with: .fade) + tableView.endUpdates() + + // 更新箭头状态 + tableView.reloadRows(at: [indexPath], with: .none) + } } } } @@ -227,6 +270,14 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let row = rows[indexPath.section][indexPath.row] + // 处理ChatButtleRow + if let buttleRow = row as? SubButtleRow { + let cell = tableView.dequeueReusableCell(withIdentifier: "ChatButtleCollectionCell", for: indexPath) as! ChatButtleCollectionCell + cell.selectionStyle = .none + cell.configure(with: buttleRow) + return cell + } + // 处理SubItemsRow if let subItemsRow = row as? SubItemsRow { let cell = tableView.dequeueReusableCell(withIdentifier: "SubItemsContainerCell", for: indexPath) as! SubItemsContainerCell