From ef162eea2c41cbf9222e4cc1d7b1b023975d9776 Mon Sep 17 00:00:00 2001 From: mh <729263080@qq.com> Date: Wed, 29 Oct 2025 18:09:34 +0800 Subject: [PATCH] chat setting mode --- .../Setting/Cell/ChatSettingBaseCell.swift | 9 +- .../Chat/Setting/Cell/ChatSwipeCell.swift | 28 ++ .../Chat/Setting/Cell/SubModeCell.swift | 140 ++++++++ .../Setting/View/ChatSettingSwipeView.swift | 320 ++++++++++++++++-- 4 files changed, 476 insertions(+), 21 deletions(-) create mode 100644 Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/SubModeCell.swift diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSettingBaseCell.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSettingBaseCell.swift index d5f2a1e..20a7e7e 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSettingBaseCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSettingBaseCell.swift @@ -6,9 +6,12 @@ // import UIKit +import SnapKit class ChatSettingBaseCell: UITableViewCell { + var containerHeightConstraint: Constraint? + lazy var containerView: UIView = { let view = UIView() view.backgroundColor = UIColor(hex: "#F6F6F6") @@ -33,7 +36,11 @@ class ChatSettingBaseCell: UITableViewCell { containerView.snp.makeConstraints { make in make.left.right.equalToSuperview().inset(20) make.top.bottom.equalToSuperview().inset(2.5) - make.height.equalTo(45.0) + containerHeightConstraint = make.height.equalTo(45.0).constraint } } + + func updateContainerHeight(_ height: CGFloat) { + containerHeightConstraint?.update(offset: height) + } } 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 b27fe5c..7239bc4 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift @@ -6,6 +6,7 @@ // import UIKit +import SnapKit // icon title 声音头像 箭头 struct ImageRow: RowModel { @@ -14,8 +15,20 @@ struct ImageRow: RowModel { let showAvatar: Bool let showArrow: Bool let showSwitch: Bool + let subItems: [ImageRow]? // 子项列表 + 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) { + self.icon = icon + self.title = title + self.showAvatar = showAvatar + self.showArrow = showArrow + self.showSwitch = showSwitch + self.subItems = subItems + } } class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable { @@ -61,13 +74,28 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable { return stackView }() + private var currentRow: ImageRow? + func configure(with row: RowModel) { guard let row = row as? ImageRow else { return } + currentRow = row iconImgView.image = UIImage(named: row.icon) titleLab.text = row.title avatarView.isHidden = !row.showAvatar arrowImgView.isHidden = !row.showArrow switchControl.isHidden = !row.showSwitch + + // 更新箭头状态 + updateArrowState(isExpanded: row.isExpanded) + + // 更新container高度(固定为50) + updateContainerHeight(50) + } + + private func updateArrowState(isExpanded: Bool) { + UIView.animate(withDuration: 0.3) { [weak self] in + self?.arrowImgView.transform = isExpanded ? CGAffineTransform(rotationAngle: .pi / 2.0) : .identity + } } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/SubModeCell.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/SubModeCell.swift new file mode 100644 index 0000000..95ec1da --- /dev/null +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/SubModeCell.swift @@ -0,0 +1,140 @@ +//// +//// SubModeCell.swift +//// Visual_Novel_iOS +//// +//// Created by mh on 2025/10/29. +//// +// +//import UIKit +// +//// SubItemsRow - 用于标识子项容器cell +//struct SubItemsRow: RowModel { +// let subItems: [ImageRow] +// var cellReuseID: String { "SubItemsContainerCell" } +// func cellHeight(tableWidth: CGFloat) -> CGFloat { +// let maxDisplayCount = 4 +// let displayCount = min(subItems.count, maxDisplayCount) +// return CGFloat(displayCount) * 40 // 每行40高度 +// } +//} +// +//// SubItemsContainerCell - 子项容器cell +//class SubModeCell: UITableViewCell, CellConfigurable { +// +// private var subItems: [ImageRow] = [] +// private var tableViewHeightConstraint: Constraint? +// +// 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 = true +// tableView.isScrollEnabled = true +// tableView.register(SubItemCell.self, forCellReuseIdentifier: "SubItemCell") +// tableView.estimatedRowHeight = 40 +// tableView.rowHeight = 40 +// 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 subItemsRow = row as? SubItemsRow else { return } +// subItems = subItemsRow.subItems +// +// tableView.reloadData() +// updateTableViewHeight() +// } +// +// private func updateTableViewHeight() { +// let maxDisplayCount = 4 +// let displayCount = min(subItems.count, maxDisplayCount) +// let height = CGFloat(displayCount) * 40 +// +// tableViewHeightConstraint?.update(offset: height) +// layoutIfNeeded() +// } +//} +// +//extension SubItemsContainerCell: UITableViewDelegate, UITableViewDataSource { +// func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { +// return subItems.count +// } +// +// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// let cell = tableView.dequeueReusableCell(withIdentifier: "SubItemCell", for: indexPath) as! SubItemCell +// let subItem = subItems[indexPath.row] +// cell.configure(with: subItem) +// return cell +// } +//} +// +//// SubItemCell - 子项cell +//class SubItemCell: UITableViewCell { +// private let titleLabel = UILabel() +// private let iconImageView = UIImageView() +// +// 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(iconImageView) +// contentView.addSubview(titleLabel) +// +// iconImageView.snp.makeConstraints { make in +// make.left.equalToSuperview().offset(40) +// make.centerY.equalToSuperview() +// make.width.height.equalTo(18) +// } +// +// titleLabel.snp.makeConstraints { make in +// make.left.equalTo(iconImageView.snp.right).offset(8) +// make.centerY.equalToSuperview() +// make.right.equalToSuperview().inset(15) +// } +// +// titleLabel.font = UIFont.systemFont(ofSize: 13) +// titleLabel.textColor = UIColor(hex: "#999999") +// } +// +// func configure(with item: ImageRow) { +// iconImageView.image = UIImage(named: item.icon) +// titleLabel.text = item.title +// } +//} 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 1f60177..bfaa535 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift @@ -6,21 +6,50 @@ // import UIKit +import SnapKit class ChatSettingSwipeView: CLContainer { var closeAction: (()->Void)? var sectionTitle: [String] = ["Switch Model", "Sound", "Maximum number of response tokens Maximum number of response tokens", "Appearance", "Background", "Historical Archives"] - var rows: [[RowModel]] = [ - [ImageRow(icon: "role_exchange_mode", title: "XL-0826-32K", showAvatar: false, showArrow: true, showSwitch: false), ImageRow(icon: "role_text_mode", title: "Short Text Mode", showAvatar: false, showArrow: true, showSwitch: false)], - [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)], - [BackgroundRow(count: 50)], - [HistoryRow(time: "", icon: "", title: "", itemCount: 30)] - ] + var rows: [[RowModel]] = [] + + // 展开状态管理:section -> rowIndex -> isExpanded + private var expandedStates: [Int: [Int: Bool]] = [:] + + override init(frame: CGRect) { + super.init(frame: frame) + + // 初始化rows数据,包含子项(至少10条数据) + let modelRow = ImageRow(icon: "role_exchange_mode", title: "XL-0826-32K", showAvatar: false, showArrow: true, showSwitch: false, subItems: [ + ImageRow(icon: "role_exchange_mode", title: "Sub Item 1", showAvatar: false, showArrow: false, showSwitch: false), + ImageRow(icon: "role_exchange_mode", title: "Sub Item 2", showAvatar: false, showArrow: false, showSwitch: false), + ImageRow(icon: "role_exchange_mode", title: "Sub Item 3", showAvatar: false, showArrow: false, showSwitch: false), + ImageRow(icon: "role_exchange_mode", title: "Sub Item 4", showAvatar: false, showArrow: false, showSwitch: false), + ImageRow(icon: "role_exchange_mode", title: "Sub Item 5", showAvatar: false, showArrow: false, showSwitch: false), +// ImageRow(icon: "role_exchange_mode", title: "Sub Item 6", showAvatar: false, showArrow: false, showSwitch: false), +// ImageRow(icon: "role_exchange_mode", title: "Sub Item 7", showAvatar: false, showArrow: false, showSwitch: false), +// ImageRow(icon: "role_exchange_mode", title: "Sub Item 8", showAvatar: false, showArrow: false, showSwitch: false), +// ImageRow(icon: "role_exchange_mode", title: "Sub Item 9", showAvatar: false, showArrow: false, showSwitch: false), +// ImageRow(icon: "role_exchange_mode", title: "Sub Item 10", showAvatar: false, showArrow: false, showSwitch: false) + ]) + + 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)], + [BackgroundRow(count: 50)], + [HistoryRow(time: "", icon: "", title: "", itemCount: 30)] + ] + + setupViews() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } lazy var titleLab: UILabel = { let lab = UILabel() @@ -62,21 +91,11 @@ class ChatSettingSwipeView: CLContainer { tableView.register(ChatFontCell.self, forCellReuseIdentifier: "ChatFontCell") tableView.register(ChatBackgroundCell.self, forCellReuseIdentifier: "ChatBackgroundCell") tableView.register(ChatHistoryCell.self, forCellReuseIdentifier: "ChatHistoryCell") + tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell") tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) return tableView }() - - override init(frame: CGRect) { - super.init(frame: frame) - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - func setupViews() { navigationView = NavigationView() @@ -151,6 +170,48 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { // 不需要额外的contentInset调整 } + 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 { + 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) + + 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) + + 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) + } + } + } + func numberOfSections(in tableView: UITableView) -> Int { return rows.count } @@ -161,9 +222,26 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let row = rows[indexPath.section][indexPath.row] + + // 处理SubItemsRow + if let subItemsRow = row as? SubItemsRow { + let cell = tableView.dequeueReusableCell(withIdentifier: "SubItemsContainerCell", for: indexPath) as! SubItemsContainerCell + cell.selectionStyle = .none + cell.configure(with: subItemsRow) + return cell + } + let cell = tableView.dequeueReusableCell(withIdentifier: row.cellReuseID, for: indexPath) cell.selectionStyle = .none - (cell as? CellConfigurable)?.configure(with: row) + + // 如果是ImageRow,设置展开状态 + if var imageRow = row as? ImageRow { + imageRow.isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false + (cell as? CellConfigurable)?.configure(with: imageRow) + } else { + (cell as? CellConfigurable)?.configure(with: row) + } + return cell } @@ -194,3 +272,205 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { return CGFLOAT_MIN } } + +// SubItemsRow - 用于标识子项容器cell +struct SubItemsRow: RowModel { + let subItems: [ImageRow] + var cellReuseID: String { "SubItemsContainerCell" } + func cellHeight(tableWidth: CGFloat) -> CGFloat { + let maxDisplayCount = 4 + let displayCount = min(subItems.count, maxDisplayCount) + return CGFloat(displayCount) * 58 // 每行40高度 + } +} + +// SubItemsContainerCell - 子项容器cell +class SubItemsContainerCell: UITableViewCell, CellConfigurable { + + private var subItems: [ImageRow] = [] + private var tableViewHeightConstraint: Constraint? + + 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 = true + tableView.isScrollEnabled = true + tableView.register(SubItemCell.self, forCellReuseIdentifier: "SubItemCell") + tableView.estimatedRowHeight = 58 + tableView.rowHeight = 58 + tableView.contentInset = .zero + tableView.scrollIndicatorInsets = .zero + tableView.showsVerticalScrollIndicator = false + 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 subItemsRow = row as? SubItemsRow else { return } + subItems = subItemsRow.subItems + + tableView.reloadData() + updateTableViewHeight() + } + + private func updateTableViewHeight() { + let maxDisplayCount = 4 + let displayCount = min(subItems.count, maxDisplayCount) + let height = CGFloat(displayCount) * 58 + + tableViewHeightConstraint?.update(offset: height) + layoutIfNeeded() + } +} + +extension SubItemsContainerCell: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return subItems.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "SubItemCell", for: indexPath) as! SubItemCell + let subItem = subItems[indexPath.row] + cell.configure(with: subItem) + cell.lineView.isHidden = subItems.count - 1 == indexPath.row + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + } +} + +// SubItemCell - 子项cell +class SubItemCell: UITableViewCell { + private lazy var titleLabel: UILabel = { + let lab = UILabel() + lab.text = "Max-0618" + lab.textColor = UIColor(hex: "#333333") + lab.font = UIFont.systemFont(ofSize: 13) + return lab + }() + + private lazy var iconImageView: UIImageView = { + let view = UIImageView() + view.cornerRadius = 12.5 + view.backgroundColor = .darkText + return view + }() + + lazy var subTitleLab: UILabel = { + let lab = UILabel() + lab.text = "Previous-generation large model" + lab.textColor = UIColor(hex: "#9494C3") + lab.font = UIFont.systemFont(ofSize: 10) + return lab + }() + + lazy var tokenLab: UILabel = { + let lab = UILabel() + lab.textColor = UIColor(hex: "#0066FF") + lab.text = "0.3 points / 1K tokens (Recommended)" + lab.font = UIFont.systemFont(ofSize: 11) + return lab + }() + + lazy var statusImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "")) + imgView.backgroundColor = .blue + return imgView + }() + + lazy var lineView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hex: "#ECECF9") + 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(iconImageView) + contentView.addSubview(titleLabel) + contentView.addSubview(subTitleLab) + contentView.addSubview(tokenLab) + contentView.addSubview(statusImgView) + contentView.addSubview(lineView) + + iconImageView.snp.makeConstraints { make in + make.left.top.equalToSuperview().offset(10) + make.width.height.equalTo(25) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(iconImageView.snp.right).offset(8) + make.top.equalTo(iconImageView.snp.top).offset(-2) + make.right.equalTo(statusImgView.snp.left).inset(-10) + } + + subTitleLab.snp.makeConstraints { make in + make.left.equalTo(titleLabel.snp.left) + make.top.equalTo(titleLabel.snp.bottom) + make.right.equalTo(titleLabel.snp.right) + } + + lineView.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(10) + make.bottom.equalToSuperview() + make.height.equalTo(1) + } + + tokenLab.snp.makeConstraints { make in + make.left.equalTo(titleLabel.snp.left) + make.right.equalTo(titleLabel.snp.right) + make.bottom.equalToSuperview().inset(5) + } + + statusImgView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().inset(10) + make.width.height.equalTo(13) + } + } + + func configure(with item: ImageRow) { + iconImageView.image = UIImage(named: item.icon) + titleLabel.text = item.title + } +}