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; final hintText = hint ?? AppLocalizations.of(context).get('pleaseSelectOrFill'); final displayText = hasValue ? value! : hintText; // 值文本长(≥8 个中文字符宽度)→ 标签最多 1/3 // 值文本短 → 标签最多 2/3 final textScaler = MediaQuery.textScalerOf(context); final valEstimate = textScaler.scale(displayText.length * 14.0); final valueNeedsMore = valEstimate > 110; final labelMaxRatio = valueNeedsMore ? 1 / 3 : 2 / 3; final labelStyle = TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textSecondary, ); final asteriskStyle = TextStyle( fontSize: AppFontSizes.subtitle, color: colors.danger, ); final valueTextStyle = TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: bold ? FontWeight.w600 : FontWeight.normal, color: hasValue ? (valueColor ?? colors.textPrimary) : colors.textPlaceholder, ); return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( padding: EdgeInsets.zero, child: Builder( key: ValueKey(displayText), builder: (ctx) => LayoutBuilder( builder: (ctx, outer) { final labelMaxWidth = (outer.maxWidth - 16) * labelMaxRatio; return Row( children: [ // ── 标签:自然宽度,ConstrainedBox 上限 ── ConstrainedBox( constraints: BoxConstraints(maxWidth: labelMaxWidth), child: _LabelContent( label: label, required: required, textScaler: textScaler, labelStyle: labelStyle, asteriskStyle: asteriskStyle, ), ), const SizedBox(width: 16), // ── 值:Expanded 填满剩余空间 → 靠右 ── Expanded( child: _ValueContent( displayText: displayText, hasValue: hasValue, readOnly: readOnly, showArrow: showArrow, bold: bold, showMoreOnOverflow: showMoreOnOverflow, valueColor: valueColor, onClear: onClear, onShowFull: (text) => _showFullValue(context, text), textStyle: valueTextStyle, ), ), ], ); }, ), ), ), ); } 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); }, ), ), ); } } /// 标签内容:文字 + 必填星号 + 溢出 tooltip class _LabelContent extends StatelessWidget { final String label; final bool required; final TextScaler textScaler; final TextStyle labelStyle; final TextStyle asteriskStyle; const _LabelContent({ required this.label, required this.required, required this.textScaler, required this.labelStyle, required this.asteriskStyle, }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final labelTp = TextPainter( text: TextSpan(text: label, style: labelStyle), textDirection: TextDirection.ltr, textScaler: textScaler, maxLines: 1, )..layout(); final astWidth = required ? (TextPainter( text: TextSpan(text: ' *', style: asteriskStyle), textDirection: TextDirection.ltr, textScaler: textScaler, maxLines: 1, )..layout()).width : 0.0; final overflows = (labelTp.width + astWidth) > constraints.maxWidth; return Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( label, style: labelStyle, maxLines: 1, softWrap: false, overflow: TextOverflow.fade, ), ), if (required) Text(' *', style: asteriskStyle), if (overflows) GestureDetector( onTap: () {}, child: Tooltip( message: label, triggerMode: TooltipTriggerMode.tap, preferBelow: false, child: Padding( padding: const EdgeInsets.only(left: 2), child: Icon( Icons.info_outline, size: 14, color: Colors.grey, ), ), ), ), ], ); }, ); } } /// 值内容:文字 + clear / more / chevron 图标 class _ValueContent extends StatelessWidget { final String displayText; final bool hasValue; final bool readOnly; final bool showArrow; final bool bold; final bool showMoreOnOverflow; final Color? valueColor; final VoidCallback? onClear; final ValueChanged onShowFull; final TextStyle textStyle; const _ValueContent({ required this.displayText, required this.hasValue, required this.readOnly, required this.showArrow, required this.bold, required this.showMoreOnOverflow, required this.valueColor, required this.onClear, required this.onShowFull, required this.textStyle, }); @override Widget build(BuildContext context) { final textScaler = MediaQuery.textScalerOf(context); return LayoutBuilder( builder: (context, constraints) { final tp = TextPainter( text: TextSpan(text: displayText, style: textStyle), textDirection: TextDirection.ltr, textScaler: textScaler, maxLines: 1, )..layout(); double iconSpace = 0; if (hasValue && onClear != null) { iconSpace = 20; } else if (showArrow && !readOnly) { iconSpace = 18; } final overflows = tp.width > (constraints.maxWidth - iconSpace); return Row( children: [ Expanded( child: Text( displayText, maxLines: 1, softWrap: false, overflow: (hasValue && readOnly && showMoreOnOverflow && overflows) ? 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 && overflows) GestureDetector( onTap: () => onShowFull(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.grey), ], ], ); }, ); } }