import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_colors.dart'; import '../shell/nav_bar_config.dart'; import '../../core/utils/responsive.dart'; import '../../shared/widgets/action_bar.dart'; import '../../shared/widgets/form_section.dart'; import '../../core/i18n/app_localizations.dart'; class AnnouncementCreatePage extends ConsumerStatefulWidget { const AnnouncementCreatePage({super.key}); @override ConsumerState createState() => _AnnouncementCreatePageState(); } class _AnnouncementCreatePageState extends ConsumerState { final _titleCtrl = TextEditingController(); final _contentCtrl = TextEditingController(); String _type = '通知公告'; bool _isTop = false; DateTime? _expiryDate; // 接收范围 int _scopeMode = 0; // 0=全员, 1=按部门, 2=按指定用户 final List _selectedDepts = []; final List _selectedUsers = []; final int _totalCoverage = 128; // 模拟覆盖人数 // 附件模拟 final List _attachments = []; static const int _maxAttachments = 5; final _types = ['通知公告', '人事与制度', '放假与活动']; final List _mockDepts = [ '市场部', '技术部', '销售部', '财务部', '人力资源部', '行政管理部', ]; @override void dispose() { _titleCtrl.dispose(); _contentCtrl.dispose(); super.dispose(); } Future _pickType() async { final l10n = AppLocalizations.of(context); final result = await showDialog( context: context, builder: (ctx) => SimpleDialog( title: Text(l10n.get('announcementTypes')), children: _types .map( (t) => SimpleDialogOption( onPressed: () => Navigator.pop(ctx, t), child: Text(t), ), ) .toList(), ), ); if (result != null) setState(() => _type = result); } Future _pickExpiryDate() async { final picked = await showDatePicker( context: context, initialDate: _expiryDate ?? DateTime.now().add(const Duration(days: 30)), firstDate: DateTime.now(), lastDate: DateTime(2030), ); if (picked != null) setState(() => _expiryDate = picked); } void _showScopeDrawer() { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), builder: (ctx) => _buildScopeDrawerContent(ctx), ); } Widget _buildScopeDrawerContent(BuildContext ctx) { return StatefulBuilder( builder: (context, setInnerState) { return Container( height: MediaQuery.of(context).size.height * 0.7, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('接收范围', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), GestureDetector( onTap: () => Navigator.pop(context), child: const Icon(Icons.close, size: 20), ), ], ), const SizedBox(height: 16), RadioGroup( groupValue: _scopeMode, onChanged: (v) { setInnerState(() => _scopeMode = v!); setState(() {}); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildScopeOption( context, '全员', '所有员工均可查看', 0, setInnerState, ), const SizedBox(height: 8), _buildScopeOption( context, '按部门', '按部门树多选', 1, setInnerState, ), const SizedBox(height: 8), _buildScopeOption( context, '按指定用户', '按员工搜索多选', 2, setInnerState, ), ], ), ), if (_scopeMode == 1) ...[ const SizedBox(height: 12), const Text('选择部门', style: TextStyle( fontSize: 13, color: AppColors.textSecondary)), const SizedBox(height: 4), ..._mockDepts.map( (dept) => CheckboxListTile( title: Text(dept, style: const TextStyle(fontSize: 14)), value: _selectedDepts.contains(dept), onChanged: (checked) { setInnerState(() { if (checked == true) { _selectedDepts.add(dept); } else { _selectedDepts.remove(dept); } }); setState(() {}); }, dense: true, contentPadding: EdgeInsets.zero, ), ), ], if (_scopeMode == 2) ...[ const SizedBox(height: 12), const Text('搜索员工', style: TextStyle( fontSize: 13, color: AppColors.textSecondary)), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2), decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(4), ), child: TextField( decoration: const InputDecoration( hintText: '输入姓名或工号搜索', hintStyle: TextStyle( fontSize: 14, color: AppColors.textPlaceholder), border: InputBorder.none, isDense: true, ), style: const TextStyle( fontSize: 14, color: AppColors.textPrimary), ), ), const SizedBox(height: 8), Text( '已选 ${_selectedUsers.length} 人', style: const TextStyle( fontSize: 12, color: AppColors.textSecondary), ), ], const Spacer(), // 覆盖统计 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.primaryLight, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('覆盖人数', style: TextStyle( fontSize: 14, color: AppColors.textPrimary)), Text( '${_scopeMode == 0 ? _totalCoverage : (_scopeMode == 1 ? _selectedDepts.length * 15 : _selectedUsers.length)} 人', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.primary), ), ], ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(vertical: 12), ), child: const Text('确认'), ), ), ], ), ); }, ); } Widget _buildScopeOption( BuildContext context, String title, String subtitle, int mode, void Function(void Function()) setInnerState, ) { final selected = _scopeMode == mode; return GestureDetector( onTap: () { setInnerState(() => _scopeMode = mode); setState(() {}); }, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: selected ? AppColors.primaryLight : AppColors.bgPage, borderRadius: BorderRadius.circular(8), border: Border.all( color: selected ? AppColors.primary : Colors.transparent, ), ), child: Row( children: [ Radio( value: mode, activeColor: AppColors.primary, ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( fontSize: 14, color: AppColors.textPrimary)), Text(subtitle, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary)), ], ), ], ), ), ); } void _showPreview() { final l10n = AppLocalizations.of(context); showDialog( context: context, barrierDismissible: true, builder: (ctx) => Dialog( insetPadding: const EdgeInsets.all(16), child: Container( width: double.infinity, height: MediaQuery.of(context).size.height * 0.85, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.get('previewTitle'), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), GestureDetector( onTap: () => Navigator.pop(ctx), child: const Icon(Icons.close, size: 20), ), ], ), const Divider(), Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 60, height: 4, color: AppColors.danger, ), const SizedBox(height: 16), Text( _titleCtrl.text.isEmpty ? '(未填写标题)' : _titleCtrl.text, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 12), Text( '$_type · 发布后将显示', style: const TextStyle( fontSize: 13, color: AppColors.textSecondary), ), const SizedBox(height: 16), Container( width: double.infinity, height: 2, color: AppColors.danger, ), const SizedBox(height: 16), Text( _contentCtrl.text.isEmpty ? '(未填写正文)' : _contentCtrl.text, style: const TextStyle( fontSize: 14, color: AppColors.textSecondary, height: 1.7), ), ], ), ), ), ], ), ), ), ); } void _confirmPublish() { final l10n = AppLocalizations.of(context); showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(l10n.get('confirmPublishTitle')), content: Text(l10n.getString('confirmPublishContent', args: {'title': _titleCtrl.text})), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text(l10n.get('cancel')), ), TextButton( onPressed: () { Navigator.pop(ctx); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.get('announcementPublished'))), ); context.pop(); }, child: Text(l10n.get('confirmPublish'), style: const TextStyle(color: AppColors.primary)), ), ], ), ); } void _saveDraft() { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('已保存为草稿')), ); } void _pickAttachment() { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('模拟:选择附件(PDF/图片/Word/Excel,≤20MB)')), ); } @override Widget build(BuildContext context) { final r = ResponsiveHelper.of(context); final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('announcementCreate'), showBack: true, showRight: true, rightWidget: TextButton( onPressed: _showPreview, child: Text(l10n.get('preview'), style: const TextStyle( fontSize: 14, color: AppColors.primary, fontWeight: FontWeight.w500)), ), onBack: () => context.pop(), ), ); return Column( children: [ Expanded( child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: r.formMaxWidth), child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ // 基本信息 FormSection( title: l10n.get('basicInfo'), children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(4), ), child: TextField( controller: _titleCtrl, decoration: InputDecoration( hintText: l10n.get('enterTitle'), hintStyle: TextStyle( fontSize: 14, color: AppColors.textPlaceholder), border: InputBorder.none, contentPadding: EdgeInsets.symmetric(vertical: 10), isDense: true, ), style: const TextStyle( fontSize: 14, color: AppColors.textPrimary), ), ), const SizedBox(height: 12), GestureDetector( onTap: _pickType, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 12), decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(_type, style: const TextStyle( fontSize: 14, color: AppColors.textPrimary)), const Icon(Icons.chevron_right, size: 14, color: AppColors.textPlaceholder), ], ), ), ), ], ), const SizedBox(height: 8), // 公告正文 FormSection( title: l10n.get('announcementContent'), children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(4), ), height: 200, child: TextField( controller: _contentCtrl, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( hintText: l10n.get('enterContent'), hintStyle: TextStyle( fontSize: 14, color: AppColors.textPlaceholder), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), style: const TextStyle( fontSize: 14, color: AppColors.textPrimary, height: 1.7), ), ), ], ), const SizedBox(height: 8), // 附件 FormSection( title: l10n.get('attachments'), actionText: _attachments.length >= _maxAttachments ? l10n.get('limitReached') : l10n.get('add'), showAction: _attachments.length < _maxAttachments, actionIcon: Icons.attach_file, onActionTap: _pickAttachment, children: [ if (_attachments.isEmpty) const Text('最多5个附件,支持PDF/图片/Word/Excel,单文件≤20MB', style: TextStyle( fontSize: 12, color: AppColors.textPlaceholder)) else Wrap( spacing: 8, runSpacing: 8, children: _attachments .asMap() .entries .map((entry) { return Chip( label: Text(entry.value, style: const TextStyle( fontSize: 12)), deleteIcon: const Icon(Icons.close, size: 16), onDeleted: () { setState(() => _attachments.removeAt(entry.key)); }, ); }).toList(), ), ], ), const SizedBox(height: 8), // 发布设置 FormSection( title: l10n.get('publishSettings'), children: [ _buildSwitchRow(l10n.get('pinAnnouncement'), _isTop, (v) { setState(() => _isTop = v); }), const Divider(height: 1, color: AppColors.border), GestureDetector( onTap: _pickExpiryDate, child: Container( height: 44, alignment: Alignment.centerLeft, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.get('validUntil'), style: TextStyle( fontSize: 14, color: AppColors.textSecondary)), Text( _expiryDate != null ? '${_expiryDate!.year}-${_expiryDate!.month.toString().padLeft(2, '0')}-${_expiryDate!.day.toString().padLeft(2, '0')}' : l10n.get('expiryNever'), style: TextStyle( fontSize: 14, color: _expiryDate != null ? AppColors.primary : AppColors.textPlaceholder, ), ), const Icon(Icons.chevron_right, size: 14, color: AppColors.textPlaceholder), ], ), ), ), const Divider(height: 1, color: AppColors.border), GestureDetector( onTap: _showScopeDrawer, child: Container( height: 44, alignment: Alignment.centerLeft, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.get('recipientScope'), style: TextStyle( fontSize: 14, color: AppColors.textSecondary)), Row( mainAxisSize: MainAxisSize.min, children: [ Text( _scopeMode == 0 ? l10n.get('allStaff') : _scopeMode == 1 ? '${l10n.get('byDept')}(${_selectedDepts.length})' : '${l10n.get('byUser')}(${_selectedUsers.length})', style: const TextStyle( fontSize: 14, color: AppColors.primary), ), Text( ' · $_totalCoverage 人', style: const TextStyle( fontSize: 12, color: AppColors.textPlaceholder), ), const SizedBox(width: 4), const Icon(Icons.chevron_right, size: 14, color: AppColors.textPlaceholder), ], ), ], ), ), ), ], ), const SizedBox(height: 80), ], ), ), ), ), ), ActionBar( centerLabel: l10n.get('saveDraft'), rightLabel: l10n.get('publish'), showLeft: false, onCenterTap: _saveDraft, onRightTap: _confirmPublish, ), ], ); } Widget _buildSwitchRow( String label, bool value, ValueChanged onChanged) { return Container( height: 44, alignment: Alignment.centerLeft, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle( fontSize: 14, color: AppColors.textSecondary)), Switch( value: value, onChanged: onChanged, activeThumbColor: AppColors.primary, ), ], ), ); } }