Browse Source

feat(会议模块): tanls重构会议模块,新增文件上传功能和会议详情视图

本次提交对会议模块进行了重构,主要包括以下内容:
1. 新增文件上传功能,支持通过腾讯云COS上传文件。
2. 新增会议详情视图,包括会议概览、思维导图、总结和发言内容等标签页。
3. 优化数据库结构,新增会议发言者表,并调整了会议和会议详情表的结构。
4. 移除不再使用的会议编辑和会议记录功能,简化了路由和页面结构。
5. 新增会议发言者命名功能,支持对发言者进行命名和编辑。
weicu
lxm 1 year ago
parent
commit
b30376a5e3
  1. 88
      lib/core/utils/upload_oss.dart
  2. 26
      lib/data/services/db/database_helper.dart
  3. 105
      lib/data/services/db/sqflite_api.dart
  4. 11
      lib/modules/meeting/bindings/meeting_edit_binding.dart
  5. 10
      lib/modules/meeting/bindings/meeting_record_binding.dart
  6. 67
      lib/modules/meeting/controllers/meeting_controller.dart
  7. 359
      lib/modules/meeting/controllers/meeting_details_controller.dart
  8. 143
      lib/modules/meeting/controllers/meeting_edit_controller.dart
  9. 324
      lib/modules/meeting/controllers/meeting_record_controller.dart
  10. 30
      lib/modules/meeting/model/meeting_details_model.dart
  11. 21
      lib/modules/meeting/model/meeting_model.dart
  12. 492
      lib/modules/meeting/views/meeting_details_view.dart
  13. 301
      lib/modules/meeting/views/meeting_edit_view.dart
  14. 363
      lib/modules/meeting/views/meeting_record_view.dart
  15. 252
      lib/modules/meeting/views/meeting_view.dart
  16. 12
      lib/modules/meeting/views/mind_map_tab.dart
  17. 12
      lib/modules/meeting/views/overview_tab.dart
  18. 202
      lib/modules/meeting/views/speaker_bottom_sheet.dart
  19. 256
      lib/modules/meeting/views/speech_tab.dart
  20. 226
      lib/modules/meeting/views/summary_tab.dart
  21. 18
      lib/routes/app_pages.dart
  22. 4
      lib/routes/app_routes.dart
  23. 1
      pubspec.yaml

88
lib/core/utils/upload_oss.dart

@ -0,0 +1,88 @@
import 'dart:async';
import 'dart:math';
import 'package:common_utils/common_utils.dart';
import 'package:dio/dio.dart';
import 'package:tencentcloud_cos_sdk_plugin/cos.dart';
import 'package:tencentcloud_cos_sdk_plugin/cos_transfer_manger.dart';
import 'package:tencentcloud_cos_sdk_plugin/pigeon.dart';
class UploadOss {
static final UploadOss _shared = UploadOss._internal();
factory UploadOss() => _shared;
static const bucket = 'deepsound-1253517901';
static const region = 'ap-guangzhou';
static CosTransferManger? _cosTransferManger;
UploadOss._internal();
static Future<CosTransferManger> getTransferManger() async {
if (_cosTransferManger == null) {
await Cos().initWithPlainSecret('AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA',
'OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc');
if (Cos().hasTransferManger(region)) {
_cosTransferManger = Cos().getTransferManger(region);
} else {
CosXmlServiceConfig serviceConfig = CosXmlServiceConfig(
region: region,
isHttps: true,
);
_cosTransferManger = await Cos().registerTransferManger(
region, serviceConfig..region = region, TransferConfig());
}
}
return _cosTransferManger!;
}
static Future<String> upload({
String? filepath,
String rootDir = 'file',
String? fileType,
Function? callback,
ProgressCallback? onSendProgress,
}) async {
String pathName =
'$rootDir/${getDate()}/${getRandom(12)}.${fileType ?? getFileType(filepath!)}';
CosTransferManger cosTransferManger = await getTransferManger();
final completer = Completer<String>();
await cosTransferManger.upload(
bucket,
pathName,
filePath: filepath,
resultListener: ResultListener(
(Map<String?, String?>? header, CosXmlResult? result) {
final url = result?.accessUrl ??
'https://$bucket.cos.$region.myqcloud.com/$pathName';
completer.complete(url);
},
(clientException, serviceException) {
completer.completeError(clientException ??
serviceException ??
Exception('Unknown error'));
},
),
);
return completer.future;
}
static String getDate() {
DateTime now = DateTime.now();
return DateUtil.formatDate(now, format: 'yyyy/MM/dd');
}
static String getRandom(int num) {
String alphabet = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM';
String left = '';
for (var i = 0; i < num; i++) {
left = left + alphabet[Random().nextInt(alphabet.length)];
}
return left;
}
static String getFileType(String path) {
List<String> array = path.split('.');
return array[array.length - 1];
}
}

26
lib/data/services/db/database_helper.dart

@ -30,11 +30,10 @@ class DatabaseHelper {
CREATE TABLE IF NOT EXISTS meeting ( CREATE TABLE IF NOT EXISTS meeting (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
title TEXT, title TEXT,
content TEXT, type TEXT,
seconds INTEGER, seconds INTEGER,
address TEXT, filepath TEXT,
longitude TEXT, audiourl TEXT,
latitude TEXT,
creationtime INTEGER creationtime INTEGER
) )
'''); ''');
@ -43,9 +42,24 @@ class DatabaseHelper {
CREATE TABLE IF NOT EXISTS meetingdetails ( CREATE TABLE IF NOT EXISTS meetingdetails (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
meetingid INTEGER, meetingid INTEGER,
audio TEXT, address TEXT,
personnel TEXT,
taskid TEXT,
tasktype INTEGER,
overview TEXT,
summary TEXT,
mindmap TEXT
)
''');
// 商务会议助手转写内容
batch.execute('''
CREATE TABLE IF NOT EXISTS meetingspeaker (
id INTEGER PRIMARY KEY,
meetingid INTEGER,
content TEXT, content TEXT,
aisummary TEXT starttime INTEGER,
endtime INTEGER,
speaker TEXT
) )
'''); ''');
await batch.commit(); await batch.commit();

105
lib/data/services/db/sqflite_api.dart

@ -31,26 +31,32 @@ class SqfliteApi {
}; };
} }
// 获取商务会议助手转写内容
static Future getMeetingSpeaker(id) async {
final db = await DatabaseHelper().database;
return await db.query(
'meetingspeaker',
where: 'meetingid = ?',
whereArgs: [id],
);
}
// 插入商务会议助手 // 插入商务会议助手
static Future insertMeeting(Map<String, Object?> meeting) async { static Future insertMeeting(Map<String, Object?> meeting) async {
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
try { try {
int id = 0; int id = 0;
await db.transaction((txn) async { await db.transaction((txn) async {
id = await txn.insert('meeting', { id = await txn.insert('meeting', meeting);
'title': meeting['title'],
'content': meeting['firstContent'],
'seconds': meeting['seconds'],
'address': meeting['address'],
'longitude': meeting['longitude'],
'latitude': meeting['latitude'],
'creationtime': meeting['creationTime'],
});
await txn.insert('meetingdetails', { await txn.insert('meetingdetails', {
'meetingid': id, 'meetingid': id,
'audio': meeting['audio'], 'address': '',
'content': meeting['content'], 'personnel': '',
'aisummary': meeting['aisummary'], 'taskid': '',
'tasktype': 0,
'overview': '',
'summary': '',
'mindmap': '',
}); });
}); });
return id; return id;
@ -66,34 +72,65 @@ class SqfliteApi {
} }
} }
// 修改商务会议助手 // 修改商务会议助手详情内容
static Future editMeeting(id, Map<String, Object?> meeting) async { static Future editMeeting(id, Map<String, Object?> meeting) async {
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
try { try {
await db.transaction((txn) async { await db.update(
await txn.update(
'meeting',
{
'title': meeting['title'],
'content': meeting['firstContent'],
'seconds': meeting['seconds'],
'address': meeting['address'],
'longitude': meeting['longitude'],
'latitude': meeting['latitude'],
},
where: 'id = ?',
whereArgs: [id],
);
await txn.update(
'meetingdetails', 'meetingdetails',
{ meeting,
'content': meeting['content'],
'aisummary': meeting['aisummary'],
},
where: 'meetingid = ?', where: 'meetingid = ?',
whereArgs: [id], whereArgs: [id],
); );
}); return true;
} catch (e) {
Get.snackbar(
'error'.tr, // 错误
'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
}
// 插入商务会议助手转写内容
static Future insertMeetingSpeaker(List<Map<String, Object?>> data) async {
final db = await DatabaseHelper().database;
try {
final batch = db.batch();
for (var item in data) {
batch.insert('meetingspeaker', item);
}
await batch.commit();
return true;
} catch (e) {
Get.snackbar(
'error'.tr, // 错误
'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
}
// 修改商务会议助手转写内容
static Future editMeetingSpeaker(List data) async {
final db = await DatabaseHelper().database;
try {
final batch = db.batch();
for (var item in data) {
batch.update(
'meetingspeaker',
{'content': item['content']},
where: 'id = ?',
whereArgs: [item['id']],
);
}
await batch.commit();
return true; return true;
} catch (e) { } catch (e) {
Get.snackbar( Get.snackbar(
@ -121,6 +158,8 @@ class SqfliteApi {
await txn.delete('meeting', where: 'id = ?', whereArgs: [id]); await txn.delete('meeting', where: 'id = ?', whereArgs: [id]);
await txn await txn
.delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]); .delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]);
await txn
.delete('meetingspeaker', where: 'meetingid = ?', whereArgs: [id]);
}); });
} }
} }

