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