chat setting chat mode展开折叠
This commit is contained in:
parent
c4a34fd419
commit
4c4c578d0b
|
|
@ -0,0 +1,250 @@
|
||||||
|
//
|
||||||
|
// ChatModeContainerCell.swift
|
||||||
|
// Visual_Novel_iOS
|
||||||
|
//
|
||||||
|
// Created by mh on 2025/01/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SnapKit
|
||||||
|
|
||||||
|
// MARK: - 数据模型
|
||||||
|
struct ChatModeItem {
|
||||||
|
let id: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
let isVip: Bool
|
||||||
|
var isSelected: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - RowModel
|
||||||
|
struct ChatModeRow: RowModel {
|
||||||
|
let items: [ChatModeItem]
|
||||||
|
var cellReuseID: String { "ChatModeContainerCell" }
|
||||||
|
|
||||||
|
func cellHeight(tableWidth: CGFloat) -> CGFloat {
|
||||||
|
let rowHeight: CGFloat = 58
|
||||||
|
return CGFloat(items.count) * rowHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ChatModeContainerCell
|
||||||
|
class ChatModeContainerCell: UITableViewCell, CellConfigurable {
|
||||||
|
|
||||||
|
private var items: [ChatModeItem] = []
|
||||||
|
private var selectedItemId: String?
|
||||||
|
private var tableViewHeightConstraint: Constraint?
|
||||||
|
|
||||||
|
private lazy var tableView: UITableView = {
|
||||||
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.backgroundColor = UIColor(hex: "#F5F5FF")
|
||||||
|
tableView.showsVerticalScrollIndicator = false
|
||||||
|
tableView.isScrollEnabled = false
|
||||||
|
tableView.register(ChatModeItemCell.self, forCellReuseIdentifier: "ChatModeItemCell")
|
||||||
|
tableView.estimatedRowHeight = 58
|
||||||
|
tableView.rowHeight = 58
|
||||||
|
tableView.contentInset = .zero
|
||||||
|
tableView.scrollIndicatorInsets = .zero
|
||||||
|
tableView.bounces = false
|
||||||
|
tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
tableView.layer.cornerRadius = 15
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupViews() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
selectionStyle = .none
|
||||||
|
|
||||||
|
contentView.addSubview(tableView)
|
||||||
|
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview().inset(20)
|
||||||
|
make.top.bottom.equalToSuperview().inset(2.5)
|
||||||
|
tableViewHeightConstraint = make.height.equalTo(0).constraint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(with row: RowModel) {
|
||||||
|
guard let modeRow = row as? ChatModeRow else { return }
|
||||||
|
items = modeRow.items
|
||||||
|
|
||||||
|
// 找到选中的item
|
||||||
|
if let selected = items.first(where: { $0.isSelected }) {
|
||||||
|
selectedItemId = selected.id
|
||||||
|
}
|
||||||
|
|
||||||
|
tableView.reloadData()
|
||||||
|
updateTableViewHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTableViewHeight() {
|
||||||
|
let rowHeight: CGFloat = 58
|
||||||
|
let totalHeight = CGFloat(items.count) * rowHeight
|
||||||
|
|
||||||
|
tableViewHeightConstraint?.update(offset: totalHeight)
|
||||||
|
layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDataSource & UITableViewDelegate
|
||||||
|
extension ChatModeContainerCell: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return items.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatModeItemCell", for: indexPath) as! ChatModeItemCell
|
||||||
|
let item = items[indexPath.row]
|
||||||
|
let isSelected = item.id == selectedItemId
|
||||||
|
cell.configure(with: item, isSelected: isSelected)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
let item = items[indexPath.row]
|
||||||
|
|
||||||
|
// 更新选中状态
|
||||||
|
selectedItemId = item.id
|
||||||
|
|
||||||
|
// 重新加载所有cell以更新选中状态
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ChatModeItemCell
|
||||||
|
class ChatModeItemCell: UITableViewCell {
|
||||||
|
|
||||||
|
private lazy var containerView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: "#F5F5FF")
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleLabel: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.textColor = UIColor(hex: "#333333")
|
||||||
|
lab.font = UIFont.systemFont(ofSize: 13)
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var vipBadge: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.text = "VIP"
|
||||||
|
lab.textColor = .black
|
||||||
|
lab.font = UIFont.systemFont(ofSize: 10, weight: .bold)
|
||||||
|
lab.backgroundColor = UIColor(hex: "#FFD700")
|
||||||
|
lab.textAlignment = .center
|
||||||
|
lab.layer.cornerRadius = 6
|
||||||
|
lab.clipsToBounds = true
|
||||||
|
lab.isHidden = true
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var subtitleLabel: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.textColor = UIColor(hex: "#9494C3")
|
||||||
|
lab.font = UIFont.systemFont(ofSize: 11)
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var selectionIndicator: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.layer.cornerRadius = 10
|
||||||
|
view.backgroundColor = UIColor(hex: "#333333")
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var selectedDot: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: "#00CC88")
|
||||||
|
view.layer.cornerRadius = 5
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupViews() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
selectionStyle = .none
|
||||||
|
|
||||||
|
contentView.addSubview(containerView)
|
||||||
|
containerView.addSubview(titleLabel)
|
||||||
|
containerView.addSubview(vipBadge)
|
||||||
|
containerView.addSubview(subtitleLabel)
|
||||||
|
containerView.addSubview(selectionIndicator)
|
||||||
|
selectionIndicator.addSubview(selectedDot)
|
||||||
|
|
||||||
|
containerView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview().inset(10)
|
||||||
|
make.top.bottom.equalToSuperview().inset(2.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(10)
|
||||||
|
make.top.equalToSuperview().offset(8)
|
||||||
|
make.right.lessThanOrEqualTo(vipBadge.snp.left).offset(-8)
|
||||||
|
}
|
||||||
|
|
||||||
|
vipBadge.snp.makeConstraints { make in
|
||||||
|
make.left.equalTo(titleLabel.snp.right).offset(8)
|
||||||
|
make.centerY.equalTo(titleLabel)
|
||||||
|
make.width.equalTo(24)
|
||||||
|
make.height.equalTo(14)
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitleLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalTo(titleLabel)
|
||||||
|
make.top.equalTo(titleLabel.snp.bottom).offset(2)
|
||||||
|
make.right.lessThanOrEqualTo(selectionIndicator.snp.left).offset(-10)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionIndicator.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.right.equalToSuperview().inset(10)
|
||||||
|
make.width.height.equalTo(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedDot.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.width.height.equalTo(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(with item: ChatModeItem, isSelected: Bool) {
|
||||||
|
titleLabel.text = item.title
|
||||||
|
subtitleLabel.text = item.subtitle
|
||||||
|
vipBadge.isHidden = !item.isVip
|
||||||
|
|
||||||
|
// 更新选中状态
|
||||||
|
if isSelected {
|
||||||
|
selectionIndicator.backgroundColor = UIColor(hex: "#00CC88")
|
||||||
|
selectedDot.isHidden = false
|
||||||
|
} else {
|
||||||
|
selectionIndicator.backgroundColor = UIColor(hex: "#333333")
|
||||||
|
selectedDot.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,12 +17,13 @@ struct ImageRow: RowModel {
|
||||||
let showSwitch: Bool
|
let showSwitch: Bool
|
||||||
let subItems: [ImageRow]? // 子项列表
|
let subItems: [ImageRow]? // 子项列表
|
||||||
let buttleItems: [ChatButtleRow]?
|
let buttleItems: [ChatButtleRow]?
|
||||||
|
let chatModeItems: [ChatModeItem]?
|
||||||
var isExpanded: Bool = false // 展开状态
|
var isExpanded: Bool = false // 展开状态
|
||||||
|
|
||||||
var cellReuseID: String { "ChatSwipeCell" }
|
var cellReuseID: String { "ChatSwipeCell" }
|
||||||
func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 }
|
func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 }
|
||||||
|
|
||||||
init(icon: String, title: String, showAvatar: Bool = false, showArrow: Bool = false, showSwitch: Bool = false, subItems: [ImageRow]? = nil, buttleItems: [ChatButtleRow]? = nil) {
|
init(icon: String, title: String, showAvatar: Bool = false, showArrow: Bool = false, showSwitch: Bool = false, subItems: [ImageRow]? = nil, buttleItems: [ChatButtleRow]? = nil, chatModeItems: [ChatModeItem]? = nil) {
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.title = title
|
self.title = title
|
||||||
self.showAvatar = showAvatar
|
self.showAvatar = showAvatar
|
||||||
|
|
@ -30,6 +31,7 @@ struct ImageRow: RowModel {
|
||||||
self.showSwitch = showSwitch
|
self.showSwitch = showSwitch
|
||||||
self.subItems = subItems
|
self.subItems = subItems
|
||||||
self.buttleItems = buttleItems
|
self.buttleItems = buttleItems
|
||||||
|
self.chatModeItems = chatModeItems
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class ChatSettingSwipeView: CLContainer {
|
||||||
[modelRow, ImageRow(icon: "role_text_mode", title: "Short Text Mode", showAvatar: false, showArrow: false, showSwitch: true)],
|
[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)],
|
[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")],
|
[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), buttleRow],
|
[FontRow(count: "20", icon: "role_font", title: "Font size"), ImageRow(icon: "role_chat_mode", title: "Chat Mode", showAvatar: false, showArrow: true, showSwitch: false, chatModeItems: createChatModeItems()), buttleRow],
|
||||||
[BackgroundRow(count: 50)],
|
[BackgroundRow(count: 50)],
|
||||||
[HistoryRow(time: "", icon: "", title: "", itemCount: 30)]
|
[HistoryRow(time: "", icon: "", title: "", itemCount: 30)]
|
||||||
]
|
]
|
||||||
|
|
@ -111,6 +111,7 @@ class ChatSettingSwipeView: CLContainer {
|
||||||
tableView.register(ChatHistoryCell.self, forCellReuseIdentifier: "ChatHistoryCell")
|
tableView.register(ChatHistoryCell.self, forCellReuseIdentifier: "ChatHistoryCell")
|
||||||
tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell")
|
tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell")
|
||||||
tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell")
|
tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell")
|
||||||
|
tableView.register(ChatModeContainerCell.self, forCellReuseIdentifier: "ChatModeContainerCell")
|
||||||
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
@ -192,6 +193,45 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
guard let imageRow = rows[indexPath.section][indexPath.row] as? ImageRow,
|
guard let imageRow = rows[indexPath.section][indexPath.row] as? ImageRow,
|
||||||
imageRow.showArrow else { return }
|
imageRow.showArrow else { return }
|
||||||
|
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
// 处理 Chat Mode 展开
|
||||||
|
if imageRow.title == "Chat Mode", let chatModeItems = imageRow.chatModeItems, !chatModeItems.isEmpty {
|
||||||
|
let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false
|
||||||
|
expandedStates[indexPath.section, default: [:]][indexPath.row] = !isExpanded
|
||||||
|
|
||||||
|
if !isExpanded {
|
||||||
|
// 展开:先更新箭头状态(数据源还未改变)
|
||||||
|
|
||||||
|
// 然后更新数据源并插入新行
|
||||||
|
let modeRow = ChatModeRow(items: chatModeItems)
|
||||||
|
rows[indexPath.section].insert(modeRow, at: indexPath.row + 1)
|
||||||
|
let insertIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
||||||
|
|
||||||
|
tableView.performBatchUpdates({
|
||||||
|
tableView.insertRows(at: [insertIndexPath], with: .fade)
|
||||||
|
tableView.reloadRows(at: [indexPath], with: .none)
|
||||||
|
}, completion: nil)
|
||||||
|
} else {
|
||||||
|
// 折叠:先更新箭头状态
|
||||||
|
|
||||||
|
// 然后删除数据源和行
|
||||||
|
if indexPath.row + 1 < rows[indexPath.section].count,
|
||||||
|
rows[indexPath.section][indexPath.row + 1] is ChatModeRow {
|
||||||
|
rows[indexPath.section].remove(at: indexPath.row + 1)
|
||||||
|
let deleteIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
||||||
|
|
||||||
|
tableView.performBatchUpdates({
|
||||||
|
tableView.deleteRows(at: [deleteIndexPath], with: .fade)
|
||||||
|
tableView.reloadRows(at: [indexPath], with: .none)
|
||||||
|
|
||||||
|
}, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 统一处理:优先判断 Chat buttle(使用集合视图)
|
// 统一处理:优先判断 Chat buttle(使用集合视图)
|
||||||
if imageRow.title == "Chat buttle", let subItems = imageRow.buttleItems, !subItems.isEmpty {
|
if imageRow.title == "Chat buttle", let subItems = imageRow.buttleItems, !subItems.isEmpty {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
@ -270,6 +310,14 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let row = rows[indexPath.section][indexPath.row]
|
let row = rows[indexPath.section][indexPath.row]
|
||||||
|
|
||||||
|
// 处理ChatModeRow
|
||||||
|
if let modeRow = row as? ChatModeRow {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatModeContainerCell", for: indexPath) as! ChatModeContainerCell
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
cell.configure(with: modeRow)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
// 处理ChatButtleRow
|
// 处理ChatButtleRow
|
||||||
if let buttleRow = row as? SubButtleRow {
|
if let buttleRow = row as? SubButtleRow {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatButtleCollectionCell", for: indexPath) as! ChatButtleCollectionCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatButtleCollectionCell", for: indexPath) as! ChatButtleCollectionCell
|
||||||
|
|
@ -326,4 +374,12 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
|
||||||
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||||
return CGFLOAT_MIN
|
return CGFLOAT_MIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 创建ChatMode示例数据
|
||||||
|
private func createChatModeItems() -> [ChatModeItem] {
|
||||||
|
return [
|
||||||
|
ChatModeItem(id: "1", title: "Dialogue Mode", subtitle: "Previous-generation large model", isVip: false, isSelected: true),
|
||||||
|
ChatModeItem(id: "2", title: "Immersive Mode", subtitle: "Previous-generation large model", isVip: true, isSelected: false)
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue