From 91f8cfcc4d42f20a02a5b31064fbbe5459779641 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 7 Jul 2025 14:40:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(translation):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E7=9A=84?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E5=8A=A0=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加分页加载功能以优化长历史记录的显示性能 采用reverse模式显示最新记录并支持滚动加载更多 新增加载指示器和错误重试机制 --- .../controllers/translation_controller.dart | 170 ++++++++++++++++-- .../translation/views/translation_view.dart | 153 ++++++++++++++-- 2 files changed, 291 insertions(+), 32 deletions(-) diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index cff3a3247..eb1bf7785 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -26,6 +26,8 @@ enum AudioOutputDevice { } class TranslationController extends GetxController { + static const String TAG = 'TranslationController'; + // 服务 final AsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = @@ -56,6 +58,14 @@ class TranslationController extends GetxController { // 主页面显示限制:只显示最近20条,保证界面性能和用户体验 static const int _maxDisplayItems = 20; + // 分页加载相关状态 + final isLoadingMore = false.obs; // 是否正在加载更多历史记录 + final hasMoreHistory = true.obs; // 是否还有更多历史记录 + final currentPage = 1.obs; // 当前页码 + final pageSize = 20; // 每页记录数量 + final isLoadingMoreError = false.obs; // 加载更多时是否出错 + bool _scrollListenerSetup = false; // 滚动监听器设置标志 + RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 RxInt lastActiveSpeaker = 0.obs; // 上一次状态 // 状态变量 @@ -178,10 +188,8 @@ class TranslationController extends GetxController { // 初始化服务 _initServices(); - // 使用微任务在布局完成后自动滚动到底部 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); + // 🔑 reverse模式下不需要手动滚动,ListView会自然显示最新内容 + // 参考Agent模块的实现模式 } // 加载保存的语言设置 @@ -724,18 +732,28 @@ class TranslationController extends GetxController { } } - // 滚动到底部 - void _scrollToBottom() { + // 滚动到底部(适配reverse模式) + void _scrollToBottom({bool animate = true}) { if (scrollController.hasClients && (translationHistory.isNotEmpty || currentSessionId != null)) { try { - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); + // 🔑 reverse模式下:滚动到顶部(position = 0)来显示最新消息 + // 正常模式下:滚动到底部(maxScrollExtent)来显示最新消息 + const targetPosition = 0.0; // reverse模式下最新消息在顶部 + + if (animate) { + // 有动画的滚动,用于新消息添加时 + scrollController.animateTo( + targetPosition, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } else { + // 无动画的跳转,用于初始化时 + scrollController.jumpTo(targetPosition); + } } catch (e) { - Logger.error('滚动到底部失败: ${e.toString()}'); + Logger.error('滚动到顶部失败(reverse模式): ${e.toString()}'); } } } @@ -849,22 +867,37 @@ class TranslationController extends GetxController { // 【注意】存储中的记录都是完成态记录(isIntermediate = false) // 因为保存时已经过滤掉了中间态记录,确保数据质量 + // 初始化分页状态 + currentPage.value = 1; + isLoadingMore.value = false; + isLoadingMoreError.value = false; + // 主页面只显示最近的20条完成态记录,保证界面性能 if (allHistory.length > _maxDisplayItems) { translationHistory.value = allHistory.sublist(allHistory.length - _maxDisplayItems); + // 检查是否还有更多历史记录 + hasMoreHistory.value = true; } else { translationHistory.value = allHistory; + hasMoreHistory.value = false; } - // 使用微任务确保在视图完全渲染后滚动到底部 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); + // 设置滚动监听器 + _setupScrollListener(); + + // 🔑 reverse模式下不需要滚动到底部,ListView会自然显示最新记录 + // 参考Agent模块:使用reverse:true时不需要手动滚动到底部 } catch (e) { _storage.remove(_historyKey); Logger.error('加载翻译历史失败: ${e.toString()}'); } + } else { + // 没有历史数据时也要初始化分页状态 + hasMoreHistory.value = false; + currentPage.value = 1; + isLoadingMore.value = false; + isLoadingMoreError.value = false; } } @@ -894,6 +927,109 @@ class TranslationController extends GetxController { } } + // 设置滚动监听器(适配reverse模式) + void _setupScrollListener() { + if (_scrollListenerSetup) return; // 防止重复设置 + + scrollController.addListener(() { + // 🔑 reverse模式下:检查是否滚动到底部(实际是历史记录的顶部) + // 参考Agent模块的实现 + if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { + // 当距离底部100像素时触发加载更多历史记录 + if (hasMoreHistory.value && !isLoadingMore.value) { + Logger.d(TAG, '触发加载更多翻译历史记录(reverse模式)'); + loadMoreHistory(); + } + } + }); + + _scrollListenerSetup = true; + } + + // 加载更多历史记录(分页加载) + Future loadMoreHistory() async { + // 防止重复加载 + if (isLoadingMore.value || !hasMoreHistory.value) { + Logger.d(TAG, '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreHistory=${hasMoreHistory.value}'); + return; + } + + try { + isLoadingMore.value = true; + isLoadingMoreError.value = false; + + final nextPage = currentPage.value + 1; + Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...'); + + // 从存储中获取所有历史记录 + final historyJson = _storage.read(_historyKey); + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List allHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + // 计算分页参数 + final totalItems = allHistory.length; + final totalPages = (totalItems / pageSize).ceil(); + + // 修复分页索引计算:从最新记录向前分页 + // 第1页:最新20条 (index: totalItems-20 到 totalItems) + // 第2页:接下来20条 (index: totalItems-40 到 totalItems-20) + // 第3页:再接下来20条 (index: totalItems-60 到 totalItems-40) + final endIndex = totalItems - ((nextPage - 1) * pageSize); + final startIndex = totalItems - (nextPage * pageSize); + + Logger.d(TAG, '分页计算: 总记录=$totalItems, 总页数=$totalPages, 当前页=$nextPage, startIndex=$startIndex, endIndex=$endIndex'); + + if (endIndex > 0) { + // 获取当前页的数据,确保索引不越界 + final actualStartIndex = startIndex < 0 ? 0 : startIndex; + final actualEndIndex = endIndex > totalItems ? totalItems : endIndex; + + final pageItems = allHistory.sublist(actualStartIndex, actualEndIndex); + Logger.d(TAG, '实际索引: start=$actualStartIndex, end=$actualEndIndex, 获取${pageItems.length}条记录'); + + if (pageItems.isNotEmpty) { + // 🔑 简化延迟,参考Agent模块(300ms vs 1000ms) + await Future.delayed(const Duration(milliseconds: 300)); + + // 🔑 reverse模式下:将新记录插入到现有记录列表的开头 + // 由于reverse显示,用户看到的顺序不会改变,无需复杂的位置保持 + final updatedHistory = [...pageItems, ...translationHistory]; + translationHistory.assignAll(updatedHistory); + + // 更新分页状态 + currentPage.value = nextPage; + // 修复hasMoreHistory判断:当startIndex <= 0时表示已经到达最早的记录 + hasMoreHistory.value = startIndex > 0; + + Logger.i(TAG, + '成功加载 ${pageItems.length} 条历史记录,当前页: $nextPage/$totalPages, 还有更多: ${hasMoreHistory.value}'); + } else { + // 没有更多记录 + hasMoreHistory.value = false; + Logger.i(TAG, '没有更多翻译历史记录'); + } + } else { + // 索引超出范围,没有更多记录 + hasMoreHistory.value = false; + Logger.i(TAG, '已到达历史记录开头'); + } + } else { + // 没有存储数据 + hasMoreHistory.value = false; + Logger.i(TAG, '存储中没有更多历史记录'); + } + } catch (e) { + Logger.e(TAG, '加载更多历史记录失败: $e'); + isLoadingMoreError.value = true; + } finally { + isLoadingMore.value = false; + } + } + + + // 获取所有完成态历史记录(供历史页面使用) // 【修复】简化实现,合并存储和内存中的完成态记录 List getAllHistory() { @@ -1024,7 +1160,7 @@ class TranslationController extends GetxController { // 滚动到底部 (公共方法,用于向后兼容) void scrollToBottom({bool animate = true}) { - _scrollToBottom(); + _scrollToBottom(animate: animate); } diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 1cd6779e3..e9e02b251 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -109,6 +109,7 @@ class TranslationView extends GetView { )), centerTitle: true, actions: [ + Obx(() { if (controller.isRecording.value) { // 录音中:显示红色计时器样式,点击可停止录音 @@ -169,19 +170,19 @@ class TranslationView extends GetView { }, tooltip: 'viewAudioFiles'.tr, // 查看录音文件 ), - // 添加历史记录按钮 - IconButton( - icon: Icon( - Icons.history, - color: isDarkMode ? Colors.white : Colors.black, - size: 24.sp, - ), - onPressed: () async { - await controller.stopAll(); - Get.toNamed(Routes.translationHistory); - }, - tooltip: 'translationHistory'.tr, // 翻译历史 - ), + // 历史记录按钮 + // IconButton( + // icon: Icon( + // Icons.history, + // color: isDarkMode ? Colors.white : Colors.black, + // size: 24.sp, + // ), + // onPressed: () async { + // await controller.stopAll(); + // Get.toNamed(Routes.translationHistory); + // }, + // tooltip: 'translationHistory'.tr, // 翻译历史 + // ), ], ); } @@ -289,10 +290,12 @@ class TranslationView extends GetView { Widget _buildTranslationContent(bool isDarkMode) { return Obx(() { - final itemCount = controller.translationHistory.length; + final itemCount = controller.translationHistory.length + + (controller.hasMoreHistory.value ? 1 : 0); return ListView.builder( controller: controller.scrollController, + reverse: true, // 🔑 关键优化:采用reverse模式,参考Agent模块 padding: EdgeInsets.only( left: 16.w, right: 16.w, @@ -301,8 +304,21 @@ class TranslationView extends GetView { ), itemCount: itemCount, itemBuilder: (context, index) { + // 如果是最后一项且有更多历史记录,显示加载指示器(reverse模式下) + if (index == controller.translationHistory.length && controller.hasMoreHistory.value) { + return _buildLoadMoreIndicator(); + } + + // reverse模式下需要反转索引来获取正确的历史记录 + final historyIndex = controller.translationHistory.length - 1 - index; + + // 检查索引是否有效(reverse模式下的索引验证) + if (historyIndex < 0 || historyIndex >= controller.translationHistory.length) { + return const SizedBox.shrink(); + } + // 获取当前项 - final historyItem = controller.translationHistory[index]; + final historyItem = controller.translationHistory[historyIndex]; // 构建项目widget return _buildTranslationItemWidget(context, historyItem); @@ -716,4 +732,111 @@ class TranslationView extends GetView { ), ); } + + // 构建加载更多指示器 + Widget _buildLoadMoreIndicator() { + return Obx(() { + if (controller.isLoadingMore.value) { + // 正在加载状态 - 增强视觉效果 + return Container( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: const AlwaysStoppedAnimation( + AppColors.primary, + ), + ), + ), + const SizedBox(height: 12), + Text( + '正在加载历史记录...', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } else if (controller.isLoadingMoreError.value) { + // 加载失败状态 - 增强视觉效果 + return Container( + padding: const EdgeInsets.symmetric(vertical: 18), + child: Center( + child: GestureDetector( + onTap: () => controller.loadMoreHistory(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.red[200]!, width: 1), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.refresh, + size: 18.sp, + color: Colors.red[600], + ), + SizedBox(width: 8.w), + Text( + '加载失败,点击重试', + style: TextStyle( + fontSize: 14.sp, + color: Colors.red[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ); + } else { + // 默认状态(有更多记录但未加载)- 增强视觉效果 + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.keyboard_arrow_up, + size: 16.sp, + color: Colors.grey[500], + ), + SizedBox(width: 4.w), + Text( + '向上滑动查看更多历史', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ), + ); + } + }); + } }