feat: 添加消息中心及顶栏未读角标功能

新增消息中心页面,实现未读消息轮询与展示
为各 Tab 页面顶栏添加通知图标及未读数角标
新增通知 API 接口与未读数状态管理
实现消息标记已读、全部已读及下拉刷新功能
This commit is contained in:
andyesfly committed 2026-10-06 13:16:55 +08:00
1 parent 66013ee069
commit 7ad604ff8b
9 files changed
+575 -5

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/api_client.dart';
import '../../../core/network/api_response.dart';
import '../../../core/providers.dart';
import '../../../core/utils/id_util.dart';
final noticeApiProvider = Provider<NoticeApi>(
(ref) => NoticeApi(ref.watch(apiClientProvider)),
);
/// 站内通知,与后端 `sys_notice` / `/api/notices` 契约对齐。
///
/// 管理端接口固定 `receiver_type='user'`,字段序列化为驼峰(主键 BIGINT 为字符串)。
class Notice {
const Notice({
required this.id,
required this.noticeType,
required this.title,
required this.content,
required this.bizType,
required this.bizId,
required this.isRead,
required this.receiverType,
required this.ruleId,
required this.batchId,
required this.createdAt,
});
final int id;
final String noticeType;
final String title;
final String content;
final String bizType;
final int? bizId;
/// 0 未读 / 1 已读
final int isRead;
final String receiverType;
final int? ruleId;
final int? batchId;
final String? createdAt;
bool get unread => isRead == 0;
factory Notice.fromJson(Map<String, dynamic> json) => Notice(
id: toIntOr(json['id'], 0),
noticeType: json['noticeType'] as String? ?? '',
title: json['title'] as String? ?? '',
content: json['content'] as String? ?? '',
bizType: json['bizType'] as String? ?? '',
bizId: toIntOrNull(json['bizId']),
isRead: toIntOr(json['isRead'], 0),
receiverType: json['receiverType'] as String? ?? 'user',
ruleId: toIntOrNull(json['ruleId']),
batchId: toIntOrNull(json['batchId']),
createdAt: (json['created_at'] ?? json['createdAt']) as String?,
);
Notice copyWith({int? isRead}) => Notice(
id: id,
noticeType: noticeType,
title: title,
content: content,
bizType: bizType,
bizId: bizId,
isRead: isRead ?? this.isRead,
receiverType: receiverType,
ruleId: ruleId,
batchId: batchId,
createdAt: createdAt,
);
}
/// `/notices` 分页结果。
class NoticePage {
const NoticePage({
required this.list,
required this.total,
required this.page,
required this.pageSize,
});
final List<Notice> list;
final int total;
final int page;
final int pageSize;
factory NoticePage.fromJson(Map<String, dynamic> json, int fallbackPageSize) =>
NoticePage(
list: (json['list'] as List? ?? [])
.map((e) => Notice.fromJson((e as Map).cast<String, dynamic>()))
.toList(),
total: toIntOr(json['total'], 0),
page: toIntOr(json['page'], 1),
pageSize: toIntOr(json['pageSize'], fallbackPageSize),
);
}
/// 站内消息接口,对应后端 `notice.controller.ts`(管理端)。
class NoticeApi {
NoticeApi(this._client);
final ApiClient _client;
Future<ApiResponse<NoticePage>> list({
int page = 1,
int pageSize = 15,
int? isRead,
}) => _client.get<NoticePage>(
'/notices',
query: {'page': page, 'pageSize': pageSize, 'isRead': isRead},
parse: (d) =>
NoticePage.fromJson((d as Map).cast<String, dynamic>(), pageSize),
);
/// 未读数量(顶栏红点)。
Future<ApiResponse<int>> unreadCount() => _client.get<int>(
'/notices/unread-count',
parse: (d) => toIntOr((d as Map)['count'], 0),
);
/// 单条标记已读。
Future<ApiResponse<void>> markRead(int id) =>
_client.put<void>('/notices/$id/read');
/// 全部标记已读,返回本次更新条数。
Future<ApiResponse<int>> markAllRead() => _client.put<int>(
'/notices/read-all',
parse: (d) => toIntOr((d as Map)['updated'], 0),
);
}