11
lib/modules/meeting/bindings/meeting_edit_binding.dart

@ -1,11 +0,0 @@
import 'package:get/get.dart';
import '../controllers/meeting_edit_controller.dart';
class MeetingEditBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingEditController>(() => MeetingEditController());
}
}

10
lib/modules/meeting/bindings/meeting_record_binding.dart

@ -1,10 +0,0 @@
import 'package:get/get.dart';
import '../controllers/meeting_record_controller.dart';
class MeetingRecordBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingRecordController>(() => MeetingRecordController());
}
}

67
lib/modules/meeting/controllers/meeting_controller.dart

@ -1,10 +1,19 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/upload_oss.dart';
import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/db/sqflite_api.dart';
class MeetingController extends GetxController { class MeetingController extends GetxController {
final ScrollController listController = ScrollController(); final ScrollController listController = ScrollController();
final AudioPlayer _audioPlayer = AudioPlayer();
// 是否还有更多数据 // 是否还有更多数据
bool isLoadingMore = true; bool isLoadingMore = true;
@ -18,6 +27,7 @@ class MeetingController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
listController.addListener(_onScroll); listController.addListener(_onScroll);
Permission.storage.request();
_getDataList(); _getDataList();
} }
@ -25,6 +35,7 @@ class MeetingController extends GetxController {
void onClose() { void onClose() {
listController.dispose(); listController.dispose();
titleController.dispose(); titleController.dispose();
_audioPlayer.dispose();
super.onClose(); super.onClose();
} }
@ -48,9 +59,50 @@ class MeetingController extends GetxController {
} }
} }
void addAudio() async {
EasyLoading.show();
FilePickerResult? result = await FilePicker.platform.pickFiles(
withData: true,
);
if (result != null) {
PlatformFile file = result.files.first;
String filePath = await _saveFile(file);
String fileName = file.name;
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
var audiourl =
await UploadOss.upload(filepath: filePath, rootDir: 'audio');
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
int creationTime = DateTime.now().millisecondsSinceEpoch;
Map<String, Object?> data = {
'title': fileName,
'type': 'NOTE',
'seconds': seconds,
'filepath': filePath,
'audiourl': audiourl,
'creationtime': creationTime,
};
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
dataList.insert(0, {
'id': meetingId,
...data,
});
}
}
EasyLoading.dismiss();
}
Future<String> _saveFile(PlatformFile file) async {
final directory = await getApplicationDocumentsDirectory();
final String filePath = '${directory.path}/${file.name}';
final File localFile = File(filePath);
await localFile.writeAsBytes(file.bytes!);
return filePath;
}
// 修改标题 // 修改标题
void editMeetingTitle(int id, String title) async { void editMeetingTitle(int id, String title) async {
if (title.isEmpty) return; if (title.isNotEmpty) {
SqfliteApi.editMeetingTitle(id, title); SqfliteApi.editMeetingTitle(id, title);
int index = dataList.indexWhere((item) => item['id'] == id); int index = dataList.indexWhere((item) => item['id'] == id);
if (index != -1) { if (index != -1) {
@ -58,13 +110,24 @@ class MeetingController extends GetxController {
dataMap['title'] = title; dataMap['title'] = title;
dataList[index] = dataMap; dataList[index] = dataMap;
} }
}
Get.back(); Get.back();
} }
// 删除数据 // 删除数据
void deleteMeeting(int id) { void deleteMeeting(int id, String filepath) async {
dataList.removeWhere((item) => item['id'] == id); dataList.removeWhere((item) => item['id'] == id);
// 删除数据库中的记录 // 删除数据库中的记录
SqfliteApi.deleteMeeting(id); SqfliteApi.deleteMeeting(id);
_deleteFile(filepath);
}
// 删除文件
Future<void> _deleteFile(String filepath) async {
File file = File(filepath);
bool fileExists = await file.exists();
if (fileExists) {
await file.delete();
}
} }
} }

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

