announcement_detail_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import '../../core/theme/app_colors.dart';
  6. import '../shell/nav_bar_config.dart';
  7. import '../../core/utils/date_utils.dart' as du;
  8. import '../../core/i18n/app_localizations.dart';
  9. import 'announcement_list_controller.dart';
  10. import 'announcement_model.dart';
  11. class AnnouncementDetailPage extends ConsumerStatefulWidget {
  12. final String id;
  13. const AnnouncementDetailPage({super.key, required this.id});
  14. @override
  15. ConsumerState<AnnouncementDetailPage> createState() =>
  16. _AnnouncementDetailPageState();
  17. }
  18. class _AnnouncementDetailPageState
  19. extends ConsumerState<AnnouncementDetailPage> {
  20. late AnnouncementModel _item;
  21. final bool _isAdmin = true;
  22. @override
  23. void initState() {
  24. super.initState();
  25. _item = mockAnnouncements.firstWhere(
  26. (e) => e.id == widget.id,
  27. orElse: () => mockAnnouncements.first,
  28. );
  29. // 停留 ≥2s 自动标记已读
  30. Timer(const Duration(seconds: 2), () {
  31. if (mounted) {
  32. ScaffoldMessenger.of(context).showSnackBar(
  33. const SnackBar(
  34. content: Text('已标记为已读'),
  35. duration: Duration(seconds: 1),
  36. ),
  37. );
  38. }
  39. });
  40. }
  41. @override
  42. Widget build(BuildContext context) {
  43. final l10n = AppLocalizations.of(context);
  44. ref
  45. .read(navBarConfigProvider.notifier)
  46. .update(
  47. NavBarConfig(
  48. title: l10n.get('announcementDetail'),
  49. showBack: true,
  50. onBack: () => context.pop(),
  51. ),
  52. );
  53. _item = mockAnnouncements.firstWhere(
  54. (e) => e.id == widget.id,
  55. orElse: () => mockAnnouncements.first,
  56. );
  57. return SingleChildScrollView(
  58. child: Column(
  59. crossAxisAlignment: CrossAxisAlignment.start,
  60. children: [
  61. // 已过期红色横幅
  62. if (_item.isExpired)
  63. Container(
  64. width: double.infinity,
  65. padding: const EdgeInsets.symmetric(
  66. horizontal: 16, vertical: 10),
  67. color: AppColors.danger,
  68. child: Text(
  69. '该公告已于 ${du.DateUtils.formatDateTime(_item.expiryDate!)} 过期',
  70. style: const TextStyle(
  71. fontSize: 13, color: Colors.white),
  72. textAlign: TextAlign.center,
  73. ),
  74. ),
  75. // 红头文件样式标题区
  76. Container(
  77. width: double.infinity,
  78. padding: const EdgeInsets.all(24),
  79. color: AppColors.bgCard,
  80. child: Column(
  81. crossAxisAlignment: CrossAxisAlignment.start,
  82. children: [
  83. // 红色上划线(红头文件风格)
  84. Container(
  85. width: 60,
  86. height: 4,
  87. color: AppColors.danger,
  88. ),
  89. const SizedBox(height: 16),
  90. Text(
  91. _item.title,
  92. style: const TextStyle(
  93. fontSize: 20,
  94. fontWeight: FontWeight.w700,
  95. color: AppColors.textPrimary,
  96. height: 1.4,
  97. ),
  98. ),
  99. const SizedBox(height: 12),
  100. Row(
  101. children: [
  102. _buildTypeTag(_item.typeLabel),
  103. const SizedBox(width: 12),
  104. Text(_item.publisherName,
  105. style: const TextStyle(
  106. fontSize: 13,
  107. color: AppColors.textSecondary)),
  108. const SizedBox(width: 12),
  109. Text(
  110. du.DateUtils.formatDateTime(_item.publishTime),
  111. style: const TextStyle(
  112. fontSize: 13,
  113. color: AppColors.textPlaceholder)),
  114. ],
  115. ),
  116. ],
  117. ),
  118. ),
  119. Container(
  120. width: double.infinity,
  121. height: 2,
  122. color: AppColors.danger),
  123. // 正文内容
  124. Padding(
  125. padding: const EdgeInsets.all(16),
  126. child: Container(
  127. width: double.infinity,
  128. padding: const EdgeInsets.all(20),
  129. decoration: BoxDecoration(
  130. color: AppColors.bgCard,
  131. borderRadius: BorderRadius.circular(8),
  132. ),
  133. child: Column(
  134. crossAxisAlignment: CrossAxisAlignment.start,
  135. children: [
  136. const Text('各部门、各位同事:',
  137. style: TextStyle(
  138. fontSize: 14,
  139. color: AppColors.textPrimary,
  140. height: 1.7)),
  141. const SizedBox(height: 12),
  142. Text(
  143. _item.content,
  144. style: const TextStyle(
  145. fontSize: 14,
  146. color: AppColors.textSecondary,
  147. height: 1.7),
  148. ),
  149. ],
  150. ),
  151. ),
  152. ),
  153. // 附件列表
  154. if (_item.attachments.isNotEmpty)
  155. Padding(
  156. padding: const EdgeInsets.symmetric(horizontal: 16),
  157. child: Column(
  158. crossAxisAlignment: CrossAxisAlignment.start,
  159. children: [
  160. const Text('附件下载',
  161. style: TextStyle(
  162. fontSize: 14,
  163. fontWeight: FontWeight.w600,
  164. color: AppColors.textPrimary)),
  165. const SizedBox(height: 8),
  166. ..._item.attachments.map(
  167. (att) => GestureDetector(
  168. onTap: () {
  169. ScaffoldMessenger.of(context).showSnackBar(
  170. SnackBar(content: Text('模拟下载:$att')),
  171. );
  172. },
  173. child: Container(
  174. width: double.infinity,
  175. margin: const EdgeInsets.only(bottom: 8),
  176. padding: const EdgeInsets.all(12),
  177. decoration: BoxDecoration(
  178. color: AppColors.bgCard,
  179. borderRadius: BorderRadius.circular(8),
  180. ),
  181. child: Row(
  182. children: [
  183. const Icon(Icons.description_outlined,
  184. size: 20, color: AppColors.primary),
  185. const SizedBox(width: 8),
  186. Expanded(
  187. child: Text(att,
  188. style: const TextStyle(
  189. fontSize: 14,
  190. color: AppColors.textPrimary)),
  191. ),
  192. Text('${(att.length * 100) ~/ 1000}KB',
  193. style: const TextStyle(
  194. fontSize: 12,
  195. color: AppColors.textPlaceholder)),
  196. const SizedBox(width: 8),
  197. const Icon(Icons.download_outlined,
  198. size: 16,
  199. color: AppColors.textPlaceholder),
  200. ],
  201. ),
  202. ),
  203. ),
  204. ),
  205. ],
  206. ),
  207. ),
  208. // 管理员增量:已读/未读统计 + DING
  209. if (_isAdmin)
  210. Padding(
  211. padding: const EdgeInsets.all(16),
  212. child: Container(
  213. width: double.infinity,
  214. padding: const EdgeInsets.all(16),
  215. decoration: BoxDecoration(
  216. color: AppColors.bgCard,
  217. borderRadius: BorderRadius.circular(8),
  218. ),
  219. child: Column(
  220. crossAxisAlignment: CrossAxisAlignment.start,
  221. children: [
  222. const Text('全员触达率',
  223. style: TextStyle(
  224. fontSize: 14,
  225. fontWeight: FontWeight.w600,
  226. color: AppColors.textPrimary)),
  227. const SizedBox(height: 12),
  228. Row(
  229. children: [
  230. GestureDetector(
  231. onTap: () {
  232. ScaffoldMessenger.of(context).showSnackBar(
  233. const SnackBar(
  234. content:
  235. Text('模拟:展开已读员工列表')),
  236. );
  237. },
  238. child: Container(
  239. padding: const EdgeInsets.symmetric(
  240. horizontal: 12, vertical: 6),
  241. decoration: BoxDecoration(
  242. color: AppColors.successBg,
  243. borderRadius:
  244. BorderRadius.circular(16),
  245. ),
  246. child: Text(
  247. '已读 ${_item.readCount} 人',
  248. style: const TextStyle(
  249. fontSize: 12,
  250. color: AppColors.success),
  251. ),
  252. ),
  253. ),
  254. const SizedBox(width: 12),
  255. GestureDetector(
  256. onTap: () {
  257. ScaffoldMessenger.of(context).showSnackBar(
  258. const SnackBar(
  259. content:
  260. Text('模拟:展开未读员工列表')),
  261. );
  262. },
  263. child: Container(
  264. padding: const EdgeInsets.symmetric(
  265. horizontal: 12, vertical: 6),
  266. decoration: BoxDecoration(
  267. color: AppColors.bgPage,
  268. borderRadius:
  269. BorderRadius.circular(16),
  270. ),
  271. child: Text(
  272. '未读 ${_item.unreadCount} 人',
  273. style: const TextStyle(
  274. fontSize: 12,
  275. color: AppColors.statusGray),
  276. ),
  277. ),
  278. ),
  279. ],
  280. ),
  281. const SizedBox(height: 12),
  282. GestureDetector(
  283. onTap: () {
  284. ScaffoldMessenger.of(context).showSnackBar(
  285. SnackBar(
  286. content: Text(
  287. '已向 ${_item.unreadCount} 名未读员工发送催办通知'),
  288. ),
  289. );
  290. },
  291. child: Container(
  292. width: double.infinity,
  293. padding: const EdgeInsets.symmetric(
  294. horizontal: 16, vertical: 12),
  295. decoration: BoxDecoration(
  296. color: AppColors.danger,
  297. borderRadius:
  298. BorderRadius.circular(20),
  299. ),
  300. child: const Text(
  301. 'DING 催办',
  302. textAlign: TextAlign.center,
  303. style: TextStyle(
  304. fontSize: 14,
  305. fontWeight: FontWeight.w600,
  306. color: Colors.white),
  307. ),
  308. ),
  309. ),
  310. ],
  311. ),
  312. ),
  313. ),
  314. const SizedBox(height: 24),
  315. ],
  316. ),
  317. );
  318. }
  319. Widget _buildTypeTag(String type) {
  320. Color bgColor;
  321. Color textColor;
  322. switch (type) {
  323. case '人事与制度':
  324. bgColor = AppColors.successBg;
  325. textColor = AppColors.success;
  326. break;
  327. case '放假与活动':
  328. bgColor = AppColors.warningBg;
  329. textColor = AppColors.warning;
  330. break;
  331. default:
  332. bgColor = AppColors.primaryLight;
  333. textColor = AppColors.primary;
  334. }
  335. return Container(
  336. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  337. decoration: BoxDecoration(
  338. color: bgColor,
  339. borderRadius: BorderRadius.circular(3),
  340. ),
  341. child: Text(type, style: TextStyle(fontSize: 11, color: textColor)),
  342. );
  343. }
  344. }