feat: 增强日历功能并集成 AgentScope 代理服务

This commit is contained in:
qzl
2026-03-11 15:28:29 +08:00
parent e55e445906
commit e20e7d2a02
85 changed files with 5175 additions and 885 deletions
@@ -0,0 +1,110 @@
import 'package:social_app/core/api/i_api_client.dart';
class InboxApi {
final IApiClient _client;
static const _prefix = '/api/v1/inbox/messages';
InboxApi(this._client);
Future<List<InboxMessageResponse>> getMessages({bool? isRead}) async {
final queryParams = isRead != null ? '?is_read=$isRead' : '';
final response = await _client.get('$_prefix$queryParams');
final List<dynamic> data = response.data;
return data.map((json) => InboxMessageResponse.fromJson(json)).toList();
}
Future<InboxMessageResponse> markAsRead(String messageId) async {
final response = await _client.patch('$_prefix/$messageId/read');
return InboxMessageResponse.fromJson(response.data);
}
}
class InboxMessageResponse {
final String id;
final String recipientId;
final String? senderId;
final InboxMessageType messageType;
final String? scheduleItemId;
final String? friendshipId;
final String? content;
final bool isRead;
final InboxMessageStatus status;
final DateTime createdAt;
InboxMessageResponse({
required this.id,
required this.recipientId,
this.senderId,
required this.messageType,
this.scheduleItemId,
this.friendshipId,
this.content,
required this.isRead,
required this.status,
required this.createdAt,
});
factory InboxMessageResponse.fromJson(Map<String, dynamic> json) {
return InboxMessageResponse(
id: json['id'] as String,
recipientId: json['recipient_id'] as String,
senderId: json['sender_id'] as String?,
messageType: InboxMessageType.fromJson(json['message_type'] as String),
scheduleItemId: json['schedule_item_id'] as String?,
friendshipId: json['friendship_id'] as String?,
content: json['content'] as String?,
isRead: json['is_read'] as bool,
status: InboxMessageStatus.fromJson(json['status'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
);
}
InboxMessageResponse copyWith({bool? isRead}) {
return InboxMessageResponse(
id: id,
recipientId: recipientId,
senderId: senderId,
messageType: messageType,
scheduleItemId: scheduleItemId,
friendshipId: friendshipId,
content: content,
isRead: isRead ?? this.isRead,
status: status,
createdAt: createdAt,
);
}
}
enum InboxMessageType {
friendRequest('friend_request'),
calendar('calendar'),
system('system'),
group('group');
final String value;
const InboxMessageType(this.value);
static InboxMessageType fromJson(String json) {
return InboxMessageType.values.firstWhere(
(e) => e.value == json,
orElse: () => InboxMessageType.system,
);
}
}
enum InboxMessageStatus {
pending('pending'),
accepted('accepted'),
rejected('rejected'),
dismissed('dismissed');
final String value;
const InboxMessageStatus(this.value);
static InboxMessageStatus fromJson(String json) {
return InboxMessageStatus.values.firstWhere(
(e) => e.value == json,
orElse: () => InboxMessageStatus.pending,
);
}
}
@@ -1,33 +1,214 @@
import 'dart:convert';
import 'package:flutter/material.dart' hide BackButton;
import 'package:go_router/go_router.dart';
import '../../../../core/di/injection.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/page_header.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../../friends/data/friends_api.dart';
import '../../data/inbox_api.dart';
class MessageInviteListScreen extends StatelessWidget {
class MessageWithFriend {
final InboxMessageResponse message;
final FriendRequestResponse? friendRequest;
const MessageWithFriend({required this.message, this.friendRequest});
}
class MessageInviteListScreen extends StatefulWidget {
const MessageInviteListScreen({super.key});
@override
State<MessageInviteListScreen> createState() =>
_MessageInviteListScreenState();
}
class _MessageInviteListScreenState extends State<MessageInviteListScreen> {
late final InboxApi _inboxApi;
late final FriendsApi _friendsApi;
List<MessageWithFriend> _unreadMessages = [];
List<MessageWithFriend> _readMessages = [];
bool _isLoading = false;
int _activeTabIndex = 0;
@override
void initState() {
super.initState();
_inboxApi = sl<InboxApi>();
_friendsApi = sl<FriendsApi>();
_loadMessages();
}
Future<void> _loadMessages() async {
if (mounted) {
setState(() => _isLoading = true);
}
try {
final unreadRaw = await _inboxApi.getMessages(isRead: false);
final readRaw = await _inboxApi.getMessages(isRead: true);
final unread = await _enrichWithFriendDetails(unreadRaw);
final read = await _enrichWithFriendDetails(readRaw);
if (!mounted) return;
setState(() {
_unreadMessages = unread;
_readMessages = read;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() => _isLoading = false);
Toast.show(context, '消息加载失败,请稍后重试', type: ToastType.error);
}
}
Future<List<MessageWithFriend>> _enrichWithFriendDetails(
List<InboxMessageResponse> messages,
) async {
final futures = messages.map(_fetchFriendRequest);
final results = await Future.wait(futures);
final enriched = <MessageWithFriend>[];
for (int i = 0; i < messages.length; i++) {
final message = messages[i];
final friendRequest = results[i];
enriched.add(
MessageWithFriend(message: message, friendRequest: friendRequest),
);
}
return enriched;
}
Future<FriendRequestResponse?> _fetchFriendRequest(
InboxMessageResponse message,
) async {
if (message.messageType != InboxMessageType.friendRequest ||
message.friendshipId == null) {
return null;
}
try {
return await _friendsApi.getRequestById(message.friendshipId!);
} catch (_) {
return null;
}
}
Future<void> _handleMessageTap(MessageWithFriend item) async {
final message = item.message;
switch (message.messageType) {
case InboxMessageType.calendar:
context.push('/messages/invites/${message.id}');
return;
case InboxMessageType.friendRequest:
if (item.friendRequest == null) {
Toast.show(context, '发送者信息加载失败,请下拉重试', type: ToastType.error);
return;
}
if (message.isRead) {
_showFriendRequestReadOnlySheet(item);
} else {
_showFriendRequestActionSheet(item);
}
return;
case InboxMessageType.system:
case InboxMessageType.group:
return;
}
}
void _showFriendRequestReadOnlySheet(MessageWithFriend item) {
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) {
return _FriendRequestSheet(
message: item.message,
friendRequest: item.friendRequest!,
isReadOnly: true,
);
},
);
}
void _showFriendRequestActionSheet(MessageWithFriend item) {
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (ctx) {
return _FriendRequestSheet(
message: item.message,
friendRequest: item.friendRequest!,
isReadOnly: false,
onAccept: () async {
Navigator.pop(ctx);
await _processFriendRequest(item, accept: true);
},
onDecline: () async {
Navigator.pop(ctx);
await _processFriendRequest(item, accept: false);
},
);
},
);
}
Future<void> _processFriendRequest(
MessageWithFriend item, {
required bool accept,
}) async {
final message = item.message;
final friendshipId = message.friendshipId;
if (friendshipId == null) {
Toast.show(context, '好友请求数据缺失', type: ToastType.error);
return;
}
try {
if (accept) {
await _friendsApi.acceptRequest(friendshipId);
if (mounted) {
Toast.show(context, '已接受好友请求', type: ToastType.success);
}
} else {
await _friendsApi.declineRequest(friendshipId);
if (mounted) {
Toast.show(context, '已拒绝好友请求', type: ToastType.success);
}
}
await _inboxApi.markAsRead(message.id);
await _loadMessages();
} catch (e) {
if (mounted) {
Toast.show(context, '处理失败,请稍后重试', type: ToastType.error);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.messageBg,
backgroundColor: AppColors.background,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PageHeader(leading: BackButton(onPressed: () => context.pop())),
_buildHeader(context),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRemindTag(),
const SizedBox(height: 12),
_buildInviteCard(context),
const Spacer(),
],
),
),
child: _isLoading
? const Center(
child: CircularProgressIndicator(
color: AppColors.blue500,
),
)
: _activeTabIndex == 0
? _buildMessageList(_unreadMessages, isUnread: true)
: _buildMessageList(_readMessages, isUnread: false),
),
],
),
@@ -35,102 +216,400 @@ class MessageInviteListScreen extends StatelessWidget {
);
}
Widget _buildRemindTag() {
return Container(
height: 24,
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: AppColors.messageTagBg,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'消息提醒',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.blue600,
),
),
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 20, 8),
child: Row(
children: [
BackButton(onPressed: () => context.pop()),
const SizedBox(width: 12),
Expanded(child: _buildTabs()),
const SizedBox(width: 56),
],
),
);
}
Widget _buildInviteCard(BuildContext context) {
Widget _buildTabs() {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.slate100,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Expanded(child: _buildTab(0, '未读', Icons.mark_email_unread_outlined)),
const SizedBox(width: 4),
Expanded(child: _buildTab(1, '已读', Icons.mark_email_read_outlined)),
],
),
);
}
Widget _buildTab(int index, String label, IconData icon) {
final isSelected = _activeTabIndex == index;
return GestureDetector(
onTap: () => context.push('/messages/invites/1'),
onTap: () {
if (_activeTabIndex != index) {
setState(() => _activeTabIndex = index);
}
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.messageCardBorder),
color: isSelected ? AppColors.white : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColors.messageCalendarBg,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.calendar_today_outlined,
size: 20,
color: AppColors.blue500,
),
),
],
),
const SizedBox(height: 8),
const Text(
'事件:产品评审会 2026-02-12 14:00',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.slate700,
),
),
const SizedBox(height: 8),
const Text(
'邀请人:李文浩',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
),
),
const SizedBox(height: 8),
const Text(
'点击查看详情并处理邀请',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColors.slate400,
),
),
],
Icon(
icon,
size: 16,
color: isSelected ? AppColors.slate900 : AppColors.slate500,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? AppColors.slate900 : AppColors.slate500,
),
),
const SizedBox(width: 8),
Icon(
Icons.chevron_right,
size: 16,
color: AppColors.messageArrowColor,
),
if (index == 0 && _unreadMessages.isNotEmpty) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
color: AppColors.red500,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_unreadMessages.length > 99
? '99+'
: _unreadMessages.length.toString(),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColors.white,
),
),
),
],
],
),
),
);
}
Widget _buildMessageList(
List<MessageWithFriend> messages, {
required bool isUnread,
}) {
return RefreshIndicator(
onRefresh: _loadMessages,
color: AppColors.blue500,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: messages.isEmpty ? 1 : messages.length,
itemBuilder: (context, index) {
if (messages.isEmpty) {
return SizedBox(
height: MediaQuery.sizeOf(context).height * 0.6,
child: _buildEmptyState(isUnread: isUnread),
);
}
final item = messages[index];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _MessageCard(
item: item,
onTap: () => _handleMessageTap(item),
),
);
},
),
);
}
Widget _buildEmptyState({required bool isUnread}) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.slate100,
shape: BoxShape.circle,
),
child: Icon(
isUnread ? Icons.notifications_none : Icons.inbox_outlined,
size: 36,
color: AppColors.slate400,
),
),
const SizedBox(height: 16),
Text(
isUnread ? '暂无未读消息' : '暂无已读消息',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.slate500,
),
),
const SizedBox(height: 8),
Text(
isUnread ? '有新消息时会在这里显示' : '处理过的消息会显示在这里',
style: const TextStyle(fontSize: 13, color: AppColors.slate400),
),
],
),
);
}
}
class _MessageCard extends StatelessWidget {
final MessageWithFriend item;
final VoidCallback onTap;
const _MessageCard({required this.item, required this.onTap});
InboxMessageResponse get message => item.message;
FriendRequestResponse? get friendRequest => item.friendRequest;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: message.isRead
? AppColors.borderSecondary
: AppColors.blue100,
width: message.isRead ? 1 : 1.5,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.emerald500.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.person_add_outlined,
size: 22,
color: AppColors.emerald500,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_title(),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
),
const SizedBox(height: 6),
Text(
_content(),
style: const TextStyle(
fontSize: 13,
color: AppColors.slate500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
String _title() {
if (message.messageType == InboxMessageType.friendRequest) {
if (friendRequest == null) {
return '好友请求信息加载失败';
}
return '${friendRequest!.sender.username} 请求添加您为好友';
}
if (message.messageType == InboxMessageType.calendar) {
try {
final data =
jsonDecode(message.content ?? '{}') as Map<String, dynamic>;
return data['title'] as String? ?? '日历邀请';
} catch (_) {
return '日历邀请';
}
}
return '系统消息';
}
String _content() => message.content ?? '点击查看详情';
}
class _FriendRequestSheet extends StatelessWidget {
final InboxMessageResponse message;
final FriendRequestResponse friendRequest;
final bool isReadOnly;
final VoidCallback? onAccept;
final VoidCallback? onDecline;
const _FriendRequestSheet({
required this.message,
required this.friendRequest,
required this.isReadOnly,
this.onAccept,
this.onDecline,
});
@override
Widget build(BuildContext context) {
final status = friendRequest.status;
final statusText = status == 'accepted'
? '已接受'
: status == 'rejected'
? '已拒绝'
: '已处理';
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
decoration: const BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.slate300,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.emerald500.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.person_add_outlined,
size: 32,
color: AppColors.emerald500,
),
),
const SizedBox(height: 16),
Text(
'${friendRequest.sender.username} 请求添加您为好友',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
textAlign: TextAlign.center,
),
if ((message.content ?? '').isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'备注: ${message.content}',
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
),
],
if (isReadOnly) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColors.slate100,
borderRadius: BorderRadius.circular(20),
),
child: Text(
statusText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.slate600,
),
),
),
] else ...[
const SizedBox(height: 28),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: onDecline,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: AppColors.slate300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'拒绝',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.slate600,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: onAccept,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.blue500,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'接受',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.white,
),
),
),
),
],
),
],
SizedBox(height: MediaQuery.of(context).padding.bottom + 12),
],
),
);
}
}