notification_detail_screen.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import 'package:flutter/material.dart';
  2. import '../../../core/l10n/app_localizations.dart';
  3. import '../../../core/theme/app_colors.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
  6. import 'package:go_router/go_router.dart';
  7. import '../../../core/network/dio_client.dart';
  8. import '../../../data/models/announcement/announcement.dart';
  9. import '../../../data/services/announcement_service.dart';
  10. import '../../../providers/announcement_unread_provider.dart';
  11. /// 公告详情 Provider(按 id 异步加载)
  12. final _announcementDetailProvider =
  13. FutureProvider.autoDispose.family<WebInfoContent?, String>((ref, id) {
  14. final dio = ref.read(dioClientProvider);
  15. return AnnouncementService(dio).getAnnouncementDetail(id);
  16. });
  17. class NotificationDetailScreen extends ConsumerStatefulWidget {
  18. const NotificationDetailScreen({super.key, required this.id});
  19. final String id;
  20. @override
  21. ConsumerState<NotificationDetailScreen> createState() =>
  22. _NotificationDetailScreenState();
  23. }
  24. class _NotificationDetailScreenState
  25. extends ConsumerState<NotificationDetailScreen> {
  26. @override
  27. void initState() {
  28. super.initState();
  29. WidgetsBinding.instance.addPostFrameCallback((_) {
  30. final id = int.tryParse(widget.id);
  31. if (id != null) markAnnouncementsRead(ref, id: id);
  32. });
  33. }
  34. @override
  35. Widget build(BuildContext context) {
  36. final cs = Theme.of(context).colorScheme;
  37. final isDark = Theme.of(context).brightness == Brightness.dark;
  38. final asyncDetail = ref.watch(_announcementDetailProvider(widget.id));
  39. return Scaffold(
  40. appBar: AppBar(
  41. elevation: 0,
  42. leading: IconButton(
  43. icon: const Icon(Icons.chevron_left, size: 28),
  44. onPressed: () => context.pop(),
  45. ),
  46. title: Text(
  47. AppLocalizations.of(context)!.announcementDetail,
  48. style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
  49. ),
  50. centerTitle: true,
  51. ),
  52. body: RefreshIndicator(
  53. onRefresh: () => ref.refresh(_announcementDetailProvider(widget.id).future),
  54. child: asyncDetail.when(
  55. loading: () => SingleChildScrollView(
  56. physics: const AlwaysScrollableScrollPhysics(),
  57. child: SizedBox(
  58. height: MediaQuery.of(context).size.height - 100,
  59. child: const Center(child: CircularProgressIndicator()),
  60. ),
  61. ),
  62. error: (e, _) => SingleChildScrollView(
  63. physics: const AlwaysScrollableScrollPhysics(),
  64. child: SizedBox(
  65. height: MediaQuery.of(context).size.height - 100,
  66. child: Center(
  67. child: Text(AppLocalizations.of(context)!.loadFailed,
  68. style: TextStyle(color: cs.onSurface.withAlpha(153))),
  69. ),
  70. ),
  71. ),
  72. data: (detail) {
  73. if (detail == null) {
  74. return SingleChildScrollView(
  75. physics: const AlwaysScrollableScrollPhysics(),
  76. child: SizedBox(
  77. height: MediaQuery.of(context).size.height - 100,
  78. child: Center(
  79. child: Text(AppLocalizations.of(context)!.announcementNotFound,
  80. style: TextStyle(color: cs.onSurface.withAlpha(153))),
  81. ),
  82. ),
  83. );
  84. }
  85. return SingleChildScrollView(
  86. physics: const AlwaysScrollableScrollPhysics(),
  87. padding: const EdgeInsets.all(16),
  88. child: Column(
  89. crossAxisAlignment: CrossAxisAlignment.start,
  90. children: [
  91. // 标题卡片
  92. Container(
  93. width: double.infinity,
  94. padding: const EdgeInsets.all(16),
  95. decoration: BoxDecoration(
  96. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  97. borderRadius: BorderRadius.circular(10),
  98. ),
  99. child: Column(
  100. crossAxisAlignment: CrossAxisAlignment.start,
  101. children: [
  102. Text(
  103. detail.title,
  104. style: TextStyle(
  105. color: cs.onSurface,
  106. fontSize: 15,
  107. fontWeight: FontWeight.w600,
  108. ),
  109. ),
  110. const SizedBox(height: 6),
  111. Text(
  112. detail.createTime,
  113. style: TextStyle(
  114. color: cs.onSurface.withAlpha(153),
  115. fontSize: 12,
  116. ),
  117. ),
  118. ],
  119. ),
  120. ),
  121. const SizedBox(height: 16),
  122. // HTML 正文
  123. HtmlWidget(
  124. detail.content,
  125. textStyle: TextStyle(
  126. color: cs.onSurface,
  127. fontSize: 14,
  128. height: 1.7,
  129. ),
  130. ),
  131. ],
  132. ),
  133. );
  134. },
  135. ),
  136. ),
  137. );
  138. }
  139. }