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? onChanged, ValueChanged? 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? 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? 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? 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? 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? imageUrls, required String text, // 描述 required List? signUrls, /// 签字 required List? 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> 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> 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)), ], ), ); }, ); } }