Browse Source

Merge commit '3f3d40b9d249a967c3aecfe902356e4e3bb15841' into dev

newdev_shunjiawei
lxm 1 year ago
parent
commit
fea3a5ddd7
  1. 6
      .gitignore
  2. 10
      lib/data/services/network/api.dart
  3. 2
      lib/data/services/network/dio_manager.dart
  4. 52
      lib/modules/agent/views/agent_view.dart
  5. 497
      lib/modules/agent/views/message_bubble.dart
  6. 3
      lib/modules/home/controllers/home_controller.dart
  7. 43
      lib/modules/profile/controllers/profile_controller.dart

6
.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

10
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,
// );
}
//注销账号

2
lib/data/services/network/dio_manager.dart

@ -31,7 +31,7 @@ class DioManager {
Future request<T>(
NWMethod method,
String path, {
Map? params,
Object? params,
Map<String, dynamic>? queryParameters,
}) async {
Response response = await dio.request(

52
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(

497
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<AgentController> {
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<AgentController> {
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
this.mate,
this.meta,
}) : super(key: key);
@override
@ -38,8 +39,8 @@ class MessageBubble extends GetView<AgentController> {
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<AgentController> {
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<AgentController> {
],
),
),
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<AgentController> {
/// 构建头像
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<String>('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<AgentController> {
}
// 判断消息内容,显示特殊卡片
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<String, dynamic>.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<String, dynamic>.from(e)).toList();
final now = mate!['card_weather']['day'] as Map<String, dynamic>;
final now = meta!['card_weather']['day'] as Map<String, dynamic>;
//天气
return _buildWeatherContent(
context,
@ -291,122 +336,123 @@ class MessageBubble extends GetView<AgentController> {
),
),
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<AgentController> {
'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,
);
}),
],
),
),
);
}

3
lib/modules/home/controllers/home_controller.dart

@ -103,6 +103,9 @@ class HomeController extends GetxController {
// 设置监听器监听蓝牙服务状态变化
_setupStateListeners();
// 在初始化时自动触发一次刷新设备连接状态
refreshDeviceConnection();
}
/// 设置状态监听器

43
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<void> _syncWithBackend() async {
try {
// 准备数据
Map<String, dynamic> 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<int> 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');

Loading…
Cancel
Save