@ -1,67 +1,374 @@
import 'dart:async';
import 'dart:convert';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart'; import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:uuid/uuid.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/upload_oss.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/open_ai_service_adapter.dart';
import '../model/meeting_details_model.dart'; import '../model/meeting_details_model.dart';
import '../model/meeting_model.dart'; import '../model/meeting_model.dart';
class MeetingDetailsController extends GetxController class MeetingDetailsController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
late AnimationController lottieController; late ScrollController scrollController;
final AudioPlayer audioPlayer = AudioPlayer(); late TabController tabController;
final RxInt currentIndex = 0.obs;
final PlayerController playerController = PlayerController();
RxBool isTop = false.obs;
RxInt durationMax = 0.obs;
int id = 0; int id = 0;
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
Rx<MeetingModel> meetingData = MeetingModel().obs; Rx<MeetingModel> meetingData = MeetingModel().obs;
Rx<MeetingDetailsModel> dataDetails = MeetingDetailsModel().obs; Rx<MeetingDetailsModel> meetingDetails = MeetingDetailsModel().obs;
RxList textList = [].obs;
String allText = '';
RxBool isSpeakerText = false.obs;
List editSpeakerTextList = [];
RxInt editSpeakerTextId = 0.obs;
String editSpeakerTextChange = '';
final AiService _aiService = Get.find<OpenAIServiceAdapter>();
// Agent 信息
late final DBAgent _agent;
String _userId = '';
StreamSubscription? _aiResponseSubscription;
int seconds = 0; bool _isDisposed = false;
RxBool isPlay = false.obs; final String _appid = '2316081933';
final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
final String _cluster = 'volc_auc_meeting_flash';
final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc';
Map<String, String> _headers = {};
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
lottieController = AnimationController(vsync: this); scrollController = ScrollController(initialScrollOffset: 0.0);
scrollController.addListener(_onScroll);
tabController = TabController(length: 4, vsync: this);
tabController.addListener(() {
currentIndex.value = tabController.index;
});
final Map<String, dynamic> args = Get.arguments ?? {}; final Map<String, dynamic> args = Get.arguments ?? {};
id = args['id']; id = args['id'];
_headers = {
'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json',
};
_getAgent();
_getDataDetails(); _getDataDetails();
} }
@override @override
void onClose() { void onClose() {
lottieController.dispose(); scrollController.dispose();
audioPlayer.dispose(); tabController.dispose();
playerController.dispose();
_aiResponseSubscription?.cancel();
_isDisposed = true;
super.onClose(); super.onClose();
} }
void _onScroll() async {
double pixels = scrollController.position.pixels;
double maxScrollExtent = scrollController.position.maxScrollExtent;
if (!isTop.value && pixels > 5) {
await scrollController.animateTo(
maxScrollExtent,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
);
if (pixels > maxScrollExtent - 2) {
isTop.value = true;
}
}
if (isTop.value && pixels < maxScrollExtent - 5) {
await scrollController.animateTo(
0,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
);
if (pixels < 2) {
isTop.value = false;
}
}
}
void _getAgent() async {
final GetStorage storage = GetStorage();
final List<DBAgent> agents = storage.read("AGENTS");
Map userInfo = storage.read("user_info");
_userId = userInfo['user']['uid'];
_agent = agents.firstWhere((agent) => agent.id == 'meet_assistant');
}
void _getDataDetails() async { void _getDataDetails() async {
final data = await SqfliteApi.getMeetingDetails(id); final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data); meetingData.value = MeetingModel.fromJson(data);
dataDetails.value = MeetingDetailsModel.fromJson(data['details']); meetingDetails.value = MeetingDetailsModel.fromJson(data['details']);
audioPlayer.setAudioSource(AudioSource.file(dataDetails.value.audio)); await playerController.preparePlayer(
audioPlayer.playerStateStream.listen((state) async { path: meetingData.value.filepath,
if (state.processingState == ProcessingState.completed) { noOfSamples: 1200,
await audioPlayer.seek(Duration.zero); );
audioPlayer.pause(); durationMax.value = await playerController.getDuration(DurationType.max);
lottieController.stop(); playerController.setFinishMode(finishMode: FinishMode.pause);
isPlay.value = false; if (meetingDetails.value.tasktype == 1) {
List speakerList = await SqfliteApi.getMeetingSpeaker(id);
textList.value = List.from(speakerList);
} }
});
isLoading.value = false; isLoading.value = false;
} }
void start() async { void editSpeakerText() async {
if (isPlay.value) { if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) {
lottieController.stop(); Map? foundElement = editSpeakerTextList.firstWhere(
audioPlayer.pause(); (element) => element['id'] == editSpeakerTextId.value,
isPlay.value = false; orElse: () => null,
);
if (foundElement != null) {
foundElement['content'] = editSpeakerTextChange;
} else {
editSpeakerTextList.add({
'id': editSpeakerTextId.value,
'content': editSpeakerTextChange,
});
}
}
if (editSpeakerTextList.isNotEmpty) {
EasyLoading.show();
var result = await SqfliteApi.editMeetingSpeaker(editSpeakerTextList);
if (result != null) {
for (var element in editSpeakerTextList) {
int index =
textList.indexWhere((item) => item['id'] == element['id']);
if (index != -1) {
final dataMap = Map<String, dynamic>.from(textList[index]);
dataMap['content'] = element['content'];
textList[index] = dataMap;
}
}
isSpeakerText.value = false;
editSpeakerTextId.value = 0;
editSpeakerTextList = [];
}
EasyLoading.dismiss();
} else { } else {
lottieController.repeat(); isSpeakerText.value = false;
audioPlayer.play(); editSpeakerTextId.value = 0;
isPlay.value = true; editSpeakerTextList = [];
}
}
void editSpeakerName() {}
void speakerCreate() async {
EasyLoading.show();
await _audioSpeaker();
EasyLoading.dismiss();
}
void summaryCreate() async {
EasyLoading.show();
if (meetingDetails.value.tasktype != 1) {
await _audioSpeaker();
}
if (allText.isEmpty) {
String text = '';
for (var element in textList) {
if (element['speaker'].isNotEmpty) {
text += '${element['speaker']}:';
}
text += '${element['content']}\n';
}
allText = text;
}
_aiServiceListen();
// const uuid = Uuid();
// _aiService.send(
// sid: uuid.v4(),
// messages: [
// {'role': 'user', 'content': allText},
// ],
// systemPrompt: _agent.systemPrompt,
// );
}
void _aiServiceListen() {
String summaryText = '';
// _aiResponseSubscription = _aiService.start().listen((AIStreamEvent event) {
// summaryText += event.content ?? '';
// if (event.type == StreamEventType.complete) {
// SqfliteApi.editMeeting(id, {'summary': summaryText});
// meetingDetails.value.summary = summaryText;
// meetingDetails.refresh();
// EasyLoading.dismiss();
// }
// });
}
Future<void> _audioSpeaker() async {
String audiourl = meetingData.value.audiourl;
if (audiourl.isEmpty) {
audiourl = await UploadOss.upload(
filepath: meetingData.value.filepath,
rootDir: 'audio',
);
meetingData.value.audiourl = audiourl;
}
String taskId = meetingDetails.value.taskid;
if (taskId.isEmpty) {
taskId = await _submitTask(audiourl);
meetingDetails.value.taskid = taskId;
await SqfliteApi.editMeeting(id, {'taskid': taskId});
}
await _fileRecognize(taskId);
}
Future<String> _submitTask(String audiourl) async {
String format = _getFileType(audiourl);
final requestBody = jsonEncode({
"app": {
"appid": _appid,
"token": _token,
"cluster": _cluster,
},
"user": {
"uid": _userId,
},
"audio": {
"format": format,
"url": audiourl,
},
"additions": {
"with_speaker_info": "True",
}
});
try {
final response = await http.post(
Uri.parse('$_serviceUrl/submit'),
headers: _headers,
body: requestBody,
);
if (response.statusCode != 200) {
throw Exception('Submit failed with status: ${response.statusCode}');
}
final respDic = jsonDecode(response.body);
final taskId = respDic['resp']['id'] as String;
Logger.debug('taskId: $taskId');
return taskId;
} catch (e) {
Logger.error('Submit error: $e');
rethrow;
}
}
String _getFileType(String path) {
List<String> array = path.split('.');
return array[array.length - 1];
}
Future<Map<String, dynamic>> _queryTask(String taskId) async {
final queryBody = jsonEncode({
"appid": _appid,
"token": _token,
"id": taskId,
"cluster": _cluster,
});
try {
final response = await http.post(
Uri.parse('$_serviceUrl/query'),
headers: _headers,
body: queryBody,
);
if (response.statusCode != 200) {
throw Exception('Query failed with status: ${response.statusCode}');
}
final respDic = jsonDecode(response.body);
return respDic;
} catch (e) {
Logger.error('Query error: $e');
rethrow;
}
}
Future<void> _fileRecognize(String taskId) async {
try {
final startTime = DateTime.now().millisecondsSinceEpoch;
while (!_isDisposed) {
await Future.delayed(const Duration(seconds: 2));
if (_isDisposed) return;
final respDic = await _queryTask(taskId);
final code = respDic['resp']['code'] as int;
if (code == 1000) {
await _addTextList(respDic['resp']['utterances']);
return;
} else if (code < 2000) {
Logger.debug("FileRecognize failed");
return;
}
final currentTime = DateTime.now().millisecondsSinceEpoch;
if (currentTime - startTime > 300 * 1000) {
Logger.debug('wait time exceeds 300s');
return;
}
}
} catch (e) {
Logger.error('Error occurred: $e');
}
}
Future<void> _addTextList(List utterances) async {
String text = '';
List<Map<String, Object?>> list = [];
for (var element in utterances) {
String speaker = '';
if (element['additions'].isNotEmpty) {
speaker = 'Speaker ${element['additions']['speaker']}';
text += '$speaker:';
}
list.add({
'meetingid': id,
'content': element['text'],
'starttime': element['start_time'],
'endtime': element['end_time'],
'speaker': speaker,
});
text += '${element['text']}\n';
}
var result = await SqfliteApi.insertMeetingSpeaker(list);
if (result != null) {
await SqfliteApi.editMeeting(id, {'tasktype': 1});
List speakerList = await SqfliteApi.getMeetingSpeaker(id);
textList.value = List.from(speakerList);
meetingDetails.value.tasktype = 1;
meetingDetails.refresh();
} }
allText = text;
} }
} }

143
lib/modules/meeting/controllers/meeting_edit_controller.dart

@ -1,143 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/volcano_ai_service.dart';
import 'meeting_controller.dart';
import 'meeting_details_controller.dart';
class MeetingEditController extends GetxController
with GetSingleTickerProviderStateMixin {
late AiService _aiService;
final MeetingController meetingController = Get.find<MeetingController>();
final MeetingDetailsController meetingDetailsController =
Get.find<MeetingDetailsController>();
late AnimationController lottieController;
late TextEditingController titleController;
late TextEditingController contentController;
final AudioPlayer audioPlayer = AudioPlayer();
int creationTime = 0;
RxString address = ''.obs;
String _longitude = '';
String _latitude = '';
int id = 0;
RxBool isPlay = false.obs;
@override
void onInit() {
super.onInit();
lottieController = AnimationController(vsync: this);
final Map<String, dynamic> args = Get.arguments ?? {};
id = args['id'];
_getDataDetails();
}
@override
void onClose() {
lottieController.dispose();
titleController.dispose();
contentController.dispose();
audioPlayer.dispose();
super.onClose();
}
void _getDataDetails() async {
titleController = TextEditingController(
text: meetingDetailsController.meetingData.value.title,
);
contentController = TextEditingController(
text: meetingDetailsController.dataDetails.value.content,
);
creationTime = meetingDetailsController.meetingData.value.creationtime;
address.value = meetingDetailsController.meetingData.value.address;
_longitude = meetingDetailsController.meetingData.value.longitude;
_latitude = meetingDetailsController.meetingData.value.latitude;
audioPlayer.setAudioSource(
AudioSource.file(meetingDetailsController.dataDetails.value.audio));
audioPlayer.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
await audioPlayer.seek(Duration.zero);
audioPlayer.pause();
lottieController.stop();
isPlay.value = false;
}
});
}
void start() async {
if (isPlay.value) {
lottieController.stop();
audioPlayer.pause();
isPlay.value = false;
} else {
lottieController.repeat();
audioPlayer.play();
isPlay.value = true;
}
}
void confirm() async {
EasyLoading.show();
_aiService = Get.find<VolcanoAIService>();
String aiSummary = await _aiService.sendMessage(
systemPrompt: '',
messages: [
{'role': 'user', 'content': contentController.text},
{'role': 'user', 'content': '帮我总结一下这次会议'},
],
);
String content = contentController.text;
String firstContent =
content.length >= 30 ? content.substring(0, 30) : content;
String title = titleController.text.isNotEmpty
? titleController.text
: meetingDetailsController.meetingData.value.title;
Map<String, Object?> data = {
'title': title,
'firstContent': firstContent.replaceAll("\n", ""),
'seconds': meetingDetailsController.seconds,
'content': contentController.text,
'aisummary': aiSummary,
'address': address.value,
'longitude': _longitude,
'latitude': _latitude,
};
final result = await SqfliteApi.editMeeting(id, data);
if (result != null) {
int index = meetingController.dataList.indexWhere(
(item) => item['id'] == id,
);
if (index != -1) {
final dataMap =
Map<String, dynamic>.from(meetingController.dataList[index]);
dataMap['title'] = title;
dataMap['content'] = firstContent.replaceAll("\n", "");
dataMap['seconds'] = meetingDetailsController.seconds;
dataMap['address'] = address.value;
meetingController.dataList[index] = dataMap;
}
meetingDetailsController.meetingData.value.title = title;
meetingDetailsController.meetingData.value.address = address.value;
meetingDetailsController.meetingData.value.longitude = _longitude;
meetingDetailsController.meetingData.value.latitude = _latitude;
meetingDetailsController.dataDetails.value.content =
contentController.text;
meetingDetailsController.dataDetails.value.aisummary = aiSummary;
meetingDetailsController.meetingData.refresh();
meetingDetailsController.dataDetails.refresh();
Get.back();
}
EasyLoading.dismiss();
}
}

