diff --git a/.gitignore b/.gitignore index 339524773..c05ba28a1 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,9 @@ coverage/ *.js_ *.js.deps *.js.map +linux/flutter/generated_plugin_registrant.cc +linux/flutter/generated_plugin_registrant.h +linux/flutter/generated_plugins.cmake +windows/flutter/generated_plugin_registrant.cc +windows/flutter/generated_plugin_registrant.h +windows/flutter/generated_plugins.cmake diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 77e1caee9..1bb13c1c8 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -38,12 +38,20 @@ class Api { } //更新用户设置 - static updateUserSetting([params]) { + static updateUserSetting([dynamic params]) { return DioManager().request( NWMethod.post, '/api/home/user_setting', params: params, ); + // options: params is dio.FormData + // ? Options( + // headers: { + // 'Content-Type': 'multipart/form-data', + // }, + // ) + // : null, + // ); } //注销账号 diff --git a/lib/data/services/network/dio_manager.dart b/lib/data/services/network/dio_manager.dart index 5fe3ed8eb..7833ed582 100644 --- a/lib/data/services/network/dio_manager.dart +++ b/lib/data/services/network/dio_manager.dart @@ -31,7 +31,7 @@ class DioManager { Future request( NWMethod method, String path, { - Map? params, + Object? params, Map? queryParameters, }) async { Response response = await dio.request( diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index dd642fd4e..f4bed8cc6 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -149,28 +149,32 @@ class AgentView extends StatelessWidget { // 聊天内容 Expanded( child: Obx( - () => ListView.builder( - controller: controller.scrollController, - reverse: true, - padding: - const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - itemCount: controller.messages.length, - itemBuilder: (context, index) { - final message = controller - .messages[controller.messages.length - 1 - index]; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MessageBubble( - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - mate: message.meta, - ), - ); - }, + () => Align( + alignment: Alignment.topCenter, + child: ListView.builder( + controller: controller.scrollController, + shrinkWrap: true, + reverse: true, + padding: + const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + itemCount: controller.messages.length, + itemBuilder: (context, index) { + final message = controller + .messages[controller.messages.length - 1 - index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MessageBubble( + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + meta: message.meta, + ), + ); + }, + ), ), ), ), @@ -308,7 +312,7 @@ class AgentView extends StatelessWidget { if (isTextMode) { // 文本输入模式 - 注意观察图片模式时的高度变化 return Container( - height: 44, + //height: 44, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey[200], @@ -378,7 +382,7 @@ class AgentView extends StatelessWidget { } }, child: Container( - height: 44, + height: 50, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 2c13b6f80..ab64e7c77 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -2,6 +2,7 @@ import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; +import 'package:get_storage/get_storage.dart'; import 'dart:io'; import '../controllers/agent_controller.dart'; @@ -14,7 +15,7 @@ class MessageBubble extends GetView { final bool isRecognizing; final bool hasImage; final String? imagePath; - final Map? mate; + final Map? meta; const MessageBubble({ Key? key, @@ -24,7 +25,7 @@ class MessageBubble extends GetView { this.isRecognizing = false, this.hasImage = false, this.imagePath, - this.mate, + this.meta, }) : super(key: key); @override @@ -38,8 +39,8 @@ class MessageBubble extends GetView { isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!isUser) _buildAvatar(isUser, context), - const SizedBox(width: 8), + //if (!isUser) _buildAvatar(isUser, context), + //const SizedBox(width: 8), Flexible( child: Column( crossAxisAlignment: @@ -49,15 +50,23 @@ class MessageBubble extends GetView { padding: hasImage ? (message == '[图片]' ? EdgeInsets.zero - : const EdgeInsets.all(12)) - : const EdgeInsets.all(12), + : isUser + ? const EdgeInsets.fromLTRB( + 12, 12, 16, 12) // 用户消息右侧多一点内边距 + : const EdgeInsets.fromLTRB( + 16, 12, 12, 12)) // AI消息左侧多一点内边距 + : isUser + ? const EdgeInsets.fromLTRB( + 12, 12, 16, 12) // 用户消息右侧多一点内边距 + : const EdgeInsets.fromLTRB( + 16, 12, 12, 12), // AI消息左侧多一点内边距 decoration: BoxDecoration( color: _getBubbleColor(context), borderRadius: BorderRadius.circular(18).copyWith( - bottomLeft: isUser + topLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), - bottomRight: !isUser + topRight: !isUser ? const Radius.circular(18) : const Radius.circular(4), ), @@ -85,8 +94,8 @@ class MessageBubble extends GetView { ], ), ), - const SizedBox(width: 8), - if (isUser) _buildAvatar(isUser, context), + //const SizedBox(width: 8), + //if (isUser) _buildAvatar(isUser, context), ], ), ); @@ -105,26 +114,61 @@ class MessageBubble extends GetView { /// 构建头像 Widget _buildAvatar(bool isUser, BuildContext context) { - return Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: isUser ? Colors.grey[300] : Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Icon( - isUser ? Icons.person : Icons.smart_toy_outlined, - color: isUser ? Colors.grey[700] : Colors.white, - size: 20, - ), - ); + if (isUser) { + final storage = GetStorage(); + final localAvatarPath = storage.read('local_avatar') ?? ''; + + return Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: localAvatarPath.isEmpty ? Colors.grey[300] : null, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: localAvatarPath.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(18), + child: Image.file( + File(localAvatarPath), + fit: BoxFit.cover, + ), + ) + : Icon( + Icons.person, + color: Colors.grey[700], + size: 20, + ), + ); + } else { + // AI头像保持不变 + return Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.smart_toy_outlined, + color: Colors.white, + size: 20, + ), + ); + } } /// 构建消息内容 @@ -195,28 +239,29 @@ class MessageBubble extends GetView { } // 判断消息内容,显示特殊卡片 - if (!isUser && mate != null) { - if (mate!.keys.contains('card_music')) { - final musicId = mate!['card_music']['id'].toString(); - final musicName = mate!['card_music']['name'] as String; - final musicUrl = mate!['card_music']['url'] as String; - final musicSinger = mate!['card_music']['sgener'] as String; - final musicImage = mate!['card_music']['image'] as String; + if (!isUser && meta != null) { + // 判断消息类型,显示不同的卡片 + if (meta!.keys.contains('card_music')) { + final musicId = meta!['card_music']['id'].toString(); + final musicName = meta!['card_music']['name'] as String; + final musicUrl = meta!['card_music']['url'] as String; + final musicSinger = meta!['card_music']['sgener'] as String; + final musicImage = meta!['card_music']['image'] as String; //音乐卡片 return _buildMusicItem( context, musicId, musicName, musicUrl, musicSinger, musicImage); - } else if (mate!.keys.contains('card_musiclist')) { - final rawList = mate!['card_musiclist']['musics'] as List; + } else if (meta!.keys.contains('card_musiclist')) { + final rawList = meta!['card_musiclist']['musics'] as List; final playlist = rawList.map((e) => Map.from(e)).toList(); //音乐播放列表 return _buildMusicContent(context, playlist); - } else if (mate!.keys.contains('card_weather')) { - final city = mate!['card_weather']['city'] as String; - final rawList = mate!['card_weather']['dayweather'] as List; + } else if (meta!.keys.contains('card_weather')) { + final city = meta!['card_weather']['city'] as String; + final rawList = meta!['card_weather']['dayweather'] as List; final daylist = rawList.map((e) => Map.from(e)).toList(); - final now = mate!['card_weather']['day'] as Map; + final now = meta!['card_weather']['day'] as Map; //天气 return _buildWeatherContent( context, @@ -291,122 +336,123 @@ class MessageBubble extends GetView { ), ), const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.03), - blurRadius: 3, - spreadRadius: 1, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.music_note, - size: 16, - color: Theme.of(context).primaryColor.withOpacity(0.7), - ), - const SizedBox(width: 6), - Text( - '播放列表'.tr, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Colors.grey[700], - ), - ), - ], - ), - const SizedBox(height: 10), - - // 动态生成音乐条目 - ...playlist.asMap().entries.map((entry) { - final index = entry.key; - final item = entry.value; - final id = item["id"]?.toString() ?? ""; - final name = item["name"] ?? "未知歌曲"; - final url = item["url"] ?? ""; - final singer = item["sgener"] ?? "未知歌手"; - final image = item["image"] ?? ""; - return Column( - children: [ - _buildMusicItem(context, id, name, url, singer, image), - if (index != playlist.length - 1) - Divider(height: 1, thickness: 1, color: Colors.grey[100]), - ], - ); - }).toList(), - const SizedBox(height: 8), - Center( - child: TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - minimumSize: Size.zero, - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - '查看更多推荐', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ), - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, + // Container( + // padding: const EdgeInsets.all(12), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(12), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.03), + // blurRadius: 3, + // spreadRadius: 1, + // offset: const Offset(0, 1), + // ), + // ], + // ), + // child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () {}, - icon: Icon(Icons.play_arrow, size: 16), - label: Text('播放全部'), - style: OutlinedButton.styleFrom( - foregroundColor: Theme.of(context).primaryColor, - side: BorderSide( - color: Theme.of(context).primaryColor.withOpacity(0.5)), - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + Row( + children: [ + Icon( + Icons.music_note, + size: 16, + color: Theme.of(context).primaryColor.withOpacity(0.7), + ), + const SizedBox(width: 6), + Text( + '播放列表'.tr, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.grey[700], ), - textStyle: TextStyle(fontSize: 12), ), - ), + ], ), - SizedBox(width: 8), - Expanded( - child: OutlinedButton.icon( + const SizedBox(height: 10), + + // 动态生成音乐条目 + ...playlist.asMap().entries.take(4).map((entry) { + final index = entry.key; + final item = entry.value; + final id = item["id"]?.toString() ?? ""; + final name = item["name"] ?? "未知歌曲"; + final url = item["url"] ?? ""; + final singer = item["sgener"] ?? "未知歌手"; + final image = item["image"] ?? ""; + return Column( + children: [ + _buildMusicItem(context, id, name, url, singer, image), + if (index != playlist.length - 1) + Divider(height: 1, thickness: 1, color: Colors.grey[100]), + ], + ); + }).toList(), + const SizedBox(height: 8), + Center( + child: TextButton( onPressed: () {}, - icon: Icon(Icons.shuffle, size: 16), - label: Text('随机播放'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.grey[700], - side: BorderSide(color: Colors.grey[400]!), - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + style: TextButton.styleFrom( + minimumSize: Size.zero, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + '查看更多推荐', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w500, ), - textStyle: TextStyle(fontSize: 12), ), ), ), ], ), + //), + //const SizedBox(height: 10), + // Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Expanded( + // child: OutlinedButton.icon( + // onPressed: () {}, + // icon: Icon(Icons.play_arrow, size: 16), + // label: Text('播放全部'), + // style: OutlinedButton.styleFrom( + // foregroundColor: Theme.of(context).primaryColor, + // side: BorderSide( + // color: Theme.of(context).primaryColor.withOpacity(0.5)), + // padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(16), + // ), + // textStyle: TextStyle(fontSize: 12), + // ), + // ), + // ), + // SizedBox(width: 8), + // Expanded( + // child: OutlinedButton.icon( + // onPressed: () {}, + // icon: Icon(Icons.shuffle, size: 16), + // label: Text('随机播放'), + // style: OutlinedButton.styleFrom( + // foregroundColor: Colors.grey[700], + // side: BorderSide(color: Colors.grey[400]!), + // padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(16), + // ), + // textStyle: TextStyle(fontSize: 12), + // ), + // ), + // ), + // ], + // ), ], ); } @@ -571,79 +617,102 @@ class MessageBubble extends GetView { 'coverUrl': image, }; controller.musicStatus(); //刷新状态 - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - Container( - width: 38, - height: 38, - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.music_note, - size: 18, - color: Theme.of(context).primaryColor, - ), + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 3, + spreadRadius: 1, + offset: const Offset(0, 1), ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey[800], + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + image: image.isNotEmpty + ? DecorationImage( + image: NetworkImage(image), + fit: BoxFit.cover, + ) + : null, + ), + child: image.isEmpty + ? Icon( + Icons.music_note, + size: 18, + color: Theme.of(context).primaryColor, + ) + : null, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey[800], + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 2), - Text( - singer, - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + SizedBox(height: 2), + Text( + singer, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, - ), - ], + ], + ), ), - ), - Obx(() { - final isThisPlaying = controller.isPlaying.value && - controller.currentUrl.value == url; + Obx(() { + final isThisPlaying = controller.isPlaying.value && + controller.currentUrl.value == url; - return IconButton( - onPressed: () { - if (controller.isPlaying.value) { - if (controller.currentUrl.value == url) { - controller.musicPause(); + return IconButton( + onPressed: () { + if (controller.isPlaying.value) { + if (controller.currentUrl.value == url) { + controller.musicPause(); + } else { + controller.musicPlay(song); + } } else { - controller.musicPlay(song); + if (controller.currentUrl.value == url) { + controller.musicResume(); + } else { + controller.musicPlay(song); + } } - } else { - if (controller.currentUrl.value == url) { - controller.musicResume(); - } else { - controller.musicPlay(song); - } - } - }, - icon: Icon( - isThisPlaying ? Icons.pause_circle : Icons.play_circle, - color: Theme.of(context).primaryColor, - ), - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - iconSize: 24, - ); - }), - ], + }, + icon: Icon( + isThisPlaying ? Icons.pause_circle : Icons.play_circle, + color: Theme.of(context).primaryColor, + ), + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + iconSize: 36, + ); + }), + ], + ), ), ); } diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 7fd3e6f0a..501918a38 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -103,6 +103,9 @@ class HomeController extends GetxController { // 设置监听器监听蓝牙服务状态变化 _setupStateListeners(); + + // 在初始化时自动触发一次刷新设备连接状态 + refreshDeviceConnection(); } /// 设置状态监听器 diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index 5bee74c6d..011ff285e 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart' as dio; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:flutter/material.dart'; @@ -14,6 +15,7 @@ import 'package:dio/dio.dart'; import '../../../routes/app_routes.dart'; import '../../../modules/login/controllers/googleLogin.dart'; import '../../../modules/login/controllers/facebookLogin.dart'; +//import 'package:http_parser/http_parser.dart'; // 新增此导入 class ProfileController extends GetxController { static ProfileController get to => Get.find(); @@ -150,25 +152,29 @@ class ProfileController extends GetxController { // 更新用户信息到后端 Future _syncWithBackend() async { try { - // 准备数据 - Map data = { - "image": "", // 默认空字符串 - "language": currentLanguage.value, //当前用户语言 - "name": userInfo.value.name, //当前用户名 - }; - - // 如果有头像,转换为base64 + // 创建FormData对象用于上传 + dio.FormData formData = dio.FormData(); + + // 添加基本用户信息 + formData.fields.add(MapEntry('language', currentLanguage.value)); + formData.fields.add(MapEntry('name', userInfo.value.name)); + + // 如果有头像,添加到FormData if (localAvatar.value.isNotEmpty) { - //如果本地头像路径不为空,即代表有头像 try { File imageFile = File(localAvatar.value); if (await imageFile.exists()) { - List imageBytes = await imageFile.readAsBytes(); - String base64Image = base64Encode(imageBytes); - data["image"] = base64Image; - - // // 验证base64数据 - 将base64转回图片并保存到本地 - // await _verifyBase64Image(base64Image); + // 添加文件到FormData + formData.files.add( + MapEntry( + 'file', + await dio.MultipartFile.fromFile( + imageFile.path, + filename: 'avatar.jpg', + ), + ), + ); + Logger().d('已添加头像文件到请求'); } else { Logger().e('头像文件不存在'); } @@ -177,9 +183,10 @@ class ProfileController extends GetxController { } } - Logger().d('准备更新用户信息: $data'); - // 调用API - final response = await Api.updateUserSetting(data); + Logger().d('准备更新用户信息: ${formData.fields}'); + + // 调用API,使用FormData + final response = await Api.updateUserSetting(formData); Logger().d('--更新用户信息响应: $response'); if (response != null) { Logger().d('更新成功: $response');