feat: 增强日历功能并集成 AgentScope 代理服务
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/toast/index.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/page_header.dart' as widgets;
|
||||
import '../../../friends/data/friends_api.dart';
|
||||
import '../../../users/data/models/user_response.dart';
|
||||
@@ -20,10 +21,12 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
final _searchFocusNode = FocusNode();
|
||||
|
||||
List<FriendResponse> _friends = [];
|
||||
List<FriendRequestResponse> _pendingRequests = [];
|
||||
List<UserResponse> _searchResults = [];
|
||||
bool _isLoading = true;
|
||||
bool _isSearching = false;
|
||||
bool _hasSearched = false;
|
||||
String? _sendingRequestUserId;
|
||||
final Set<String> _sentRequestIds = {};
|
||||
Set<String> _friendIds = {};
|
||||
|
||||
@@ -37,10 +40,16 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
try {
|
||||
final friendsApi = sl<FriendsApi>();
|
||||
final friends = await friendsApi.getFriends();
|
||||
final outgoingRequests = await friendsApi.getOutgoingRequests();
|
||||
final pendingRequests = outgoingRequests
|
||||
.where((r) => r.status == 'pending')
|
||||
.toList();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_friends = friends;
|
||||
_friendIds = friends.map((f) => f.friend.id).toSet();
|
||||
_sentRequestIds.addAll(outgoingRequests.map((r) => r.recipient.id));
|
||||
_pendingRequests = pendingRequests;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -98,18 +107,28 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
}
|
||||
|
||||
Future<void> _sendFriendRequest(String targetUserId, String? content) async {
|
||||
if (_sendingRequestUserId != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setState(() {
|
||||
_sendingRequestUserId = targetUserId;
|
||||
});
|
||||
final friendsApi = sl<FriendsApi>();
|
||||
await friendsApi.sendRequest(targetUserId);
|
||||
await friendsApi.sendRequest(targetUserId, content: content);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sentRequestIds.add(targetUserId);
|
||||
_sendingRequestUserId = null;
|
||||
});
|
||||
Toast.show(context, '好友请求已发送', type: ToastType.success);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sendingRequestUserId = null;
|
||||
});
|
||||
Toast.show(context, '发送失败,请稍后重试', type: ToastType.error);
|
||||
}
|
||||
}
|
||||
@@ -117,50 +136,121 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
|
||||
void _showAddFriendDialog(UserResponse user) {
|
||||
final controller = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('添加好友'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('向 ${user.username} 发送好友请求'),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '验证消息(可选)',
|
||||
hintText: '你好,我是...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
maxLength: 200,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
|
||||
),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(sheetContext).size.height * 0.7,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.lg,
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.lg,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(AppRadius.xxl),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: AppSpacing.xs,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.slate300,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Text(
|
||||
'添加 ${user.username}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
const Text(
|
||||
'发送一条验证信息,方便对方确认你的身份',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.slate500),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.slate50,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
minLines: 2,
|
||||
maxLength: 200,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '你好,我是...',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.all(AppSpacing.lg),
|
||||
counterStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
text: '取消',
|
||||
isOutlined: true,
|
||||
onPressed: () => Navigator.pop(sheetContext),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: _buildSendButton(
|
||||
user.id,
|
||||
controller,
|
||||
sheetContext,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height:
|
||||
MediaQuery.of(sheetContext).padding.bottom +
|
||||
AppSpacing.sm,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.dispose();
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
_sendFriendRequest(
|
||||
user.id,
|
||||
controller.text.isEmpty ? null : controller.text,
|
||||
);
|
||||
},
|
||||
child: const Text('发送'),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((_) => controller.dispose());
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -187,6 +277,12 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
_buildSearchRow(),
|
||||
_buildSearchResults(),
|
||||
const SizedBox(height: 16),
|
||||
if (_pendingRequests.isNotEmpty) ...[
|
||||
_buildSectionTitle('新的联系人'),
|
||||
const SizedBox(height: 8),
|
||||
_buildPendingRequestCard(_pendingRequests),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_buildSectionTitle('全部联系人'),
|
||||
const SizedBox(height: 8),
|
||||
if (_isLoading)
|
||||
@@ -320,12 +416,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
children: [
|
||||
for (int i = 0; i < _searchResults.length; i++) ...[
|
||||
_buildSearchResultItem(_searchResults[i]),
|
||||
if (i < _searchResults.length - 1)
|
||||
Container(
|
||||
height: 1,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14),
|
||||
color: const Color(0xFFEEF2F7),
|
||||
),
|
||||
if (i < _searchResults.length - 1) _buildDivider(),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -344,31 +435,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: _getAvatarBackground(avatarColor),
|
||||
borderRadius: BorderRadius.circular(21),
|
||||
border: Border.all(color: _getAvatarBorder(avatarColor)),
|
||||
),
|
||||
child: user.avatarUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(21),
|
||||
child: Image.network(
|
||||
user.avatarUrl!,
|
||||
width: 42,
|
||||
height: 42,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Icon(
|
||||
Icons.person,
|
||||
size: 18,
|
||||
color: _getAvatarColor(user.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(Icons.person, size: 18, color: _getAvatarColor(user.id)),
|
||||
),
|
||||
_buildAvatar(user.avatarUrl, user.id, avatarColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -506,6 +573,61 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPendingRequestCard(List<FriendRequestResponse> requests) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFE3EAF6)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < requests.length; i++) ...[
|
||||
_buildPendingRequestItem(requests[i]),
|
||||
if (i < requests.length - 1) _buildDivider(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPendingRequestItem(FriendRequestResponse request) {
|
||||
final recipient = request.recipient;
|
||||
final color = _getAvatarColor(recipient.id);
|
||||
|
||||
return Container(
|
||||
height: 70,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildAvatar(recipient.avatarUrl, recipient.id, color),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
recipient.username,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'等待对方确认',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.slate500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactCard(List<FriendResponse> friends) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -517,12 +639,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
children: [
|
||||
for (int i = 0; i < friends.length; i++) ...[
|
||||
_buildContactItem(friends[i]),
|
||||
if (i < friends.length - 1)
|
||||
Container(
|
||||
height: 1,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14),
|
||||
color: const Color(0xFFEEF2F7),
|
||||
),
|
||||
if (i < friends.length - 1) _buildDivider(),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -540,28 +657,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: _getAvatarBackground(color),
|
||||
borderRadius: BorderRadius.circular(21),
|
||||
border: Border.all(color: _getAvatarBorder(color)),
|
||||
),
|
||||
child: friendInfo.avatarUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(21),
|
||||
child: Image.network(
|
||||
friendInfo.avatarUrl!,
|
||||
width: 42,
|
||||
height: 42,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Icon(Icons.person, size: 18, color: color),
|
||||
),
|
||||
)
|
||||
: Icon(Icons.person, size: 18, color: color),
|
||||
),
|
||||
_buildAvatar(friendInfo.avatarUrl, friendInfo.id, color),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -614,4 +710,82 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
if (color == AppColors.violet500) return const Color(0xFFE4E8FA);
|
||||
return const Color(0xFFDDE8FB);
|
||||
}
|
||||
|
||||
Widget _buildDivider() {
|
||||
return Container(
|
||||
height: 1,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14),
|
||||
color: const Color(0xFFEEF2F7),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(String? avatarUrl, String userId, Color color) {
|
||||
return Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: _getAvatarBackground(color),
|
||||
borderRadius: BorderRadius.circular(21),
|
||||
border: Border.all(color: _getAvatarBorder(color)),
|
||||
),
|
||||
child: avatarUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(21),
|
||||
child: Image.network(
|
||||
avatarUrl,
|
||||
width: 42,
|
||||
height: 42,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Icon(
|
||||
Icons.person,
|
||||
size: 18,
|
||||
color: _getAvatarColor(userId),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(Icons.person, size: 18, color: _getAvatarColor(userId)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendButton(
|
||||
String userId,
|
||||
TextEditingController controller,
|
||||
BuildContext sheetContext,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: ElevatedButton(
|
||||
onPressed: _sendingRequestUserId == userId
|
||||
? null
|
||||
: () async {
|
||||
final content = controller.text.trim();
|
||||
Navigator.pop(sheetContext);
|
||||
await _sendFriendRequest(
|
||||
userId,
|
||||
content.isEmpty ? null : content,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.blue500,
|
||||
foregroundColor: AppColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
),
|
||||
child: _sendingRequestUserId == userId
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.white,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'发送',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user