announcement_create_page.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../core/theme/app_colors.dart';
  5. import '../shell/nav_bar_config.dart';
  6. import '../../core/utils/responsive.dart';
  7. import '../../shared/widgets/action_bar.dart';
  8. import '../../core/i18n/app_localizations.dart';
  9. import '../../shared/widgets/form_section.dart';
  10. class AnnouncementCreatePage extends ConsumerStatefulWidget {
  11. const AnnouncementCreatePage({super.key});
  12. @override
  13. ConsumerState<AnnouncementCreatePage> createState() =>
  14. _AnnouncementCreatePageState();
  15. }
  16. class _AnnouncementCreatePageState
  17. extends ConsumerState<AnnouncementCreatePage> {
  18. final _titleCtrl = TextEditingController();
  19. final _contentCtrl = TextEditingController();
  20. String _type = '通知公告';
  21. String _scope = '全员';
  22. bool _isTop = false;
  23. DateTime _expiryDate = DateTime.now().add(const Duration(days: 30));
  24. final _types = ['通知公告', '人事与制度', '放假与活动'];
  25. @override
  26. void dispose() {
  27. _titleCtrl.dispose();
  28. _contentCtrl.dispose();
  29. super.dispose();
  30. }
  31. @override
  32. Widget build(BuildContext context) {
  33. final r = ResponsiveHelper.of(context);
  34. final l10n = AppLocalizations.of(context);
  35. ref
  36. .read(navBarConfigProvider.notifier)
  37. .update(
  38. NavBarConfig(
  39. title: l10n.get('announcementCreate'),
  40. showBack: true,
  41. onBack: () => context.pop(),
  42. ),
  43. );
  44. return Column(
  45. children: [
  46. Expanded(
  47. child: Align(
  48. alignment: Alignment.topCenter,
  49. child: ConstrainedBox(
  50. constraints: BoxConstraints(maxWidth: r.formMaxWidth),
  51. child: SingleChildScrollView(
  52. padding: const EdgeInsets.symmetric(vertical: 8),
  53. child: Column(
  54. children: [
  55. // 基本信息
  56. FormSection(
  57. title: '基本信息',
  58. children: [
  59. // 标题输入
  60. Container(
  61. padding: const EdgeInsets.symmetric(
  62. horizontal: 10,
  63. vertical: 12,
  64. ),
  65. decoration: BoxDecoration(
  66. color: AppColors.bgPage,
  67. borderRadius: BorderRadius.circular(4),
  68. ),
  69. child: TextField(
  70. controller: _titleCtrl,
  71. decoration: const InputDecoration(
  72. hintText: '请输入公告标题',
  73. hintStyle: TextStyle(
  74. fontSize: 14,
  75. color: AppColors.textPlaceholder,
  76. ),
  77. border: InputBorder.none,
  78. contentPadding: EdgeInsets.zero,
  79. isDense: true,
  80. ),
  81. style: const TextStyle(
  82. fontSize: 14,
  83. color: AppColors.textPrimary,
  84. ),
  85. ),
  86. ),
  87. const SizedBox(height: 12),
  88. // 公告类型
  89. _buildSelectRow('公告类型', _type, _pickType),
  90. ],
  91. ),
  92. const SizedBox(height: 16),
  93. // 公告内容
  94. FormSection(
  95. title: '公告内容',
  96. children: [
  97. Container(
  98. padding: const EdgeInsets.all(12),
  99. decoration: BoxDecoration(
  100. color: AppColors.bgPage,
  101. borderRadius: BorderRadius.circular(4),
  102. ),
  103. height: 200,
  104. child: TextField(
  105. controller: _contentCtrl,
  106. maxLines: null,
  107. expands: true,
  108. textAlignVertical: TextAlignVertical.top,
  109. decoration: const InputDecoration(
  110. hintText: '请输入公告正文内容...',
  111. hintStyle: TextStyle(
  112. fontSize: 14,
  113. color: AppColors.textPlaceholder,
  114. ),
  115. border: InputBorder.none,
  116. contentPadding: EdgeInsets.zero,
  117. ),
  118. style: const TextStyle(
  119. fontSize: 14,
  120. color: AppColors.textPrimary,
  121. height: 1.7,
  122. ),
  123. ),
  124. ),
  125. ],
  126. ),
  127. const SizedBox(height: 16),
  128. // 发布设置
  129. FormSection(
  130. title: '发布设置',
  131. children: [
  132. // 置顶开关
  133. _buildSwitchRow('置顶公告', _isTop, (v) {
  134. setState(() => _isTop = v);
  135. }),
  136. const Divider(height: 1, color: AppColors.border),
  137. // 有效期
  138. _buildSelectRow(
  139. '有效期至',
  140. '${_expiryDate.year}-${_expiryDate.month.toString().padLeft(2, '0')}-${_expiryDate.day.toString().padLeft(2, '0')}',
  141. _pickExpiryDate,
  142. ),
  143. const Divider(height: 1, color: AppColors.border),
  144. // 接收范围
  145. _buildSelectRow('接收范围', _scope, _pickScope),
  146. ],
  147. ),
  148. const SizedBox(height: 16),
  149. // 附件
  150. FormSection(
  151. title: '附件',
  152. children: [
  153. Container(
  154. height: 80,
  155. decoration: BoxDecoration(
  156. color: AppColors.bgPage,
  157. borderRadius: BorderRadius.circular(4),
  158. ),
  159. child: const Center(
  160. child: Column(
  161. mainAxisAlignment: MainAxisAlignment.center,
  162. children: [
  163. Icon(
  164. Icons.add_photo_alternate_outlined,
  165. size: 28,
  166. color: AppColors.primary,
  167. ),
  168. SizedBox(height: 4),
  169. Text(
  170. '添加附件',
  171. style: TextStyle(
  172. fontSize: 12,
  173. color: AppColors.primary,
  174. ),
  175. ),
  176. ],
  177. ),
  178. ),
  179. ),
  180. ],
  181. ),
  182. const SizedBox(height: 80),
  183. ],
  184. ),
  185. ),
  186. ),
  187. ),
  188. ),
  189. // ActionBar
  190. ActionBar(
  191. centerLabel: '存草稿',
  192. rightLabel: '发布',
  193. showLeft: false,
  194. onCenterTap: _saveDraft,
  195. onRightTap: _submit,
  196. ),
  197. ],
  198. );
  199. }
  200. Widget _buildSelectRow(String label, String value, VoidCallback onTap) {
  201. return GestureDetector(
  202. onTap: onTap,
  203. child: Container(
  204. height: 44,
  205. alignment: Alignment.centerLeft,
  206. child: Row(
  207. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  208. children: [
  209. Text(
  210. label,
  211. style: const TextStyle(
  212. fontSize: 14,
  213. color: AppColors.textSecondary,
  214. ),
  215. ),
  216. Row(
  217. mainAxisSize: MainAxisSize.min,
  218. children: [
  219. Text(
  220. value,
  221. style: const TextStyle(
  222. fontSize: 14,
  223. color: AppColors.primary,
  224. ),
  225. ),
  226. const SizedBox(width: 4),
  227. const Icon(
  228. Icons.chevron_right,
  229. size: 14,
  230. color: AppColors.textPlaceholder,
  231. ),
  232. ],
  233. ),
  234. ],
  235. ),
  236. ),
  237. );
  238. }
  239. Widget _buildSwitchRow(
  240. String label,
  241. bool value,
  242. ValueChanged<bool> onChanged,
  243. ) {
  244. return Container(
  245. height: 44,
  246. alignment: Alignment.centerLeft,
  247. child: Row(
  248. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  249. children: [
  250. Text(
  251. label,
  252. style: const TextStyle(
  253. fontSize: 14,
  254. color: AppColors.textSecondary,
  255. ),
  256. ),
  257. Switch(
  258. value: value,
  259. onChanged: onChanged,
  260. activeColor: AppColors.primary,
  261. ),
  262. ],
  263. ),
  264. );
  265. }
  266. Future<void> _pickType() async {
  267. final result = await showDialog<String>(
  268. context: context,
  269. builder: (ctx) => SimpleDialog(
  270. title: const Text('选择公告类型'),
  271. children: _types
  272. .map(
  273. (t) => SimpleDialogOption(
  274. onPressed: () => Navigator.pop(ctx, t),
  275. child: Text(t),
  276. ),
  277. )
  278. .toList(),
  279. ),
  280. );
  281. if (result != null) setState(() => _type = result);
  282. }
  283. Future<void> _pickExpiryDate() async {
  284. final picked = await showDatePicker(
  285. context: context,
  286. initialDate: _expiryDate,
  287. firstDate: DateTime.now(),
  288. lastDate: DateTime(2030),
  289. );
  290. if (picked != null) setState(() => _expiryDate = picked);
  291. }
  292. Future<void> _pickScope() async {
  293. final result = await showDialog<String>(
  294. context: context,
  295. builder: (ctx) => SimpleDialog(
  296. title: const Text('选择接收范围'),
  297. children: ['全员', '部门']
  298. .map(
  299. (s) => SimpleDialogOption(
  300. onPressed: () => Navigator.pop(ctx, s),
  301. child: Text(s),
  302. ),
  303. )
  304. .toList(),
  305. ),
  306. );
  307. if (result != null) setState(() => _scope = result);
  308. }
  309. void _saveDraft() {
  310. // TODO: 存草稿逻辑
  311. ScaffoldMessenger.of(
  312. context,
  313. ).showSnackBar(const SnackBar(content: Text('已保存草稿')));
  314. }
  315. Future<void> _submit() async {
  316. await Future.delayed(const Duration(milliseconds: 500));
  317. if (context.mounted) {
  318. ScaffoldMessenger.of(
  319. context,
  320. ).showSnackBar(const SnackBar(content: Text('公告发布成功')));
  321. context.pop();
  322. }
  323. }
  324. }