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
@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import '../../../features/notifications/data/models/notification_item.dart';
import '../../theme/design_tokens.dart';
class NotificationDetailBottomSheet extends StatefulWidget {
const NotificationDetailBottomSheet({
super.key,
required this.item,
required this.onMarkRead,
});
final NotificationItem item;
final Future<void> Function() onMarkRead;
@override
State<NotificationDetailBottomSheet> createState() =>
_NotificationDetailBottomSheetState();
}
class _NotificationDetailBottomSheetState
extends State<NotificationDetailBottomSheet> {
@override
void initState() {
super.initState();
widget.onMarkRead();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Container(
height: MediaQuery.of(context).size.height * 0.5,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppRadius.lg),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
margin: const EdgeInsets.only(top: AppSpacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Row(
children: [
Expanded(
child: Text(
widget.item.title,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.close, color: colors.onSurfaceVariant),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Text(
_formatTime(widget.item.createdAt),
style: textTheme.labelSmall?.copyWith(color: colors.outline),
),
),
const SizedBox(height: AppSpacing.md),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Text(
widget.item.body,
style: textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
height: 1.6,
),
),
),
),
],
),
);
}
String _formatTime(DateTime dt) {
final now = DateTime.now();
final diff = now.difference(dt);
if (diff.inMinutes < 1) return '刚刚';
if (diff.inHours < 1) return '${diff.inMinutes}分钟前';
if (diff.inDays < 1) return '${diff.inHours}小时前';
if (diff.inDays < 30) return '${diff.inDays}天前';
return '${dt.month}/${dt.day}';
}
}
Future<void> showNotificationDetailBottomSheet({
required BuildContext context,
required NotificationItem item,
required Future<void> Function() onMarkRead,
}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
NotificationDetailBottomSheet(item: item, onMarkRead: onMarkRead),
);
}