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:
142
apps/im_app/lib/data/remote/call_log_request.dart
Normal file
142
apps/im_app/lib/data/remote/call_log_request.dart
Normal 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: []);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user