2025-08-14 15:05:48 +08:00
|
|
|
|
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';
|
|
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
/// punish_modal.dart
|
|
|
|
|
|
/// 动态高度:isp == false 时较矮,isp == true 时较高;切换时带动画,并兼容键盘弹出。
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
|
|
|
|
|
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})
|
2025-09-06 17:38:07 +08:00
|
|
|
|
: super(key: key);
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
|
|
|
|
|
@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();
|
2025-09-06 17:38:07 +08:00
|
|
|
|
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'] ?? '');
|
2025-08-14 15:05:48 +08:00
|
|
|
|
dateText = widget.initial?['DATE'];
|
2025-09-06 17:38:07 +08:00
|
|
|
|
isp = widget.initial?['ISPUNISH'] == '1';
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
|
|
|
|
|
_amountFocus.addListener(() {
|
2025-09-06 17:38:07 +08:00
|
|
|
|
if (!_amountFocus.hasFocus) _checkNumber();
|
2025-08-14 15:05:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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)) {
|
2025-09-06 17:38:07 +08:00
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
|
const SnackBar(content: Text('请输入有效的金额(最多两位小数)')),
|
|
|
|
|
|
);
|
2025-08-14 15:05:48 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-09-06 17:38:07 +08:00
|
|
|
|
if (dateText?.isEmpty ?? true) {
|
2025-08-14 15:05:48 +08:00
|
|
|
|
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) {
|
2025-09-06 17:38:07 +08:00
|
|
|
|
// 注意: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;
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
// 比例配置:若想微调大小请改这里
|
|
|
|
|
|
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)),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
2025-09-06 17:38:07 +08:00
|
|
|
|
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)),
|
|
|
|
|
|
],
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
2025-09-06 17:38:07 +08:00
|
|
|
|
),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
// 中间内容:使用 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),
|
|
|
|
|
|
),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
const SizedBox(height: 8),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
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),
|
|
|
|
|
|
],
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
// Footer 固定(不随内容滚动)
|
|
|
|
|
|
Container(
|
|
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
2025-09-06 17:38:07 +08:00
|
|
|
|
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)),
|
|
|
|
|
|
),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
2025-09-06 17:38:07 +08:00
|
|
|
|
],
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
2025-09-06 17:38:07 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
2025-09-06 17:38:07 +08:00
|
|
|
|
),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2025-09-06 17:38:07 +08:00
|
|
|
|
);
|
|
|
|
|
|
});
|
2025-08-14 15:05:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 17:38:07 +08:00
|
|
|
|
/// showPunishDialog:外层仅限制 maxHeight(受键盘影响)
|
2025-08-14 15:05:48 +08:00
|
|
|
|
Future<void> showPunishDialog(
|
2025-09-06 17:38:07 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-08-14 15:05:48 +08:00
|
|
|
|
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,
|
2025-09-06 17:38:07 +08:00
|
|
|
|
child: SafeArea(
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
|
constraints: BoxConstraints(maxHeight: availableHeight, maxWidth: maxWidth),
|
|
|
|
|
|
child: Material(
|
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
|
child: PunishModal(initial: initial, onSubmit: onSubmit),
|
|
|
|
|
|
),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|