feat: integrate invite API and improve notification handling

- Add invite code display and binding functionality via API
- Fix notification unread count sync on auth state change
- Improve notification mark read with server state validation
- Add auth state listener to trigger notification refresh
- Add YaoCoinConverter for coin-to-yao type conversion
- Remove YaoLegend from divination screens (UI cleanup)
- Abbreviate relation labels in yao detail view
- Add re-register notice to account delete screen
- Update 'coins' terminology to 'points' in localization
- Fix backend points consumption to only run in CHAT mode
- Add HttpxAuthNoiseFilter to suppress auth endpoint logging
- Fix notification static_schema import path
- Add test coverage for notification bloc error handling
- Update AGENTS.md page header rules and image handling
- Delete deprecated run-dev.sh script
This commit is contained in:
qzl
2026-04-13 14:52:22 +08:00
parent da947f9f08
commit 1e22f27de2
52 changed files with 1419 additions and 307 deletions
@@ -1,6 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/notification/notification_detail_bottom_sheet.dart';
import '../../data/models/notification_item.dart';
import '../../data/models/notification_payload.dart';
import '../../data/repositories/notification_repository.dart';
@@ -13,12 +16,14 @@ class NotificationCenterScreen extends StatefulWidget {
required this.repository,
this.onNavigateToRoute,
this.onOpenUrl,
this.onUnreadCountChanged,
});
final NotificationRepository repository;
final void Function(String route, {String? entityId, String? tab})?
onNavigateToRoute;
final void Function(String url)? onOpenUrl;
final Future<void> Function()? onUnreadCountChanged;
@override
State<NotificationCenterScreen> createState() =>
@@ -55,6 +60,7 @@ class _NotificationCenterScreenState extends State<NotificationCenterScreen> {
return Scaffold(
appBar: AppBar(
title: const Text('通知'),
centerTitle: true,
actions: [
if (state.items.any((item) => !item.isRead))
TextButton(
@@ -136,15 +142,32 @@ class _NotificationCenterScreenState extends State<NotificationCenterScreen> {
final item = state.items[index];
return NotificationListItem(
item: item,
onTap: () => _handleNotificationTap(item),
onTap: () => _handleNotificationTap(context, item),
);
},
);
}
void _handleNotificationTap(NotificationItem item) {
Future<void> _handleNotificationTap(
BuildContext context,
NotificationItem item,
) async {
final wasUnread = !item.isRead;
if (!item.isRead) {
_bloc.handleEvent(MarkNotificationRead(notificationId: item.id));
await _bloc.handleEvent(MarkNotificationRead(notificationId: item.id));
final updatedIndex = _bloc.state.items.indexWhere((n) => n.id == item.id);
if (wasUnread &&
updatedIndex >= 0 &&
_bloc.state.items[updatedIndex].isRead) {
await widget.onUnreadCountChanged?.call();
}
}
if (context.mounted) {
await showNotificationDetailBottomSheet(
context: context,
item: item,
onMarkRead: () async {},
);
}
_executePayload(item.payload);
}
@@ -161,6 +184,15 @@ class _NotificationCenterScreenState extends State<NotificationCenterScreen> {
}
void _onMarkAllRead() {
_bloc.handleEvent(MarkAllNotificationsRead());
unawaited(_markAllRead());
}
Future<void> _markAllRead() async {
final unreadBefore = _bloc.state.items.any((item) => !item.isRead);
await _bloc.handleEvent(MarkAllNotificationsRead());
final unreadAfter = _bloc.state.items.any((item) => !item.isRead);
if (unreadBefore && !unreadAfter) {
await widget.onUnreadCountChanged?.call();
}
}
}