2025-08-14 15:05:48 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
|
|
|
|
|
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
2025-08-14 18:14:15 +08:00
|
|
|
|
import 'package:qhd_prevention/customWidget/dotted_border_box.dart';
|
2025-08-14 15:05:48 +08:00
|
|
|
|
|
|
|
|
|
class MultiTextFieldWithTitle extends StatefulWidget {
|
|
|
|
|
final String label;
|
|
|
|
|
final List<String> texts;
|
|
|
|
|
final bool isEditable;
|
|
|
|
|
final String hintText;
|
|
|
|
|
final double fontSize;
|
|
|
|
|
final bool isRequired;
|
|
|
|
|
final ValueChanged<List<String>> onTextsChanged;
|
|
|
|
|
|
|
|
|
|
const MultiTextFieldWithTitle({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.label,
|
|
|
|
|
required this.isEditable,
|
|
|
|
|
required this.hintText,
|
|
|
|
|
required this.onTextsChanged,
|
|
|
|
|
this.fontSize = 15,
|
2025-08-16 14:13:23 +08:00
|
|
|
|
this.texts = const [],
|
2025-08-14 15:05:48 +08:00
|
|
|
|
this.isRequired = true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<MultiTextFieldWithTitle> createState() =>
|
|
|
|
|
_MultiTextFieldWithTitleState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _MultiTextFieldWithTitleState extends State<MultiTextFieldWithTitle> {
|
|
|
|
|
final List<TextEditingController> _controllers = [];
|
|
|
|
|
final List<FocusNode> _focusNodes = [];
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2025-08-16 14:13:23 +08:00
|
|
|
|
|
|
|
|
|
// 根据传入的初始 texts 初始化 controllers(可编辑时也赋值)
|
|
|
|
|
if (widget.texts.isNotEmpty) {
|
|
|
|
|
for (final t in widget.texts) {
|
|
|
|
|
final controller = TextEditingController(text: t);
|
|
|
|
|
final node = FocusNode();
|
|
|
|
|
controller.addListener(() {
|
|
|
|
|
widget.onTextsChanged(_getAllTexts());
|
|
|
|
|
});
|
|
|
|
|
_controllers.add(controller);
|
|
|
|
|
_focusNodes.add(node);
|
2025-08-16 14:03:56 +08:00
|
|
|
|
}
|
2025-08-16 14:13:23 +08:00
|
|
|
|
} else {
|
|
|
|
|
// 如果没有初始值,至少创建一个空的控制器(可编辑场景需要输入框)
|
|
|
|
|
final controller = TextEditingController();
|
|
|
|
|
final node = FocusNode();
|
|
|
|
|
controller.addListener(() {
|
|
|
|
|
widget.onTextsChanged(_getAllTexts());
|
|
|
|
|
});
|
|
|
|
|
_controllers.add(controller);
|
|
|
|
|
_focusNodes.add(node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 触发一次回调,确保父组件拿到初始值
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (mounted) widget.onTextsChanged(_getAllTexts());
|
2025-08-16 14:10:57 +08:00
|
|
|
|
});
|
2025-08-16 14:03:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 14:13:23 +08:00
|
|
|
|
@override
|
|
|
|
|
void didUpdateWidget(covariant MultiTextFieldWithTitle oldWidget) {
|
|
|
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
|
|
|
|
|
|
// 当外部传入的 texts 发生变化(例如父组件替换了初始列表),
|
|
|
|
|
// 且当前 controllers 与之不一致时,进行同步更新。
|
|
|
|
|
// 为避免覆盖用户正在编辑的数据,这里只在长度或内容明显不同时才同步。
|
|
|
|
|
final newTexts = widget.texts;
|
|
|
|
|
bool needSync = false;
|
|
|
|
|
|
|
|
|
|
if (newTexts.length != _controllers.length) {
|
|
|
|
|
needSync = true;
|
|
|
|
|
} else {
|
|
|
|
|
// 长度相等时比较内容
|
|
|
|
|
for (var i = 0; i < newTexts.length; i++) {
|
|
|
|
|
final controllerText = _controllers[i].text;
|
|
|
|
|
if (controllerText != newTexts[i]) {
|
|
|
|
|
needSync = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (needSync) {
|
|
|
|
|
// 清理旧的 controllers / nodes
|
|
|
|
|
for (var c in _controllers) {
|
|
|
|
|
c.dispose();
|
|
|
|
|
}
|
|
|
|
|
for (var n in _focusNodes) {
|
|
|
|
|
n.dispose();
|
|
|
|
|
}
|
|
|
|
|
_controllers.clear();
|
|
|
|
|
_focusNodes.clear();
|
|
|
|
|
|
|
|
|
|
// 重新创建
|
|
|
|
|
if (newTexts.isNotEmpty) {
|
|
|
|
|
for (final t in newTexts) {
|
|
|
|
|
final controller = TextEditingController(text: t);
|
|
|
|
|
final node = FocusNode();
|
|
|
|
|
controller.addListener(() {
|
|
|
|
|
widget.onTextsChanged(_getAllTexts());
|
|
|
|
|
});
|
|
|
|
|
_controllers.add(controller);
|
|
|
|
|
_focusNodes.add(node);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
final controller = TextEditingController();
|
|
|
|
|
final node = FocusNode();
|
|
|
|
|
controller.addListener(() {
|
|
|
|
|
widget.onTextsChanged(_getAllTexts());
|
|
|
|
|
});
|
|
|
|
|
_controllers.add(controller);
|
|
|
|
|
_focusNodes.add(node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 通知父组件
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (mounted) widget.onTextsChanged(_getAllTexts());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-14 15:05:48 +08:00
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
for (var controller in _controllers) {
|
|
|
|
|
controller.dispose();
|
|
|
|
|
}
|
|
|
|
|
for (var node in _focusNodes) {
|
|
|
|
|
node.dispose();
|
|
|
|
|
}
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 14:13:23 +08:00
|
|
|
|
// _addTextField 现在支持传入初始文本
|
|
|
|
|
void _addTextField([String initialText = '']) {
|
2025-08-14 15:05:48 +08:00
|
|
|
|
setState(() {
|
2025-08-16 14:13:23 +08:00
|
|
|
|
final newController = TextEditingController(text: initialText);
|
2025-08-14 15:05:48 +08:00
|
|
|
|
final newFocusNode = FocusNode();
|
|
|
|
|
|
|
|
|
|
newController.addListener(() {
|
|
|
|
|
widget.onTextsChanged(_getAllTexts());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_controllers.add(newController);
|
|
|
|
|
_focusNodes.add(newFocusNode);
|
|
|
|
|
widget.onTextsChanged(_getAllTexts());
|
2025-08-16 14:13:23 +08:00
|
|
|
|
// 自动聚焦到新创建的输入框(延迟到下一帧)
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
final idx = _controllers.length - 1;
|
|
|
|
|
_focusNodes[idx].requestFocus();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-14 15:05:48 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _removeTextField(int index) async {
|
|
|
|
|
if (_controllers.length <= 1) return;
|
2025-08-16 14:13:23 +08:00
|
|
|
|
final confirmed = await CustomAlertDialog.showConfirm(
|
|
|
|
|
context,
|
|
|
|
|
title: '提示',
|
|
|
|
|
content: '确定删除检查情况吗?',
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
confirmText: '确定',
|
2025-08-14 15:05:48 +08:00
|
|
|
|
);
|
2025-08-16 14:13:23 +08:00
|
|
|
|
if (!confirmed) return;
|
|
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
|
_controllers[index].dispose();
|
|
|
|
|
_focusNodes[index].dispose();
|
|
|
|
|
|
|
|
|
|
_controllers.removeAt(index);
|
|
|
|
|
_focusNodes.removeAt(index);
|
|
|
|
|
widget.onTextsChanged(_getAllTexts());
|
|
|
|
|
});
|
2025-08-14 15:05:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<String> _getAllTexts() {
|
|
|
|
|
return _controllers.map((c) => c.text).toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 12),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// 标题行
|
|
|
|
|
InkWell(
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Flexible(
|
|
|
|
|
fit: FlexFit.loose,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
if (widget.isRequired && widget.isEditable)
|
|
|
|
|
Text('* ', style: TextStyle(color: Colors.red)),
|
|
|
|
|
Flexible(
|
|
|
|
|
child: Text(
|
|
|
|
|
widget.label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: widget.fontSize,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
if (widget.isEditable)
|
|
|
|
|
CustomButton(
|
|
|
|
|
text: " 添加 ",
|
|
|
|
|
height: 30,
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
vertical: 2,
|
|
|
|
|
horizontal: 5,
|
|
|
|
|
),
|
|
|
|
|
backgroundColor: Colors.blue,
|
2025-08-16 14:13:23 +08:00
|
|
|
|
onPressed: () => _addTextField(),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
|
|
|
|
// 输入框区域 - 高度自适应
|
|
|
|
|
Column(
|
|
|
|
|
children: [
|
|
|
|
|
// 可编辑状态
|
|
|
|
|
if (widget.isEditable)
|
|
|
|
|
..._controllers.asMap().entries.map((entry) {
|
|
|
|
|
final index = entry.key;
|
|
|
|
|
return _buildTextFieldWithDelete(index);
|
|
|
|
|
}).toList(),
|
|
|
|
|
|
|
|
|
|
// 不可编辑状态
|
|
|
|
|
if (!widget.isEditable)
|
|
|
|
|
...widget.texts.map((c) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
2025-08-14 18:14:15 +08:00
|
|
|
|
child: SizedBox(
|
2025-08-16 14:13:23 +08:00
|
|
|
|
width: double.maxFinite,
|
|
|
|
|
child: DottedBorderBox(
|
|
|
|
|
child: Text(
|
|
|
|
|
c,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: widget.fontSize,
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
),
|
2025-08-14 18:14:15 +08:00
|
|
|
|
),
|
2025-08-16 14:13:23 +08:00
|
|
|
|
)),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildTextFieldWithDelete(int index) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
|
|
|
child: Stack(
|
|
|
|
|
children: [
|
|
|
|
|
// 输入框
|
|
|
|
|
Padding(
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 7),
|
2025-08-14 18:14:15 +08:00
|
|
|
|
child: SizedBox(
|
2025-08-16 14:13:23 +08:00
|
|
|
|
width: double.maxFinite,
|
|
|
|
|
child: DottedBorderBox(
|
|
|
|
|
child: TextField(
|
|
|
|
|
controller: _controllers[index],
|
|
|
|
|
decoration: InputDecoration(hintText: widget.hintText),
|
|
|
|
|
focusNode: _focusNodes[index],
|
|
|
|
|
keyboardType: TextInputType.multiline,
|
|
|
|
|
maxLines: 3,
|
|
|
|
|
minLines: 3,
|
|
|
|
|
style: TextStyle(fontSize: widget.fontSize),
|
|
|
|
|
),
|
|
|
|
|
)),
|
2025-08-14 15:05:48 +08:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 删除按钮(叠加在左上角)
|
|
|
|
|
if (index > 0)
|
|
|
|
|
Positioned(
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
onTap: () => _removeTextField(index),
|
|
|
|
|
child: Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.red,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
),
|
|
|
|
|
padding: const EdgeInsets.all(4),
|
|
|
|
|
child: const Icon(Icons.close, size: 10, color: Colors.white),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|