announcement_detail_page.dart 13 KB

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