QinGang_interested/lib/customWidget/item_list_widget.dart

1521 lines
51 KiB
Dart
Raw Normal View History

2025-12-12 09:11:30 +08:00
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/tools/tools.dart';
import 'package:flutter/services.dart';
class ItemListWidget {
static const Color detailtextColor = Colors.black54;
static const double requiredInset = 0;
static const double horizontal_inset = 12;
static const double vertical_inset = 5;
/// 单行水平排列:
/// - 可编辑时:标题 + TextField
/// - 不可编辑时:标题 + 带省略号的文本
static Widget singleLineTitleText({
required String label, // 标题文本
required bool isEditable, // 是否可编辑
String? text, // 显示的初始文本(编辑/非编辑模式都会显示)
String hintText = '请输入',
double fontSize = 14, // 字体大小
bool isRequired = true,
bool strongRequired = false,
ValueChanged<String>? onChanged,
ValueChanged<String>? onFieldSubmitted,
int maxLines = 5,
bool showMaxLength = false,
// 新增:数字输入控制
bool isNumericInput = false,
int maxDecimalPlaces = 2,
TextInputType keyboardType = TextInputType.text,
}) {
// 数字输入键盘
final actualKeyboardType =
isNumericInput
? const TextInputType.numberWithOptions(decimal: true)
: keyboardType;
// 数字输入格式化器
final List<TextInputFormatter>? numericFormatters =
isNumericInput
? [
FilteringTextInputFormatter.allow(RegExp(r'[\d\.]')),
TextInputFormatter.withFunction((oldValue, newValue) {
final newText = newValue.text;
if (newText.isEmpty) return newValue;
if (newText.split('.').length > 2) return oldValue;
final regex = RegExp(
r'^\d*\.?\d{0,' + maxDecimalPlaces.toString() + r'}$',
);
if (regex.hasMatch(newText)) return newValue;
return oldValue;
}),
]
: null;
return Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Row(
mainAxisAlignment:
isEditable
? MainAxisAlignment.start
: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
if ((isRequired && isEditable) || strongRequired)
Text('* ', style: TextStyle(color: Colors.red)),
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(width: 8),
/// 可编辑模式
isEditable
? Expanded(
child: TextFormField(
initialValue: text ?? '',
autofocus: false,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
keyboardType: actualKeyboardType,
maxLength: showMaxLength ? 120 : null,
style: TextStyle(fontSize: fontSize),
maxLines: 1,
inputFormatters: numericFormatters,
decoration: InputDecoration(
isDense: true,
hintText: hintText,
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
),
)
/// 只读模式
: Expanded(
child: Text(
text ?? '',
maxLines: maxLines,
style: TextStyle(fontSize: fontSize, color: detailtextColor),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
/// 多行垂直排列:
/// - 可编辑时:标题 + 可扩展的多行 TextField
/// - 不可编辑时:标题 + 带滚动的多行文本
static Widget multiLineTitleTextField({
required String label, // 标题文本
required bool isEditable, // 是否可编辑
TextEditingController? controller, // 编辑时使用的控制器
String? text, // 不可编辑时显示的文本
double fontSize = 14, // 字体大小
double height = 110, // 整体高度
bool isRequired = true,
String hintText = '请输入',
ValueChanged<String>? onChanged,
bool showMaxLength = false,
}) {
return Container(
// 统一左右 padding保证标题和内容在同一左侧基线
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 12),
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (isRequired && isEditable)
Text('* ', style: TextStyle(color: Colors.red)),
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Expanded(
child:
isEditable
? TextFormField(
autofocus: false,
initialValue: controller == null ? text : null,
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
onChanged: onChanged,
maxLength: showMaxLength ? 120 : null,
// 垂直顶部对齐
textAlignVertical: TextAlignVertical.top,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: hintText,
// 去掉 TextField 默认内边距
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
),
)
: SingleChildScrollView(
// 去掉多余的 padding
padding: EdgeInsets.zero,
child: Text(
text ?? '',
style: TextStyle(
fontSize: fontSize,
color: detailtextColor,
),
),
),
),
],
),
);
}
static Widget multiLineAutoTitleTextField({
required String label,
required bool isEditable,
TextEditingController? controller,
String? text,
double fontSize = 14,
double height = 110, // 编辑时保留原行为
bool isRequired = true,
String hintText = '请输入',
ValueChanged<String>? onChanged,
bool showMaxLength = false,
double? maxDisplayHeight, // 可选:在父无高度约束时作为“可用高度”参考
}) {
return LayoutBuilder(
builder: (context, constraints) {
// 内边距(与你的 UI 保持一致)
const horizontalPadding = 12.0;
const verticalPadding = 5.0;
final content = text ?? '';
final textStyle = TextStyle(fontSize: fontSize, color: detailtextColor);
// 编辑状态:保持原来固定 height 与 expands:true 的行为
if (isEditable) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: verticalPadding,
horizontal: horizontalPadding,
),
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (isRequired)
const Text('* ', style: TextStyle(color: Colors.red)),
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Expanded(
child: TextFormField(
autofocus: false,
initialValue: controller == null ? text : null,
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
onChanged: onChanged,
maxLength: showMaxLength ? 120 : null,
textAlignVertical: TextAlignVertical.top,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: hintText,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
),
),
),
],
),
);
}
// ---------- 不可编辑:测量并决定是否需要滚动 ----------
// 计算可用于内容的最大宽度(减去左右 padding
final maxWidth =
(constraints.maxWidth.isFinite
? constraints.maxWidth
: MediaQuery.of(context).size.width) -
horizontalPadding * 2;
final safeMaxWidth =
maxWidth > 0
? maxWidth
: MediaQuery.of(context).size.width - horizontalPadding * 2;
// 使用 TextPainter 测量文本高度(不限制行数)
final tp = TextPainter(
text: TextSpan(text: content, style: textStyle),
textDirection: TextDirection.ltr,
textWidthBasis: TextWidthBasis.parent,
);
tp.layout(maxWidth: safeMaxWidth);
final textHeight = tp.size.height;
// 估算标签与间距所需高度
final labelHeight = fontSize + 8; // label + 上下间距估算
final neededHeight = labelHeight + 8 + textHeight + verticalPadding * 2;
// availableHeight若父是 bounded 则用 constraints.maxHeight否则使用 maxDisplayHeight 或屏幕高度比例作为阈值
double availableHeight;
if (constraints.maxHeight.isFinite) {
availableHeight = constraints.maxHeight;
} else {
availableHeight =
maxDisplayHeight ?? MediaQuery.of(context).size.height * 0.4;
}
final needsScroll = neededHeight > availableHeight;
// 返回 Widget不固定高度
return Container(
padding: const EdgeInsets.symmetric(
vertical: verticalPadding,
horizontal: horizontalPadding,
),
// 不设置 height让其在可扩展父容器中自然撑开
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (isRequired)
const Text('* ', style: TextStyle(color: Colors.red)),
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
// 如果需要滚动则约束高度并内包 SingleChildScrollView
if (needsScroll)
// ConstrainedBox 限制最大高度,避免无限膨胀
ConstrainedBox(
constraints: BoxConstraints(
// 留出 label 的高度,最大不超过 availableHeight - labelHeight
maxHeight: (availableHeight - labelHeight - 16).clamp(
80.0,
availableHeight,
),
),
child: SingleChildScrollView(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
child: Text(content, style: textStyle),
),
)
else
// 父高度足够,直接展示文本(自然撑高)
Text(content, style: textStyle),
],
),
);
},
);
}
/// 单行可点击选择:
/// - 可编辑时:标题 + “请选择”提示 + 右箭头
/// - 不可编辑时:标题 + 文本内容
static Widget selectableLineTitleTextRightButton({
required String label, // 标题文本
required bool isEditable, // 是否可点击
required String text, // 显示内容或提示
VoidCallback? onTap, // 点击回调
double fontSize = 14, // 字体大小
bool isClean = false,
bool isTip = false,
VoidCallback? onTapClean, // 清除回调
VoidCallback? onTapTip, // 提醒回调
bool isRequired = true,
String cleanText = '清除',
bool strongRequired = false,
double horizontalnum = horizontal_inset,
double verticalInset = vertical_inset,
}) {
return InkWell(
onTap: isEditable ? onTap : null,
child: Container(
padding: EdgeInsets.symmetric(
vertical: verticalInset,
horizontal: horizontalnum,
),
child: Row(
children: [
// 1. 标题
Row(
children: [
if ((isRequired && isEditable) || strongRequired)
Text('* ', style: TextStyle(color: Colors.red)),
Row(
children: [
Text(
label,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
if (isTip)
Column(
children: [
IconButton(
onPressed: onTapTip,
icon: Icon(
Icons.error_outline,
color: Colors.blue,
size: 20,
),
),
const SizedBox(),
],
),
],
),
],
),
if (isClean)
Column(
children: [
CustomButton(
text: cleanText,
height: 20,
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 0),
textStyle: TextStyle(fontSize: 11, color: Colors.white),
borderRadius: 10,
backgroundColor:
cleanText.contains('清除') ? Colors.red : Colors.green,
onPressed: onTapClean,
),
SizedBox(height: 20),
],
),
const SizedBox(width: 8),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: Text(
text.isNotEmpty ? text : (isEditable ? '请选择' : ''),
maxLines: 5,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: fontSize,
color:
isEditable
? (text == '请选择'
? Colors.black87
: Colors.black)
: detailtextColor,
),
),
),
if (isEditable)
const Padding(
padding: EdgeInsets.only(left: 4),
child: Icon(Icons.chevron_right, size: 20),
),
],
),
),
],
),
),
);
}
/// 单行可点击选择:
/// - 可编辑时:标题 + “请选择”提示 + 文本框
/// - 不可编辑时:标题 + 文本内容
static Widget selectableLineTitleTextField({
required String label,
required bool isEditable,
required String text,
VoidCallback? onTap,
double fontSize = 14,
bool isClean = false,
VoidCallback? onTapClean,
bool isRequired = true,
String cleanText = '清除',
TextEditingController? controller,
}) {
return InkWell(
onTap: isEditable ? onTap : null,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 标题部分
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isRequired && isEditable)
const Text('* ', style: TextStyle(color: Colors.red)),
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
// const SizedBox(width: 5),
// 右侧 TextField + 清除按钮
Expanded(
child: Row(
children: [
// 清除按钮
if (isClean && onTapClean != null)
Column(
children: [
CustomButton(
text: cleanText,
height: 20,
padding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 0,
),
textStyle: TextStyle(
fontSize: 11,
color: Colors.white,
),
borderRadius: 10,
backgroundColor:
cleanText == '清除' ? Colors.red : Colors.green,
onPressed: onTapClean,
),
SizedBox(height: 20),
],
),
// 输入框
Expanded(
child: TextField(
controller: controller,
autofocus: false,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: '请输入',
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(vertical: 8),
border: InputBorder.none,
),
),
),
],
),
),
],
),
),
);
}
/// 两行垂直布局:
/// 第一行:可点击选择(带箭头)或仅显示标题
/// 第二行:多行输入框或多行文本展示
static Widget twoRowSelectableTitleText({
required String label, // 第一行标题
required bool isEditable, // 是否可编辑
required String text, // 显示内容或提示
TextEditingController? controller, // 第二行编辑控制器
VoidCallback? onTap, // 第一行点击回调
double fontSize = 14, // 字体大小
double row2Height = 80, // 第二行高度
bool isRequired = true,
bool showSelect = true, //是否显示选择
}) {
return Container(
padding: EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:可点击区域或纯文本标题
InkWell(
onTap: isEditable ? onTap : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
if (isRequired && isEditable)
Text('* ', style: TextStyle(color: Colors.red)),
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
if (showSelect)
Row(
children: [
Text(
isEditable ? '请选择' : '',
style: TextStyle(
fontSize: fontSize,
color: isEditable ? Colors.black : detailtextColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (isEditable) const Icon(Icons.chevron_right),
],
),
],
),
),
const SizedBox(height: 8),
Container(
height: row2Height,
padding: const EdgeInsets.symmetric(vertical: 8),
child:
isEditable
? TextField(
autofocus: false,
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: '请输入',
//contentPadding: EdgeInsets.zero,
border: InputBorder.none,
),
)
: SingleChildScrollView(
padding: EdgeInsets.zero,
child: Text(
text,
style: TextStyle(
fontSize: fontSize,
color: detailtextColor,
),
),
),
),
],
),
);
}
/// 两行垂直布局:
/// 标题 + 按钮
/// 第二行:多行输入框或多行文本展示
static Widget twoRowButtonTitleText({
required String label, // 第一行标题
required bool isEditable, // 是否可编辑
bool isInput = true, // 是否可输入
required String text, // 显示内容或提示
TextEditingController? controller, // 第二行编辑控制器
required VoidCallback? onTap, // 第一行点击回调
String buttonText = '选择其他',
required String hintText,
double fontSize = 14, // 字体大小
double row2Height = 80, // 第二行高度
bool isRequired = true,
}) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:标题 + 按钮
InkWell(
child: Row(
children: [
Flexible(
fit: FlexFit.loose, // loose 模式下它可以比最大宽度更小
child: Row(
children: [
if (isRequired && isEditable)
Text('* ', style: TextStyle(color: Colors.red)),
Flexible(
child: Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(width: 8),
if (isEditable)
CustomButton(
text: buttonText,
height: 30,
padding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 10,
),
textStyle: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
backgroundColor: Colors.blue,
onPressed: onTap,
),
],
),
),
const SizedBox(height: 8),
Container(
height: row2Height,
padding: const EdgeInsets.symmetric(vertical: 8),
child:
(isEditable && isInput)
? TextField(
autofocus: false,
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: hintText,
//contentPadding: EdgeInsets.zero,
border: InputBorder.none,
),
)
: SingleChildScrollView(
padding: EdgeInsets.zero,
child: Text(
text,
style: TextStyle(
fontSize: fontSize,
color: detailtextColor,
),
),
),
),
],
),
);
}
/// 单行布局:
/// 标题 + 文字 + 按钮
static Widget OneRowButtonTitleText({
required String label, // 标题
required String text, // 显示内容或提示
required VoidCallback? onTap, // 第一行点击回调
double fontSize = 14, // 字体大小
bool isEdit = true,
String buttonText = '气体分析详情',
double horizontalnum = horizontal_inset,
}) {
return Container(
color: Colors.white,
padding: EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontalnum,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 15),
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: fontSize,
color: detailtextColor,
),
),
),
],
),
),
if (isEdit)
CustomButton(
text: buttonText,
height: 30,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 10),
textStyle: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
backgroundColor: Colors.blue,
onPressed: onTap,
),
],
),
);
}
/// 单行布局:
/// 标题 + 按钮
static Widget OneRowButtonTitle({
required String label, // 标题
required String buttonText, // 按钮文字
required VoidCallback onTap, // 第一行点击回调
double fontSize = 14, // 字体大小
Color btnColor = Colors.blue,
bool isRequired = false,
}) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text("* ", style: TextStyle(color: Colors.red)),
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
CustomButton(
text: buttonText,
height: 30,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
backgroundColor: btnColor,
onPressed: onTap,
),
],
),
);
}
/// 单行布局:
/// 标题 + 按钮(挨着)
static Widget OneRowStartButtonTitle({
required String label, // 标题
String buttonText = '气体分析详情', // 按钮文字
String text = '', // 标题
required VoidCallback onTap, // 点击回调
double fontSize = 14, // 字体大小
Color btnColor = Colors.blue,
}) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold),
),
CustomButton(
text: buttonText,
height: 30,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
backgroundColor: btnColor,
onPressed: onTap,
),
],
),
);
}
/// 单行布局:
/// 标题 + 网络图片
static Widget OneRowImageTitle({
required String label, // 标题
required String imgPath, // 图片路径
double fontSize = 14, // 字体大小
Color btnColor = Colors.blue,
bool isRequired = false,
String text = '',
void Function(String)? onTapCallBack,
}) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
Column(
children: [
GestureDetector(
onTap: () {
if (onTapCallBack != null)
onTapCallBack('${ApiService.baseImgPath}$imgPath');
},
child:
imgPath.isNotEmpty
? Image.network(
'${ApiService.baseImgPath}${imgPath}',
width: 80,
height: 80,
)
: SizedBox(),
),
if (text.isNotEmpty) Text(text),
],
),
],
),
);
}
/// 单行布局:
/// 图片 + 标题 +箭头
static Widget OneRowImageArrowTitle({
required String label, // 标题
required String imgPath, // 图片路径
double fontSize = 14, // 字体大小
Color btnColor = Colors.black,
}) {
return Card(
shape: RoundedRectangleBorder(
// 形状
borderRadius: BorderRadius.circular(8), // 圆角
),
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(14),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Image.asset(width: 40, height: 40, imgPath),
SizedBox(width: 20),
Text(label, style: TextStyle(fontSize: 14)),
Spacer(),
Icon(Icons.chevron_right, color: Colors.grey[400]),
],
),
),
);
}
/// 两行垂直布局:
/// 标题
/// 第二行:图片
static Widget twoRowTitleAndImages({
required String title, // 第一行标题
required List<dynamic>? imageUrls,
double row2Height = 80, // 第二行高度
double fontSize = 14, // 字体大小
void Function(String)? onTapCallBack,
bool isRequired = true,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题部分
if (title.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Text(
title,
style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold),
),
),
// 图片横向滚动区域
SizedBox(
height: 80, // 图片区域固定高度
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: imageUrls?.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.only(right: 8), // 图片间距
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GestureDetector(
onTap: () {
if (onTapCallBack != null)
onTapCallBack(
'${ApiService.baseImgPath}${imageUrls![index] ?? ''}',
);
},
child: Image.network(
'${ApiService.baseImgPath}${imageUrls![index] ?? ''}',
width: 80,
// 图片宽度
height: 80,
// 图片高度
fit: BoxFit.fill,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 80,
height: 80,
color: Colors.grey[200],
child: const Center(
child: CircularProgressIndicator(),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
width: 80,
height: 80,
color: Colors.transparent,
child: SizedBox(),
);
},
),
),
),
);
},
),
),
],
);
}
/// 多行垂直布局:
/// 标题+按钮
/// 编辑框列表,多个编辑框可删除
static Widget mulRowTitleAndTextField({
required String label, // 第一行标题
required bool isEditable, // 是否可编辑
required String text, // 显示内容或提示
TextEditingController? controller, // 第二行编辑控制器
required VoidCallback? onTap, // 第一行点击回调
required String hintText,
double fontSize = 14, // 字体大小
double row2Height = 80, // 第二行高度
bool isRequired = true,
}) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:标题 + 按钮
InkWell(
child: Row(
children: [
Flexible(
fit: FlexFit.loose, // loose 模式下它可以比最大宽度更小
child: Row(
children: [
if (isRequired && isEditable)
Text('* ', style: TextStyle(color: Colors.red)),
Flexible(
child: Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(width: 8),
if (isEditable)
CustomButton(
text: "选择其他",
height: 30,
padding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 5,
),
backgroundColor: Colors.blue,
onPressed: onTap,
),
],
),
),
const SizedBox(height: 8),
Container(
height: row2Height,
padding: const EdgeInsets.symmetric(vertical: 8),
child:
isEditable
? TextField(
autofocus: false,
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: hintText,
//contentPadding: EdgeInsets.zero,
border: InputBorder.none,
),
)
: SingleChildScrollView(
padding: EdgeInsets.zero,
child: Text(
text,
style: TextStyle(
fontSize: fontSize,
color: detailtextColor,
),
),
),
),
],
),
);
}
/// 多行垂直布局:
/// 标题、图片、说明、签字信息
static Widget mulColumnRowTitleAndImages({
required String title, // 第一行标题
required List<dynamic>? imageUrls,
required String text, // 描述
required List<dynamic>? signUrls,
/// 签字
required List<dynamic>? signTimes,
/// 签字时间
double row2Height = 80, // 第二行高度
double fontSize = 14, // 字体大小
void Function(String)? onTapCallBack,
bool isRequired = true,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题部分
Padding(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Text(
title,
style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold),
),
),
// 图片横向滚动区域
SizedBox(
height: 80, // 图片区域固定高度
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: imageUrls?.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.only(right: 8), // 图片间距
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GestureDetector(
onTap: () {
if (onTapCallBack != null)
onTapCallBack(
'${ApiService.baseImgPath}${imageUrls![index] ?? ''}',
);
},
child: Image.network(
'${ApiService.baseImgPath}${imageUrls![index] ?? ''}',
width: 80,
// 图片宽度
height: 80,
// 图片高度
fit: BoxFit.fill,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 80,
height: 80,
color: Colors.grey[200],
child: const Center(
child: CircularProgressIndicator(),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
width: 80,
height: 80,
color: Colors.transparent,
child: SizedBox(),
);
},
),
),
),
);
},
),
),
Row(),
],
);
}
/// 两行垂直布局:
/// 标题
/// 第二行:多行文本展示
static Widget twoRowTitleText({
required String label, // 第一行标题
required String text, // 显示内容或提示
double fontSize = 14, // 字体大小
bool isRequired = true,
}) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: vertical_inset,
horizontal: horizontal_inset,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:标题
Text(
label,
style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
text,
maxLines: 5,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: fontSize, color: detailtextColor),
),
],
),
);
}
static Widget itemContainer(
Widget child, {
double horizontal = horizontal_inset,
double vertical = vertical_inset,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
child: child,
);
}
// static Widget aaa({}){
// return
// }
/// 安全环保检查步骤
static Widget buildFlowStepItem({
required List<Map<String, dynamic>> flowList,
}) {
final int lastDoneIndex = flowList.lastIndexWhere((e) => e['STATUS'] == 1);
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
itemCount: flowList.length + 1, // +1 用来放标题
itemBuilder: (context, i) {
if (i == 0) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'查看流程图',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
);
}
final idx = i - 1;
final item = flowList[idx];
final bool isFirst = idx == 0;
final bool isLast = idx == flowList.length - 1;
// 根据 lastDoneIndex 自动计算“进行中”
final int status;
if (idx <= lastDoneIndex) {
status = 1; // 已完成
} else if (idx == lastDoneIndex + 1) {
status = 0; // 进行中
} else {
status = -1; // 未到达
}
// 依据状态设色
final Color dotColor =
status == 1
? Colors.green
: (status == 0 ? Colors.blue : Colors.grey);
final Color textColor =
status == 1
? Colors.green
: (status == 0 ? Colors.blue : Colors.black);
return ListTile(
visualDensity: VisualDensity(vertical: -4),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: Container(
width: 24,
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
// 上方线段或占位
isFirst
? SizedBox(height: 6 + 5)
: Expanded(
child: Container(width: 1, color: Colors.grey[300]),
),
// 圆点
CircleAvatar(radius: 6, backgroundColor: dotColor),
// 下方线段或占位
isLast
? SizedBox(height: 6 + 5)
: Expanded(
child: Container(width: 1, color: Colors.grey[300]),
),
],
),
),
title: Text(
item['STEP_NAME'] ?? '',
style: TextStyle(color: textColor, fontSize: 15),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item['SIGN_USER'] != null) ...[
Text(
item['SIGN_USER'],
style: TextStyle(color: textColor, fontSize: 13),
),
] else if (item['FINISHED_SIGN_USER'] != null) ...[
Text(
item['FINISHED_SIGN_USER'],
style: TextStyle(color: textColor, fontSize: 13),
),
] else if (item['ACT_USER_NAME'] != null) ...[
Text(
item['ACT_USER_NAME'],
style: TextStyle(color: textColor, fontSize: 13),
),
],
if (item['ACT_TIME'] != null)
Text(
item['ACT_TIME'],
style: TextStyle(color: textColor, fontSize: 13),
),
],
),
);
},
);
}
/// 特殊作业步骤流程图
static Widget specialBuildFlowStepItem({
required List<Map<String, dynamic>> flowList,
}) {
// status: 1 已完成, 0 当前步骤, -99 未开始, 2 已打回, -1 已跳过
final int lastDoneIndex = flowList.lastIndexWhere((e) {
final s = e['status'];
if (s is int) return s == 1;
if (s is String) return int.tryParse(s) == 1;
return false;
});
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
itemCount: flowList.length + 1, // +1 用来放标题
itemBuilder: (context, i) {
if (i == 0) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'查看流程图',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
);
}
final idx = i - 1;
final item = flowList[idx];
final bool isFirst = idx == 0;
final bool isLast = idx == flowList.length - 1;
// 尝试读取 item 中的 status支持 int 或可解析为 int 的 String
int? statusFromItem;
final rawStatus = item['status'];
if (rawStatus is int) {
statusFromItem = rawStatus;
} else if (rawStatus is String) {
statusFromItem = int.tryParse(rawStatus);
}
// 如果 item 中没有 status则回退到根据 lastDoneIndex 推断的逻辑
final int status =
statusFromItem ??
(idx <= lastDoneIndex
? 1 // 已完成
: (idx == lastDoneIndex + 1 ? 0 : -99)); // 0=当前,-99=未开始
// 颜色映射1 -> 绿0 -> 蓝,其它 -> 灰
final Color dotColor =
status == 1
? Colors.green
: (status == 0 ? Colors.blue : Colors.grey);
final Color textColor =
status == 1
? Colors.green
: (status == 0 ? Colors.blue : Colors.black);
// 使用新的字段名stepName, actUserName, actTime
final String title = (item['stepName'] ?? '').toString();
final String? user =
(item['actUserName'] ?? item['ACT_USER_NAME'] ?? item['SIGN_USER'])
?.toString();
final String? time = (item['actTime'] ?? item['ACT_TIME'])?.toString();
return ListTile(
visualDensity: VisualDensity(vertical: -4),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: SizedBox(
width: 24,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
// 上方线段或占位
if (isFirst)
const SizedBox(height: 11) // 保持与原来相似的间距
else
Expanded(child: Container(width: 1, color: Colors.grey[300])),
// 圆点
CircleAvatar(radius: 6, backgroundColor: dotColor),
// 下方线段或占位
if (isLast)
const SizedBox(height: 11)
else
Expanded(child: Container(width: 1, color: Colors.grey[300])),
],
),
),
title: Text(title, style: TextStyle(color: textColor, fontSize: 15)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (user != null && user.isNotEmpty)
Text(user, style: TextStyle(color: textColor, fontSize: 13)),
if (rawStatus == -1)
Text('已跳过', style: TextStyle(color: textColor, fontSize: 13)),
if (time != null && time.isNotEmpty)
Text(time, style: TextStyle(color: textColor, fontSize: 13)),
],
),
);
},
);
}
}