324
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -1,324 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/speech_impl/xunfei_asr_service.dart';
import '../../../data/services/volcano_ai_service.dart';
import 'meeting_controller.dart';
class MeetingRecordController extends GetxController
with GetSingleTickerProviderStateMixin {
late AiService _aiService;
final MeetingController meetingController = Get.find<MeetingController>();
late AnimationController lottieController;
late AsrService _asrService;
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
Timer? timer;
RxBool isRecording = false.obs; //是否录音中
RxInt seconds = 0.obs;
String title = '';
int creationTime = 0;
RxString address = '正在定位'.obs;
String _longitude = '';
String _latitude = '';
RxString intermediateContent = ''.obs;
RxString finalContent = ''.obs;
StreamSubscription? _recognitionSubscription;
BytesBuilder _originaBytes = BytesBuilder(); //原始音频
@override
void onInit() {
super.onInit();
Permission.storage.request();
lottieController = AnimationController(vsync: this);
Get.put(XunfeiAsrService());
_asrService = Get.find<XunfeiAsrService>();
final Map<String, dynamic> args = Get.arguments ?? {};
title = args['title'];
creationTime = DateTime.now().millisecondsSinceEpoch;
_amapInit();
}
@override
void onClose() {
lottieController.dispose();
timer?.cancel();
_asrService.dispose();
_recognitionSubscription?.cancel();
_originaBytes = BytesBuilder();
_amapFlutter.dispose();
_locationStream?.cancel();
super.onClose();
}
void _amapInit() async {
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) {
address.value = data['address'];
_longitude = data['longitude'].toString();
_latitude = data['latitude'].toString();
_amapFlutter.stopLocation();
});
_amapFlutter.startLocation();
}
// 点击开始、暂停按钮
void recorderHandle() {
if (isRecording.value) {
_pause();
} else {
_start();
}
}
// 开始
void _start() async {
await _startRecorder();
lottieController.repeat();
isRecording.value = true;
timer = Timer.periodic(const Duration(seconds: 1), (_) => seconds++);
}
// 暂停
Future<void> _pause() async {
await _stopRecorder();
lottieController.stop();
isRecording.value = false;
timer?.cancel();
}
//开始录音
Future<void> _startRecorder() async {
final recognitionStream =
await _asrService.startContinuousRecognition(false);
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
);
}
//结束录音
Future<void> _stopRecorder() async {
await _asrService.stopContinuousRecognition();
}
// 处理语音识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.intermediateResult:
intermediateContent.value = event.text;
break;
case RecognitionEventType.finalResult:
finalContent.value += _spliceText(event);
intermediateContent.value = '';
_originaBytes.add(event.audio!);
break;
default:
// 不做任何处理
break;
}
}
String _spliceText(RecognitionEvent event) {
String eventText = event.text;
String text = '';
if (finalContent.value.isNotEmpty) {
if (event.role != '') {
final punctuations = {
',',
'。',
'!',
'?',
';',
':',
',',
'.',
':',
';',
'!',
'?'
};
final firstChar = eventText[0];
if (punctuations.contains(firstChar)) {
text += '$firstChar\n';
eventText = eventText.substring(1);
} else {
text += '\n';
}
text += '说话人${event.role}:';
}
} else {
text += '说话人${event.role}:';
}
text += eventText;
return text;
}
// 完成
void complete() async {
EasyLoading.show();
await _pause();
_aiSummary();
}
void _aiSummary() async {
_aiService = Get.find<VolcanoAIService>();
String aiSummary = await _aiService.sendMessage(
systemPrompt: '',
messages: [
{'role': 'user', 'content': finalContent.value},
{'role': 'user', 'content': '帮我总结一下这次会议'},
],
);
_save(aiSummary);
}
void _save(String aiSummary) async {
String firstContent = finalContent.value.length >= 30
? finalContent.value.substring(0, 30)
: finalContent.value;
Uint8List audio = _originaBytes.toBytes();
Uint8List wavHeader = generateWavHeader(
sampleRate: 16000,
numberOfChannels: 1,
bitsPerSample: 16,
dataLength: audio.length,
);
Uint8List wavData = Uint8List.fromList([...wavHeader, ...audio]);
String audiopPath =
await _saveAudioToFile(wavData, 'meeting_$creationTime.wav');
if (audiopPath.isNotEmpty) {
Map<String, Object?> data = {
'title': title,
'firstContent': firstContent.replaceAll("\n", ""),
'seconds': seconds.value,
'creationTime': creationTime,
'audio': audiopPath,
'content': finalContent.value,
'aisummary': aiSummary,
'address': address.value,
'longitude': _longitude,
'latitude': _latitude,
};
final result = await SqfliteApi.insertMeeting(data);
if (result != null) {
meetingController.dataList.insert(0, {
'id': result,
'title': title,
'content': firstContent.replaceAll("\n", ""),
'seconds': seconds.value,
'address': address.value,
'longitude': _longitude,
'latitude': _latitude,
'creationtime': creationTime,
});
Get.back();
}
}
EasyLoading.dismiss();
}
Future<String> _saveAudioToFile(Uint8List audioData, String fileName) async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$fileName');
// 写入文件
await file.writeAsBytes(audioData);
return file.path;
} catch (e) {
Logger.error('保存失败: $e');
return '';
}
}
Uint8List generateWavHeader({
required int sampleRate,
required int numberOfChannels,
required int bitsPerSample,
required int dataLength,
}) {
final byteRate = sampleRate * numberOfChannels * (bitsPerSample ~/ 8);
final blockAlign = numberOfChannels * (bitsPerSample ~/ 8);
final riffSize = 36 + dataLength;
final header = Uint8List(44);
var pos = 0;
// RIFF 块
header.setRange(pos, pos + 4, [0x52, 0x49, 0x46, 0x46]); // 'RIFF'
pos += 4;
// 写入 RIFF 大小(小端)
header.setRange(pos, pos + 4, [
riffSize & 0xff,
(riffSize >> 8) & 0xff,
(riffSize >> 16) & 0xff,
(riffSize >> 24) & 0xff,
]);
pos += 4;
// WAVE 标识
header.setRange(pos, pos + 4, [0x57, 0x41, 0x56, 0x45]); // 'WAVE'
pos += 4;
// fmt 子块
header.setRange(pos, pos + 4, [0x66, 0x6d, 0x74, 0x20]); // 'fmt '
pos += 4;
header.setRange(pos, pos + 4, [16, 0, 0, 0]); // 块大小
pos += 4;
header.setRange(pos, pos + 2, [1, 0]); // PCM 格式
pos += 2;
header.setRange(pos, pos + 2, [numberOfChannels, 0]); // 声道数
pos += 2;
// 采样率(小端)
header.setRange(pos, pos + 4, [
sampleRate & 0xff,
(sampleRate >> 8) & 0xff,
(sampleRate >> 16) & 0xff,
(sampleRate >> 24) & 0xff,
]);
pos += 4;
// 字节率(小端)
header.setRange(pos, pos + 4, [
byteRate & 0xff,
(byteRate >> 8) & 0xff,
(byteRate >> 16) & 0xff,
(byteRate >> 24) & 0xff,
]);
pos += 4;
// 块对齐
header.setRange(pos, pos + 2, [blockAlign, 0]);
pos += 2;
// 位深度
header.setRange(pos, pos + 2, [bitsPerSample, 0]);
pos += 2;
// data 子块
header.setRange(pos, pos + 4, [0x64, 0x61, 0x74, 0x61]); // 'data'
pos += 4;
// 数据大小(小端)
header.setRange(pos, pos + 4, [
dataLength & 0xff,
(dataLength >> 8) & 0xff,
(dataLength >> 16) & 0xff,
(dataLength >> 24) & 0xff,
]);
return header;
}
}

30
lib/modules/meeting/model/meeting_details_model.dart

@ -3,25 +3,37 @@ class MeetingDetailsModel {
late int id; late int id;
late int meetingid; late int meetingid;
late String audio; late String address;
late String content; late String personnel;
late String aisummary; late String taskid;
late int tasktype;
late String overview;
late String summary;
late String mindmap;
factory MeetingDetailsModel.fromJson(Map<String, dynamic> map) => factory MeetingDetailsModel.fromJson(Map<String, dynamic> map) =>
MeetingDetailsModel() MeetingDetailsModel()
..id = map["id"] ..id = map["id"]
..meetingid = map["meetingid"] ..meetingid = map["meetingid"]
..audio = map["audio"] ..address = map["address"]
..content = map["content"] ..personnel = map["personnel"]
..aisummary = map["aisummary"]; ..taskid = map["taskid"]
..tasktype = map["tasktype"]
..overview = map["overview"]
..summary = map["summary"]
..mindmap = map["mindmap"];
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = <String, dynamic>{}; final data = <String, dynamic>{};
data['id'] = id; data['id'] = id;
data['meetingid'] = meetingid; data['meetingid'] = meetingid;
data['audio'] = audio; data['address'] = address;
data['content'] = content; data['personnel'] = personnel;
data['aisummary'] = aisummary; data['taskid'] = taskid;
data['tasktype'] = tasktype;
data['overview'] = overview;
data['summary'] = summary;
data['mindmap'] = mindmap;
return data; return data;
} }
} }

