Files
social-app/docs/plans/2026-03-29-calendar-share-redesign.md
T

557 lines
16 KiB
Markdown

# 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<void> share(...) { ... }
// 新增批量分享
Future<ShareBatchResponse> shareBatch({
required String eventId,
required List<ShareTarget> 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<String, dynamic> 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<ShareFailure> 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<FriendResponse> friends;
final List<UserProfile> searchResults;
final List<SelectedUser> 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<CalendarShareState> {
final FriendsApi _friendsApi;
final UsersApi _usersApi;
final CalendarApi _calendarApi;
CalendarShareCubit({
required FriendsApi friendsApi,
required UsersApi usersApi,
required CalendarApi calendarApi,
}) : super(CalendarShareState.initial());
// 加载好友列表
Future<void> loadFriends() async { ... }
// 搜索用户
Future<void> 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<void> 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<bool> 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<CalendarShareDialog> createState() => _CalendarShareDialogState();
}
class _CalendarShareDialogState extends State<CalendarShareDialog> {
late final CalendarShareCubit _cubit;
final _searchController = TextEditingController();
@override
void initState() {
super.initState();
_cubit = context.read<CalendarShareCubit>();
_cubit.loadFriends();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cubit,
child: BlocBuilder<CalendarShareCubit, CalendarShareState>(
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<SelectedUser?>().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. **权限验证**: 前端权限不能超过当前用户的权限(由后端控制)