336 lines
12 KiB
Dart
336 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';
|
|||
|
|
|||
|
/// punlish_modal.dart
|
|||
|
/// 把你提供的 uniapp 弹窗翻译为 Flutter 版本。
|
|||
|
/// 提供:
|
|||
|
/// - 可复用的 PunishModal Widget
|
|||
|
/// - showPunishDialog 辅助函数
|
|||
|
/// - 一个最小的 main() 示例展示如何调用
|
|||
|
|
|||
|
class PunishModal extends StatefulWidget {
|
|||
|
/// 初始数据(可选),key 与原 uniapp 对应:
|
|||
|
/// "ISPUNISH" : '1' 或 '2'
|
|||
|
/// "REASON"
|
|||
|
/// "AMOUT"
|
|||
|
/// "RECTIFICATIONDEPT_NAME"
|
|||
|
/// "RECTIFICATIONOR_NAME"
|
|||
|
/// "DATE" (字符串)
|
|||
|
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' ? true : false;
|
|||
|
|
|||
|
// 相当于 @blur=\"checkNumber\",监听焦点失去时做检查
|
|||
|
_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?.length == 0) {
|
|||
|
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) {
|
|||
|
// 我们改为根据内容自动伸缩,但当内容过高时仍然限制最大高度并可滚动。
|
|||
|
final maxBodyHeight = MediaQuery.of(context).size.height * 0.6; // 可按需调整
|
|||
|
|
|||
|
return Material(
|
|||
|
color: Colors.transparent,
|
|||
|
child: Center(
|
|||
|
child: Container(
|
|||
|
width: MediaQuery.of(context).size.width * 0.9,
|
|||
|
// 不再强制固定高度,使用 Column 的 min size,让弹窗高度随内容变化
|
|||
|
decoration: BoxDecoration(
|
|||
|
color: Colors.white,
|
|||
|
borderRadius: BorderRadius.circular(8),
|
|||
|
),
|
|||
|
child: Column(
|
|||
|
mainAxisSize: MainAxisSize.min,
|
|||
|
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(
|
|||
|
mainAxisAlignment: MainAxisAlignment.end,
|
|||
|
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),
|
|||
|
),
|
|||
|
],
|
|||
|
),
|
|||
|
),
|
|||
|
|
|||
|
// Body (表单)
|
|||
|
// 使用 ConstrainedBox 限制最大高度,内部使用 SingleChildScrollView,这样当字段少时弹窗高度自动变小,字段多时会出现滚动。
|
|||
|
ConstrainedBox(
|
|||
|
constraints: BoxConstraints(maxHeight: maxBodyHeight),
|
|||
|
child: SingleChildScrollView(
|
|||
|
padding: const EdgeInsets.all(12),
|
|||
|
child: Form(
|
|||
|
key: _formKey,
|
|||
|
child: Column(
|
|||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
children: [
|
|||
|
// 是否进行罚款
|
|||
|
ListItemFactory.createYesNoSection(
|
|||
|
title: '是否进行罚款',
|
|||
|
horizontalPadding: 0,
|
|||
|
groupValue: isp,
|
|||
|
onChanged: (v) => setState(() => isp = v),
|
|||
|
),
|
|||
|
|
|||
|
const SizedBox(height: 8),
|
|||
|
|
|||
|
// 以下内容仅在 isp == '1' 时显示
|
|||
|
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: '',
|
|||
|
onTap: () async {
|
|||
|
DateTime? picked =
|
|||
|
await BottomDateTimePicker.showDate(
|
|||
|
context,
|
|||
|
);
|
|||
|
if (picked != null) {
|
|||
|
setState(() {
|
|||
|
dateText =
|
|||
|
DateFormat(
|
|||
|
'yyyy-MM-dd HH:mm',
|
|||
|
).format(picked);
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
),
|
|||
|
|
|||
|
],
|
|||
|
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),
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
],
|
|||
|
),
|
|||
|
),
|
|||
|
],
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
Widget _buildLabel(String text) => Padding(
|
|||
|
padding: const EdgeInsets.only(bottom: 6),
|
|||
|
child: Text(text, style: const TextStyle(fontWeight: FontWeight.w600)),
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
/// 显示showPunishDialog
|
|||
|
Future<void> showPunishDialog(
|
|||
|
BuildContext context, {
|
|||
|
Map<String, String>? initial,
|
|||
|
required void Function(Map<String, String>) onSubmit,
|
|||
|
}) {
|
|||
|
return showDialog<void>(
|
|||
|
context: context,
|
|||
|
barrierDismissible: false,
|
|||
|
builder: (context) {
|
|||
|
final viewInsets = MediaQuery.of(context).viewInsets;
|
|||
|
return AnimatedPadding(
|
|||
|
padding: viewInsets + const EdgeInsets.all(16),
|
|||
|
duration: const Duration(milliseconds: 200),
|
|||
|
curve: Curves.decelerate,
|
|||
|
child: Align(
|
|||
|
alignment: Alignment.center,
|
|||
|
child: Material(
|
|||
|
color: Colors.transparent,
|
|||
|
child: Container(
|
|||
|
// 保持原来 Dialog 的圆角和背景
|
|||
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
|||
|
child: PunishModal(initial: initial, onSubmit: onSubmit),
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
);
|
|||
|
},
|
|||
|
);
|
|||
|
}
|