From 4de04a29cccf16d0057028067ab1e481645c3a16 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 3 Mar 2026 11:14:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(meeting):=20=E6=B7=BB=E5=8A=A0=E4=BC=9A?= =?UTF-8?q?=E8=AE=AE=E5=88=86=E4=BA=AB=E5=BA=95=E9=83=A8=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E5=8F=8A=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增分享底部弹窗组件,实现分享链接、复制转写、复制总结、导出音频、导出转写、导出总结、保存思维导图和分享思维导图功能 - 分享功能包含权限控制,依据tasktype判断按钮是否可用 - 复制功能支持将转写文本和总结内容复制到剪贴板,支持Markdown转纯文本格式转换 - 导出支持导出转写TXT和总结TXT/PDF格式,PDF导出支持中文字体 - 录音播放器和会议模块新增多种错误提示文案,支持中英文、简繁体多语言 - 优化音频播放错误提示的国际化文本替换,统一调用国际化资源 - 修正部分UI组件中文字符串直接写死,改为使用国际化翻译键 - 提升底部弹窗样式和交互体验,支持深色/浅色主题自适应 - 会议详情页和各标签页使用国际化文本替代原硬编码字符串,兼容多语言环境 --- lib/core/translations/language/en_us.dart | 27 +- lib/core/translations/language/zh_cn.dart | 14 + lib/core/translations/language/zh_hk.dart | 14 + lib/core/translations/language/zh_tw.dart | 14 + .../meeting_details_controller.dart | 4 +- .../views/bottomSheet/share_bottom_sheet.dart | 282 +++++++++++++++++- .../meeting/views/meeting_details_view.dart | 8 +- .../meeting/views/meeting_home_view.dart | 4 +- .../meeting/views/tabs/overview_tab.dart | 6 +- .../meeting/views/tabs/speech_tab.dart | 2 +- .../meeting/views/tabs/summary_tab.dart | 2 +- .../views/template/template_edit_view.dart | 7 +- 12 files changed, 357 insertions(+), 27 deletions(-) diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 6c65ddab3..46eef9a7c 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -286,11 +286,11 @@ const Map enUS = { 'paymentTestInstructionsTitle': 'Instructions', 'paymentTestInstructionsContent': '1. Select a test amount (recommended: ¥0.01)\n' - '2. Tap the "WeChat Pay" button\n' - '3. The app will invoke the WeChat Pay SDK\n' - '4. Complete the payment in WeChat\n' - '5. Return to the app to view the result\n\n' - 'Note: This is a test environment. No real charge will occur.', + '2. Tap the "WeChat Pay" button\n' + '3. The app will invoke the WeChat Pay SDK\n' + '4. Complete the payment in WeChat\n' + '5. Return to the app to view the result\n\n' + 'Note: This is a test environment. No real charge will occur.', 'startingWechatPayment': 'Starting WeChat Pay...', 'startingAlipayPayment': 'Starting Alipay...', 'wechatPaymentSuccess': 'WeChat Pay successful!', @@ -635,7 +635,8 @@ const Map enUS = { 'editTemplate': 'Edit Template', 'createTemplate': 'Create Template', 'name': 'Name', - 'templateContentHint': 'Please enter template content! Tip: Click the AI button above to quickly generate template content based on the name and description you entered', + 'templateContentHint': + 'Please enter template content! Tip: Click the AI button above to quickly generate template content based on the name and description you entered', 'inputNameHint': 'Please enter name', 'inputDescHint': 'Please enter description', 'custom': 'Custom', @@ -687,6 +688,19 @@ const Map enUS = { // Meeting模块 - 更多操作相关 'reSummarize': 'Re-summarize', + // Meeting模块 - 错误提示相关 + 'analysisFailed': 'Analysis failed, please try again later', + 'transcriptionFailed': 'Transcription failed, please try again later', + 'summaryFailed': 'Summary failed, please try again later', + 'summaryFailedShort': 'Summary failed', + 'transcriptionFailedShort': 'Transcription failed', + + // Meeting模块 - 播放器相关 + 'playerNotReady': 'Audio player is not ready, please wait', + 'playFailed': 'Playback failed', + 'audioFileNotFound': 'Audio file not found', + 'playerLoadFailed': 'Audio player failed to load', + // Meeting模块 - 录音语言相关 'recordLanguage': 'Recording Language', // 'chinese': 'Chinese Mandarin (Simplified)', @@ -711,6 +725,7 @@ const Map enUS = { 'exportSummary': 'Export Summary', 'exportAs': 'Export As', 'exportingPDF': 'Exporting PDF...', + 'export': 'Export', // Meeting模块 - 发言者相关 'applyToAllParagraphs': 'Apply to All Paragraphs of This Speaker', diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 271906785..50e21b7d3 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -637,6 +637,19 @@ const Map zhCN = { // Meeting模块 - 更多操作相关 'reSummarize': '重新总结', + // Meeting模块 - 错误提示相关 + 'analysisFailed': '分析失败,请稍后重试', + 'transcriptionFailed': '转写失败,请稍后重试', + 'summaryFailed': '总结失败,请稍后重试', + 'summaryFailedShort': '总结失败', + 'transcriptionFailedShort': '转写失败', + + // Meeting模块 - 播放器相关 + 'playerNotReady': '音频播放器未准备好,请稍候', + 'playFailed': '播放失败', + 'audioFileNotFound': '音频文件不存在', + 'playerLoadFailed': '音频播放器加载失败', + // Meeting模块 - 录音语言相关 'recordLanguage': '录音语言', // 'chinese': '中文普通话(简体)', @@ -661,6 +674,7 @@ const Map zhCN = { 'exportSummary': '导出总结', 'exportAs': '导出为', 'exportingPDF': '正在导出PDF...', + 'export': '导出', // Meeting模块 - 发言者相关 'applyToAllParagraphs': '应用到这个发言者的所有段落', diff --git a/lib/core/translations/language/zh_hk.dart b/lib/core/translations/language/zh_hk.dart index ddee1fa77..4b00f7c6f 100644 --- a/lib/core/translations/language/zh_hk.dart +++ b/lib/core/translations/language/zh_hk.dart @@ -639,6 +639,19 @@ const Map zhHK = { // Meeting模块 - 更多操作相关 'reSummarize': '重新總結', + // Meeting模块 - 错误提示相关 + 'analysisFailed': '分析失敗,請稍後重試', + 'transcriptionFailed': '轉寫失敗,請稍後重試', + 'summaryFailed': '總結失敗,請稍後重試', + 'summaryFailedShort': '總結失敗', + 'transcriptionFailedShort': '轉寫失敗', + + // Meeting模块 - 播放器相关 + 'playerNotReady': '音頻播放器未準備好,請稍候', + 'playFailed': '播放失敗', + 'audioFileNotFound': '音頻文件不存在', + 'playerLoadFailed': '音頻播放器加載失敗', + // Meeting模块 - 录音语言相关 'recordLanguage': '錄音語言', // 'chinese': '中文普通话(简体)', @@ -663,6 +676,7 @@ const Map zhHK = { 'exportSummary': '導出總結', 'exportAs': '導出為', 'exportingPDF': '正在導出PDF...', + 'export': '導出', // Meeting模块 - 发言者相关 'applyToAllParagraphs': '應用到這個發言者的所有段落', diff --git a/lib/core/translations/language/zh_tw.dart b/lib/core/translations/language/zh_tw.dart index 5c413fb6f..7719d5033 100644 --- a/lib/core/translations/language/zh_tw.dart +++ b/lib/core/translations/language/zh_tw.dart @@ -639,6 +639,19 @@ const Map zhTW = { // Meeting模块 - 更多操作相关 'reSummarize': '重新總結', + // Meeting模块 - 错误提示相关 + 'analysisFailed': '分析失敗,請稍後重試', + 'transcriptionFailed': '轉寫失敗,請稍後重試', + 'summaryFailed': '總結失敗,請稍後重試', + 'summaryFailedShort': '總結失敗', + 'transcriptionFailedShort': '轉寫失敗', + + // Meeting模块 - 播放器相关 + 'playerNotReady': '音頻播放器未準備好,請稍候', + 'playFailed': '播放失敗', + 'audioFileNotFound': '音頻文件不存在', + 'playerLoadFailed': '音頻播放器加載失敗', + // Meeting模块 - 录音语言相关 'recordLanguage': '錄音語言', // 'chinese': '中文普通话(简体)', @@ -663,6 +676,7 @@ const Map zhTW = { 'exportSummary': '導出總結', 'exportAs': '導出為', 'exportingPDF': '正在導出PDF...', + 'export': '導出', // Meeting模块 - 发言者相关 'applyToAllParagraphs': '應用到這個發言者的所有段落', diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 04da10f7a..841538c0f 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -172,7 +172,7 @@ class MeetingDetailsController extends GetxController isPlayerReady.value = false; Get.snackbar( 'error'.tr, - '音频文件不存在', + 'audioFileNotFound'.tr, // 音频文件不存在 snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 3), ); @@ -265,7 +265,7 @@ class MeetingDetailsController extends GetxController Get.snackbar( 'error'.tr, - '音频播放器加载失败', + 'playerLoadFailed'.tr, // 音频播放器加载失败 snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 3), ); diff --git a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart index 9662e9167..81a4d2e63 100644 --- a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart @@ -18,6 +18,27 @@ import '../../controllers/meeting_details_controller.dart'; import '../../controllers/mind_map_controller.dart'; import 'second_level_bottom_sheet.dart'; +/// ============================================================================ +/// 分享底部弹窗组件 +/// ============================================================================ +/// 功能说明: +/// 本组件提供会议详情页面的分享功能入口,以底部弹窗形式展示8个核心功能: +/// 1. 分享链接 - 生成并分享会议网页链接 +/// 2. 复制转写 - 将会议转写文本复制到剪贴板 +/// 3. 复制总结 - 将会议总结内容复制到剪贴板 +/// 4. 导出音频 - 分享会议录音文件 +/// 5. 导出转写 - 将转写内容导出为TXT文件 +/// 6. 导出总结 - 将总结内容导出为TXT或PDF文件 +/// 7. 保存思维导图 - 将思维导图保存为图片到本地 +/// 8. 分享思维导图 - 将思维导图以图片形式分享 +/// +/// 权限控制: +/// - 复制/导出转写功能需要 tasktype >= 3 且 tasktype < 10002 +/// - 复制/导出/思维导图功能需要 tasktype >= 5 且 tasktype < 10001 +/// ============================================================================ + +/// 分享底部弹窗 - 有状态组件 +/// 使用StatefulWidget以便在build方法中多次获取主题状态 class ShareBottomSheet extends StatefulWidget { const ShareBottomSheet({super.key}); @@ -26,28 +47,50 @@ class ShareBottomSheet extends StatefulWidget { } class _ShareBottomSheetState extends State { + /// 会议详情控制器 - 通过GetX依赖注入获取 + /// 包含会议数据、转写列表、总结内容等核心数据 final _controller = Get.find(); + /// ========================================================================== + /// 构建UI界面 + /// ========================================================================== + /// 整体布局结构: + /// - SafeArea: 适配刘海屏等异形屏幕 + /// - Padding: 水平方向12.w的内边距 + /// - Column: 垂直排列的弹性布局 + /// ├─ 头部区域:会议标题 + 关闭按钮 + /// └─ 功能列表区域:3个功能分组盒子 + /// ├─ 第一组:分享链接 + /// ├─ 第二组:复制转写、复制总结 + /// └─ 第三组:导出音频、导出转写、导出总结、保存思维导图、分享思维导图 + /// ========================================================================== @override Widget build(BuildContext context) { + // 获取当前主题模式(深色/浅色),用于UI颜色适配 final isDarkMode = Theme.of(context).brightness == Brightness.dark; return SafeArea( + // top: false 表示不保护顶部安全区域,让弹窗可以延伸到状态栏下方 top: false, child: Padding( + // 水平方向12.w的内边距,使用flutter_screenutil进行屏幕适配 padding: EdgeInsets.symmetric(horizontal: 12.w), child: Column( + // 主轴尺寸最小化,只占用内容所需高度 mainAxisSize: MainAxisSize.min, children: [ + // ==================== 头部区域:会议标题栏 ==================== Container( padding: EdgeInsets.symmetric(vertical: 10.w), child: Row( children: [ + // 音频文件图标 Icon( Icons.audio_file_outlined, color: isDarkMode ? Colors.grey[500] : Colors.grey, size: 32.w, ), - 5.horizontalSpace, + 5.horizontalSpace, // 5.w的水平间距 + // 会议标题文本 - 从控制器获取 Expanded( child: Text( _controller.meetingData.value.title, @@ -57,6 +100,7 @@ class _ShareBottomSheetState extends State { ), ), ), + // 关闭按钮 - 点击后关闭底部弹窗 GestureDetector( onTap: () => Get.back(), child: Container( @@ -76,15 +120,26 @@ class _ShareBottomSheetState extends State { ], ), ), + // ==================== 功能列表区域 ==================== Padding( padding: EdgeInsets.symmetric(vertical: 10.w), child: Column( children: [ + // ------------------ 功能组1:分享链接 ------------------ _box([ _shareItem( Icons.link, 'shareLink'.tr, // 对应中文:分享链接 isBorder: false, + // 【核心功能模块1:分享链接】 + // 功能说明:生成会议分享链接并通过系统分享面板分享 + // 执行流程: + // 1. 关闭底部弹窗 + // 2. 从本地存储获取用户登录token + // 3. 获取会议ID和服务器地址 + // 4. 构建分享URL格式:https://share.deepechomeet.com/share/{serverHost}/voitrans.net/{id}/{token} + // 5. 调用SharePlus分享链接 + // 6. 分享成功后显示提示 onTap: () async { Get.back(); final GetStorage storage = GetStorage(); @@ -118,7 +173,17 @@ class _ShareBottomSheetState extends State { }, ), ]), + // ------------------ 功能组2:复制功能 ------------------ _box([ + // 【核心功能模块2:复制转写】 + // 功能说明:将会议所有转写文本复制到系统剪贴板 + // 权限控制:仅当 tasktype >= 3 且 tasktype < 10002 时可用 + // 执行流程: + // 1. 检查权限条件,不满足则按钮置灰不可点击 + // 2. 点击后关闭底部弹窗 + // 3. 调用 _getAllText() 获取格式化后的转写文本 + // 4. 调用 _copy() 方法复制到剪贴板 + // 数据格式:说话人 + 时间戳 + 内容 _shareItem(Icons.copy, 'copyTranscript'.tr, // 对应中文:复制转写 onTap: _controller.meetingDetails.value.tasktype >= 3 && _controller.meetingDetails.value.tasktype < @@ -128,6 +193,14 @@ class _ShareBottomSheetState extends State { _copy(_getAllText()); } : null), + // 【核心功能模块3:复制总结】 + // 功能说明:将会议总结内容复制到系统剪贴板 + // 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用 + // 执行流程: + // 1. 检查权限条件,不满足则按钮置灰不可点击 + // 2. 点击后关闭底部弹窗 + // 3. 调用 _markdownToPlainText() 将Markdown格式转为纯文本 + // 4. 调用 _copy() 方法复制到剪贴板 _shareItem(Icons.copy, 'copySummary'.tr, // 对应中文:复制总结 isBorder: false, onTap: _controller.meetingDetails.value.tasktype >= 5 && @@ -140,7 +213,16 @@ class _ShareBottomSheetState extends State { } : null), ]), + // ------------------ 功能组3:导出与分享功能 ------------------ _box([ + // 【核心功能模块4:导出音频】 + // 功能说明:分享会议录音音频文件 + // 执行流程: + // 1. 点击后关闭底部弹窗 + // 2. 调用 _share() 方法分享音频文件路径 + // 参数说明: + // - filepath: 从 meetingData 获取的音频文件路径 + // - title: 分享时显示的标题文本 _shareItem( Icons.multitrack_audio, 'exportAudio'.tr, // 对应中文:导出音频 onTap: () { @@ -148,6 +230,16 @@ class _ShareBottomSheetState extends State { _share(_controller.meetingData.value.filepath, 'shareMeetingAudio'.tr); // 对应中文:分享会议音频 }), + // 【核心功能模块5:导出转写】 + // 功能说明:将转写内容导出为TXT文件并分享 + // 权限控制:仅当 tasktype >= 3 且 tasktype < 10002 时可用 + // 执行流程: + // 1. 检查权限条件,不满足则按钮置灰不可点击 + // 2. 点击后关闭底部弹窗 + // 3. 获取临时目录路径 + // 4. 创建以会议标题命名的TXT文件 + // 5. 调用 _getAllText() 获取转写内容并写入文件 + // 6. 调用 _share() 方法分享文件 _shareItem( Icons.message, 'exportTranscript'.tr, // 对应中文:导出转写 onTap: _controller.meetingDetails.value.tasktype >= 3 && @@ -164,6 +256,16 @@ class _ShareBottomSheetState extends State { 'exportTranscript'.tr); // 对应中文:分享会议转写 } : null), + // 【核心功能模块6:导出总结】 + // 功能说明:将总结内容导出为TXT或PDF格式 + // 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用 + // 执行流程: + // 1. 检查权限条件,不满足则按钮置灰不可点击 + // 2. 点击后关闭底部弹窗 + // 3. 弹出二级底部弹窗供用户选择导出格式(TXT/PDF) + // 4. 根据选择执行对应导出逻辑 + // - TXT: 创建临时文件,写入纯文本内容,调用 _share() 分享 + // - PDF: 调用 _exportMarkdownPdf() 方法生成PDF _shareItem(Icons.article, 'exportSummary'.tr, // 对应中文:导出总结 onTap: _controller.meetingDetails.value.tasktype >= 5 && _controller.meetingDetails.value.tasktype < @@ -174,9 +276,11 @@ class _ShareBottomSheetState extends State { SecondLevelBottomSheet( 'exportSummary'.tr, // 对应中文:导出总结 'exportAs'.tr, // 对应中文:导出为 + // 回调函数:用户选择导出格式后的处理逻辑 + // 参数 index: 0=TXT, 1=PDF (int index) async { if (index == 0) { - // 导出TXT + // 导出TXT格式 final tempDir = await getTemporaryDirectory(); final file = File( @@ -187,10 +291,11 @@ class _ShareBottomSheetState extends State { _share(file.path, 'exportSummary'.tr); // 对应中文:分享会议总结 } else { - // 导出PDF + // 导出PDF格式 _exportMarkdownPdf(); } }, + // 二级弹窗的选项配置 const [ { 'icon': Icons.text_snippet, @@ -216,6 +321,17 @@ class _ShareBottomSheetState extends State { ); } : null), + // 【核心功能模块7:保存思维导图】 + // 功能说明:将思维导图保存为图片到本地相册 + // 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用 + // 执行流程: + // 1. 检查权限条件,不满足则按钮置灰不可点击 + // 2. 点击后关闭底部弹窗 + // 3. 设置 imageType = 1 表示保存操作 + // 4. 检查WebView是否已加载完成 + // - 已就绪:直接执行 JavaScript 的 captureImage() 函数 + // - 未就绪:设置导航委托,在页面加载完成后执行 + // 交互组件:与 MindMapController 和 WebView 协同工作 _shareItem( Icons.save_alt, 'saveMindMap'.tr, @@ -225,7 +341,7 @@ class _ShareBottomSheetState extends State { Get.back(); final mindMapController = Get.find(); - mindMapController.imageType = 1; + mindMapController.imageType = 1; // 1=保存到本地 if (mindMapController.isWebViewReady) { mindMapController.webViewController .runJavaScript('captureImage()'); @@ -244,6 +360,18 @@ class _ShareBottomSheetState extends State { } : null, ), + // 【核心功能模块8:分享思维导图】 + // 功能说明:将思维导图以图片形式通过系统分享面板分享 + // 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用 + // 执行流程: + // 1. 检查权限条件,不满足则按钮置灰不可点击 + // 2. 点击后关闭底部弹窗 + // 3. 设置 imageType = 2 表示分享操作 + // 4. 检查WebView是否已加载完成 + // - 已就绪:直接执行 JavaScript 的 captureImage() 函数 + // - 未就绪:设置导航委托,在页面加载完成后执行 + // 交互组件:与 MindMapController 和 WebView 协同工作 + // 注意:与保存思维导图的区别在于 imageType 的值不同,后续处理逻辑不同 _shareItem( Icons.lan, 'shareMindMap'.tr, // 对应中文:分享思维导图 @@ -254,7 +382,7 @@ class _ShareBottomSheetState extends State { Get.back(); final mindMapController = Get.find(); - mindMapController.imageType = 2; + mindMapController.imageType = 2; // 2=分享 if (mindMapController.isWebViewReady) { mindMapController.webViewController .runJavaScript('captureImage()'); @@ -283,6 +411,21 @@ class _ShareBottomSheetState extends State { ); } + /// ========================================================================== + /// UI组件构建方法 + /// ========================================================================== + + /// 功能分组盒子组件 + /// 用于将相关的功能项组合在一起,提供统一的圆角和背景样式 + /// + /// 参数说明: + /// - children: 子组件列表,通常为多个 _shareItem + /// + /// 样式特性: + /// - 底部外边距:10.w + /// - 裁剪行为:硬边缘裁剪 + /// - 背景色:深色模式 #2D2D2D,浅色模式白色 + /// - 圆角半径:12.r Widget _box(List children) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Container( @@ -298,6 +441,20 @@ class _ShareBottomSheetState extends State { ); } + /// 分享功能项组件 + /// 单个功能按钮的UI封装,支持图标、标题、点击事件和禁用状态 + /// + /// 参数说明: + /// - icon: 功能图标(IconData类型) + /// - title: 功能标题文本 + /// - isBorder: 是否显示底部边框,用于分隔多个功能项(默认true) + /// - onTap: 点击回调函数,为null时按钮置灰不可点击 + /// + /// 样式特性: + /// - 水平内边距:12.w,垂直内边距:14.w + /// - 禁用状态背景色:深色模式 grey[800],浅色模式 grey[200] + /// - 边框颜色:深色模式 grey[700],浅色模式 grey[300] + /// - 图标和文字透明度:启用状态1.0,禁用状态0.5 Widget _shareItem( IconData icon, String title, { @@ -324,6 +481,7 @@ class _ShareBottomSheetState extends State { ), child: Row( children: [ + // 图标 - 根据启用状态调整透明度 Opacity( opacity: onTap != null ? 1.0 : 0.5, child: Icon( @@ -331,7 +489,8 @@ class _ShareBottomSheetState extends State { color: isDarkMode ? Colors.white : Colors.black87, ), ), - 10.horizontalSpace, + 10.horizontalSpace, // 图标与文字间距 + // 标题文本 - 根据启用状态调整透明度 Expanded( child: Text( title, @@ -348,6 +507,23 @@ class _ShareBottomSheetState extends State { ); } + /// ========================================================================== + /// 核心功能方法 + /// ========================================================================== + + /// 分享文件方法 + /// 使用 SharePlus 插件分享文件到系统分享面板 + /// + /// 参数说明: + /// - filepath: 要分享的文件的完整路径 + /// - title: 分享时显示的标题/描述文本 + /// + /// 执行流程: + /// 1. 构建 ShareParams 对象,包含文本和文件列表 + /// 2. 调用 SharePlus.instance.share() 触发系统分享 + /// 3. 检查分享结果状态,成功时显示提示 + /// + /// 返回值:Future - 异步操作 Future _share(String filepath, String title) async { final result = await SharePlus.instance.share(ShareParams( text: title, @@ -358,6 +534,21 @@ class _ShareBottomSheetState extends State { } } + /// 复制文本到剪贴板方法 + /// 使用 Flutter 的 Clipboard 服务将文本复制到系统剪贴板 + /// + /// 参数说明: + /// - text: 要复制的文本内容 + /// + /// 执行流程: + /// 1. 调用 Clipboard.setData() 设置剪贴板数据 + /// 2. 复制成功后显示 GetX Snackbar 提示 + /// + /// 提示样式: + /// - 标题:'success'.tr(成功) + /// - 内容:'copySuccess'.tr(复制成功) + /// - 背景色:蓝色半透明 + /// - 文字颜色:白色 void _copy(String text) { Clipboard.setData(ClipboardData( text: text, @@ -372,6 +563,27 @@ class _ShareBottomSheetState extends State { } /// 将Markdown格式转换为纯文本 + /// 用于在复制或导出时将富文本格式转为纯文本,便于阅读和处理 + /// + /// 参数说明: + /// - markdown: Markdown格式的原始文本 + /// + /// 返回值:String - 转换后的纯文本 + /// + /// 转换规则: + /// 1. 移除粗体标记 **text** 或 __text__ + /// 2. 移除斜体标记 *text* 或 _text_ + /// 3. 移除删除线标记 ~~text~~ + /// 4. 移除代码块标记 ```code``` + /// 5. 移除行内代码标记 `code` + /// 6. 移除链接标记 [text](url),保留文本 + /// 7. 移除图片标记 ![alt](url),保留alt文本 + /// 8. 移除标题标记 # ## ### 等 + /// 9. 移除引用标记 > + /// 10. 移除无序列表标记 - * + + /// 11. 移除有序列表标记 1. 2. 等 + /// 12. 移除水平分割线 --- *** ___ + /// 13. 清理多余空行 String _markdownToPlainText(String markdown) { String text = markdown; @@ -428,22 +640,61 @@ class _ShareBottomSheetState extends State { return text.trim(); } + /// 获取所有转写文本 + /// 将会议转写列表格式化为带说话人和时间戳的文本 + /// + /// 返回值:String - 格式化后的完整转写文本 + /// + /// 数据格式: + /// 说话人 时间戳 + /// 内容 + /// + /// 示例输出: + /// 张三 09:30:15 + /// 大家好,会议开始。 + /// 李四 09:30:20 + /// 收到,请继续。 + /// + /// 数据来源: + /// - _controller.textList: 转写数据列表 + /// - speaker: 说话人名称 + /// - starttime: 开始时间戳(毫秒) + /// - content: 转写内容 String _getAllText() { String text = ''; for (var element in _controller.textList) { + // 添加说话人信息(如果存在) if (element['speaker'].isNotEmpty) { text += '${element['speaker']} '; } + // 添加格式化后的时间戳(UTC时间,HH:mm:ss格式) text += '${DateUtil.formatDateMs( element['starttime'], format: 'HH:mm:ss', isUtc: true, )}\n'; + // 添加转写内容 text += '${element['content']}\n'; } return text; } + /// 获取资源文件的缩略图文件 + /// 将assets中的图片资源复制到临时目录,用于分享预览 + /// + /// 参数说明: + /// - assetPath: 资源文件在assets目录中的路径 + /// - fileName: 保存到临时目录的文件名 + /// + /// 返回值:Future - 临时文件的XFile对象 + /// + /// 执行流程: + /// 1. 使用 rootBundle.load() 加载资源文件字节数据 + /// 2. 获取应用临时目录 + /// 3. 创建目标文件并写入字节数据 + /// 4. 返回XFile对象供分享使用 + /// + /// 注意:当前代码中此方法未被实际调用(注释状态) Future getAssetThumbnailFile(String assetPath, String fileName) async { final byteData = await rootBundle.load(assetPath); final tempDir = await getTemporaryDirectory(); @@ -452,6 +703,25 @@ class _ShareBottomSheetState extends State { return XFile(file.path); } + /// 导出Markdown为PDF文件 + /// 将会议总结内容转换为PDF格式并分享 + /// + /// 执行流程: + /// 1. 显示加载提示 "正在导出PDF..." + /// 2. 获取临时目录路径 + /// 3. 创建以会议标题命名的PDF文件 + /// 4. 使用 htmltopdfwidgets 插件将Markdown转换为PDF组件列表 + /// - 加载 NotoSansSC 字体以支持中文显示 + /// 5. 创建PDF文档并添加多页内容 + /// 6. 保存PDF文件到临时目录 + /// 7. 调用 _share() 方法分享PDF文件 + /// 8. 关闭加载提示 + /// + /// 依赖组件: + /// - htmltopdfwidgets: 用于Markdown到PDF的转换 + /// - NotoSansSC-Regular.ttf: 中文字体资源 + /// + /// 返回值:Future - 异步操作 Future _exportMarkdownPdf() async { EasyLoading.show(status: 'exportingPDF'.tr); // 对应中文:正在导出PDF... final tempDir = await getTemporaryDirectory(); diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index ea87ad120..58d5b3369 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -96,7 +96,7 @@ class MeetingDetailsView extends GetView { if (!controller.isPlayerReady.value) { Get.snackbar( 'error'.tr, - '音频播放器未准备好,请稍候', + 'playerNotReady'.tr, // 音频播放器未准备好,请稍候 snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 2), ); @@ -112,7 +112,7 @@ class MeetingDetailsView extends GetView { } catch (e) { Get.snackbar( 'error'.tr, - '播放失败: ${e.toString()}', + "${'playFailed'.tr}: ${e.toString()}", // 播放失败 snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 2), ); @@ -542,7 +542,7 @@ class MeetingDetailsView extends GetView { if (!controller.isPlayerReady.value) { Get.snackbar( 'error'.tr, - '音频播放器未准备好,请稍候', + 'playerNotReady'.tr, // 音频播放器未准备好,请稍候 snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 2), ); @@ -560,7 +560,7 @@ class MeetingDetailsView extends GetView { } catch (e) { Get.snackbar( 'error'.tr, - '播放失败: ${e.toString()}', + "${'playFailed'.tr}: ${e.toString()}", // 播放失败 snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 2), ); diff --git a/lib/modules/meeting/views/meeting_home_view.dart b/lib/modules/meeting/views/meeting_home_view.dart index 86a960cf0..86160c47a 100644 --- a/lib/modules/meeting/views/meeting_home_view.dart +++ b/lib/modules/meeting/views/meeting_home_view.dart @@ -268,10 +268,10 @@ class _MeetingHomeViewState extends State tasktypeText = 'summaryComplete'.tr; // 对应中文:总结完成 break; case 10001: - tasktypeText = '总结失败'; + tasktypeText = 'summaryFailedShort'.tr; // 总结失败 break; case 10002: - tasktypeText = '转写失败'; + tasktypeText = 'transcriptionFailedShort'.tr; // 转写失败 break; default: tasktypeText = ''; diff --git a/lib/modules/meeting/views/tabs/overview_tab.dart b/lib/modules/meeting/views/tabs/overview_tab.dart index 335237619..87e43f459 100644 --- a/lib/modules/meeting/views/tabs/overview_tab.dart +++ b/lib/modules/meeting/views/tabs/overview_tab.dart @@ -144,7 +144,7 @@ class _OverviewTabState extends State { width: 150.w, child: _button( Icons.auto_awesome, - '重新生成', + 'regenerate'.tr, // 重新生成 () { Get.bottomSheet( const GenerateBottomSheet(), @@ -165,7 +165,7 @@ class _OverviewTabState extends State { width: 150.w, child: _button( Icons.auto_awesome, - '重新总结', + 'reSummarize'.tr, // 重新总结 () { Get.bottomSheet( const GenerateBottomSheet( @@ -274,7 +274,7 @@ class _OverviewTabState extends State { Text( _controller.meetingDetails.value.tasktype < 10000 ? 'aiAnalysisDesc'.tr // 对应中文:AI分析识别录音中的关键事件,让你选择重要事件进行AI生成。 - : '分析失败,请稍后重试', + : 'analysisFailed'.tr, // 分析失败,请稍后重试 style: TextStyle( color: MeetingUIUtils.getSecondaryTextColor(isDarkMode), fontSize: 14.sp, diff --git a/lib/modules/meeting/views/tabs/speech_tab.dart b/lib/modules/meeting/views/tabs/speech_tab.dart index f0c85d81f..bcf6e1224 100644 --- a/lib/modules/meeting/views/tabs/speech_tab.dart +++ b/lib/modules/meeting/views/tabs/speech_tab.dart @@ -266,7 +266,7 @@ class SpeechTab extends GetView { height: 200.w, alignment: Alignment.center, child: Text( - '转写失败,请稍后重试', + 'transcriptionFailed'.tr, // 转写失败,请稍后重试 style: TextStyle( color: MeetingUIUtils.getSecondaryTextColor(isDarkMode), fontSize: 14.sp, diff --git a/lib/modules/meeting/views/tabs/summary_tab.dart b/lib/modules/meeting/views/tabs/summary_tab.dart index 6e81c36f5..352afc71f 100644 --- a/lib/modules/meeting/views/tabs/summary_tab.dart +++ b/lib/modules/meeting/views/tabs/summary_tab.dart @@ -344,7 +344,7 @@ class _SummaryTabState extends State { ) : Center( child: Text( - '总结失败,请稍后重试', + 'summaryFailed'.tr, // 总结失败,请稍后重试 style: TextStyle( color: MeetingUIUtils.getSecondaryTextColor( isDarkMode), diff --git a/lib/modules/meeting/views/template/template_edit_view.dart b/lib/modules/meeting/views/template/template_edit_view.dart index e075b954f..0c31f3603 100644 --- a/lib/modules/meeting/views/template/template_edit_view.dart +++ b/lib/modules/meeting/views/template/template_edit_view.dart @@ -81,7 +81,9 @@ class _TemplateEditViewState extends State { onPressed: () => Get.back(), ), title: Text( - widget.data != null ? 'editTemplate'.tr : 'createTemplate'.tr, // 编辑模版/创建模版 + widget.data != null + ? 'editTemplate'.tr + : 'createTemplate'.tr, // 编辑模版/创建模版 style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, @@ -108,7 +110,8 @@ class _TemplateEditViewState extends State { _promptController, minLines: 8, maxLines: 100, - hintText: 'templateContentHint'.tr, // 请输入模版内容!温馨提示:点击上方AI,根据上面填写的名称和描述,可以帮助你快速生成模版内容 + hintText: 'templateContentHint' + .tr, // 请输入模版内容!温馨提示:点击上方AI,根据上面填写的名称和描述,可以帮助你快速生成模版内容 ), ], ),