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 cdd6349..6dc5ae9 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatSwipeCell.swift @@ -18,12 +18,13 @@ struct ImageRow: RowModel { let subItems: [ImageRow]? // 子项列表 let buttleItems: [ChatButtleRow]? let chatModeItems: [ChatModeItem]? + let voiceActorItems: [VoiceActorItem]? 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, chatModeItems: [ChatModeItem]? = nil) { + init(icon: String, title: String, showAvatar: Bool = false, showArrow: Bool = false, showSwitch: Bool = false, subItems: [ImageRow]? = nil, buttleItems: [ChatButtleRow]? = nil, chatModeItems: [ChatModeItem]? = nil, voiceActorItems: [VoiceActorItem]? = nil) { self.icon = icon self.title = title self.showAvatar = showAvatar @@ -32,6 +33,7 @@ struct ImageRow: RowModel { self.subItems = subItems self.buttleItems = buttleItems self.chatModeItems = chatModeItems + self.voiceActorItems = voiceActorItems } } @@ -50,10 +52,12 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable { return lab }() - lazy var avatarView : UIImageView = { + var avatarView : UIImageView = { let imgView = UIImageView() imgView.cornerRadius = 10.5 imgView.backgroundColor = .blue + imgView.contentMode = .scaleAspectFill + imgView.clipsToBounds = true return imgView }() @@ -89,6 +93,12 @@ class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable { arrowImgView.isHidden = !row.showArrow switchControl.isHidden = !row.showSwitch + // 如果有 voice actor items,设置默认选中的头像 + if row.showAvatar, let voiceActorItems = row.voiceActorItems, + let selectedItem = voiceActorItems.first(where: { $0.isSelected }) { + avatarView.image = UIImage(named: selectedItem.avatarImage) + } + // 更新箭头状态 updateArrowState(isExpanded: row.isExpanded) diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/VoiceActorContainerCell.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/VoiceActorContainerCell.swift new file mode 100644 index 0000000..a1c5723 --- /dev/null +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/VoiceActorContainerCell.swift @@ -0,0 +1,502 @@ +// +// VoiceActorContainerCell.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/01/27. +// + +import UIKit +import SnapKit + +// MARK: - 数据模型 +struct VoiceActorItem { + let id: String + let name: String + let description: String + let avatarImage: String + let gender: String // "male", "female", "all" + var isSelected: Bool = false +} + +// MARK: - RowModel +struct VoiceActorRow: RowModel { + let items: [VoiceActorItem] + var cellReuseID: String { "VoiceActorContainerCell" } + + func cellHeight(tableWidth: CGFloat) -> CGFloat { + let filterHeight: CGFloat = 50 // 筛选器高度 + let maxRows: CGFloat = 5.5 // 最多显示5.5行 + let rowHeight: CGFloat = 80 // 每行高度 + let maxListHeight = maxRows * rowHeight + return filterHeight + maxListHeight + } +} + +// MARK: - VoiceActorContainerCell +class VoiceActorContainerCell: UITableViewCell, CellConfigurable { + + private var allItems: [VoiceActorItem] = [] + private var filteredItems: [VoiceActorItem] = [] + private var selectedItemId: String? + private var tableViewHeightConstraint: Constraint? + var selectedItemChanged: ((VoiceActorItem) -> Void)? // 选中项改变回调 + + // MARK: - 筛选器视图 + private lazy var filterView: VoiceActorFilterView = { + let view = VoiceActorFilterView() + view.filterChanged = { [weak self] filterType in + self?.filterItems(by: filterType) + } + return view + }() + + 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 = true + tableView.isScrollEnabled = true + tableView.register(VoiceActorItemCell.self, forCellReuseIdentifier: "VoiceActorItemCell") + tableView.estimatedRowHeight = 80 + tableView.rowHeight = 80 + tableView.contentInset = .zero + tableView.scrollIndicatorInsets = .zero + tableView.showsVerticalScrollIndicator = false + tableView.bounces = false + tableView.layer.cornerRadius = 15 + tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + 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(filterView) + contentView.addSubview(tableView) + + filterView.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalToSuperview().offset(2.5) + make.height.equalTo(50) + } + + tableView.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalTo(filterView.snp.bottom) + make.bottom.equalToSuperview().inset(2.5) + tableViewHeightConstraint = make.height.equalTo(0).constraint + } + } + + func configure(with row: RowModel) { + guard let voiceActorRow = row as? VoiceActorRow else { return } + allItems = voiceActorRow.items + + // 恢复选中状态 + if let selected = allItems.first(where: { $0.isSelected }) { + selectedItemId = selected.id + } + + filterItems(by: .all) + } + + private func filterItems(by filterType: VoiceActorFilterView.FilterType) { + var items: [VoiceActorItem] + + switch filterType { + case .all: + items = allItems + case .male: + items = allItems.filter { $0.gender == "male" || $0.gender == "all" } + case .female: + items = allItems.filter { $0.gender == "female" || $0.gender == "all" } + } + + // 恢复选中状态 + filteredItems = items.map { item in + var mutableItem = item + mutableItem.isSelected = (item.id == selectedItemId) + return mutableItem + } + + tableView.reloadData() + updateTableViewHeight() + } + + private func updateTableViewHeight() { + let rowHeight: CGFloat = 80 + let maxRows: CGFloat = 5.5 + let displayCount = min(CGFloat(filteredItems.count), maxRows) + let height = displayCount * rowHeight + + tableViewHeightConstraint?.update(offset: height) + layoutIfNeeded() + } +} + +// MARK: - UITableViewDataSource & UITableViewDelegate +extension VoiceActorContainerCell: UITableViewDataSource, UITableViewDelegate { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return filteredItems.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "VoiceActorItemCell", for: indexPath) as! VoiceActorItemCell + let item = filteredItems[indexPath.row] + + // 交替背景色(浅紫色和浅蓝色) + let bgColor = indexPath.row % 2 == 0 ? UIColor(hex: "#F5F5FF") : UIColor(hex: "#E8F0FF") + cell.configure(with: item, backgroundColor: bgColor) + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let item = filteredItems[indexPath.row] + selectedItemId = item.id + + // 更新所有数据的选中状态 + allItems = allItems.map { var i = $0; i.isSelected = (i.id == item.id); return i } + filteredItems = filteredItems.map { var i = $0; i.isSelected = (i.id == item.id); return i } + + tableView.reloadData() + + // 通知外部选中项改变 + selectedItemChanged?(item) + } + + func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath) as? VoiceActorItemCell { + cell.isHovered = true + } + } + + func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath) as? VoiceActorItemCell { + cell.isHovered = false + } + } +} + +// MARK: - VoiceActorFilterView +class VoiceActorFilterView: UIView { + enum FilterType: String { + case all = "All" + case male = "Male" + case female = "Female" + } + + var selectedFilter: FilterType = .all { + didSet { + updateButtonStates() + } + } + + var filterChanged: ((FilterType) -> Void)? + + lazy var filterStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [allButton, maleButton, femaleButton]) + stackView.spacing = 20 + stackView.alignment = .center + // 让 stackView 按内容自适应宽度,不强行拉伸 + stackView.distribution = .fill + // 提高水平方向的 Hugging/Compression,保证“贴内容” + stackView.setContentHuggingPriority(.required, for: .horizontal) + stackView.setContentCompressionResistancePriority(.required, for: .horizontal) + return stackView + }() + + private lazy var allButton: UIView = { + return createFilterButton(title: "All", tag: 0) + }() + + private lazy var maleButton: UIView = { + return createFilterButton(title: "Male", tag: 1) + }() + + private lazy var femaleButton: UIView = { + return createFilterButton(title: "Female", tag: 2) + }() + + private var filledCircles: [UIView: UIView] = [:] + private var circleBorders: [UIView: UIView] = [:] + private var titleLabels: [UIView: UILabel] = [:] + + override init(frame: CGRect) { + super.init(frame: frame) + + self.cornerRadius = 15.0 + setupViews() + updateButtonStates() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupViews() { + backgroundColor = UIColor(hex: "#1A1A2E") + addSubview(filterStackView) + + filterStackView.snp.makeConstraints { make in + make.left.equalToSuperview().inset(12) + make.centerY.equalToSuperview() + // 根据内容自适应,最长不超过右侧 12 间距 + make.right.lessThanOrEqualToSuperview().inset(12) + } + } + + private func createFilterButton(title: String, tag: Int) -> UIView { + // 使用自定义容器视图替代 UIButton,以便精确控制布局 + let containerView = UIView() + containerView.tag = tag + + // 创建单选按钮图标(圆圈) + let circleView = UIView() + circleView.backgroundColor = .clear + circleView.layer.borderWidth = 2 + // 默认 60% 白色描边 + circleView.layer.borderColor = UIColor(white: 1, alpha: 0.6).cgColor + circleView.layer.cornerRadius = 5 + containerView.addSubview(circleView) + + let filledCircle = UIView() + filledCircle.backgroundColor = .white + filledCircle.layer.cornerRadius = 5 + filledCircle.isHidden = true + containerView.addSubview(filledCircle) + + // 创建文字标签 + let titleLabel = UILabel() + titleLabel.text = title + titleLabel.textColor = UIColor(white: 1, alpha: 0.6) // 默认未选中状态 + titleLabel.font = UIFont.systemFont(ofSize: 14) + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + titleLabel.setContentHuggingPriority(.required, for: .horizontal) + containerView.addSubview(titleLabel) + + // 布局约束:圆圈 (10x10) -> 间距 (5pt) -> 文字标签 + circleView.snp.makeConstraints { make in + make.left.equalToSuperview() + make.centerY.equalToSuperview() + make.width.height.equalTo(10) + } + + filledCircle.snp.makeConstraints { make in + make.center.equalTo(circleView) + make.width.height.equalTo(10) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(circleView.snp.right).offset(5) // 5pt 间距 + make.right.equalToSuperview() + make.centerY.equalToSuperview() + make.top.bottom.greaterThanOrEqualToSuperview() // 确保容器高度足够 + } + + // 容器视图的约束:根据内容自适应 + containerView.setContentHuggingPriority(.required, for: .horizontal) + containerView.setContentCompressionResistancePriority(.required, for: .horizontal) + + // 添加点击手势 + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(filterButtonTapped(_:))) + containerView.addGestureRecognizer(tapGesture) + containerView.isUserInteractionEnabled = true + + // 保存引用以便更新状态 + filledCircles[containerView] = filledCircle + circleBorders[containerView] = circleView + titleLabels[containerView] = titleLabel + + return containerView + } + + @objc private func filterButtonTapped(_ sender: UITapGestureRecognizer) { + guard let containerView = sender.view else { return } + let filters: [FilterType] = [.all, .male, .female] + selectedFilter = filters[containerView.tag] + filterChanged?(selectedFilter) + } + + private func updateButtonStates() { + let buttons = [allButton, maleButton, femaleButton] + let filters: [FilterType] = [.all, .male, .female] + + for (index, button) in buttons.enumerated() { + let isSelected = filters[index] == selectedFilter + // 选中:文字纯白、显示实心圆 + // 未选中:文字 60% 白、空心圆(60% 白描边) + titleLabels[button]?.textColor = UIColor(white: 1, alpha: isSelected ? 1.0 : 0.6) + filledCircles[button]?.isHidden = !isSelected + circleBorders[button]?.layer.borderColor = UIColor(white: 1, alpha: isSelected ? 1.0 : 0.6).cgColor + } + } +} + +// MARK: - VoiceActorItemCell +class VoiceActorItemCell: UITableViewCell { + + private lazy var containerView: UIView = { + let view = UIView() + view.layer.cornerRadius = 12 + return view + }() + + private lazy var avatarImageView: UIImageView = { + let view = UIImageView() + view.cornerRadius = 27.5 + view.backgroundColor = .darkText + view.contentMode = .scaleAspectFill + view.clipsToBounds = true + return view + }() + + private lazy var playButton: UIButton = { + let btn = UIButton(type: .custom) + if let playImage = UIImage(named: "voice_play") { + btn.setImage(playImage, for: .normal) + } else { + let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .bold) + btn.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal) + btn.tintColor = .white + } + btn.backgroundColor = UIColor(hex: "#0066FF") + btn.layer.cornerRadius = 12 + btn.imageEdgeInsets = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8) + btn.imageView?.contentMode = .scaleAspectFit + return btn + }() + + private lazy var titleLabel: UILabel = { + let lab = UILabel() + lab.textColor = UIColor(hex: "#333333") + lab.font = UIFont.systemFont(ofSize: 13, weight: .medium) + return lab + }() + + private lazy var subTitleLab: UILabel = { + let lab = UILabel() + lab.textColor = UIColor(hex: "#9494C3") + lab.font = UIFont.italicSystemFont(ofSize: 11) + return lab + }() + + private lazy var selectionIndicator: UIView = { + let view = UIView() + view.layer.cornerRadius = 10 + view.backgroundColor = UIColor.black + return view + }() + + private var hoverBorderView: UIView? + + var item: VoiceActorItem? + var isHovered: Bool = false { + didSet { + updateHoverState() + } + } + + 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(avatarImageView) + containerView.addSubview(playButton) + containerView.addSubview(titleLabel) + containerView.addSubview(subTitleLab) + containerView.addSubview(selectionIndicator) + + containerView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.bottom.equalToSuperview().inset(2.5) + } + + avatarImageView.snp.makeConstraints { make in + make.left.top.equalToSuperview().offset(10) + make.width.height.equalTo(55) + make.bottom.equalToSuperview().offset(-10) + } + + playButton.snp.makeConstraints { make in + make.bottom.right.equalTo(avatarImageView).offset(2) + make.width.height.equalTo(24) + } + + selectionIndicator.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().inset(10) + make.width.height.equalTo(20) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(avatarImageView.snp.right).offset(10) + make.right.equalTo(selectionIndicator.snp.left).offset(-10) + make.centerY.equalToSuperview().offset(-8) + } + + subTitleLab.snp.makeConstraints { make in + make.left.right.equalTo(titleLabel) + make.top.equalTo(titleLabel.snp.bottom).offset(2) + } + + // 创建hover边框视图 + hoverBorderView = UIView() + hoverBorderView?.backgroundColor = .clear + hoverBorderView?.layer.borderWidth = 2 + hoverBorderView?.layer.borderColor = UIColor(hex: "#FF69B4").cgColor + hoverBorderView?.layer.cornerRadius = 14 + hoverBorderView?.isHidden = true + if let hoverView = hoverBorderView { + containerView.addSubview(hoverView) + hoverView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + } + + func configure(with item: VoiceActorItem, backgroundColor: UIColor? = nil) { + self.item = item + avatarImageView.image = UIImage(named: item.avatarImage) + titleLabel.text = item.name + subTitleLab.text = item.description + + // 设置背景色(交替) + containerView.backgroundColor = backgroundColor ?? UIColor(hex: "#F5F5FF") + + updateSelectionState(isSelected: item.isSelected) + } + + func updateSelectionState(isSelected: Bool) { + selectionIndicator.backgroundColor = isSelected ? UIColor(hex: "#00CC88") : UIColor.black + } + + private func updateHoverState() { + hoverBorderView?.isHidden = !isHovered + } +} 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 ed62913..4b99f70 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/View/ChatSettingSwipeView.swift @@ -12,7 +12,7 @@ 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 sectionTitle: [String] = ["Switch Model", "Sound", "Maximum number of response tokens", "Appearance", "Background", "Historical Archives"] var rows: [[RowModel]] = [] // 展开状态管理:section -> rowIndex -> isExpanded @@ -55,7 +55,7 @@ class ChatSettingSwipeView: CLContainer { 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)], + [ImageRow(icon: "role_voice", title: "Voice actor", showAvatar: true, showArrow: true, showSwitch: false, voiceActorItems: createVoiceActorItems()), 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, chatModeItems: createChatModeItems()), buttleRow], [BackgroundRow(count: 50)], @@ -112,6 +112,7 @@ class ChatSettingSwipeView: CLContainer { tableView.register(SubItemsContainerCell.self, forCellReuseIdentifier: "SubItemsContainerCell") tableView.register(ChatButtleCollectionCell.self, forCellReuseIdentifier: "ChatButtleCollectionCell") tableView.register(ChatModeContainerCell.self, forCellReuseIdentifier: "ChatModeContainerCell") + tableView.register(VoiceActorContainerCell.self, forCellReuseIdentifier: "VoiceActorContainerCell") tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) return tableView }() @@ -196,6 +197,37 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { tableView.deselectRow(at: indexPath, animated: true) + // 处理 Voice actor 展开 + if imageRow.title == "Voice actor", let voiceActorItems = imageRow.voiceActorItems, !voiceActorItems.isEmpty { + let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false + expandedStates[indexPath.section, default: [:]][indexPath.row] = !isExpanded + + if !isExpanded { + // 展开:插入 VoiceActorRow + let actorRow = VoiceActorRow(items: voiceActorItems) + rows[indexPath.section].insert(actorRow, 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 VoiceActorRow { + 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 Mode 展开 if imageRow.title == "Chat Mode", let chatModeItems = imageRow.chatModeItems, !chatModeItems.isEmpty { let isExpanded = expandedStates[indexPath.section]?[indexPath.row] ?? false @@ -310,6 +342,20 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let row = rows[indexPath.section][indexPath.row] + // 处理VoiceActorRow + if let actorRow = row as? VoiceActorRow { + let cell = tableView.dequeueReusableCell(withIdentifier: "VoiceActorContainerCell", for: indexPath) as! VoiceActorContainerCell + cell.selectionStyle = .none + + // 设置选中回调,更新父cell头像 + cell.selectedItemChanged = { [weak self] item in + self?.updateVoiceActorAvatar(item, at: indexPath.section, rowIndex: indexPath.row - 1) + } + + cell.configure(with: actorRow) + return cell + } + // 处理ChatModeRow if let modeRow = row as? ChatModeRow { let cell = tableView.dequeueReusableCell(withIdentifier: "ChatModeContainerCell", for: indexPath) as! ChatModeContainerCell @@ -382,4 +428,52 @@ extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource { ChatModeItem(id: "2", title: "Immersive Mode", subtitle: "Previous-generation large model", isVip: true, isSelected: false) ] } + + // MARK: - 创建VoiceActor示例数据 + private func createVoiceActorItems() -> [VoiceActorItem] { + return [ + VoiceActorItem(id: "1", name: "Name 1", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: true), + VoiceActorItem(id: "2", name: "Name 2", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false), + VoiceActorItem(id: "3", name: "Name 3", description: "Anime-Style Boy", avatarImage: "voice_actor_2", gender: "male", isSelected: false), + VoiceActorItem(id: "4", name: "Name 4", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false), + VoiceActorItem(id: "5", name: "Name 5", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false), + VoiceActorItem(id: "6", name: "Name 6", description: "Anime-Style Boy", avatarImage: "voice_actor_2", gender: "male", isSelected: false), + VoiceActorItem(id: "7", name: "Name 7", description: "Anime-Style Girl", avatarImage: "voice_actor_1", gender: "female", isSelected: false), + VoiceActorItem(id: "8", name: "Name 8", description: "Anime-Style Boy", avatarImage: "voice_actor_2", gender: "male", isSelected: false), + ] + } + + // MARK: - 更新Voice Actor头像 + private func updateVoiceActorAvatar(_ item: VoiceActorItem, at section: Int, rowIndex: Int) { + guard section >= 0 && section < rows.count, + rowIndex >= 0 && rowIndex < rows[section].count else { return } + + // 更新数据源中的选中状态 + if var imageRow = rows[section][rowIndex] as? ImageRow, + var voiceActorItems = imageRow.voiceActorItems { + // 更新所有items的选中状态 + voiceActorItems = voiceActorItems.map { var i = $0; i.isSelected = (i.id == item.id); return i } + + // 由于 ImageRow 是 struct,需要重新创建 + let updatedRow = ImageRow( + icon: imageRow.icon, + title: imageRow.title, + showAvatar: imageRow.showAvatar, + showArrow: imageRow.showArrow, + showSwitch: imageRow.showSwitch, + subItems: imageRow.subItems, + buttleItems: imageRow.buttleItems, + chatModeItems: imageRow.chatModeItems, + voiceActorItems: voiceActorItems + ) + rows[section][rowIndex] = updatedRow + } + + // 刷新父cell以更新头像 + let parentIndexPath = IndexPath(row: rowIndex, section: section) + if let cell = tableView.cellForRow(at: parentIndexPath) as? ChatSwipeCell { + // 更新avatarView的图像 + cell.avatarView.image = UIImage(named: item.avatarImage) + } + } }