Browse Source

feat(translation): 实现翻译历史记录的分页加载功能

添加分页加载功能以优化长历史记录的显示性能
采用reverse模式显示最新记录并支持滚动加载更多
新增加载指示器和错误重试机制
newdev_shunjiawei
lxm 1 year ago
parent
commit
ad531af7bf
  1. 170
      lib/modules/translation/controllers/translation_controller.dart
  2. 153
      lib/modules/translation/views/translation_view.dart

170
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<AsrService>();
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<void> 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<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> 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<TranslationItem> getAllHistory() {
@ -1024,7 +1160,7 @@ class TranslationController extends GetxController {
// 滚动到底部 (公共方法,用于向后兼容)
void scrollToBottom({bool animate = true}) {
_scrollToBottom();
_scrollToBottom(animate: animate);
}

153
lib/modules/translation/views/translation_view.dart

@ -109,6 +109,7 @@ class TranslationView extends GetView<TranslationController> {
)),
centerTitle: true,
actions: [
Obx(() {
if (controller.isRecording.value) {
// 录音中:显示红色计时器样式,点击可停止录音
@ -169,19 +170,19 @@ class TranslationView extends GetView<TranslationController> {
},
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<TranslationController> {
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<TranslationController> {
),
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<TranslationController> {
),
);
}
// 构建加载更多指示器
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<Color>(
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],
),
),
],
),
),
),
);
}
});
}
}

Loading…
Cancel
Save