diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatModeContainerCell.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatModeContainerCell.swift new file mode 100644 index 0000000..f4edbc8 --- /dev/null +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatModeContainerCell.swift @@ -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 + } + } +} 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 4658530..cdd6349 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift @@ -17,12 +17,13 @@ struct ImageRow: RowModel { let showSwitch: Bool let subItems: [ImageRow]? // 子项列表 let buttleItems: [ChatButtleRow]? + let chatModeItems: [ChatModeItem]? 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, 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.title = title self.showAvatar = showAvatar @@ -30,6 +31,7 @@ struct ImageRow: RowModel { self.showSwitch = showSwitch self.subItems = subItems self.buttleItems = buttleItems + self.chatModeItems = chatModeItems } } 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 a1e3431..ed62913 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift @@ -57,7 +57,7 @@ class ChatSettingSwipeView: CLContainer { [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), 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)], [HistoryRow(time: "", icon: "", title: "", itemCount: 30)] ] @@ -111,6 +111,7 @@ class ChatSettingSwipeView: CLContainer { tableView.register(ChatHistoryCell.self, forCellReuseIdentifier: "ChatHistoryCell") tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell") tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell") + tableView.register(ChatModeContainerCell.self, forCellReuseIdentifier: "ChatModeContainerCell") tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) return tableView }() @@ -192,6 +193,45 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let imageRow = rows[indexPath.section][indexPath.row] as? ImageRow, 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(使用集合视图) if imageRow.title == "Chat buttle", let subItems = imageRow.buttleItems, !subItems.isEmpty { tableView.deselectRow(at: indexPath, animated: true) @@ -270,6 +310,14 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 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 if let buttleRow = row as? SubButtleRow { 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 { 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) + ] + } }