21
lib/modules/meeting/model/meeting_model.dart

@ -3,32 +3,29 @@ class MeetingModel {
late int id; late int id;
late String title; late String title;
late String content; late String type;
late int seconds; late int seconds;
late String address; late String filepath;
late String longitude; late String audiourl;
late String latitude;
late int creationtime; late int creationtime;
factory MeetingModel.fromJson(Map<String, dynamic> map) => MeetingModel() factory MeetingModel.fromJson(Map<String, dynamic> map) => MeetingModel()
..id = map["id"] ..id = map["id"]
..title = map["title"] ..title = map["title"]
..content = map["content"] ..type = map["type"]
..seconds = map["seconds"] ..seconds = map["seconds"]
..address = map["address"] ..filepath = map["filepath"]
..longitude = map["longitude"] ..audiourl = map["audiourl"]
..latitude = map["latitude"]
..creationtime = map["creationtime"]; ..creationtime = map["creationtime"];
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = <String, dynamic>{}; final data = <String, dynamic>{};
data['id'] = id; data['id'] = id;
data['title'] = title; data['title'] = title;
data['content'] = content; data['type'] = type;
data['seconds'] = seconds; data['seconds'] = seconds;
data['address'] = address; data['filepath'] = filepath;
data['longitude'] = longitude; data['audiourl'] = audiourl;
data['latitude'] = latitude;
data['creationtime'] = creationtime; data['creationtime'] = creationtime;
return data; return data;
} }

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

@ -1,12 +1,15 @@
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:common_utils/common_utils.dart'; import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../controllers/meeting_details_controller.dart'; import '../controllers/meeting_details_controller.dart';
import 'mind_map_tab.dart';
import 'overview_tab.dart';
import 'speech_tab.dart';
import 'summary_tab.dart';
class MeetingDetailsView extends GetView<MeetingDetailsController> { class MeetingDetailsView extends GetView<MeetingDetailsController> {
const MeetingDetailsView({super.key}); const MeetingDetailsView({super.key});
@ -15,312 +18,355 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Obx(
() => Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode), appBar: _buildAppBar(isDarkMode),
body: Obx(() => !controller.isLoading.value body: NestedScrollView(
? _buildBody(isDarkMode) controller: controller.scrollController,
: const SizedBox()), headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
); return [
} SliverAppBar(
automaticallyImplyLeading: false,
// 构建应用栏 expandedHeight: 210.w,
PreferredSizeWidget _buildAppBar(bool isDarkMode) { backgroundColor: Colors.transparent,
return AppBar( flexibleSpace: FlexibleSpaceBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white, collapseMode: CollapseMode.pin,
elevation: 0.5, background: Align(
leading: IconButton( alignment: Alignment.topCenter,
icon: Icon( child: Container(
Icons.arrow_back_ios, height: 200.w,
color: isDarkMode ? Colors.white : Colors.black, decoration: BoxDecoration(
size: 20.sp, color: Colors.grey[200]!,
), borderRadius: BorderRadius.vertical(
onPressed: () => Get.back(), bottom: Radius.circular(20.r),
),
title: Text(
'recordingDetails'.tr, //录音详情
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
), ),
), ),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Container(
margin: EdgeInsets.only(bottom: 5.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: AudioFileWaveforms(
size: Size(350.w, 120.w),
playerController: controller.playerController,
playerWaveStyle: PlayerWaveStyle(
fixedWaveColor: Colors.grey[300]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 600,
),
),
),
SizedBox(
width: 350.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( StreamBuilder<int>(
child: Text( stream: controller.playerController
controller.meetingData.value.title, .onCurrentDurationChanged,
style: TextStyle( builder: (context, snapshot) {
color: isDarkMode ? Colors.white : Colors.black87, final position = snapshot.data ?? 0;
fontSize: 17.sp, return Text(
DateUtil.formatDateMs(
position,
format: 'HH:mm:ss',
isUtc: true,
), ),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
), ),
);
},
), ),
10.horizontalSpace, Obx(
Text( () => Text(
DateUtil.formatDateMs( DateUtil.formatDateMs(
controller.meetingData.value.creationtime, controller.durationMax.value,
format: DateFormats.y_mo_d_h_m, format: 'HH:mm:ss',
isUtc: true,
), ),
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], color: isDarkMode
fontSize: 14.sp, ? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
),
), ),
), ),
], ],
), ),
Padding( ),
padding: EdgeInsets.symmetric(vertical: 5.w), Row(
child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Icon( Icon(
Icons.location_on, Icons.replay_10,
size: 16.w, color: Colors.black,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], size: 30.w,
), ),
Expanded( StreamBuilder<PlayerState>(
child: Text( stream: controller
controller.meetingData.value.address, .playerController.onPlayerStateChanged,
style: TextStyle( builder: (context, snapshot) {
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], bool isPlay =
fontSize: 14.sp, snapshot.data?.isPlaying ?? false;
return GestureDetector(
onTap: () {
if (isPlay) {
controller.playerController
.pausePlayer();
} else {
controller.playerController
.startPlayer();
}
},
child: Icon(
isPlay
? Icons.pause_circle
: Icons.play_circle,
color: Colors.black,
size: 48.w,
), ),
overflow: TextOverflow.ellipsis, );
},
), ),
Icon(
Icons.forward_10,
color: Colors.black,
size: 30.w,
), ),
], ],
), ),
),
_audioWave(isDarkMode),
_recordContent(isDarkMode),
_bottomButton(isDarkMode),
], ],
), ),
);
}
// 音波
Widget _audioWave(bool isDarkMode) {
return Container(
padding: EdgeInsets.only(bottom: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
), ),
],
), ),
child: Column( ),
children: [ ),
SizedBox( ];
height: 70.w,
child: Row(
children: [
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
}, },
body: _buildBody(context, isDarkMode),
), ),
), ),
Expanded( );
child: Lottie.asset( }
'assets/lottie/audio_wave.json',
controller: controller.lottieController, // 构建应用栏
fit: BoxFit.fill, PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor:
controller.isTop.value ? Colors.grey[50] : Colors.grey[200],
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: controller.isSpeakerText.value
? GestureDetector(
onTap: () {
controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
},
child: Center(
child: Text(
'cancel'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
), ),
), ),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
), ),
)
: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
), ),
], onPressed: () => Get.back(),
), ),
title: Opacity(
opacity: controller.isTop.value ? 1.0 : 0.0,
child: Container(
width: 170.w,
padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 3.w),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(30.r),
), ),
Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
StreamBuilder<Duration>( StreamBuilder<PlayerState>(
stream: controller.audioPlayer.positionStream, stream: controller.playerController.onPlayerStateChanged,
builder: (context, snapshot) { builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero; bool isPlay = snapshot.data?.isPlaying ?? false;
return Text( return GestureDetector(
DateUtil.formatDateMs( onTap: () {
position.inMilliseconds, if (isPlay) {
format: 'HH:mm:ss', controller.playerController.pausePlayer();
isUtc: true, } else {
), controller.playerController.startPlayer();
style: TextStyle( }
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], },
fontSize: 12.sp, child: Icon(
isPlay ? Icons.pause_circle : Icons.play_circle,
color: Colors.black,
size: 30.w,
), ),
); );
}, },
), ),
GestureDetector( 4.horizontalSpace,
onTap: controller.start, AudioFileWaveforms(
child: Container( size: Size(130.w, 30.w),
width: 30.w, playerController: controller.playerController,
height: 30.w, playerWaveStyle: PlayerWaveStyle(
decoration: BoxDecoration( fixedWaveColor: Colors.grey[400]!,
color: isDarkMode ? AppColors.primary : Colors.blue, liveWaveColor: Colors.red,
borderRadius: BorderRadius.circular( seekLineColor: Colors.red,
12.w, scaleFactor: 100,
), ),
), ),
child: Icon( ],
controller.isPlay.value ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 25.w,
), ),
), ),
), ),
StreamBuilder<Duration?>( centerTitle: true,
stream: controller.audioPlayer.durationStream, actions: [
builder: (context, snapshot) { if (controller.isSpeakerText.value)
final duration = snapshot.data ?? Duration.zero; GestureDetector(
controller.seconds = duration.inSeconds; onTap: controller.editSpeakerText,
return Text( child: Padding(
DateUtil.formatDateMs( padding: EdgeInsets.symmetric(
duration.inMilliseconds, horizontal: 12.w,
format: 'HH:mm:ss', vertical: 6.w,
isUtc: true,
), ),
child: Text(
'save'.tr,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], color: isDarkMode ? AppColors.primary : Colors.blue,
fontSize: 12.sp, fontSize: 16.sp,
), ),
);
},
), ),
],
), ),
],
), ),
); if (!controller.isSpeakerText.value)
} IconButton(
icon: Icon(
// 记录内容 Icons.ios_share,
Widget _recordContent(bool isDarkMode) { color: isDarkMode ? Colors.white : Colors.black,
return Expanded( size: 22.sp,
child: Container(
width: double.infinity,
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: DefaultTabController(
length: 2,
child: Column(
children: [
SizedBox(
height: 40.w,
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: isDarkMode ? AppColors.primary : Colors.blue,
labelColor: isDarkMode ? AppColors.primary : Colors.blue,
labelStyle: TextStyle(
color: isDarkMode
? Colors.white
: const Color.fromARGB(221, 197, 196, 196),
fontSize: 14.sp,
), ),
tabs: const [ onPressed: () {},
Tab(text: 'AI总结'),
Tab(text: '会议原文'),
],
), ),
if (!controller.isSpeakerText.value)
IconButton(
icon: Container(
width: 22.w,
height: 22.w,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: isDarkMode ? Colors.white : Colors.black,
width: 2,
), ),
10.verticalSpace,
Expanded(
child: TabBarView(
children: [
SingleChildScrollView(
child: Text(
controller.dataDetails.value.aisummary,
), ),
child: Icon(
Icons.more_horiz,
color: isDarkMode ? Colors.white : Colors.black,
size: 16.sp,
), ),
SingleChildScrollView(
child: Text(
controller.dataDetails.value.content,
), ),
onPressed: () {},
), ),
], ],
);
}
// 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) {
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w),
margin: EdgeInsets.symmetric(vertical: 10.w),
child: Obx(
() => Row(
children: [
_iconItem(Icons.summarize, '概述', 0),
_iconItem(Icons.message, '转写', 1),
_iconItem(Icons.article, '总结', 2),
_iconItem(Icons.lan, '思维导图', 3),
],
), ),
), ),
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: TabBarView(
controller: controller.tabController,
physics: const NeverScrollableScrollPhysics(),
children: const [
OverviewTab(),
SpeechTab(),
SummaryTab(),
MindMapTab(),
], ],
), ),
), ),
), ),
],
); );
} }
// 底部按钮 Widget _iconItem(IconData icon, String title, int index) {
Widget _bottomButton(bool isDarkMode) { bool isActive = controller.currentIndex.value == index;
return Container(
padding: EdgeInsets.only(top: 20.w, bottom: 10.w), return Expanded(
alignment: Alignment.center, flex: isActive ? 1 : 0,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
Get.toNamed(Routes.meetingEdit, arguments: { if (isActive) return;
'id': controller.id, controller.tabController.index = index;
}); controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
}, },
child: Container( child: Container(
width: 130.w, height: 45.w,
height: 48.w, padding: EdgeInsets.symmetric(horizontal: 18.w),
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue, color: isActive ? Colors.black : Colors.grey[200],
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.edit_calendar, icon,
color: isActive ? Colors.white : Colors.grey[600],
size: 20.sp,
),
if (isActive)
Padding(
padding: EdgeInsets.only(left: 5.w),
child: Text(
title,
style: TextStyle(
color: Colors.white, color: Colors.white,
size: 20.w, fontSize: 16.sp,
),
), ),
5.horizontalSpace,
Text(
'编辑',
style: TextStyle(color: Colors.white, fontSize: 16.sp),
), ),
], ],
), ),

