QinGang_interested/lib/tools/MultiTextFieldWithTitle.dart

486 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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