QinGang_interested/lib/tools/MultiTextFieldWithTitle.dart

486 lines
16 KiB
Dart
Raw Normal View History

2026-04-10 17:25:59 +08:00
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/dotted_border_box.dart';
import 'package:qhd_prevention/customWidget/photo_picker_row.dart';
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/tools/tools.dart';
// 操作类型枚举
enum OperationType {
// 检查
check,
// 安全措施
measure,
}
class MultiTextFieldWithTitle extends StatefulWidget {
final String label;
final List<dynamic> items; // 改为包含文本和图片的列表(外部结构任意字段都会保留)
final bool isEditable;
final bool isAddImage;
final String hintText;
final double fontSize;
final bool isRequired;
final bool isDeletFirst;
final OperationType operationType;
final ValueChanged<List<Map<String, dynamic>>> onItemsChanged;
const MultiTextFieldWithTitle({
super.key,
required this.label,
required this.isEditable,
required this.hintText,
required this.onItemsChanged,
this.fontSize = 15,
this.items = const [],
this.isRequired = true,
this.isAddImage = true,
this.isDeletFirst = false,
this.operationType = OperationType.check,
});
@override
State<MultiTextFieldWithTitle> createState() =>
_MultiTextFieldWithTitleState();
}
class _MultiTextFieldWithTitleState extends State<MultiTextFieldWithTitle> {
final List<TextEditingController> _controllers = [];
final List<FocusNode> _focusNodes = [];
/// 保存每一项的完整 map来自 widget.items 的深复制),这样可以保留额外字段
final List<Map<String, dynamic>> _itemsData = [];
@override
void initState() {
super.initState();
_initializeFromItems();
}
void _initializeFromItems() {
// 释放旧资源
for (var c in _controllers) {
c.dispose();
}
for (var n in _focusNodes) {
n.dispose();
}
_controllers.clear();
_focusNodes.clear();
_itemsData.clear();
if (widget.items.isNotEmpty) {
for (final rawItem in widget.items) {
// 保守地把外部 item 转成 Map 并深复制一份(避免引用同一对象)
Map<String, dynamic> item;
if (rawItem is Map<String, dynamic>) {
item = Map<String, dynamic>.from(rawItem);
} else {
// 如果传入不是 map尝试包成 map
item = {'content': rawItem?.toString() ?? ''};
}
// 兼容字段:优先 content再兜底空字符串
final text = item['content'] ?? '';
// 规范化:如果只有 imgPath字符串存在也在 imgPaths 中保持 list 表示
if (item.containsKey('imgPath') && !item.containsKey('imgPaths')) {
final v = item['imgPath'];
if (v == null) {
item['imgPaths'] = <String>[];
} else if (v is String && v.isNotEmpty) {
item['imgPaths'] = [v];
} else if (v is List) {
item['imgPaths'] = List<String>.from(v.map((e) => e.toString()));
} else {
item['imgPaths'] = <String>[];
}
} else if (!item.containsKey('imgPaths')) {
item['imgPaths'] = <String>[];
} else {
// 确保 imgPaths 是 List<String>
final v = item['imgPaths'];
if (v == null) {
item['imgPaths'] = <String>[];
} else if (v is List) {
item['imgPaths'] = List<String>.from(v.map((e) => e.toString()));
} else if (v is String && v.isNotEmpty) {
item['imgPaths'] = [v];
} else {
item['imgPaths'] = <String>[];
}
}
final controller = TextEditingController(text: text);
final node = FocusNode();
controller.addListener(_onDataChanged);
_controllers.add(controller);
_focusNodes.add(node);
_itemsData.add(item);
}
} else {
// 没有初始值,创建一个空项
if (widget.operationType == OperationType.check)
_addNewItem(initialize: true);
}
// 触发一次回调(保证外面拿到初始结构)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _onDataChanged();
});
}
@override
void didUpdateWidget(covariant MultiTextFieldWithTitle oldWidget) {
super.didUpdateWidget(oldWidget);
// 当外部传入的 items 发生变化时,重新初始化
if (oldWidget.items != widget.items) {
_initializeFromItems();
}
}
@override
void dispose() {
for (var c in _controllers) {
c.dispose();
}
for (var n in _focusNodes) {
n.dispose();
}
super.dispose();
}
void _onDataChanged() {
widget.onItemsChanged(_getAllItems());
}
// 获取当前每一项的图片路径列表List<String>
List<String> _getImageListForIndex(int index) {
if (index < 0 || index >= _itemsData.length) return <String>[];
final v = _itemsData[index]['imgPaths'];
if (v == null) return <String>[];
if (v is List<String>) return List<String>.from(v);
if (v is List) return List<String>.from(v.map((e) => e.toString()));
if (v is String && v.isNotEmpty) return [v];
return <String>[];
}
// 将新的图片列表写回 itemsData同时也同步 imgPath 单字符串字段以兼容外部期待
void _setImageListForIndex(int index, List<String> list) {
if (index < 0 || index >= _itemsData.length) return;
_itemsData[index]['imgPaths'] = List<String>.from(list);
// 同时更新单值字段,保持兼容(取第一张或空字符串)
_itemsData[index]['imgPath'] = list.isNotEmpty ? list.first : '';
}
// 添加新条目
void _addNewItem({bool initialize = false}) {
// initialize 标识初始化阶段(避免重复触发回调两次,但我们仍然会触发 onDataChanged
setState(() {
final newController = TextEditingController();
final newFocusNode = FocusNode();
newController.addListener(_onDataChanged);
// 新建项:尽量包含 content、imgPath、imgPaths其他字段为空外部已有项的字段会被保留
final Map<String, dynamic> newItem = {
'content': '',
'imgPath': '',
'imgPaths': <String>[],
};
_controllers.add(newController);
_focusNodes.add(newFocusNode);
_itemsData.add(newItem);
// 只有在非初始化时触发回调;但仍然需要回调让调用方获取最新结构(加上 initialize 选项)
if (!initialize) _onDataChanged();
});
}
// 删除条目
void _removeItem(int index) async {
if (_controllers.length <= 1) return;
final confirmed = await CustomAlertDialog.showConfirm(
context,
title: '提示',
content: '确定删除此项吗?',
cancelText: '取消',
confirmText: '确定',
);
if (!confirmed) return;
setState(() {
_controllers[index].dispose();
_focusNodes[index].dispose();
_controllers.removeAt(index);
_focusNodes.removeAt(index);
_itemsData.removeAt(index);
_onDataChanged();
});
}
// 返回当前所有项的完整 Map 列表(保留原有字段,只更新 content/imgPaths/imgPath
List<Map<String, dynamic>> _getAllItems() {
final List<Map<String, dynamic>> items = [];
for (int i = 0; i < _itemsData.length; i++) {
final Map<String, dynamic> copy = Map<String, dynamic>.from(
_itemsData[i],
);
// 确保 content 与图片字段同步最新值
copy['content'] = _controllers[i].text;
// 保证 imgPaths 是 List<String>
final imgs = _getImageListForIndex(i);
copy['imgPaths'] = imgs;
copy['imgPath'] = imgs.isNotEmpty ? imgs.first : '';
items.add(copy);
}
return items;
}
@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)
const 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,
onPressed: _addNewItem,
),
],
),
),
const SizedBox(height: 8),
// 条目区域
Column(
children: [
..._controllers.asMap().entries.map((entry) {
final index = entry.key;
final itemMap = _itemsData[index];
final item = {
'content': _controllers[index].text,
'imgPaths': _getImageListForIndex(index),
// 这里只是便于 _buildItem 使用;真实的完整 map 存在于 _itemsData
};
return _buildItem(index, itemMap);
}).toList(),
],
),
],
),
);
}
Widget _buildItem(int index, Map<String, dynamic> fullItemMap) {
final text = _controllers[index].text;
final imageList = _getImageListForIndex(index);
final itemTitle =
widget.operationType == OperationType.check
? '检查情况${index + 1}'
: '其他安全措施${index + 1}';
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行(显示"检查情况1、2、3..."
if (_controllers.length > 1)
Padding(
padding: const EdgeInsets.only(top: 0, left: 8, right: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (index > 0)
Text(
itemTitle,
style: TextStyle(
fontSize: widget.fontSize,
fontWeight: FontWeight.bold,
),
),
if (widget.isEditable && index > 0)
IconButton(
onPressed: () => _removeItem(index),
icon: const Icon(
Icons.close,
color: Colors.red,
size: 30,
),
),
],
),
),
// 内容区域
Padding(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 7),
child: SizedBox(
width: double.maxFinite,
child: DottedBorderBox(
child:
widget.isEditable
? TextField(
controller: _controllers[index],
decoration: InputDecoration(
hintText: widget.hintText,
),
focusNode: _focusNodes[index],
keyboardType: TextInputType.multiline,
maxLines: 3,
style: TextStyle(fontSize: widget.fontSize),
)
: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text.isEmpty ? '暂无内容' : text,
style: TextStyle(
fontSize: widget.fontSize,
color: text.isEmpty ? Colors.grey : Colors.black,
),
),
),
),
),
),
// 图片区域(编辑态)
if (widget.isEditable && widget.isAddImage)
RepairedPhotoSection(
title: '图片',
maxCount: 1,
isEdit: widget.isEditable,
followInitialUpdates: true,
// 关键:只传当前条目的图片数组
initialMediaPaths: imageList,
// 当本地文件变化(用户选择/删除)回调,用它同步到 _itemsData
onChanged: (files) {
// setState(() {
// if (files.isNotEmpty) {
// imageList[index] = files.first.path;
// } else {
// imageList[index] = '';
// }
// });
// _onDataChanged();
},
onMediaAdded: (localPath) async {
// 也可能单独使用该回调,本处把它当成新增单张图片
final List<String> current = _getImageListForIndex(index);
current.add(localPath);
setState(() {
_setImageListForIndex(index, current);
});
_onDataChanged();
},
onMediaRemoved: (localPath) async {
final List<String> current = _getImageListForIndex(index);
current.removeWhere((p) => p == localPath);
setState(() {
_setImageListForIndex(index, current);
});
_onDataChanged();
},
onMediaTapped: (path) async {
presentOpaque(SingleImageViewer(imageUrl: path), context);
},
onAiIdentify: () {},
),
// 非编辑态显示只读图片
if (!widget.isEditable && imageList.isNotEmpty)
_buildReadOnlyImage(imageList.first),
],
),
);
}
Widget _buildReadOnlyImage(String imagePath) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
'图片:',
style: TextStyle(
fontSize: widget.fontSize - 1,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
GestureDetector(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: _getImageProvider(imagePath),
fit: BoxFit.cover,
),
),
),
onTap: () {
presentOpaque(SingleImageViewer(imageUrl: imagePath), context);
},
),
],
),
);
}
ImageProvider _getImageProvider(String imagePath) {
if (imagePath.startsWith('http')) {
return NetworkImage(imagePath);
} else {
return FileImage(File(imagePath));
}
}
}