301
lib/modules/meeting/views/meeting_edit_view.dart

@ -1,301 +0,0 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../controllers/meeting_edit_controller.dart';
class MeetingEditView extends GetView<MeetingEditController> {
const MeetingEditView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
actions: [
GestureDetector(
onTap: controller.confirm,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.w,
),
child: Text(
'save'.tr,
style: TextStyle(
color: isDarkMode ? AppColors.primary : Colors.blue,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: controller.titleController,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
decoration: InputDecoration(
hintText: 'maxCharacters'.tr,
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 16.sp,
),
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10.w,
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Text(
DateUtil.formatDateMs(
controller.creationTime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
),
Row(
children: [
Icon(
Icons.location_on,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Expanded(
child: Obx(
() => Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
overflow: TextOverflow.ellipsis,
),
),
),
],
),
_audioWave(isDarkMode),
_recordContent(isDarkMode),
],
),
);
}
// 音波
Widget _audioWave(bool isDarkMode) {
return Container(
padding: EdgeInsets.only(bottom: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
SizedBox(
height: 70.w,
child: Row(
children: [
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
StreamBuilder<Duration>(
stream: controller.audioPlayer.positionStream,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
return Text(
DateUtil.formatDateMs(
position.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
),
GestureDetector(
onTap: controller.start,
child: Container(
width: 30.w,
height: 30.w,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(
12.w,
),
),
child: Obx(
() => Icon(
controller.isPlay.value ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 25.w,
),
),
),
),
StreamBuilder<Duration?>(
stream: controller.audioPlayer.durationStream,
builder: (context, snapshot) {
final duration = snapshot.data ?? Duration.zero;
return Text(
DateUtil.formatDateMs(
duration.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
),
],
),
],
),
);
}
// 记录内容
Widget _recordContent(bool isDarkMode) {
return Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: controller.contentController,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 14.sp,
),
maxLines: 50,
decoration: InputDecoration(
hintText: '编辑记录内容',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 14.sp,
),
fillColor: Colors.transparent,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
);
}
}

363
lib/modules/meeting/views/meeting_record_view.dart

@ -1,363 +0,0 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/select_location_map.dart';
import '../controllers/meeting_record_controller.dart';
class MeetingRecordView extends GetView<MeetingRecordController> {
const MeetingRecordView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
_showAlertDialog(isDarkMode);
},
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () {
_showAlertDialog(isDarkMode);
},
),
title: Padding(
padding: const EdgeInsets.only(right: 56),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(
() => Icon(
Icons.keyboard_voice,
color: isDarkMode
? controller.seconds.value > 0
? AppColors.primary
: Colors.white
: controller.seconds.value > 0
? Colors.blue
: Colors.black,
),
),
5.horizontalSpace,
Obx(
() => Text(
DateUtil.formatDateMs(
controller.seconds.value * 1000,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode
? controller.seconds.value > 0
? AppColors.primary
: Colors.white
: controller.seconds.value > 0
? Colors.blue
: Colors.black,
),
),
),
],
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
10.verticalSpace,
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Row(
children: [
Expanded(
child: Text(
controller.title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 17.sp,
),
),
),
10.horizontalSpace,
Text(
DateUtil.formatDateMs(
controller.creationTime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
],
),
),
GestureDetector(
onTap: () {
Get.to(() => const SelectLocationMap());
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
child: Row(
children: [
Icon(
Icons.location_on,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Expanded(
child: Obx(
() => Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
),
_recordContent(isDarkMode),
_bottomButton(isDarkMode),
],
);
}
// 记录内容
Widget _recordContent(bool isDarkMode) {
return Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
reverse: true,
child: Obx(
() => Text(
'${controller.finalContent.value}${controller.intermediateContent.value}',
),
),
),
),
),
);
}
// 底部按钮
Widget _bottomButton(bool isDarkMode) {
return Container(
margin: EdgeInsets.only(top: 10.h),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 3,
offset: const Offset(0, -1),
),
],
),
child: SafeArea(
child: SizedBox(
height: 80.h,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 语音录制按钮
Container(
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(
child: Obx(
() => IconButton(
icon: Icon(
controller.isRecording.value
? Icons.mic
: Icons.mic_none,
),
color: controller.isRecording.value
? AppColors.primary
: Colors.grey.shade400,
iconSize: 28.r,
padding: EdgeInsets.zero,
onPressed: controller.recorderHandle,
),
),
),
),
10.horizontalSpace,
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
10.horizontalSpace,
// 关闭按钮
Container(
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(
child: IconButton(
icon: Icon(
Icons.close,
color: isDarkMode ? Colors.white : Colors.grey.shade600,
size: 28.r,
),
padding: EdgeInsets.zero,
onPressed: () {
_showAlertDialog(isDarkMode);
},
),
),
),
],
),
),
),
),
);
}
// 显示对话框
void _showAlertDialog(bool isDarkMode) {
if (controller.seconds.value > 0) {
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'注意',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Text(
'你想结束录音吗?',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
TextButton(
onPressed: () {
Get.back();
controller.complete();
},
child: Text(
'confirm'.tr, // 确定
style: TextStyle(
color: isDarkMode ? Colors.red[300] : Colors.red[600],
),
),
),
],
),
);
} else {
Get.back();
}
}
}

252
lib/modules/meeting/views/meeting_view.dart

