import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; import '../../core/i18n/app_localizations.dart'; /// 表单字段行,左侧标签 + 右侧值 + 可选箭头。 /// /// [required] 为 true 时标签前显示红色星号。 class FormFieldRow extends StatelessWidget { final String label; final String? value; final String? hint; final bool showArrow; final bool readOnly; final bool required; final VoidCallback? onTap; final VoidCallback? onClear; final bool bold; final bool showMoreOnOverflow; final Color? valueColor; const FormFieldRow({ super.key, required this.label, this.value, this.hint, this.showArrow = true, this.readOnly = false, this.required = false, this.onTap, this.onClear, this.bold = false, this.showMoreOnOverflow = true, this.valueColor, }); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final hasValue = value != null && value!.isNotEmpty; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( padding: EdgeInsets.zero, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( flex: 1, child: LayoutBuilder( builder: (context, constraints) { final labelStyle = TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textSecondary, ); final requiredStyle = TextStyle( fontSize: AppFontSizes.subtitle, color: colors.danger, ); final labelText = label; final reqText = required ? ' *' : ''; // 测量完整标签宽度 final fullTp = TextPainter( text: TextSpan( text: '$labelText$reqText', style: labelStyle, children: required ? [TextSpan(text: reqText, style: requiredStyle)] : null, ), textDirection: TextDirection.ltr, textScaler: MediaQuery.textScalerOf(context), maxLines: 1, )..layout(); final labelOverflows = fullTp.width > constraints.maxWidth; return Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text.rich( TextSpan( children: [ TextSpan(text: labelText, style: labelStyle), if (required) TextSpan(text: reqText, style: requiredStyle), ], ), maxLines: 1, softWrap: false, overflow: TextOverflow.fade, ), ), if (labelOverflows) GestureDetector( onTap: () {}, child: Tooltip( message: labelText, triggerMode: TooltipTriggerMode.tap, preferBelow: false, child: Padding( padding: const EdgeInsets.only(left: 2), child: Icon( Icons.info_outline, size: 14, color: colors.textPlaceholder, ), ), ), ), ], ); }, ), ), const SizedBox(width: 16), Expanded( flex: 2, child: LayoutBuilder( builder: (context, constraints) { final textStyle = TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: bold ? FontWeight.w600 : FontWeight.normal, color: hasValue ? (valueColor ?? colors.textPrimary) : colors.textPlaceholder, ); final displayText = hasValue ? value! : (hint ?? AppLocalizations.of( context, ).get('pleaseSelectOrFill')); // 测量文字宽度(需传入 textScaler,否则按 1.0 倍测量, // 与 Text 组件的实际渲染宽度不一致) final tp = TextPainter( text: TextSpan(text: displayText, style: textStyle), textDirection: TextDirection.ltr, textScaler: MediaQuery.textScalerOf(context), maxLines: 1, )..layout(); // 预先计算图标占位(close/chevron 固定出现,iconSpace > 0) // 更多图标是条件出现的,iconSpace 保持 0,不预扣宽度 double iconSpace = 0; if (hasValue && onClear != null) { iconSpace = 20; } else if (showArrow && !readOnly) { iconSpace = 18; } final textOverflows = tp.width > (constraints.maxWidth - iconSpace); return Row( children: [ Expanded( child: Text( displayText, maxLines: 1, softWrap: false, overflow: (hasValue && readOnly && showMoreOnOverflow && textOverflows) ? TextOverflow.fade : TextOverflow.ellipsis, textAlign: TextAlign.end, style: textStyle, ), ), if (hasValue && onClear != null) GestureDetector( onTap: onClear, child: const Padding( padding: EdgeInsets.only(left: 4), child: Icon( Icons.close, size: 16, color: Color(0xFFBBBBBB), ), ), ) else if (hasValue && readOnly && showMoreOnOverflow && textOverflows) GestureDetector( onTap: () => _showFullValue(context, displayText), child: const Padding( padding: EdgeInsets.only(left: 4), child: Icon( Icons.more_horiz, size: 16, color: Color(0xFFBBBBBB), ), ), ) else if (showArrow && !readOnly) ...[ const SizedBox(width: 4), Icon( Icons.chevron_right, size: 14, color: colors.textPlaceholder, ), ], ], ); }, ), ), ], ), ), ); } void _showFullValue(BuildContext context, String text) { final l10n = AppLocalizations.of(context); showDialog( context: context, builder: (ctx) => TDAlertDialog( title: label, content: text, buttonStyle: TDDialogButtonStyle.text, rightBtn: TDDialogButtonOptions( title: l10n.get('close'), action: () => Navigator.pop(ctx), ), leftBtn: TDDialogButtonOptions( title: l10n.get('copy'), action: () { Clipboard.setData(ClipboardData(text: text)); Navigator.pop(ctx); }, ), ), ); } }