# Calendar Share Redesign - Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 重写日历分享功能,支持选择多个好友/用户并单独设置权限,同时后端新增批量分享接口。 **Architecture:** - **后端**: 新增 `POST /api/v1/schedule-items/{id}/share/batch` 批量分享接口,原有单用户接口保留。 - **前端**: 重构 `CalendarEventShareScreen` 和 `CalendarShareDialog`,移除硬编码手机号输入,改为用户搜索 + 多选 + 独立权限设置。 **Tech Stack:** Flutter (Riverpod/BLoC), FastAPI (Pydantic), PostgreSQL, 现有 API 契约 --- ## 背景与现状 ### 现有流程 ``` 用户 → CalendarEventShareScreen → CalendarShareDialog ├── 手机号输入 + 前缀选择 ├── 权限开关 (View/Edit/Invite) └── POST /api/v1/schedule-items/{id}/share ``` ### 问题 1. 仅支持单用户分享 2. 需要手动输入手机号,不支持好友选择 3. 权限是全局的,无法为不同用户设置不同权限 ### 目标流程 ``` 用户 → CalendarEventShareScreen → CalendarShareDialog ├── 搜索框 (好友/用户名/手机号搜索) ├── 用户列表 (可多选) │ └── 每行: 头像 + 名字 + 权限开关 (选中后显示) ├── 已选用户 Chips └── POST /api/v1/schedule-items/{id}/share/batch ``` --- ## Task 1: 后端 - 新增批量分享接口 **Files:** - Create: `backend/src/v1/schedule_items/schemas.py` (新增 `ScheduleItemShareBatchRequest`) - Modify: `backend/src/v1/schedule_items/router.py` (新增 `/share/batch` 端点) - Modify: `backend/src/v1/schedule_items/service.py` (新增 `share_batch` 方法) - Verify: `backend/tests/**` - [ ] **Step 1: 创建批量分享请求 Schema** ```python # backend/src/v1/schedule_items/schemas.py class ScheduleItemShareBatchRequest(BaseModel): shares: List[ShareTarget] = Field(..., description="List of users to share with") permission_view: bool = Field(True, description="Default view permission") permission_edit: bool = Field(False, description="Default edit permission") permission_invite: bool = Field(False, description="Default invite permission") class ShareTarget(BaseModel): phone: str = Field(..., pattern=r"^\+861[3-9]\d{9}$") permission_view: Optional[bool] = None permission_edit: Optional[bool] = None permission_invite: Optional[bool] = None class ScheduleItemShareBatchResponse(BaseModel): message: str success_count: int failure_count: int failures: List[ShareFailure] = [] class ShareFailure(BaseModel): phone: str reason: str ``` - [ ] **Step 2: 实现批量分享服务方法** ```python # backend/src/v1/schedule_items/service.py async def share_batch( db: AsyncSession, current_user: User, schedule_item_id: UUID, shares: List[ShareTarget], default_permission_view: bool = True, default_permission_edit: bool = False, default_permission_invite: bool = False, ) -> Tuple[int, int, List[Dict]]: """批量分享日程给多个用户""" # 1. 验证当前用户是否有邀请权限 # 2. 对每个 share 调用现有的 share_single 逻辑 # 3. 返回 (成功数, 失败数, 失败详情列表) ``` - [ ] **Step 3: 新增批量分享路由** ```python # backend/src/v1/schedule_items/router.py @router.post("/{item_id}/share/batch", response_model=ScheduleItemShareBatchResponse) async def share_batch( item_id: UUID, request: ScheduleItemShareBatchRequest, current_user: User = Depends(get_current_user), service: ScheduleItemService = Depends(), ): """批量分享日程给多个用户""" success_count, failure_count, failures = await service.share_batch( db=db, current_user=current_user, schedule_item_id=item_id, shares=request.shares, default_permission_view=request.permission_view, default_permission_edit=request.permission_edit, default_permission_invite=request.permission_invite, ) return ScheduleItemShareBatchResponse( message="Batch share completed", success_count=success_count, failure_count=failure_count, failures=[ShareFailure(**f) for f in failures], ) ``` - [ ] **Step 4: 编写测试** ```python # backend/tests/v1/test_schedule_items.py @pytest.mark.asyncio async def test_share_batch_success(): """测试批量分享成功""" # Arrange # Act # Assert @pytest.mark.asyncio async def test_share_batch_partial_failure(): """测试批量分享部分失败(非好友用户应被拒绝)""" # Arrange # Act # Assert ``` - [ ] **Step 5: 运行测试验证** ```bash cd backend && uv run pytest tests/v1/test_schedule_items.py -v -k "share_batch" ``` --- ## Task 2: 前端 - 新增批量分享 API 方法 **Files:** - Modify: `apps/lib/features/calendar/data/apis/calendar_api.dart` - [ ] **Step 1: 新增批量分享 API 方法** ```dart // apps/lib/features/calendar/data/apis/calendar_api.dart class CalendarApi { // 现有单用户分享保留 Future share(...) { ... } // 新增批量分享 Future shareBatch({ required String eventId, required List shares, bool permissionView = true, bool permissionEdit = false, bool permissionInvite = false, }) async { final response = await _dio.post( '/api/v1/schedule-items/$eventId/share/batch', data: { 'shares': shares.map((s) => s.toJson()).toList(), 'permission_view': permissionView, 'permission_edit': permissionEdit, 'permission_invite': permissionInvite, }, ); return ShareBatchResponse.fromJson(response.data); } } class ShareTarget { final String phone; final bool? permissionView; final bool? permissionEdit; final bool? permissionInvite; Map toJson() => { 'phone': phone, if (permissionView != null) 'permission_view': permissionView, if (permissionEdit != null) 'permission_edit': permissionEdit, if (permissionInvite != null) 'permission_invite': permissionInvite, }; } class ShareBatchResponse { final String message; final int successCount; final int failureCount; final List failures; } ``` - [ ] **Step 2: 验证 API 编译** ```bash cd apps && flutter analyze lib/features/calendar/data/apis/calendar_api.dart ``` --- ## Task 3: 前端 - 创建分享页面状态管理 **Files:** - Create: `apps/lib/features/calendar/presentation/cubits/calendar_share_cubit.dart` - Create: `apps/lib/features/calendar/presentation/cubits/calendar_share_state.dart` - [ ] **Step 1: 创建 State** ```dart // apps/lib/features/calendar/presentation/cubits/calendar_share_state.dart enum CalendarShareStatus { initial, loading, success, failure } class SelectedUser { final String id; final String username; final String? avatarUrl; final String phone; bool permissionView; bool permissionEdit; bool permissionInvite; } class CalendarShareState { final CalendarShareStatus status; final List friends; final List searchResults; final List selectedUsers; final String searchQuery; final String? errorMessage; // 权限默认值(应用于新选中的用户) bool defaultPermissionView; bool defaultPermissionEdit; bool defaultPermissionInvite; } ``` - [ ] **Step 2: 创建 Cubit** ```dart // apps/lib/features/calendar/presentation/cubits/calendar_share_cubit.dart class CalendarShareCubit extends Cubit { final FriendsApi _friendsApi; final UsersApi _usersApi; final CalendarApi _calendarApi; CalendarShareCubit({ required FriendsApi friendsApi, required UsersApi usersApi, required CalendarApi calendarApi, }) : super(CalendarShareState.initial()); // 加载好友列表 Future loadFriends() async { ... } // 搜索用户 Future searchUsers(String query) async { ... } // 切换用户选中状态 void toggleUser(String userId) { ... } // 更新单个用户的权限 void updateUserPermission(String userId, {bool? view, bool? edit, bool? invite}) { ... } // 移除已选用户 void removeUser(String userId) { ... } // 设置默认权限(对新选用户生效) void setDefaultPermissions({bool? view, bool? edit, bool? invite}) { ... } // 发送批量分享 Future share(String eventId) async { ... } } ``` - [ ] **Step 3: 验证编译** ```bash cd apps && flutter analyze lib/features/calendar/presentation/cubits/calendar_share_cubit.dart ``` --- ## Task 4: 前端 - 重构分享对话框 UI **Files:** - Create: `apps/lib/features/calendar/presentation/widgets/user_select_list.dart` - Create: `apps/lib/features/calendar/presentation/widgets/user_select_item.dart` - Modify: `apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart` - Modify: `apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart` ### 4.1 用户选择列表组件 - [ ] **Step 1: 创建 UserSelectItem** ```dart // apps/lib/features/calendar/presentation/widgets/user_select_item.dart class UserSelectItem extends StatelessWidget { final UserBasicInfo user; final bool isSelected; final bool showPermissions; // 选中后才显示权限开关 final SelectedUser? selectedUser; // 如果选中,包含权限状态 final ValueChanged onToggle; final Function(PermissionType, bool) onPermissionChanged; @override Widget build(BuildContext context) { return ListTile( leading: Avatar(url: user.avatarUrl, name: user.username), title: Text(user.username), subtitle: showPermissions && isSelected ? Row( children: [ _PermissionChip('V', selectedUser?.permissionView ?? true, (v) => onPermissionChanged(PermissionType.view, v)), _PermissionChip('E', selectedUser?.permissionEdit ?? false, (v) => onPermissionChanged(PermissionType.edit, v)), _PermissionChip('I', selectedUser?.permissionInvite ?? false, (v) => onPermissionChanged(PermissionType.invite, v)), ], ) : null, trailing: Checkbox(value: isSelected, onChanged: (v) => onToggle(v ?? false)), ); } } ``` ### 4.2 重构 CalendarShareDialog - [ ] **Step 2: 重构对话框** ```dart // apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart class CalendarShareDialog extends StatefulWidget { // ... existing props @override State createState() => _CalendarShareDialogState(); } class _CalendarShareDialogState extends State { late final CalendarShareCubit _cubit; final _searchController = TextEditingController(); @override void initState() { super.initState(); _cubit = context.read(); _cubit.loadFriends(); } @override Widget build(BuildContext context) { return BlocProvider.value( value: _cubit, child: BlocBuilder( builder: (context, state) { return Column( mainAxisSize: MainAxisSize.min, children: [ // Header _buildHeader(context, state), // 搜索框 TextField( controller: _searchController, decoration: InputDecoration( hintText: '搜索好友或输入手机号...', prefixIcon: Icon(Icons.search), ), onChanged: (value) => _cubit.searchUsers(value), ), // 已选用户 Chips if (state.selectedUsers.isNotEmpty) ...[ Wrap( spacing: 8, children: state.selectedUsers.map((user) { return Chip( label: Text('${user.username} (${_getPermissionSummary(user)})'), onDeleted: () => _cubit.removeUser(user.id), ); }).toList(), ), ], // 用户列表 Flexible( child: _buildUserList(state), ), // 发送按钮 AppButton( text: '发送邀请 (${state.selectedUsers.length}人)', onPressed: state.selectedUsers.isEmpty ? null : () => _handleShare(context, state), ), ], ); }, ), ); } Widget _buildUserList(CalendarShareState state) { // 如果有搜索结果,显示搜索结果;否则显示好友列表 final users = state.searchQuery.isNotEmpty ? state.searchResults : state.friends.map((f) => f.friend).toList(); return ListView.builder( shrinkWrap: true, itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; final selectedUser = state.selectedUsers.cast().firstWhere( (s) => s?.id == user.id, orElse: () => null, ); final isSelected = selectedUser != null; return UserSelectItem( user: user, isSelected: isSelected, showPermissions: isSelected, selectedUser: selectedUser, onToggle: (selected) { if (selected) { _cubit.addUser(user, state.defaultPermissionView, state.defaultPermissionEdit, state.defaultPermissionInvite); } else { _cubit.removeUser(user.id); } }, onPermissionChanged: (type, value) { _cubit.updateUserPermission(user.id, type: type, value: value); }, ); }, ); } } ``` - [ ] **Step 3: 移除顶部栏,简化 CalendarEventShareScreen** ```dart // apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart class CalendarEventShareScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ // 直接显示分享对话框,不需要额外顶部栏 Expanded( child: CalendarShareDialog( eventId: eventId, eventTitle: event.title, canInvite: event.canInvite, canEdit: event.canEdit, ), ), ], ), ), ); } } ``` - [ ] **Step 4: 验证编译** ```bash cd apps && flutter analyze lib/features/calendar/presentation/widgets/calendar_share_dialog.dart ``` --- ## Task 5: 集成测试 **Files:** - Create: `apps/test/features/calendar/presentation/widgets/calendar_share_dialog_test.dart` - [ ] **Step 1: 编写 Widget 测试** ```dart void main() { testWidgets('选择用户后显示权限开关', (tester) async { // Arrange // Act // Assert }); testWidgets('已选用户显示在 Chips 中', (tester) async { // Arrange // Act // Assert }); } ``` - [ ] **Step 2: 运行测试** ```bash cd apps && flutter test test/features/calendar/presentation/widgets/calendar_share_dialog_test.dart ``` --- ## 依赖关系 ``` Task 1 (后端批量接口) ↓ Task 2 (前端批量分享 API) ↓ Task 3 (分享状态管理 Cubit) ↓ Task 4 (分享对话框 UI) ↓ Task 5 (集成测试) ``` --- ## 风险与注意事项 1. **后端兼容性**: 批量接口上线前,前端使用原有单用户接口 2. **搜索防抖**: 搜索用户时需要 debounce 避免频繁请求 3. **好友列表分页**: 如果好友数量大,需要分页加载 4. **权限验证**: 前端权限不能超过当前用户的权限(由后端控制)