# 最近呼叫(Recent Calls)— 架构文档 > 对应 Gitea issues #46-#48(内部编号 #42-#44) > 参考实现:`im-client-im-dev` lib/views/call_log/ + lib/managers/call_log_mgr.dart --- ## 1. 功能范围 | Issue | 功能 | 状态 | |-------|------|------| | #42 | 最近呼叫列表页 UI(全部/未接 Tab + CallLogTile) | ✅ 已实现 | | #43 | 通话记录 API 接入(FetchCallLogsRequest + UseCase) | ✅ 已实现 | | #44 | RecentCallsViewModel + 路由接入 | ✅ 已实现 | --- ## 2. 目录结构 ``` features/settings/ ├── di/ │ └── settings_providers.dart # 新增 fetchCallLogsUseCaseProvider ├── presentation/ │ └── recent_calls_view_model.dart # RecentCallsViewModel + RecentCallsState ├── usecases/ │ └── fetch_call_logs_usecase.dart # POST /app/api/call/records → DB └── view/ └── recent_calls_page.dart # RecentCallsPage + TabBar + _CallLogTile data/remote/ └── call_log_request.dart # FetchCallLogsRequest + CallLogDto app/router/ ├── app_route_name.dart # 新增 settingsRecentCalls ├── app_router.dart # 新增 GoRoute └── guards/auth_guard.dart # 新增 case settingsRecentCalls ``` --- ## 3. 数据流 ``` RecentCallsPage (build) └─ ref.watch(allCallLogsProvider) ← DB Stream(实时) └─ RecentCallsViewModel.loadCallLogs() └─ FetchCallLogsUseCase.execute() └─ POST /app/api/call/records (start_from=0, status=-1) ├─ 响应 List → CallLog Domain └─ CallLogRepository.insertOrReplaceCallLogs() └─ Drift DB → allCallLogsProvider 自动刷新 → UI 进入页面: └─ RecentCallsViewModel.markAllRead() └─ CallLogRepository.markAllAsRead() ``` --- ## 4. API 端点 | 操作 | Method | Path | 参数 | |------|--------|------|------| | 拉取通话记录 | POST | `/app/api/call/records` | `start_from` (int, 时间戳), `status` (-1=全部) | ### 响应字段映射 | 服务端字段 | Dart 字段 | 类型 | |-----------|-----------|------| | `rtc_channel_id` | `id` (String) | String | | `inviter_id` / `caller_id` | `callerId` | int? | | `receiver_id` | `receiverId` | int? | | `chat_id` | `chatId` | int? | | `duration` | `duration` | int? (秒) | | `video_call` | `videoCall` | int? (0=语音, 1=视频) | | `created_at` | `createdAt` | int? (秒级时间戳) | | `updated_at` | `updatedAt` | int? | | `ended_at` | `endedAt` | int? | | `status` | `status` | int? | | `is_read` | `isRead` | int? (0/1) | --- ## 5. 通话状态枚举 ```dart // status 值含义(参考 im-client-im-dev CallEvent) // 0 = Ringing/Pending // 1 = Connected // 2 = Ended(正常结束) // 3 = Busy(CallOptBusy) // 4 = Cancelled(CallOptCancel) // 5 = Timeout(CallTimeOut) // 6 = Declined(CallBusy) // 未接来电:callerId != currentUid AND status in {3, 4, 5, 6} ``` --- ## 6. ViewModel 设计 ```dart class RecentCallsState { final int tabIndex; // 0=全部, 1=未接来电 final bool isLoading; final String? error; } class RecentCallsViewModel extends Notifier { void setTab(int index) // 切换 Tab Future loadCallLogs() // 拉取 + 写入 DB Future markAllRead() // 标记已读 } ``` --- ## 7. UI 结构 ### RecentCallsPage ``` Scaffold AppBar: 「最近呼叫」 Column TabBar(全部 / 未接来电) Expanded TabBarView AllCallsTab → allCallLogsProvider 全部 MissedTab → 按 isMissed 过滤 ``` ### _CallLogTile ``` ListTile leading: _CallTypeIcon(电话/摄像头/红色, 40pt 圆形) title: 对方 UID 显示("@J{uid}") subtitle: 通话类型文字 + 时长 trailing: 相对时间 ``` **未接来电判断**:`callerId != currentUid` AND `status in {3,4,5,6}` --- ## 8. 路由 ``` /settings/recent-calls → RecentCallsPage(parentNavigatorKey=_rootKey) ``` `auth_guard.dart` switch 已补充 `settingsRecentCalls` case。 --- ## 9. 设计决策 - **无用户名映射**:当前 CallLog 实体只有 UID,无昵称。显示 `@J{uid}` 占位, 后续可接入联系人 Repository 做名称查询。 - **增量拉取策略**:当前实现 `start_from=0`(全量),首次打开可能慢; 后续可从本地最新 `updatedAt` 做增量同步(参考 CallLogMgr.loadRemoteCallLog)。 - **离线可用**:数据先写 DB,UI 监听 `allCallLogsProvider`(DB Stream), 无网络时仍能展示缓存记录。