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),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|