292 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			292 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
	
| import 'package:flutter/material.dart';
 | ||
| import 'package:intl/intl.dart';
 | ||
| import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
 | ||
| import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
 | ||
| import 'package:qhd_prevention/customWidget/toast_util.dart';
 | ||
| import 'package:qhd_prevention/pages/home/tap/item_list_widget.dart';
 | ||
| 
 | ||
| /// punish_modal.dart
 | ||
| /// 动态高度:isp == false 时较矮,isp == true 时较高;切换时带动画,并兼容键盘弹出。
 | ||
| 
 | ||
| class PunishModal extends StatefulWidget {
 | ||
|   final Map<String, String>? initial;
 | ||
|   final void Function(Map<String, String> form) onSubmit;
 | ||
| 
 | ||
|   const PunishModal({Key? key, this.initial, required this.onSubmit})
 | ||
|       : super(key: key);
 | ||
| 
 | ||
|   @override
 | ||
|   _PunishModalState createState() => _PunishModalState();
 | ||
| }
 | ||
| 
 | ||
| class _PunishModalState extends State<PunishModal> {
 | ||
|   late bool isp = false; // 默认否
 | ||
|   late TextEditingController reasonController;
 | ||
|   late TextEditingController amountController;
 | ||
|   late TextEditingController deptController;
 | ||
|   late TextEditingController personController;
 | ||
|   String? dateText;
 | ||
| 
 | ||
|   final FocusNode _amountFocus = FocusNode();
 | ||
|   final _formKey = GlobalKey<FormState>();
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|     reasonController = TextEditingController(text: widget.initial?['REASON'] ?? '');
 | ||
|     amountController = TextEditingController(text: widget.initial?['AMOUT'] ?? '');
 | ||
|     deptController = TextEditingController(text: widget.initial?['RECTIFICATIONDEPT_NAME'] ?? '');
 | ||
|     personController = TextEditingController(text: widget.initial?['RECTIFICATIONOR_NAME'] ?? '');
 | ||
|     dateText = widget.initial?['DATE'];
 | ||
|     isp = widget.initial?['ISPUNISH'] == '1';
 | ||
| 
 | ||
|     _amountFocus.addListener(() {
 | ||
|       if (!_amountFocus.hasFocus) _checkNumber();
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   void dispose() {
 | ||
|     reasonController.dispose();
 | ||
|     amountController.dispose();
 | ||
|     deptController.dispose();
 | ||
|     personController.dispose();
 | ||
|     _amountFocus.dispose();
 | ||
|     super.dispose();
 | ||
|   }
 | ||
| 
 | ||
|   void _checkNumber() {
 | ||
|     final text = amountController.text.trim();
 | ||
|     if (text.isEmpty) return;
 | ||
|     final reg = RegExp(r"^\d+(?:\.\d{1,2})?$");
 | ||
|     if (!reg.hasMatch(text)) {
 | ||
|       ScaffoldMessenger.of(context).showSnackBar(
 | ||
|         const SnackBar(content: Text('请输入有效的金额(最多两位小数)')),
 | ||
|       );
 | ||
|       final match = RegExp(r"\d+(?:\.\d{1,2})?").firstMatch(text);
 | ||
|       if (match != null) {
 | ||
|         amountController.text = match.group(0)!;
 | ||
|       } else {
 | ||
|         amountController.clear();
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   void _onConfirm() {
 | ||
|     if (isp) {
 | ||
|       if (reasonController.text.trim().isEmpty) {
 | ||
|         ToastUtil.showNormal(context, '请填写处罚原因');
 | ||
|         return;
 | ||
|       }
 | ||
|       if (amountController.text.trim().isEmpty) {
 | ||
|         ToastUtil.showNormal(context, '请填写处罚金额');
 | ||
|         return;
 | ||
|       }
 | ||
|       if (dateText?.isEmpty ?? true) {
 | ||
|         ToastUtil.showNormal(context, '请选择下发处罚时间');
 | ||
|         return;
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     final result = {
 | ||
|       'ISPUNISH': isp ? "1" : "2",
 | ||
|       'REASON': reasonController.text.trim(),
 | ||
|       'AMOUT': amountController.text.trim(),
 | ||
|       'RECTIFICATIONDEPT_NAME': deptController.text.trim(),
 | ||
|       'RECTIFICATIONOR_NAME': personController.text.trim(),
 | ||
|       'DATE': dateText ?? '',
 | ||
|     };
 | ||
| 
 | ||
|     widget.onSubmit(result);
 | ||
|     Navigator.of(context).pop();
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     // 注意:showPunishDialog 会限制外层 maxHeight(可被键盘缩小)
 | ||
|     // 在这里我们读取父级给定的最大高度,并基于 isp 决定目标高度百分比
 | ||
|     return LayoutBuilder(builder: (context, constraints) {
 | ||
|       // constraints.maxHeight 是外部 ConstrainedBox(showPunishDialog)给的 availableHeight
 | ||
|       final double availableMaxHeight = constraints.maxHeight.isFinite ? constraints.maxHeight : MediaQuery.of(context).size.height * 0.8;
 | ||
|       final double maxWidth = constraints.maxWidth.isFinite ? constraints.maxWidth : MediaQuery.of(context).size.width * 0.9;
 | ||
| 
 | ||
|       // 比例配置:若想微调大小请改这里
 | ||
|       const double expandedRatio = 0.75; // isp == true 时占 availableMaxHeight 的比例
 | ||
|       const double collapsedRatio = 0.3; // isp == false 时占 availableMaxHeight 的比例
 | ||
| 
 | ||
|       final double targetHeight = isp
 | ||
|           ? (availableMaxHeight * expandedRatio).clamp(260.0, availableMaxHeight)
 | ||
|           : (availableMaxHeight * collapsedRatio).clamp(180.0, availableMaxHeight);
 | ||
| 
 | ||
|       // AnimatedContainer 用于在 isp 切换时平滑过渡高度
 | ||
|       return Center(
 | ||
|         child: AnimatedContainer(
 | ||
|           duration: const Duration(milliseconds: 250),
 | ||
|           curve: Curves.easeInOut,
 | ||
|           width: maxWidth,
 | ||
|           height: targetHeight,
 | ||
|           child: Material(
 | ||
|             color: Colors.transparent,
 | ||
|             child: Container(
 | ||
|               decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
 | ||
|               child: Column(
 | ||
|                 mainAxisSize: MainAxisSize.max,
 | ||
|                 children: [
 | ||
|                   // Header
 | ||
|                   Container(
 | ||
|                     padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
 | ||
|                     decoration: const BoxDecoration(
 | ||
|                       color: Colors.white,
 | ||
|                       borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
 | ||
|                     ),
 | ||
|                     child: Row(
 | ||
|                       children: [
 | ||
|                         const Expanded(child: Text('处罚', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600))),
 | ||
|                         IconButton(onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: Colors.red)),
 | ||
|                       ],
 | ||
|                     ),
 | ||
|                   ),
 | ||
| 
 | ||
|                   // 中间内容:使用 Expanded + SingleChildScrollView 保证在高度受限时可以滚动
 | ||
|                   Expanded(
 | ||
|                     child: SingleChildScrollView(
 | ||
|                       padding: const EdgeInsets.all(12),
 | ||
|                       child: Form(
 | ||
|                         key: _formKey,
 | ||
|                         child: Column(
 | ||
|                           crossAxisAlignment: CrossAxisAlignment.start,
 | ||
|                           children: [
 | ||
|                             // 是否进行罚款(切换 isp 时会触发 setState,进而调整 AnimatedContainer 高度)
 | ||
|                             ListItemFactory.createYesNoSection(
 | ||
|                               title: '是否进行罚款',
 | ||
|                               horizontalPadding: 0,
 | ||
|                               groupValue: isp,
 | ||
|                               onChanged: (v) => setState(() => isp = v),
 | ||
|                             ),
 | ||
| 
 | ||
|                             const SizedBox(height: 8),
 | ||
| 
 | ||
|                             if (isp) ...[
 | ||
|                               ItemListWidget.singleLineTitleText(
 | ||
|                                 label: '处罚原因',
 | ||
|                                 isEditable: true,
 | ||
|                                 controller: reasonController,
 | ||
|                                 hintText: '请输入处罚原因',
 | ||
|                               ),
 | ||
|                               const Divider(),
 | ||
|                               ItemListWidget.singleLineTitleText(
 | ||
|                                 label: '处罚金额(元)',
 | ||
|                                 isEditable: true,
 | ||
|                                 keyboardType: const TextInputType.numberWithOptions(decimal: true),
 | ||
|                                 controller: amountController,
 | ||
|                                 hintText: '请输入处罚金额',
 | ||
|                               ),
 | ||
|                               const Divider(),
 | ||
|                               ItemListWidget.singleLineTitleText(
 | ||
|                                 label: '被处罚单位',
 | ||
|                                 isEditable: false,
 | ||
|                                 text: widget.initial?['RECTIFICATIONDEPT_NAME'] ?? '',
 | ||
|                               ),
 | ||
|                               const Divider(),
 | ||
|                               ItemListWidget.singleLineTitleText(
 | ||
|                                 label: '被处罚人',
 | ||
|                                 isEditable: false,
 | ||
|                                 text: widget.initial?['RECTIFICATIONOR_NAME'] ?? '',
 | ||
|                               ),
 | ||
|                               const Divider(),
 | ||
|                               ItemListWidget.selectableLineTitleTextRightButton(
 | ||
|                                 label: '下发处罚时间',
 | ||
|                                 isEditable: true,
 | ||
|                                 text: dateText ?? '',
 | ||
|                                 onTap: () async {
 | ||
|                                   DateTime? picked = await BottomDateTimePicker.showDate(context);
 | ||
|                                   if (picked != null) {
 | ||
|                                     setState(() {
 | ||
|                                       dateText = DateFormat('yyyy-MM-dd HH:mm').format(picked);
 | ||
|                                     });
 | ||
|                                   }
 | ||
|                                 },
 | ||
|                               ),
 | ||
|                             ] else ...[
 | ||
|                               // isp == false 时,你或许希望显示少量提示或空白占位,这里留空
 | ||
|                               const SizedBox(height: 6),
 | ||
|                             ],
 | ||
| 
 | ||
|                             const SizedBox(height: 12),
 | ||
|                           ],
 | ||
|                         ),
 | ||
|                       ),
 | ||
|                     ),
 | ||
|                   ),
 | ||
| 
 | ||
|                   // Footer 固定(不随内容滚动)
 | ||
|                   Container(
 | ||
|                     padding: const EdgeInsets.all(8),
 | ||
|                     decoration: const BoxDecoration(
 | ||
|                       color: Colors.white,
 | ||
|                       borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
 | ||
|                     ),
 | ||
|                     child: Row(
 | ||
|                       children: [
 | ||
|                         Expanded(child: OutlinedButton(onPressed: () => Navigator.of(context).pop(), child: const Text('关闭'))),
 | ||
|                         const SizedBox(width: 8),
 | ||
|                         Expanded(
 | ||
|                           child: ElevatedButton(
 | ||
|                             style: ElevatedButton.styleFrom(
 | ||
|                               foregroundColor: Colors.green,
 | ||
|                               backgroundColor: Colors.white,
 | ||
|                               side: const BorderSide(color: Colors.green),
 | ||
|                             ),
 | ||
|                             onPressed: _onConfirm,
 | ||
|                             child: const Text('确认', style: TextStyle(color: Colors.green)),
 | ||
|                           ),
 | ||
|                         ),
 | ||
|                       ],
 | ||
|                     ),
 | ||
|                   ),
 | ||
|                 ],
 | ||
|               ),
 | ||
|             ),
 | ||
|           ),
 | ||
|         ),
 | ||
|       );
 | ||
|     });
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// showPunishDialog:外层仅限制 maxHeight(受键盘影响)
 | ||
| Future<void> showPunishDialog(
 | ||
|     BuildContext context, {
 | ||
|       Map<String, String>? initial,
 | ||
|       required void Function(Map<String, String>) onSubmit,
 | ||
|     }) {
 | ||
|   final mq = MediaQuery.of(context);
 | ||
|   final viewInsets = mq.viewInsets; // 键盘高度
 | ||
|   // availableHeight = 屏高 - 键盘高度 - 外围 padding
 | ||
|   final double availableHeight = mq.size.height - viewInsets.vertical - 32.0;
 | ||
|   final double maxWidth = mq.size.width * 0.9;
 | ||
| 
 | ||
|   return showDialog<void>(
 | ||
|     context: context,
 | ||
|     barrierDismissible: false,
 | ||
|     builder: (context) {
 | ||
|       return AnimatedPadding(
 | ||
|         padding: viewInsets + const EdgeInsets.all(16),
 | ||
|         duration: const Duration(milliseconds: 200),
 | ||
|         curve: Curves.decelerate,
 | ||
|         child: SafeArea(
 | ||
|           child: Center(
 | ||
|             child: ConstrainedBox(
 | ||
|               constraints: BoxConstraints(maxHeight: availableHeight, maxWidth: maxWidth),
 | ||
|               child: Material(
 | ||
|                 color: Colors.transparent,
 | ||
|                 child: PunishModal(initial: initial, onSubmit: onSubmit),
 | ||
|               ),
 | ||
|             ),
 | ||
|           ),
 | ||
|         ),
 | ||
|       );
 | ||
|     },
 | ||
|   );
 | ||
| }
 |