feat(settings): 收藏列表 + 最近呼叫全量实现(#42~#45)

## 收藏(Gitea #42~#45)
- `FetchFavoritesRequest` / `DeleteFavoriteRequest`:ApiRequestable,对齐 iOS FavouriteService
- `FetchFavoritesUseCase`:GET 分页拉取 → upsert FavoriteRepository
- `DeleteFavoriteUseCase`:POST delete → 同步删本地 DB
- `FavoritesViewModel`:分页/刷新/加载更多/删除,DB Stream 驱动
- `FavoritesPage`:列表 + RefreshIndicator + Dismissible 左滑删除 + 类型图标 + 空状态
- `AppRouteName.settingsFavorites` + 路由注册 + auth guard
- `settings_page.dart` 收藏行 onTap 接入导航

## 最近呼叫(框架,API 对接待续)
- `CallLogRequest` / `FetchCallLogsUseCase` / `RecentCallsViewModel`
- `RecentCallsPage`:双 Tab(全部/未接)+ _CallLogTile(图标/时长/时间)
- `AppRouteName.settingsRecentCalls` + 路由注册

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
pp-bot
2026-03-24 20:30:56 +09:00
parent db10d1fcd2
commit 8744e2c0b7
16 changed files with 1389 additions and 2 deletions

View File

@@ -0,0 +1,142 @@
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/core/foundation/api_paths.dart';
import 'package:im_app/domain/entities/call_log.dart';
// ── 通话记录 DTO ──────────────────────────────────────────────────────────────
/// 服务端单条通话记录 DTO
///
/// 字段对齐 `im-client-im-dev` Call.fromJson
/// - `rtc_channel_id` → [id]主键String
/// - `inviter_id` / `caller_id` → [callerId]
/// - `video_call` → [videoCall]0=语音, 1=视频)
/// - `status` = 3/4/5/6 → 未接来电(配合客户端侧判断)
class CallLogDto {
final String id;
final int? callerId;
final int? receiverId;
final int? chatId;
final int? duration;
final int? videoCall;
final int? createdAt;
final int? updatedAt;
final int? endedAt;
final int? status;
final int? isDeleted;
final int? deletedAt;
final int? isRead;
const CallLogDto({
required this.id,
this.callerId,
this.receiverId,
this.chatId,
this.duration,
this.videoCall,
this.createdAt,
this.updatedAt,
this.endedAt,
this.status,
this.isDeleted,
this.deletedAt,
this.isRead,
});
factory CallLogDto.fromJson(Map<String, dynamic> json) => CallLogDto(
id: (json['rtc_channel_id'] ?? json['channel_id'] ?? json['id'] ?? '')
.toString(),
callerId:
(json['inviter_id'] ?? json['caller_id'] as num?)?.toInt(),
receiverId: (json['receiver_id'] as num?)?.toInt(),
chatId: (json['chat_id'] as num?)?.toInt(),
duration: (json['duration'] as num?)?.toInt(),
videoCall: (json['video_call'] as num?)?.toInt(),
createdAt: (json['created_at'] as num?)?.toInt(),
updatedAt: (json['updated_at'] as num?)?.toInt(),
endedAt: (json['ended_at'] as num?)?.toInt(),
status: (json['status'] as num?)?.toInt(),
isDeleted: (json['is_deleted'] as num?)?.toInt(),
deletedAt: (json['deleted_at'] as num?)?.toInt(),
isRead: (json['is_read'] as num?)?.toInt(),
);
CallLog toEntity() => CallLog(
id: id,
callerId: callerId,
receiverId: receiverId,
chatId: chatId,
duration: duration,
videoCall: videoCall,
createdAt: createdAt,
updatedAt: updatedAt,
endedAt: endedAt,
status: status,
isDeleted: isDeleted,
deletedAt: deletedAt,
isRead: isRead,
);
}
// ── FetchCallLogsResponse ────────────────────────────────────────────────────
class FetchCallLogsResponse {
final List<CallLogDto> records;
const FetchCallLogsResponse({required this.records});
factory FetchCallLogsResponse.fromJson(Map<String, dynamic> json) {
final list = json['list'] ?? json['records'] ?? json['data'] ?? [];
return FetchCallLogsResponse(
records: (list as List)
.map((item) => CallLogDto.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
}
// ── FetchCallLogsRequest ─────────────────────────────────────────────────────
/// POST /app/api/call/records — 拉取通话记录
///
/// [startFrom] Unix 时间戳增量拉取用0 表示全量。
/// [status] -1 = 全部状态。
class FetchCallLogsRequest
extends ApiRequestable<FetchCallLogsResponse> {
final int startFrom;
final int status;
const FetchCallLogsRequest({
this.startFrom = 0,
this.status = -1,
});
@override
String get path => ApiPaths.callRecords;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> get parameters => {
'start_from': startFrom,
'status': status,
};
@override
FetchCallLogsResponse? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data == null) return FetchCallLogsResponse(records: []);
if (data is List) {
return FetchCallLogsResponse(
records: data
.map((item) => CallLogDto.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
if (data is Map<String, dynamic>) {
return FetchCallLogsResponse.fromJson(data);
}
return FetchCallLogsResponse(records: []);
}
}