announcement_detail_page.dart 13 KB

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