Browse Source

feat(meeting): 添加会议分享底部弹窗及多语言支持

- 新增分享底部弹窗组件,实现分享链接、复制转写、复制总结、导出音频、导出转写、导出总结、保存思维导图和分享思维导图功能
- 分享功能包含权限控制,依据tasktype判断按钮是否可用
- 复制功能支持将转写文本和总结内容复制到剪贴板,支持Markdown转纯文本格式转换
- 导出支持导出转写TXT和总结TXT/PDF格式,PDF导出支持中文字体
- 录音播放器和会议模块新增多种错误提示文案,支持中英文、简繁体多语言
- 优化音频播放错误提示的国际化文本替换,统一调用国际化资源
- 修正部分UI组件中文字符串直接写死,改为使用国际化翻译键
- 提升底部弹窗样式和交互体验,支持深色/浅色主题自适应
- 会议详情页和各标签页使用国际化文本替代原硬编码字符串,兼容多语言环境
newdev_shunjiawei
lxm 7 months ago
parent
commit
b4fb143418
  1. 27
      lib/core/translations/language/en_us.dart
  2. 14
      lib/core/translations/language/zh_cn.dart
  3. 14
      lib/core/translations/language/zh_hk.dart
  4. 14
      lib/core/translations/language/zh_tw.dart
  5. 4
      lib/modules/meeting/controllers/meeting_details_controller.dart
  6. 282
      lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart
  7. 8
      lib/modules/meeting/views/meeting_details_view.dart
  8. 4
      lib/modules/meeting/views/meeting_home_view.dart
  9. 6
      lib/modules/meeting/views/tabs/overview_tab.dart
  10. 2
      lib/modules/meeting/views/tabs/speech_tab.dart
  11. 2
      lib/modules/meeting/views/tabs/summary_tab.dart
  12. 7
      lib/modules/meeting/views/template/template_edit_view.dart

27
lib/core/translations/language/en_us.dart

