Files
social-app/docs/plans/2026-03-30-calendar-detail-show-subscribers.md
T

215 lines
5.7 KiB
Markdown

# Calendar Detail - Show Subscribers Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 在日历详情页显示已订阅此日历的用户列表
**Architecture:**
- 后端:在 `GET /api/v1/schedule-items/{id}` 响应中返回 `subscribers` 列表
- 前端:在详情页渲染订阅者列表组件
**Tech Stack:** Flutter, FastAPI, PostgreSQL
---
## Task 1: 后端 - 返回订阅者列表
**Files:**
- Modify: `backend/src/v1/schedule_items/schemas.py`
- Modify: `backend/src/v1/schedule_items/service.py`
- Verify: `backend/tests/integration/v1/test_schedule_items_routes.py`
- [ ] **Step 1: 新增 SubscriberInfo Schema**
```python
# backend/src/v1/schedule_items/schemas.py
class SubscriberInfo(BaseModel):
"""订阅者信息"""
user_id: UUID
username: str
avatar_url: str | None = None
permission: int # 位标志: 1=view, 2=invite, 4=edit
status: str # active, pending, paused, unsubscribed
subscribed_at: datetime
```
- [ ] **Step 2: 修改 ScheduleItemResponse**
```python
class ScheduleItemResponse(BaseModel):
# ... existing fields ...
subscribers: List[SubscriberInfo] = [] # 新增
```
- [ ] **Step 3: 修改 service.get_by_id 填充 subscribers**
```python
# backend/src/v1/schedule_items/service.py
async def get_by_id(self, item_id: UUID) -> ScheduleItemResponse:
item = await self._repository.get_by_id(item_id)
if not item:
raise ScheduleItemNotFoundError(item_id)
# 获取订阅者列表
subscriptions = await self._repository.get_subscriptions_by_item_id(item_id)
subscribers = []
for sub in subscriptions:
if sub.status == 'active': # 只返回活跃订阅者
user = await self._user_repo.get_by_id(sub.subscriber_id)
if user:
subscribers.append(SubscriberInfo(
user_id=user.id,
username=user.username,
avatar_url=user.avatar_url,
permission=sub.permission,
status=sub.status,
subscribed_at=sub.created_at,
))
return ScheduleItemResponse(
# ... existing fields ...,
subscribers=subscribers,
)
```
- [ ] **Step 4: 编写集成测试**
```python
# backend/tests/integration/v1/test_schedule_items_routes.py
async def test_get_item_returns_subscribers(self):
"""测试获取日程时返回订阅者列表"""
# Arrange: 创建日程,添加订阅者
# Act: GET /api/v1/schedule-items/{id}
# Assert: 响应包含 subscribers 字段
```
---
## Task 2: 前端 - Model 更新
**Files:**
- Modify: `apps/lib/features/calendar/data/models/schedule_item_model.dart`
- [ ] **Step 1: 新增 Subscriber 模型**
```dart
class Subscriber {
final String userId;
final String username;
final String? avatarUrl;
final int permission;
final String status;
final DateTime subscribedAt;
bool get canView => (permission & 1) != 0;
bool get canEdit => (permission & 4) != 0;
bool get canInvite => (permission & 2) != 0;
}
```
- [ ] **Step 2: 在 ScheduleItemModel 中添加 subscribers 字段**
```dart
class ScheduleItemModel {
// ... existing fields ...
final List<Subscriber> subscribers;
}
```
- [ ] **Step 3: 更新 fromJson**
```dart
factory ScheduleItemModel.fromJson(Map<String, dynamic> json) {
return ScheduleItemModel(
// ... existing fields ...,
subscribers: (json['subscribers'] as List<dynamic>?)
?.map((s) => Subscriber.fromJson(s as Map<String, dynamic>))
.toList() ?? [],
);
}
```
---
## Task 3: 前端 - 详情页渲染订阅者
**Files:**
- Modify: `apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart`
- [ ] **Step 1: 在 _buildMetaSurface 或新方法中渲染订阅者**
```dart
Widget _buildSubscribersSurface(ScheduleItemModel event) {
if (event.subscribers.isEmpty) {
return const SizedBox.shrink();
}
return Container(
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: _colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: _colorScheme.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.calendarDetailSubscribers,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: _colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.md),
...event.subscribers.map((sub) => _buildSubscriberRow(sub)),
],
),
);
}
Widget _buildSubscriberRow(Subscriber subscriber) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
child: Row(
children: [
Avatar(url: subscriber.avatarUrl, name: subscriber.username),
const SizedBox(width: AppSpacing.sm),
Expanded(child: Text(subscriber.username)),
if (subscriber.canEdit) Icon(Icons.edit, size: 16),
if (subscriber.canInvite) Icon(Icons.person_add, size: 16),
],
),
);
}
```
- [ ] **Step 2: 在 build 方法中添加**
```dart
// 在 _buildExtraSurface 之后添加
if (event.subscribers.isNotEmpty) [
const SizedBox(height: AppSpacing.md),
_buildSubscribersSurface(event),
],
```
- [ ] **Step 3: 添加 l10n 文本**
```arb
// apps/lib/l10n/app_zh.arb
"calendarDetailSubscribers": "已订阅 ({count}人)"
```
---
## Task 4: 验证
- [ ] 运行后端测试: `cd backend && uv run pytest -v -k "subscriber"`
- [ ] 运行前端分析: `cd apps && flutter analyze`
- [ ] 手动测试分享流程