5.7 KiB
5.7 KiB
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
# 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
class ScheduleItemResponse(BaseModel):
# ... existing fields ...
subscribers: List[SubscriberInfo] = [] # 新增
- Step 3: 修改 service.get_by_id 填充 subscribers
# 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: 编写集成测试
# 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 模型
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 字段
class ScheduleItemModel {
// ... existing fields ...
final List<Subscriber> subscribers;
}
- Step 3: 更新 fromJson
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 或新方法中渲染订阅者
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 方法中添加
// 在 _buildExtraSurface 之后添加
if (event.subscribers.isNotEmpty) [
const SizedBox(height: AppSpacing.md),
_buildSubscribersSurface(event),
],
- Step 3: 添加 l10n 文本
// apps/lib/l10n/app_zh.arb
"calendarDetailSubscribers": "已订阅 ({count}人)"
Task 4: 验证
- 运行后端测试:
cd backend && uv run pytest -v -k "subscriber" - 运行前端分析:
cd apps && flutter analyze - 手动测试分享流程