@ -2,6 +2,7 @@ import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
@ -19,6 +20,10 @@ class MeetingView extends GetView<MeetingController> {
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode), appBar: _buildAppBar(isDarkMode),
body: _buildBody(context, isDarkMode), body: _buildBody(context, isDarkMode),
floatingActionButton: FloatingActionButton(
onPressed: controller.addAudio,
child: const Icon(Icons.add),
),
); );
} }
@ -49,50 +54,15 @@ class MeetingView extends GetView<MeetingController> {
// 构建主体内容 // 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) { Widget _buildBody(BuildContext context, bool isDarkMode) {
return Stack( return Obx(
children: [
Obx(
() => ListView.builder( () => ListView.builder(
itemCount: controller.dataList.length, itemCount: controller.dataList.length,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
MeetingModel data = MeetingModel data = MeetingModel.fromJson(controller.dataList[index]);
MeetingModel.fromJson(controller.dataList[index]);
return _item(context, isDarkMode, data); return _item(context, isDarkMode, data);
}, },
), ),
),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: () {
_bottomSheet(context, isDarkMode);
},
child: Container(
//width: 130.w,
height: 48.w,
margin: EdgeInsets.symmetric(vertical: 20.w),
padding: EdgeInsets.symmetric(horizontal: 14.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisSize: MainAxisSize.min, // 添加这行使Row宽度自适应内容
//mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.keyboard_voice, color: Colors.white),
5.horizontalSpace,
Text(
'startMeeting'.tr,// 开始会议
style: TextStyle(color: Colors.white, fontSize: 16.sp),
),
],
),
),
),
),
],
); );
} }
@ -103,52 +73,11 @@ class MeetingView extends GetView<MeetingController> {
'id': data.id, 'id': data.id,
}); });
}, },
onLongPressStart: (details) { onLongPressStart: (LongPressStartDetails details) {
RenderBox? renderBox = _onLongPressItem(context, isDarkMode, details, data);
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: [
_popupMenuEntry(
Icon(
Icons.edit_calendar,
color: isDarkMode ? Colors.white : Colors.black87,
),
'edit'.tr,// 编辑
isDarkMode ? Colors.white : Colors.black87,
() {
controller.titleController.text = data.title;
_bottomSheet(context, isDarkMode, id: data.id);
},
),
_popupMenuEntry(
const Icon(Icons.delete_outline, color: Colors.red),
'delete'.tr,// 删除
isDarkMode ? Colors.white : Colors.black87,
() {
controller.deleteMeeting(data.id);
},
),
],
);
}, },
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w), margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
@ -164,93 +93,123 @@ class MeetingView extends GetView<MeetingController> {
], ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
children: [
Expanded(
child: Text(
data.title, data.title,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp, fontSize: 18.sp,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Row(
children: [
Icon(
Icons.calendar_month,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
), ),
10.horizontalSpace, 2.horizontalSpace,
Text( Text(
DateUtil.formatDateMs( DateUtil.formatDateMs(
data.creationtime, data.creationtime,
format: DateFormats.y_mo_d_h_m, format: DateFormats.y_mo_d_h_m,
), ),
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54, color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp, fontSize: 12.sp,
), ),
), ),
], 20.horizontalSpace,
Icon(
Icons.access_time,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
), ),
Row( 2.horizontalSpace,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text( Text(
data.content.isNotEmpty ? data.content : 'noContent'.tr,// 无内容 '${data.seconds}s',
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54, color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp, fontSize: 12.sp,
), ),
overflow: TextOverflow.ellipsis,
), ),
Row( ],
children: [ ),
Icon( ),
Icons.location_on, Container(
size: 14.w, padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w),
color: isDarkMode ? Colors.white70 : Colors.black54, margin: EdgeInsets.only(top: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(4.w),
), ),
Expanded(
child: Text( child: Text(
data.address, data.type,
style: TextStyle( style: TextStyle(
color: isDarkMode color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
? Colors.white70 fontSize: 10.sp,
: Colors.black54,
fontSize: 12.sp,
), ),
overflow: TextOverflow.ellipsis,
), ),
), ),
], ],
), ),
],
), ),
);
}
void _onLongPressItem(BuildContext context, bool isDarkMode,
LongPressStartDetails details, MeetingModel data) {
Haptics.vibrate(HapticsType.success);
RenderBox? renderBox =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
), ),
Container( Offset.zero & renderBox.size,
width: 76.w, );
height: 28.w,
margin: EdgeInsets.only(left: 10.w), showMenu(
alignment: Alignment.center, context: context,
decoration: BoxDecoration( position: position,
color: isDarkMode ? AppColors.primary : Colors.blue, color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(28.w), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
), ),
child: Text( items: [
DateUtil.formatDateMs( _popupMenuEntry(
data.seconds * 1000, Icon(
format: 'HH:mm:ss', Icons.edit_calendar,
isUtc: true, size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
), ),
style: TextStyle(color: Colors.white, fontSize: 14.sp), 'edit'.tr, // 编辑
isDarkMode ? Colors.white : Colors.black87,
() {
controller.titleController.text = data.title;
_bottomSheet(context, isDarkMode, data.id);
},
), ),
_popupMenuEntry(
Icon(
Icons.delete_outline,
size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red,
), ),
], 'delete'.tr, // 删除
isDarkMode ? Colors.red[700]! : Colors.red,
() {
controller.deleteMeeting(data.id, data.filepath);
},
), ),
], ],
),
),
); );
} }
@ -263,14 +222,17 @@ class MeetingView extends GetView<MeetingController> {
return PopupMenuItem( return PopupMenuItem(
onTap: onTap, onTap: onTap,
child: SizedBox( child: SizedBox(
width: 120.w, width: 140.w,
child: Row( child: Row(
children: [ children: [
icon, icon,
8.horizontalSpace, 8.horizontalSpace,
Text( Text(
title, title,
style: TextStyle(color: color), style: TextStyle(
color: color,
fontSize: 14.sp,
),
), ),
], ],
), ),
@ -280,9 +242,9 @@ class MeetingView extends GetView<MeetingController> {
void _bottomSheet( void _bottomSheet(
BuildContext context, BuildContext context,
bool isDarkMode, { bool isDarkMode,
int? id, int id,
}) { ) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -298,7 +260,7 @@ class MeetingView extends GetView<MeetingController> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
id != null ? 'editMeetingTitle'.tr : 'meetingTitle'.tr, // 编辑会议标题 'editMeetingTitle'.tr, // 编辑会议标题
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp, fontSize: 16.sp,
@ -363,7 +325,7 @@ class MeetingView extends GetView<MeetingController> {
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Text( child: Text(
'cancel'.tr,// 取消 'cancel'.tr, // 取消
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: color:
@ -374,20 +336,8 @@ class MeetingView extends GetView<MeetingController> {
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
int time = DateTime.now().millisecondsSinceEpoch;
String title = controller.titleController.text; String title = controller.titleController.text;
if (id != null) {
controller.editMeetingTitle(id, title); controller.editMeetingTitle(id, title);
} else {
if (title.isEmpty) {
title =
'${DateUtil.formatDateMs(time, format: DateFormats.y_mo_d)} ${'meeting'.tr}';
}
Get.back();
Get.toNamed(Routes.meetingRecord, arguments: {
'title': title,
});
}
}, },
child: Container( child: Container(
width: 120.w, width: 120.w,
@ -398,7 +348,7 @@ class MeetingView extends GetView<MeetingController> {
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Text( child: Text(
'confirm'.tr,// 确认 'confirm'.tr, // 确认
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14.sp, fontSize: 14.sp,
@ -414,8 +364,6 @@ class MeetingView extends GetView<MeetingController> {
), ),
); );
}, },
).then((value) { );
controller.titleController.text = '';
});
} }
} }

12
lib/modules/meeting/views/mind_map_tab.dart

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class MindMapTab extends StatelessWidget {
const MindMapTab({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Text(''),
);
}
}

12
lib/modules/meeting/views/overview_tab.dart

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class OverviewTab extends StatelessWidget {
const OverviewTab({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Text(''),
);
}
}

202
lib/modules/meeting/views/speaker_bottom_sheet.dart

@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class SpeakerBottomSheet extends StatelessWidget {
const SpeakerBottomSheet({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'命名当前发言者',
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
Icons.close,
size: 18.w,
color: Colors.grey[400],
),
),
)
],
),
),
Divider(
height: 1,
color: Colors.grey[300],
),
Container(
height: 46.w,
margin: EdgeInsets.symmetric(vertical: 15.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
),
Container(
constraints: BoxConstraints(maxHeight: 1.sh - 390.w),
child: ListView.builder(
itemCount: 30,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return Container(
padding: EdgeInsets.only(left: 10.w),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Colors.grey[300]!,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (index == 0)
Text(
'最近使用过的名称',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600]!,
),
),
SizedBox(
height: 42.w,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'龙哥',
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
),
),
),
],
),
);
},
),
),
_row('应用到当前段落', false),
_row('应用到这个发言者的所有段落', true, description: '总结将根据模版自动更新'),
Padding(
padding: EdgeInsets.only(top: 10.w, bottom: 20.w),
child: GestureDetector(
onTap: () {},
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'保存',
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
),
),
),
),
),
],
),
);
}
Widget _row(String name, bool isActive, {String? description}) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
children: [
isActive ? _round() : _circle(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
),
if (description != null)
Text(
description,
style: TextStyle(
fontSize: 10.sp,
color: Colors.grey[500]!,
),
),
],
),
),
],
),
);
}
Widget _circle() {
return Container(
width: 18.w,
height: 18.w,
margin: EdgeInsets.only(right: 6.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18.r),
border: Border.all(
width: 1.5,
color: Colors.grey[300]!,
),
),
);
}
Widget _round() {
return Container(
width: 18.w,
height: 18.w,
padding: EdgeInsets.all(2.w),
margin: EdgeInsets.only(right: 6.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18.r),
border: Border.all(
width: 1.5,
color: Colors.black87,
),
),
child: Container(
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(18.r),
),
),
);
}
}

