import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.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'; import '../../core/theme/app_colors_extension.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) => TDAlertDialog.vertical( title: l10n.get('announcementTypes'), buttons: _types .map( (t) => TDDialogButtonOptions( title: t, action: () => Navigator.pop(ctx, t), ), ) .toList(), ), ); if (result != null) setState(() => _type = result); } void _pickExpiryDate() { final l10n = AppLocalizations.of(context); final initial = _expiryDate ?? DateTime.now().add(const Duration(days: 30)); TDPicker.showDatePicker( context, title: l10n.get('selectExpiryDate'), useYear: true, useMonth: true, useDay: true, useHour: false, useMinute: false, initialDate: [initial.year, initial.month, initial.day], onConfirm: (selected) { setState(() { _expiryDate = DateTime( selected['year'] ?? initial.year, selected['month'] ?? initial.month, selected['day'] ?? initial.day, ); }); }, ); } 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) { final l10n = AppLocalizations.of(ctx); return StatefulBuilder( builder: (context, setInnerState) { final colors = Theme.of(context).extension()!; 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: [ Text( l10n.get('recipientScope'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.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, l10n.get('allStaff'), l10n.get('scopeAllStaff'), 0, setInnerState, ), const SizedBox(height: 8), _buildScopeOption( context, l10n.get('byDept'), l10n.get('byDeptHint'), 1, setInnerState, ), const SizedBox(height: 8), _buildScopeOption( context, l10n.get('byUser'), l10n.get('byUserHint'), 2, setInnerState, ), ], ), ), if (_scopeMode == 1) ...[ const SizedBox(height: 12), Text( l10n.get('selectDept'), style: TextStyle(fontSize: 13, color: colors.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), Text( l10n.get('searchEmployee'), style: TextStyle(fontSize: 13, color: colors.textSecondary), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 2, ), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: TDInput(hintText: l10n.get('searchEmployeeHint')), ), const SizedBox(height: 8), Text( l10n.getString('selectedCount', args: {'count': '${_selectedUsers.length}'}), style: TextStyle(fontSize: 12, color: colors.textSecondary), ), ], const Spacer(), // 覆盖统计 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('coverageCount'), style: TextStyle(fontSize: 14, color: colors.textPrimary), ), Text( '${_scopeMode == 0 ? _totalCoverage : (_scopeMode == 1 ? _selectedDepts.length * 15 : _selectedUsers.length)} ${l10n.get('personUnit')}', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: colors.primary, ), ), ], ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: TDButton( text: l10n.get('confirm'), size: TDButtonSize.large, theme: TDButtonTheme.primary, isBlock: true, onTap: () => Navigator.pop(context), ), ), ], ), ); }, ); } Widget _buildScopeOption( BuildContext context, String title, String subtitle, int mode, void Function(void Function()) setInnerState, ) { final colors = Theme.of(context).extension()!; final selected = _scopeMode == mode; return GestureDetector( onTap: () { setInnerState(() => _scopeMode = mode); setState(() {}); }, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: selected ? colors.primaryLight : colors.bgPage, borderRadius: BorderRadius.circular(8), border: Border.all( color: selected ? colors.primary : Colors.transparent, ), ), child: Row( children: [ Radio(value: mode, activeColor: colors.primary), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle(fontSize: 14, color: colors.textPrimary), ), Text( subtitle, style: TextStyle(fontSize: 12, color: colors.textSecondary), ), ], ), ], ), ), ); } void _showPreview() { final colors = Theme.of(context).extension()!; 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: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), GestureDetector( onTap: () => Navigator.pop(ctx), child: const Icon(Icons.close, size: 20), ), ], ), const TDDivider(), Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(width: 60, height: 4, color: colors.danger), const SizedBox(height: 16), Text( _titleCtrl.text.isEmpty ? l10n.get('titleNotFilled') : _titleCtrl.text, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: colors.textPrimary, ), ), const SizedBox(height: 12), Text( l10n.getString('typeAndPublishDate', args: {'type': _type}), style: TextStyle( fontSize: 13, color: colors.textSecondary, ), ), const SizedBox(height: 16), Container( width: double.infinity, height: 2, color: colors.danger, ), const SizedBox(height: 16), Text( _contentCtrl.text.isEmpty ? l10n.get('contentNotFilled') : _contentCtrl.text, style: TextStyle( fontSize: 14, color: colors.textSecondary, height: 1.7, ), ), ], ), ), ), ], ), ), ), ); } void _confirmPublish() { final l10n = AppLocalizations.of(context); showDialog( context: context, builder: (ctx) => TDAlertDialog( title: l10n.get('confirmPublishTitle'), contentWidget: Text( l10n.getString( 'confirmPublishContent', args: {'title': _titleCtrl.text}, ), ), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirmPublish'), action: () { Navigator.pop(ctx); TDToast.showText( l10n.get('announcementPublished'), context: context, ); context.pop(); }, ), ), ); } void _saveDraft() { final l10n = AppLocalizations.of(context); TDToast.showText(l10n.get('draftSavedToast'), context: context); } void _pickAttachment() { final l10n = AppLocalizations.of(context); TDToast.showText(l10n.get('attachmentPicker'), context: context); } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; 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: GestureDetector( onTap: _showPreview, child: Text( l10n.get('preview'), style: TextStyle( fontSize: 14, color: colors.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: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: TDInput( controller: _titleCtrl, hintText: l10n.get('enterTitle'), ), ), const SizedBox(height: 12), GestureDetector( onTap: _pickType, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 12, ), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _type, style: TextStyle( fontSize: 14, color: colors.textPrimary, ), ), Icon( Icons.chevron_right, size: 14, color: colors.textPlaceholder, ), ], ), ), ), ], ), const SizedBox(height: 8), // 公告正文 FormSection( title: l10n.get('announcementContent'), children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), height: 200, child: TDInput( controller: _contentCtrl, maxLines: null, hintText: l10n.get('enterContent'), ), ), ], ), 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) Text( l10n.get('attachmentLimit'), style: TextStyle( fontSize: 12, color: colors.textPlaceholder, ), ) else Wrap( spacing: 8, runSpacing: 8, children: _attachments.asMap().entries.map((entry) { return TDTag( entry.value, size: TDTagSize.medium, needCloseIcon: true, onCloseTap: () { 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); }), TDDivider(height: 1, color: colors.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: colors.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 ? colors.primary : colors.textPlaceholder, ), ), Icon( Icons.chevron_right, size: 14, color: colors.textPlaceholder, ), ], ), ), ), TDDivider(height: 1, color: colors.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: colors.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: TextStyle( fontSize: 14, color: colors.primary, ), ), Text( l10n.get('coverageCount'), style: TextStyle( fontSize: 12, color: colors.textPlaceholder, ), ), const SizedBox(width: 4), Icon( Icons.chevron_right, size: 14, color: colors.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, ) { final colors = Theme.of(context).extension()!; return Container( height: 44, alignment: Alignment.centerLeft, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle(fontSize: 14, color: colors.textSecondary), ), TDSwitch( isOn: value, onChanged: (v) { onChanged(v); return v; }, ), ], ), ); } }