@ -286,11 +286,11 @@ const Map<String, String> enUS = {
'paymentTestInstructionsTitle': 'Instructions', 'paymentTestInstructionsTitle': 'Instructions',
'paymentTestInstructionsContent': 'paymentTestInstructionsContent':
'1. Select a test amount (recommended: ¥0.01)\n' '1. Select a test amount (recommended: ¥0.01)\n'
'2. Tap the "WeChat Pay" button\n' '2. Tap the "WeChat Pay" button\n'
'3. The app will invoke the WeChat Pay SDK\n' '3. The app will invoke the WeChat Pay SDK\n'
'4. Complete the payment in WeChat\n' '4. Complete the payment in WeChat\n'
'5. Return to the app to view the result\n\n' '5. Return to the app to view the result\n\n'
'Note: This is a test environment. No real charge will occur.', 'Note: This is a test environment. No real charge will occur.',
'startingWechatPayment': 'Starting WeChat Pay...', 'startingWechatPayment': 'Starting WeChat Pay...',
'startingAlipayPayment': 'Starting Alipay...', 'startingAlipayPayment': 'Starting Alipay...',
'wechatPaymentSuccess': 'WeChat Pay successful!', 'wechatPaymentSuccess': 'WeChat Pay successful!',
@ -635,7 +635,8 @@ const Map<String, String> enUS = {
'editTemplate': 'Edit Template', 'editTemplate': 'Edit Template',
'createTemplate': 'Create Template', 'createTemplate': 'Create Template',
'name': 'Name', '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', 'inputNameHint': 'Please enter name',
'inputDescHint': 'Please enter description', 'inputDescHint': 'Please enter description',
'custom': 'Custom', 'custom': 'Custom',
@ -687,6 +688,19 @@ const Map<String, String> enUS = {
// Meeting模块 - 更多操作相关 // Meeting模块 - 更多操作相关
'reSummarize': 'Re-summarize', '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模块 - 录音语言相关 // Meeting模块 - 录音语言相关
'recordLanguage': 'Recording Language', 'recordLanguage': 'Recording Language',
// 'chinese': 'Chinese Mandarin (Simplified)', // 'chinese': 'Chinese Mandarin (Simplified)',
@ -711,6 +725,7 @@ const Map<String, String> enUS = {
'exportSummary': 'Export Summary', 'exportSummary': 'Export Summary',
'exportAs': 'Export As', 'exportAs': 'Export As',
'exportingPDF': 'Exporting PDF...', 'exportingPDF': 'Exporting PDF...',
'export': 'Export',
// Meeting模块 - 发言者相关 // Meeting模块 - 发言者相关
'applyToAllParagraphs': 'Apply to All Paragraphs of This Speaker', 'applyToAllParagraphs': 'Apply to All Paragraphs of This Speaker',

14
lib/core/translations/language/zh_cn.dart

@ -637,6 +637,19 @@ const Map<String, String> zhCN = {
// Meeting模块 - 更多操作相关 // Meeting模块 - 更多操作相关
'reSummarize': '重新总结', 'reSummarize': '重新总结',
// Meeting模块 - 错误提示相关
'analysisFailed': '分析失败,请稍后重试',
'transcriptionFailed': '转写失败,请稍后重试',
'summaryFailed': '总结失败,请稍后重试',
'summaryFailedShort': '总结失败',
'transcriptionFailedShort': '转写失败',
// Meeting模块 - 播放器相关
'playerNotReady': '音频播放器未准备好,请稍候',
'playFailed': '播放失败',
'audioFileNotFound': '音频文件不存在',
'playerLoadFailed': '音频播放器加载失败',
// Meeting模块 - 录音语言相关 // Meeting模块 - 录音语言相关
'recordLanguage': '录音语言', 'recordLanguage': '录音语言',
// 'chinese': '中文普通话(简体)', // 'chinese': '中文普通话(简体)',
@ -661,6 +674,7 @@ const Map<String, String> zhCN = {
'exportSummary': '导出总结', 'exportSummary': '导出总结',
'exportAs': '导出为', 'exportAs': '导出为',
'exportingPDF': '正在导出PDF...', 'exportingPDF': '正在导出PDF...',
'export': '导出',
// Meeting模块 - 发言者相关 // Meeting模块 - 发言者相关
'applyToAllParagraphs': '应用到这个发言者的所有段落', 'applyToAllParagraphs': '应用到这个发言者的所有段落',

14
lib/core/translations/language/zh_hk.dart

@ -639,6 +639,19 @@ const Map<String, String> zhHK = {
// Meeting模块 - 更多操作相关 // Meeting模块 - 更多操作相关
'reSummarize': '重新總結', 'reSummarize': '重新總結',
// Meeting模块 - 错误提示相关
'analysisFailed': '分析失敗,請稍後重試',
'transcriptionFailed': '轉寫失敗,請稍後重試',
'summaryFailed': '總結失敗,請稍後重試',
'summaryFailedShort': '總結失敗',
'transcriptionFailedShort': '轉寫失敗',
// Meeting模块 - 播放器相关
'playerNotReady': '音頻播放器未準備好,請稍候',
'playFailed': '播放失敗',
'audioFileNotFound': '音頻文件不存在',
'playerLoadFailed': '音頻播放器加載失敗',
// Meeting模块 - 录音语言相关 // Meeting模块 - 录音语言相关
'recordLanguage': '錄音語言', 'recordLanguage': '錄音語言',
// 'chinese': '中文普通话(简体)', // 'chinese': '中文普通话(简体)',
@ -663,6 +676,7 @@ const Map<String, String> zhHK = {
'exportSummary': '導出總結', 'exportSummary': '導出總結',
'exportAs': '導出為', 'exportAs': '導出為',
'exportingPDF': '正在導出PDF...', 'exportingPDF': '正在導出PDF...',
'export': '導出',
// Meeting模块 - 发言者相关 // Meeting模块 - 发言者相关
'applyToAllParagraphs': '應用到這個發言者的所有段落', 'applyToAllParagraphs': '應用到這個發言者的所有段落',

14
lib/core/translations/language/zh_tw.dart

@ -639,6 +639,19 @@ const Map<String, String> zhTW = {
// Meeting模块 - 更多操作相关 // Meeting模块 - 更多操作相关
'reSummarize': '重新總結', 'reSummarize': '重新總結',
// Meeting模块 - 错误提示相关
'analysisFailed': '分析失敗,請稍後重試',
'transcriptionFailed': '轉寫失敗,請稍後重試',
'summaryFailed': '總結失敗,請稍後重試',
'summaryFailedShort': '總結失敗',
'transcriptionFailedShort': '轉寫失敗',
// Meeting模块 - 播放器相关
'playerNotReady': '音頻播放器未準備好,請稍候',
'playFailed': '播放失敗',
'audioFileNotFound': '音頻文件不存在',
'playerLoadFailed': '音頻播放器加載失敗',
// Meeting模块 - 录音语言相关 // Meeting模块 - 录音语言相关
'recordLanguage': '錄音語言', 'recordLanguage': '錄音語言',
// 'chinese': '中文普通话(简体)', // 'chinese': '中文普通话(简体)',
@ -663,6 +676,7 @@ const Map<String, String> zhTW = {
'exportSummary': '導出總結', 'exportSummary': '導出總結',
'exportAs': '導出為', 'exportAs': '導出為',
'exportingPDF': '正在導出PDF...', 'exportingPDF': '正在導出PDF...',
'export': '導出',
// Meeting模块 - 发言者相关 // Meeting模块 - 发言者相关
'applyToAllParagraphs': '應用到這個發言者的所有段落', 'applyToAllParagraphs': '應用到這個發言者的所有段落',

4
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -172,7 +172,7 @@ class MeetingDetailsController extends GetxController
isPlayerReady.value = false; isPlayerReady.value = false;
Get.snackbar( Get.snackbar(
'error'.tr, 'error'.tr,
'音频文件不存在', 'audioFileNotFound'.tr, // 音频文件不存在
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
); );
@ -265,7 +265,7 @@ class MeetingDetailsController extends GetxController
Get.snackbar( Get.snackbar(
'error'.tr, 'error'.tr,
'音频播放器加载失败', 'playerLoadFailed'.tr, // 音频播放器加载失败
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
); );

282
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 '../../controllers/mind_map_controller.dart';
import 'second_level_bottom_sheet.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 { class ShareBottomSheet extends StatefulWidget {
const ShareBottomSheet({super.key}); const ShareBottomSheet({super.key});
@ -26,28 +47,50 @@ class ShareBottomSheet extends StatefulWidget {
} }
class _ShareBottomSheetState extends State<ShareBottomSheet> { class _ShareBottomSheetState extends State<ShareBottomSheet> {
/// 会议详情控制器 - 通过GetX依赖注入获取
/// 包含会议数据、转写列表、总结内容等核心数据
final _controller = Get.find<MeetingDetailsController>(); final _controller = Get.find<MeetingDetailsController>();
/// ==========================================================================
/// 构建UI界面
/// ==========================================================================
/// 整体布局结构:
/// - SafeArea: 适配刘海屏等异形屏幕
/// - Padding: 水平方向12.w的内边距
/// - Column: 垂直排列的弹性布局
/// ├─ 头部区域:会议标题 + 关闭按钮
/// └─ 功能列表区域:3个功能分组盒子
/// ├─ 第一组:分享链接
/// ├─ 第二组:复制转写、复制总结
/// └─ 第三组:导出音频、导出转写、导出总结、保存思维导图、分享思维导图
/// ==========================================================================
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 获取当前主题模式(深色/浅色),用于UI颜色适配
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return SafeArea( return SafeArea(
// top: false 表示不保护顶部安全区域,让弹窗可以延伸到状态栏下方
top: false, top: false,
child: Padding( child: Padding(
// 水平方向12.w的内边距,使用flutter_screenutil进行屏幕适配
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column( child: Column(
// 主轴尺寸最小化,只占用内容所需高度
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// ==================== 头部区域:会议标题栏 ====================
Container( Container(
padding: EdgeInsets.symmetric(vertical: 10.w), padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row( child: Row(
children: [ children: [
// 音频文件图标
Icon( Icon(
Icons.audio_file_outlined, Icons.audio_file_outlined,
color: isDarkMode ? Colors.grey[500] : Colors.grey, color: isDarkMode ? Colors.grey[500] : Colors.grey,
size: 32.w, size: 32.w,
), ),
5.horizontalSpace, 5.horizontalSpace, // 5.w的水平间距
// 会议标题文本 - 从控制器获取
Expanded( Expanded(
child: Text( child: Text(
_controller.meetingData.value.title, _controller.meetingData.value.title,
@ -57,6 +100,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
), ),
), ),
), ),
// 关闭按钮 - 点击后关闭底部弹窗
GestureDetector( GestureDetector(
onTap: () => Get.back(), onTap: () => Get.back(),
child: Container( child: Container(
@ -76,15 +120,26 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
], ],
), ),
), ),
// ==================== 功能列表区域 ====================
Padding( Padding(
padding: EdgeInsets.symmetric(vertical: 10.w), padding: EdgeInsets.symmetric(vertical: 10.w),
child: Column( child: Column(
children: [ children: [
// ------------------ 功能组1:分享链接 ------------------
_box([ _box([
_shareItem( _shareItem(
Icons.link, Icons.link,
'shareLink'.tr, // 对应中文:分享链接 'shareLink'.tr, // 对应中文:分享链接
isBorder: false, 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 { onTap: () async {
Get.back(); Get.back();
final GetStorage storage = GetStorage(); final GetStorage storage = GetStorage();
@ -118,7 +173,17 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
}, },
), ),
]), ]),
// ------------------ 功能组2:复制功能 ------------------
_box([ _box([
// 【核心功能模块2:复制转写】
// 功能说明:将会议所有转写文本复制到系统剪贴板
// 权限控制:仅当 tasktype >= 3 且 tasktype < 10002 时可用
// 执行流程:
// 1. 检查权限条件,不满足则按钮置灰不可点击
// 2. 点击后关闭底部弹窗
// 3. 调用 _getAllText() 获取格式化后的转写文本
// 4. 调用 _copy() 方法复制到剪贴板
// 数据格式:说话人 + 时间戳 + 内容
_shareItem(Icons.copy, 'copyTranscript'.tr, // 对应中文:复制转写 _shareItem(Icons.copy, 'copyTranscript'.tr, // 对应中文:复制转写
onTap: _controller.meetingDetails.value.tasktype >= 3 && onTap: _controller.meetingDetails.value.tasktype >= 3 &&
_controller.meetingDetails.value.tasktype < _controller.meetingDetails.value.tasktype <
@ -128,6 +193,14 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
_copy(_getAllText()); _copy(_getAllText());
} }
: null), : null),
// 【核心功能模块3:复制总结】
// 功能说明:将会议总结内容复制到系统剪贴板
// 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用
// 执行流程:
// 1. 检查权限条件,不满足则按钮置灰不可点击
// 2. 点击后关闭底部弹窗
// 3. 调用 _markdownToPlainText() 将Markdown格式转为纯文本
// 4. 调用 _copy() 方法复制到剪贴板
_shareItem(Icons.copy, 'copySummary'.tr, // 对应中文:复制总结 _shareItem(Icons.copy, 'copySummary'.tr, // 对应中文:复制总结
isBorder: false, isBorder: false,
onTap: _controller.meetingDetails.value.tasktype >= 5 && onTap: _controller.meetingDetails.value.tasktype >= 5 &&
@ -140,7 +213,16 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
} }
: null), : null),
]), ]),
// ------------------ 功能组3:导出与分享功能 ------------------
_box([ _box([
// 【核心功能模块4:导出音频】
// 功能说明:分享会议录音音频文件
// 执行流程:
// 1. 点击后关闭底部弹窗
// 2. 调用 _share() 方法分享音频文件路径
// 参数说明:
// - filepath: 从 meetingData 获取的音频文件路径
// - title: 分享时显示的标题文本
_shareItem( _shareItem(
Icons.multitrack_audio, 'exportAudio'.tr, // 对应中文:导出音频 Icons.multitrack_audio, 'exportAudio'.tr, // 对应中文:导出音频
onTap: () { onTap: () {
@ -148,6 +230,16 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
_share(_controller.meetingData.value.filepath, _share(_controller.meetingData.value.filepath,
'shareMeetingAudio'.tr); // 对应中文:分享会议音频 'shareMeetingAudio'.tr); // 对应中文:分享会议音频
}), }),
// 【核心功能模块5:导出转写】
// 功能说明:将转写内容导出为TXT文件并分享
// 权限控制:仅当 tasktype >= 3 且 tasktype < 10002 时可用
// 执行流程:
// 1. 检查权限条件,不满足则按钮置灰不可点击
// 2. 点击后关闭底部弹窗
// 3. 获取临时目录路径
// 4. 创建以会议标题命名的TXT文件
// 5. 调用 _getAllText() 获取转写内容并写入文件
// 6. 调用 _share() 方法分享文件
_shareItem( _shareItem(
Icons.message, 'exportTranscript'.tr, // 对应中文:导出转写 Icons.message, 'exportTranscript'.tr, // 对应中文:导出转写
onTap: _controller.meetingDetails.value.tasktype >= 3 && onTap: _controller.meetingDetails.value.tasktype >= 3 &&
@ -164,6 +256,16 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
'exportTranscript'.tr); // 对应中文:分享会议转写 'exportTranscript'.tr); // 对应中文:分享会议转写
} }
: null), : null),
// 【核心功能模块6:导出总结】
// 功能说明:将总结内容导出为TXT或PDF格式
// 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用
// 执行流程:
// 1. 检查权限条件,不满足则按钮置灰不可点击
// 2. 点击后关闭底部弹窗
// 3. 弹出二级底部弹窗供用户选择导出格式(TXT/PDF)
// 4. 根据选择执行对应导出逻辑
// - TXT: 创建临时文件,写入纯文本内容,调用 _share() 分享
// - PDF: 调用 _exportMarkdownPdf() 方法生成PDF
_shareItem(Icons.article, 'exportSummary'.tr, // 对应中文:导出总结 _shareItem(Icons.article, 'exportSummary'.tr, // 对应中文:导出总结
onTap: _controller.meetingDetails.value.tasktype >= 5 && onTap: _controller.meetingDetails.value.tasktype >= 5 &&
_controller.meetingDetails.value.tasktype < _controller.meetingDetails.value.tasktype <
@ -174,9 +276,11 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
SecondLevelBottomSheet( SecondLevelBottomSheet(
'exportSummary'.tr, // 对应中文:导出总结 'exportSummary'.tr, // 对应中文:导出总结
'exportAs'.tr, // 对应中文:导出为 'exportAs'.tr, // 对应中文:导出为
// 回调函数:用户选择导出格式后的处理逻辑
// 参数 index: 0=TXT, 1=PDF
(int index) async { (int index) async {
if (index == 0) { if (index == 0) {
// 导出TXT // 导出TXT格式
final tempDir = final tempDir =
await getTemporaryDirectory(); await getTemporaryDirectory();
final file = File( final file = File(
@ -187,10 +291,11 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
_share(file.path, _share(file.path,
'exportSummary'.tr); // 对应中文:分享会议总结 'exportSummary'.tr); // 对应中文:分享会议总结
} else { } else {
// 导出PDF // 导出PDF格式
_exportMarkdownPdf(); _exportMarkdownPdf();
} }
}, },
// 二级弹窗的选项配置
const [ const [
{ {
'icon': Icons.text_snippet, 'icon': Icons.text_snippet,
@ -216,6 +321,17 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
); );
} }
: null), : null),
// 【核心功能模块7:保存思维导图】
// 功能说明:将思维导图保存为图片到本地相册
// 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用
// 执行流程:
// 1. 检查权限条件,不满足则按钮置灰不可点击
// 2. 点击后关闭底部弹窗
// 3. 设置 imageType = 1 表示保存操作
// 4. 检查WebView是否已加载完成
// - 已就绪:直接执行 JavaScript 的 captureImage() 函数
// - 未就绪:设置导航委托,在页面加载完成后执行
// 交互组件:与 MindMapController 和 WebView 协同工作
_shareItem( _shareItem(
Icons.save_alt, Icons.save_alt,
'saveMindMap'.tr, 'saveMindMap'.tr,
@ -225,7 +341,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
Get.back(); Get.back();
final mindMapController = final mindMapController =
Get.find<MindMapController>(); Get.find<MindMapController>();
mindMapController.imageType = 1; mindMapController.imageType = 1; // 1=保存到本地
if (mindMapController.isWebViewReady) { if (mindMapController.isWebViewReady) {
mindMapController.webViewController mindMapController.webViewController
.runJavaScript('captureImage()'); .runJavaScript('captureImage()');
@ -244,6 +360,18 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
} }
: null, : null,
), ),
// 【核心功能模块8:分享思维导图】
// 功能说明:将思维导图以图片形式通过系统分享面板分享
// 权限控制:仅当 tasktype >= 5 且 tasktype < 10001 时可用
// 执行流程:
// 1. 检查权限条件,不满足则按钮置灰不可点击
// 2. 点击后关闭底部弹窗
// 3. 设置 imageType = 2 表示分享操作
// 4. 检查WebView是否已加载完成
// - 已就绪:直接执行 JavaScript 的 captureImage() 函数
// - 未就绪:设置导航委托,在页面加载完成后执行
// 交互组件:与 MindMapController 和 WebView 协同工作
// 注意:与保存思维导图的区别在于 imageType 的值不同,后续处理逻辑不同
_shareItem( _shareItem(
Icons.lan, Icons.lan,
'shareMindMap'.tr, // 对应中文:分享思维导图 'shareMindMap'.tr, // 对应中文:分享思维导图
@ -254,7 +382,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
Get.back(); Get.back();
final mindMapController = final mindMapController =
Get.find<MindMapController>(); Get.find<MindMapController>();
mindMapController.imageType = 2; mindMapController.imageType = 2; // 2=分享
if (mindMapController.isWebViewReady) { if (mindMapController.isWebViewReady) {
mindMapController.webViewController mindMapController.webViewController
.runJavaScript('captureImage()'); .runJavaScript('captureImage()');
@ -283,6 +411,21 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
); );
} }
/// ==========================================================================
/// UI组件构建方法
/// ==========================================================================
/// 功能分组盒子组件
/// 用于将相关的功能项组合在一起,提供统一的圆角和背景样式
///
/// 参数说明:
/// - children: 子组件列表,通常为多个 _shareItem
///
/// 样式特性:
/// - 底部外边距:10.w
/// - 裁剪行为:硬边缘裁剪
/// - 背景色:深色模式 #2D2D2D,浅色模式白色
/// - 圆角半径:12.r
Widget _box(List<Widget> children) { Widget _box(List<Widget> children) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container( return Container(
@ -298,6 +441,20 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
); );
} }
/// 分享功能项组件
/// 单个功能按钮的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( Widget _shareItem(
IconData icon, IconData icon,
String title, { String title, {
@ -324,6 +481,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
), ),
child: Row( child: Row(
children: [ children: [
// 图标 - 根据启用状态调整透明度
Opacity( Opacity(
opacity: onTap != null ? 1.0 : 0.5, opacity: onTap != null ? 1.0 : 0.5,
child: Icon( child: Icon(
@ -331,7 +489,8 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
), ),
), ),
10.horizontalSpace, 10.horizontalSpace, // 图标与文字间距
// 标题文本 - 根据启用状态调整透明度
Expanded( Expanded(
child: Text( child: Text(
title, title,
@ -348,6 +507,23 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
); );
} }
/// ==========================================================================
/// 核心功能方法
/// ==========================================================================
/// 分享文件方法
/// 使用 SharePlus 插件分享文件到系统分享面板
///
/// 参数说明:
/// - filepath: 要分享的文件的完整路径
/// - title: 分享时显示的标题/描述文本
///
/// 执行流程:
/// 1. 构建 ShareParams 对象,包含文本和文件列表
/// 2. 调用 SharePlus.instance.share() 触发系统分享
/// 3. 检查分享结果状态,成功时显示提示
///
/// 返回值:Future<void> - 异步操作
Future<void> _share(String filepath, String title) async { Future<void> _share(String filepath, String title) async {
final result = await SharePlus.instance.share(ShareParams( final result = await SharePlus.instance.share(ShareParams(
text: title, text: title,
@ -358,6 +534,21 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
} }
} }
/// 复制文本到剪贴板方法
/// 使用 Flutter 的 Clipboard 服务将文本复制到系统剪贴板
///
/// 参数说明:
/// - text: 要复制的文本内容
///
/// 执行流程:
/// 1. 调用 Clipboard.setData() 设置剪贴板数据
/// 2. 复制成功后显示 GetX Snackbar 提示
///
/// 提示样式:
/// - 标题:'success'.tr(成功)
/// - 内容:'copySuccess'.tr(复制成功)
/// - 背景色:蓝色半透明
/// - 文字颜色:白色
void _copy(String text) { void _copy(String text) {
Clipboard.setData(ClipboardData( Clipboard.setData(ClipboardData(
text: text, text: text,
@ -372,6 +563,27 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
} }
/// 将Markdown格式转换为纯文本 /// 将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 _markdownToPlainText(String markdown) {
String text = markdown; String text = markdown;
@ -428,22 +640,61 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
return text.trim(); return text.trim();
} }
/// 获取所有转写文本
/// 将会议转写列表格式化为带说话人和时间戳的文本
///
/// 返回值:String - 格式化后的完整转写文本
///
/// 数据格式:
/// 说话人 时间戳
/// 内容
///
/// 示例输出:
/// 张三 09:30:15
/// 大家好,会议开始。
/// 李四 09:30:20
/// 收到,请继续。
///
/// 数据来源:
/// - _controller.textList: 转写数据列表
/// - speaker: 说话人名称
/// - starttime: 开始时间戳(毫秒)
/// - content: 转写内容
String _getAllText() { String _getAllText() {
String text = ''; String text = '';
for (var element in _controller.textList) { for (var element in _controller.textList) {
// 添加说话人信息(如果存在)
if (element['speaker'].isNotEmpty) { if (element['speaker'].isNotEmpty) {
text += '${element['speaker']} '; text += '${element['speaker']} ';
} }
// 添加格式化后的时间戳(UTC时间,HH:mm:ss格式)
text += '${DateUtil.formatDateMs( text += '${DateUtil.formatDateMs(
element['starttime'], element['starttime'],
format: 'HH:mm:ss', format: 'HH:mm:ss',
isUtc: true, isUtc: true,
)}\n'; )}\n';
// 添加转写内容
text += '${element['content']}\n'; text += '${element['content']}\n';
} }
return text; return text;
} }
/// 获取资源文件的缩略图文件
/// 将assets中的图片资源复制到临时目录,用于分享预览
///
/// 参数说明:
/// - assetPath: 资源文件在assets目录中的路径
/// - fileName: 保存到临时目录的文件名
///
/// 返回值:Future<XFile> - 临时文件的XFile对象
///
/// 执行流程:
/// 1. 使用 rootBundle.load() 加载资源文件字节数据
/// 2. 获取应用临时目录
/// 3. 创建目标文件并写入字节数据
/// 4. 返回XFile对象供分享使用
///
/// 注意:当前代码中此方法未被实际调用(注释状态)
Future<XFile> getAssetThumbnailFile(String assetPath, String fileName) async { Future<XFile> getAssetThumbnailFile(String assetPath, String fileName) async {
final byteData = await rootBundle.load(assetPath); final byteData = await rootBundle.load(assetPath);
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
@ -452,6 +703,25 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
return XFile(file.path); 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<void> - 异步操作
Future<void> _exportMarkdownPdf() async { Future<void> _exportMarkdownPdf() async {
EasyLoading.show(status: 'exportingPDF'.tr); // 对应中文:正在导出PDF... EasyLoading.show(status: 'exportingPDF'.tr); // 对应中文:正在导出PDF...
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();

8
lib/modules/meeting/views/meeting_details_view.dart

@ -96,7 +96,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
if (!controller.isPlayerReady.value) { if (!controller.isPlayerReady.value) {
Get.snackbar( Get.snackbar(
'error'.tr, 'error'.tr,
'音频播放器未准备好,请稍候', 'playerNotReady'.tr, // 音频播放器未准备好,请稍候
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
); );
@ -112,7 +112,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
} catch (e) { } catch (e) {
Get.snackbar( Get.snackbar(
'error'.tr, 'error'.tr,
'播放失败: ${e.toString()}', "${'playFailed'.tr}: ${e.toString()}", // 播放失败
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
); );
@ -542,7 +542,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
if (!controller.isPlayerReady.value) { if (!controller.isPlayerReady.value) {
Get.snackbar( Get.snackbar(
'error'.tr, 'error'.tr,
'音频播放器未准备好,请稍候', 'playerNotReady'.tr, // 音频播放器未准备好,请稍候
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
); );
@ -560,7 +560,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
} catch (e) { } catch (e) {
Get.snackbar( Get.snackbar(
'error'.tr, 'error'.tr,
'播放失败: ${e.toString()}', "${'playFailed'.tr}: ${e.toString()}", // 播放失败
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
); );

4
lib/modules/meeting/views/meeting_home_view.dart

@ -268,10 +268,10 @@ class _MeetingHomeViewState extends State<MeetingHomeView>
tasktypeText = 'summaryComplete'.tr; // 对应中文:总结完成 tasktypeText = 'summaryComplete'.tr; // 对应中文:总结完成
break; break;
case 10001: case 10001:
tasktypeText = '总结失败'; tasktypeText = 'summaryFailedShort'.tr; // 总结失败
break; break;
case 10002: case 10002:
tasktypeText = '转写失败'; tasktypeText = 'transcriptionFailedShort'.tr; // 转写失败
break; break;
default: default:
tasktypeText = ''; tasktypeText = '';

6
lib/modules/meeting/views/tabs/overview_tab.dart

@ -144,7 +144,7 @@ class _OverviewTabState extends State<OverviewTab> {
width: 150.w, width: 150.w,
child: _button( child: _button(
Icons.auto_awesome, Icons.auto_awesome,
'重新生成', 'regenerate'.tr, // 重新生成
() { () {
Get.bottomSheet( Get.bottomSheet(
const GenerateBottomSheet(), const GenerateBottomSheet(),
@ -165,7 +165,7 @@ class _OverviewTabState extends State<OverviewTab> {
width: 150.w, width: 150.w,
child: _button( child: _button(
Icons.auto_awesome, Icons.auto_awesome,
'重新总结', 'reSummarize'.tr, // 重新总结
() { () {
Get.bottomSheet( Get.bottomSheet(
const GenerateBottomSheet( const GenerateBottomSheet(
@ -274,7 +274,7 @@ class _OverviewTabState extends State<OverviewTab> {
Text( Text(
_controller.meetingDetails.value.tasktype < 10000 _controller.meetingDetails.value.tasktype < 10000
? 'aiAnalysisDesc'.tr // 对应中文:AI分析识别录音中的关键事件,让你选择重要事件进行AI生成。 ? 'aiAnalysisDesc'.tr // 对应中文:AI分析识别录音中的关键事件,让你选择重要事件进行AI生成。
: '分析失败,请稍后重试', : 'analysisFailed'.tr, // 分析失败,请稍后重试
style: TextStyle( style: TextStyle(
color: MeetingUIUtils.getSecondaryTextColor(isDarkMode), color: MeetingUIUtils.getSecondaryTextColor(isDarkMode),
fontSize: 14.sp, fontSize: 14.sp,

2
lib/modules/meeting/views/tabs/speech_tab.dart

@ -266,7 +266,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
height: 200.w, height: 200.w,
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
'转写失败,请稍后重试', 'transcriptionFailed'.tr, // 转写失败,请稍后重试
style: TextStyle( style: TextStyle(
color: MeetingUIUtils.getSecondaryTextColor(isDarkMode), color: MeetingUIUtils.getSecondaryTextColor(isDarkMode),
fontSize: 14.sp, fontSize: 14.sp,

2
lib/modules/meeting/views/tabs/summary_tab.dart

@ -344,7 +344,7 @@ class _SummaryTabState extends State<SummaryTab> {
) )
: Center( : Center(
child: Text( child: Text(
'总结失败,请稍后重试', 'summaryFailed'.tr, // 总结失败,请稍后重试
style: TextStyle( style: TextStyle(
color: MeetingUIUtils.getSecondaryTextColor( color: MeetingUIUtils.getSecondaryTextColor(
isDarkMode), isDarkMode),

7
lib/modules/meeting/views/template/template_edit_view.dart

@ -81,7 +81,9 @@ class _TemplateEditViewState extends State<TemplateEditView> {
onPressed: () => Get.back(), onPressed: () => Get.back(),
), ),
title: Text( title: Text(
widget.data != null ? 'editTemplate'.tr : 'createTemplate'.tr, // 编辑模版/创建模版 widget.data != null
? 'editTemplate'.tr
: 'createTemplate'.tr, // 编辑模版/创建模版
style: TextStyle( style: TextStyle(
fontSize: 17.sp, fontSize: 17.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -108,7 +110,8 @@ class _TemplateEditViewState extends State<TemplateEditView> {
_promptController, _promptController,
minLines: 8, minLines: 8,
maxLines: 100, maxLines: 100,
hintText: 'templateContentHint'.tr, // 请输入模版内容!温馨提示:点击上方AI,根据上面填写的名称和描述,可以帮助你快速生成模版内容 hintText: 'templateContentHint'
.tr, // 请输入模版内容!温馨提示:点击上方AI,根据上面填写的名称和描述,可以帮助你快速生成模版内容
), ),
], ],
), ),

Loading…
Cancel
Save