256
lib/modules/meeting/views/speech_tab.dart

@ -0,0 +1,256 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_details_controller.dart';
import 'speaker_bottom_sheet.dart';
class SpeechTab extends GetView<MeetingDetailsController> {
const SpeechTab({super.key});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Obx(
() => ListView.builder(
itemCount: controller.textList.length,
padding: EdgeInsets.only(bottom: 80.w),
itemBuilder: (BuildContext context, int index) {
Map item = controller.textList[index];
TextStyle textStyle = TextStyle(
fontSize: 16.sp,
color: Colors.black87,
letterSpacing: 0,
height: 1.4,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 10.w),
child: Row(
children: [
if (item['speaker'].isNotEmpty)
Container(
width: 12.w,
height: 12.w,
margin: EdgeInsets.only(right: 2.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28.r),
border: Border.all(
color: const Color(0xFFFFC107),
width: 3.w,
),
),
),
if (item['speaker'].isNotEmpty)
GestureDetector(
onTap: () {
if (controller.isSpeakerText.value) return;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (BuildContext context) {
return const SpeakerBottomSheet();
},
);
},
child: Padding(
padding: EdgeInsets.only(right: 5.w),
child: Text(
item['speaker'],
style: TextStyle(
color: const Color(0xFFFFC107),
fontSize: 14.sp,
),
),
),
),
Text(
DateUtil.formatDateMs(
item['starttime'],
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: Colors.grey,
fontSize: 14.sp,
),
),
],
),
),
Obx(() {
if (controller.isSpeakerText.value) {
TextEditingController speakerTextController =
TextEditingController(text: item['content']);
return Obx(
() => Container(
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 5.w,
),
decoration: BoxDecoration(
color:
controller.editSpeakerTextId.value == item['id']
? Colors.blue[100]
: null,
borderRadius: BorderRadius.circular(8.w),
),
child: FocusScope(
onFocusChange: (bool hasFocus) {
if (hasFocus) {
controller.editSpeakerTextId.value = item['id'];
controller.editSpeakerTextChange = '';
} else {
if (speakerTextController.text !=
item['content']) {
Map? foundElement =
controller.editSpeakerTextList.firstWhere(
(element) => element['id'] == item['id'],
orElse: () => null,
);
if (foundElement != null) {
foundElement['content'] =
speakerTextController.text;
} else {
controller.editSpeakerTextList.add({
'id': item['id'],
'content': speakerTextController.text,
});
}
}
}
},
child: TextField(
controller: speakerTextController,
style: textStyle,
minLines: 1,
maxLines: 100,
decoration: const InputDecoration(
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
onChanged: (String value) {
controller.editSpeakerTextChange = value;
},
),
),
),
);
} else {
return Container(
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 5.w,
),
child: Text(
item['content'],
style: textStyle,
),
);
}
}),
],
);
},
),
),
Obx(
() => Positioned(
bottom: 20.w,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (controller.meetingDetails.value.tasktype == 1 &&
!controller.isSpeakerText.value)
_button(
Icons.edit,
'编辑',
() {
controller.isSpeakerText.value = true;
},
),
if (controller.meetingDetails.value.tasktype == 1 &&
!controller.isSpeakerText.value)
_button(
Icons.supervisor_account,
'命名发言者',
() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (BuildContext context) {
return const SpeakerBottomSheet();
},
);
},
),
if (controller.meetingDetails.value.tasktype != 1)
SizedBox(
width: 150.w,
child: _button(
Icons.auto_awesome,
'生成',
controller.speakerCreate,
),
),
],
),
),
),
],
);
}
Widget _button(IconData icon, String title, Function() onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 42.w,
padding: EdgeInsets.symmetric(horizontal: 15.w),
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(42.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: Colors.white,
size: 20.sp,
),
5.horizontalSpace,
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
),
),
],
),
),
);
}
}

226
lib/modules/meeting/views/summary_tab.dart

@ -0,0 +1,226 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_details_controller.dart';
import 'speaker_bottom_sheet.dart';
class SummaryTab extends GetView<MeetingDetailsController> {
const SummaryTab({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Stack(
children: [
SingleChildScrollView(
padding: EdgeInsets.only(bottom: 80.w),
child: Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Text(
controller.meetingData.value.title,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
),
Row(
children: [
Container(
padding:
EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w),
margin: EdgeInsets.only(right: 10.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[200],
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
controller.meetingData.value.type,
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 10.sp,
),
),
),
Icon(
Icons.calendar_month,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
2.horizontalSpace,
Text(
DateUtil.formatDateMs(
controller.meetingData.value.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
),
10.horizontalSpace,
Icon(
Icons.access_time,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
2.horizontalSpace,
Text(
'${controller.meetingData.value.seconds}s',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
),
],
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Divider(
height: 1,
color: isDarkMode ? Colors.grey[900] : Colors.grey[300],
),
),
Container(
padding: EdgeInsets.only(left: 20.w),
margin: EdgeInsets.symmetric(vertical: 10.w),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
width: 2,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[300]!,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'日期和时间:${DateUtil.formatDateMs(
controller.meetingData.value.creationtime,
format: DateFormats.y_mo_d_h_m,
)}',
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontSize: 16.sp,
),
),
Text(
'地点:${controller.meetingDetails.value.address.isNotEmpty ? controller.meetingDetails.value.address : '[输入地点]'}',
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontSize: 16.sp,
),
),
Text(
'与会人员:${controller.meetingDetails.value.address}',
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontSize: 16.sp,
),
),
],
),
),
Padding(
padding: EdgeInsets.only(top: 10.w),
child: MarkdownBody(
data: controller.meetingDetails.value.summary,
),
),
],
),
),
),
Obx(
() => Positioned(
bottom: 20.w,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (controller.meetingDetails.value.summary.isNotEmpty)
_button(
Icons.supervisor_account,
'编辑发言者',
() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (BuildContext context) {
return const SpeakerBottomSheet();
},
);
},
),
if (controller.meetingDetails.value.summary.isEmpty)
SizedBox(
width: 150.w,
child: _button(
Icons.auto_awesome,
'生成',
controller.summaryCreate,
),
),
],
),
),
),
],
);
}
Widget _button(IconData icon, String title, Function() onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 42.w,
padding: EdgeInsets.symmetric(horizontal: 15.w),
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(42.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: Colors.white,
size: 20.sp,
),
5.horizontalSpace,
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
),
),
],
),
),
);
}
}

18
lib/routes/app_pages.dart

@ -7,11 +7,7 @@ import '../modules/explore/views/explore_view.dart';
import '../modules/explore/bindings/explore_binding.dart'; import '../modules/explore/bindings/explore_binding.dart';
import '../modules/meeting/bindings/meeting_binding.dart'; import '../modules/meeting/bindings/meeting_binding.dart';
import '../modules/meeting/bindings/meeting_details_binding.dart'; import '../modules/meeting/bindings/meeting_details_binding.dart';
import '../modules/meeting/bindings/meeting_edit_binding.dart';
import '../modules/meeting/bindings/meeting_record_binding.dart';
import '../modules/meeting/views/meeting_details_view.dart'; import '../modules/meeting/views/meeting_details_view.dart';
import '../modules/meeting/views/meeting_edit_view.dart';
import '../modules/meeting/views/meeting_record_view.dart';
import '../modules/meeting/views/meeting_view.dart'; import '../modules/meeting/views/meeting_view.dart';
import '../modules/profile/views/profile_view.dart'; import '../modules/profile/views/profile_view.dart';
import '../modules/profile/bindings/profile_binding.dart'; import '../modules/profile/bindings/profile_binding.dart';
@ -138,13 +134,6 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.meetingRecord,
page: () => const MeetingRecordView(),
binding: MeetingRecordBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.meetingDetails, name: Routes.meetingDetails,
page: () => const MeetingDetailsView(), page: () => const MeetingDetailsView(),
@ -152,13 +141,6 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.meetingEdit,
page: () => const MeetingEditView(),
binding: MeetingEditBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.timbre, name: Routes.timbre,
page: () => const TimbreView(), page: () => const TimbreView(),

4
lib/routes/app_routes.dart

@ -20,9 +20,7 @@ abstract class Routes {
// 会议相关 // 会议相关
static const meeting = '/meeting'; static const meeting = '/meeting';
static const meetingRecord = '/meeting_record'; // 会议记录 static const meetingDetails = '/meetingDetails';
static const meetingDetails = '/meeting_details'; // 会议详情
static const meetingEdit = '/meeting_edit'; // 会议编辑
// 语言功能 // 语言功能
static const translation = '/translation'; // 实时翻译 static const translation = '/translation'; // 实时翻译

1
pubspec.yaml

@ -36,6 +36,7 @@ dependencies:
package_info_plus: ^8.3.0 package_info_plus: ^8.3.0
file_picker: ^8.0.0+1 file_picker: ^8.0.0+1
haptic_feedback: ^0.5.1+1 haptic_feedback: ^0.5.1+1
tencentcloud_cos_sdk_plugin: ^1.2.3
window_size: window_size:
git: git:

Loading…
Cancel
Save