215 lines
5.7 KiB
Markdown
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`
|
|
- [ ] 手动测试分享流程
|