16 KiB
日历邀请弹窗优化 Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: 优化日历邀请消息弹窗,显示完整信息(发送者名称 + 日历标题),使用公共弹窗组件替代所有旧弹窗代码
Architecture:
- 后端新增用户信息查询接口
- 前端创建公共弹窗组件 MessageActionSheet
- 删除所有旧的弹窗代码(好友请求、日历邀请),统一使用公共组件
Tech Stack: Flutter (Dart), FastAPI (Python)
Task 1: 后端添加用户信息查询接口
Files:
- Modify:
backend/src/v1/users/router.py - Modify:
backend/src/v1/users/service.py - Modify:
backend/src/v1/users/repository.py
Step 1: 添加 repository 方法
修改 backend/src/v1/users/repository.py,在 UserRepository 和 SQLAlchemyUserRepository 中已有 get_by_user_id 方法,确认存在。
Step 2: 添加 service 方法
修改 backend/src/v1/users/service.py,添加:
async def get_user_by_id(self, user_id: UUID) -> UserBasicInfo:
from v1.friendships.schemas import UserBasicInfo
profile = await self._repository.get_by_user_id(user_id)
if not profile:
raise HTTPException(status_code=404, detail="User not found")
return UserBasicInfo(
id=str(profile.user_id),
username=profile.username,
avatar_url=profile.avatar_url,
)
Step 3: 添加 router 接口
修改 backend/src/v1/users/router.py,添加:
@router.get("/{user_id}", response_model=UserBasicInfo)
async def get_user(
user_id: UUID,
service: Annotated[UserService, Depends(get_user_service)],
):
return await service.get_user_by_id(user_id)
Step 4: 运行 lint 和 typecheck
cd backend && uv run ruff check src/v1/users/ && uv run basedpyright src/v1/users/
Step 5: 提交
git add backend/src/v1/users/ && git commit -m "feat(users): add get user by id endpoint"
Task 2: 前端添加用户 API 接口
Files:
- Modify:
apps/lib/features/users/data/users_api.dart - Modify:
apps/lib/core/di/injection.dart
Step 1: 添加 UserBasicInfo 类和 getById 方法
修改 apps/lib/features/users/data/users_api.dart:
class UserBasicInfo {
final String id;
final String username;
final String? avatarUrl;
UserBasicInfo({
required this.id,
required this.username,
this.avatarUrl,
});
factory UserBasicInfo.fromJson(Map<String, dynamic> json) {
return UserBasicInfo(
id: json['id'] as String,
username: json['username'] as String,
avatarUrl: json['avatar_url'] as String?,
);
}
}
class UsersApi {
final IApiClient _client;
static const _prefix = '/api/v1/users';
UsersApi(this._client);
// ... existing methods
Future<UserBasicInfo> getById(String userId) async {
final response = await _client.get('$_prefix/$userId');
return UserBasicInfo.fromJson(response.data);
}
}
Step 2: 注册到 DI
修改 apps/lib/core/di/injection.dart,添加:
sl.registerLazySingleton(() => UsersApi(sl<IApiClient>()));
Step 3: 运行 flutter analyze
cd apps && flutter analyze lib/features/users/
Step 4: 提交
git add apps/lib/features/users/ apps/lib/core/di/injection.dart && git commit -m "feat(users): add getById API and UserBasicInfo"
Task 3: 创建公共弹窗组件 MessageActionSheet
Files:
- Create:
apps/lib/features/messages/ui/widgets/message_action_sheet.dart
Step 1: 创建弹窗组件
创建 apps/lib/features/messages/ui/widgets/message_action_sheet.dart:
import 'package:flutter/material.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart';
class MessageActionSheet extends StatelessWidget {
final String title;
final String? description;
final String? statusText;
final bool isReadOnly;
final VoidCallback? onAccept;
final VoidCallback? onDecline;
final IconData? icon;
final Color? iconColor;
const MessageActionSheet({
super.key,
required this.title,
this.description,
this.statusText,
this.isReadOnly = false,
this.onAccept,
this.onDecline,
this.icon,
this.iconColor,
});
@override
Widget build(BuildContext context) {
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),
if (icon != null) ...[
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: (iconColor ?? AppColors.blue500).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(icon, size: 32, color: iconColor ?? AppColors.blue500),
),
const SizedBox(height: 16),
],
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
textAlign: TextAlign.center,
),
if (description != null && description!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
description!,
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
textAlign: TextAlign.center,
),
],
if (statusText != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.slate100,
borderRadius: BorderRadius.circular(16),
),
child: Text(
statusText!,
style: const TextStyle(fontSize: 14, color: AppColors.slate600),
),
),
],
const SizedBox(height: 24),
if (!isReadOnly) ...[
Row(
children: [
Expanded(
child: AppButton(
text: '拒绝',
isOutlined: true,
onPressed: () {
Navigator.pop(context);
onDecline?.call();
},
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: AppButton(
text: '接受',
onPressed: () {
Navigator.pop(context);
onAccept?.call();
},
),
),
],
),
],
SizedBox(height: MediaQuery.of(context).padding.bottom + 12),
],
),
);
}
}
Step 2: 运行 flutter analyze
cd apps && flutter analyze lib/features/messages/ui/widgets/message_action_sheet.dart
Step 3: 提交
git add apps/lib/features/messages/ui/widgets/message_action_sheet.dart && git commit -m "feat(messages): add MessageActionSheet component"
Task 4: 重构消息列表页面,使用公共组件并删除旧代码
Files:
- Modify:
apps/lib/features/messages/ui/screens/message_invite_list_screen.dart
Step 1: 添加依赖和字段
在文件顶部添加:
import '../../../users/data/users_api.dart';
import '../widgets/message_action_sheet.dart';
在 _MessageInviteListScreenState 中添加:
late final UsersApi _usersApi;
在 initState 中添加:
_usersApi = sl<UsersApi>();
Step 2: 添加获取日历邀请信息方法
Future<(String calendarTitle, String senderName)?> _getCalendarInviteInfo(
InboxMessageResponse message,
) async {
if (message.scheduleItemId == null || message.senderId == null) {
return null;
}
try {
final calendar = await _calendarApi.getById(message.scheduleItemId!);
final sender = await _usersApi.getById(message.senderId!);
return (calendar.title, sender.username);
} catch (e) {
return null;
}
}
Step 3: 替换日历邀请弹窗方法
删除旧的 _showCalendarInviteSheet 方法,替换为:
Future<void> _showCalendarInviteSheet(InboxMessageResponse message) async {
final itemId = message.scheduleItemId;
if (itemId == null) return;
final info = await _getCalendarInviteInfo(message);
final title = info != null
? '${info.$2} 邀请你加入日历'
: '日历邀请';
final description = info?.$1;
if (!mounted) return;
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => MessageActionSheet(
title: title,
description: description,
icon: Icons.calendar_today,
iconColor: AppColors.blue500,
onAccept: () async {
try {
await _calendarApi.acceptSubscription(itemId);
await _inboxApi.markAsRead(message.id);
if (mounted) {
Toast.show(context, '已接受', type: ToastType.success);
_loadMessages();
}
} catch (e) {
if (mounted) {
Toast.show(context, '操作失败', type: ToastType.error);
}
}
},
onDecline: () async {
try {
await _calendarApi.rejectSubscription(itemId);
await _inboxApi.markAsRead(message.id);
if (mounted) {
Toast.show(context, '已拒绝', type: ToastType.success);
_loadMessages();
}
} catch (e) {
if (mounted) {
Toast.show(context, '操作失败', type: ToastType.error);
}
}
},
),
);
}
Step 4: 添加已读日历邀请弹窗方法
Future<void> _showCalendarInviteReadOnlySheet(InboxMessageResponse message) async {
final itemId = message.scheduleItemId;
if (itemId == null) return;
final info = await _getCalendarInviteInfo(message);
final title = info != null
? '${info.$2} 邀请你加入日历'
: '日历邀请';
final description = info?.$1;
final statusText = message.status.value == 'accepted' ? '已接受' : '已拒绝';
if (!mounted) return;
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => MessageActionSheet(
title: title,
description: description,
statusText: statusText,
isReadOnly: true,
icon: Icons.calendar_today,
iconColor: AppColors.blue500,
),
);
}
Step 5: 替换好友请求弹窗方法
删除旧的 _showFriendRequestReadOnlySheet 和 _showFriendRequestActionSheet 方法,替换为:
void _showFriendRequestSheet(MessageWithFriend item, {bool isReadOnly = false}) {
final message = item.message;
final friendRequest = item.friendRequest;
if (friendRequest == null) return;
final title = '${friendRequest.sender.username} 请求添加您为好友';
final description = message.content;
final statusText = isReadOnly
? (friendRequest.status == 'accepted'
? '已接受'
: friendRequest.status == 'rejected'
? '已拒绝'
: '已处理')
: null;
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (ctx) => MessageActionSheet(
title: title,
description: description,
statusText: statusText,
isReadOnly: isReadOnly,
icon: Icons.person_add_outlined,
iconColor: AppColors.emerald500,
onAccept: isReadOnly
? null
: () async {
await _processFriendRequest(item, accept: true);
},
onDecline: isReadOnly
? null
: () async {
await _processFriendRequest(item, accept: false);
},
),
);
}
Step 6: 修改 _handleMessageTap 方法
修改为调用新的统一方法:
case InboxMessageType.calendar:
final content = _parseCalendarContent(message.content);
if (content == null) return;
final type = content['type'] as String?;
if (type == 'invite') {
if (message.status.value == 'pending') {
await _showCalendarInviteSheet(message);
} else {
await _showCalendarInviteReadOnlySheet(message);
if (message.scheduleItemId != null && context.mounted) {
context.push('/calendar/events/${message.scheduleItemId}');
}
}
} else if (type == 'update') {
if (message.scheduleItemId != null) {
context.push('/calendar/events/${message.scheduleItemId}');
}
}
return;
case InboxMessageType.friendRequest:
if (item.friendRequest == null) {
Toast.show(context, '发送者信息加载失败,请下拉重试', type: ToastType.error);
return;
}
_showFriendRequestSheet(item, isReadOnly: message.isRead);
return;
Step 7: 删除旧的 _FriendRequestSheet 类
删除文件末尾的整个 _FriendRequestSheet 类(约605-749行)。
Step 8: 运行 flutter analyze
cd apps && flutter analyze lib/features/messages/ui/screens/message_invite_list_screen.dart
Step 9: 提交
git add apps/lib/features/messages/ && git commit -m "refactor(messages): use MessageActionSheet for all message types"
Task 5: 删除日历消息卡片中的旧弹窗代码
Files:
- Modify:
apps/lib/features/messages/ui/widgets/calendar_message_card.dart
Step 1: 修改 CalendarInviteCard
CalendarInviteCard 是用于列表展示的卡片,不需要显示弹窗。检查是否有不必要的硬编码,如果有则清理。
Step 2: 运行 flutter analyze
cd apps && flutter analyze lib/features/messages/ui/widgets/calendar_message_card.dart
Step 3: 提交
git add apps/lib/features/calendar_message_card.dart && git commit/messages/ui/widgets -f "chore(messages): clean up calendar message card"
Task 6: 验证和测试
Step 1: 运行完整测试
cd apps && flutter test test/features/messages/
cd backend && uv run pytest tests/unit/v1/users/ -v
Step 2: 手动测试场景
- 用户 A 发送日历邀请给用户 B
- 用户 B 打开未读消息,点击日历邀请
- 弹窗显示:"XXX 邀请你加入 [日历标题]"
- 点击接受/拒绝
- 用户 B 打开已读消息,点击日历邀请
- 弹窗显示状态标签
- 好友请求未读/已读都使用相同弹窗组件
Summary
| Task | Description |
|---|---|
| 1 | 后端添加用户信息查询接口 /api/v1/users/{user_id} |
| 2 | 前端添加 UsersApi.getById 方法 |
| 3 | 创建公共弹窗组件 MessageActionSheet |
| 4 | 重构消息列表页面,删除旧弹窗代码,统一使用 MessageActionSheet |
| 5 | 清理日历消息卡片旧代码 |
| 6 | 验证测试 |
Plan complete and saved to docs/plans/2026-03-11-calendar-invite-sheet.md. Two execution options:
-
Subagent-Driven (this session) - I dispatch fresh subagent per task, review between tasks, fast iteration
-
Parallel Session (separate) - Open new session with executing-plans, batch execution with checkpoints
Which approach?