QinGang_interested/lib/customWidget/item_list_widget.dart

1521 lines
51 KiB
Dart
Raw Permalink 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_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)),
],
),
);
},
);
}
}