import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import '../../../../core/logging/logger.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../shared/theme/app_color_palette.dart'; import '../../../../shared/theme/design_tokens.dart'; import '../../../../shared/widgets/divination/divination_terms.dart'; import '../../../../shared/widgets/divination/yao_glyph.dart'; import '../../../../shared/widgets/divination/yao_legend.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/apis/divination_api.dart'; import '../../data/models/divination_params.dart'; import '../../data/models/divination_result.dart'; import 'follow_up_chat_screen.dart'; class DivinationResultScreen extends StatefulWidget { const DivinationResultScreen({ super.key, required this.data, this.divinationApi, this.enableIntroTransition = false, }); final DivinationResultData data; final DivinationApi? divinationApi; final bool enableIntroTransition; @override State createState() => _DivinationResultScreenState(); } class _DivinationResultScreenState extends State { static final Logger _logger = getLogger('features.divination.result_screen'); bool _showIntro = false; bool _introCollapsed = false; Rect? _introTargetRect; final GlobalKey _stackKey = GlobalKey(); final GlobalKey _finalSignCardKey = GlobalKey(); bool _followUpEligibilityLoading = false; bool _canSendFollowUp = true; void _backToHome() { final navigator = Navigator.of(context); navigator.popUntil((route) => route.isFirst); } @override void initState() { super.initState(); if (widget.enableIntroTransition) { _showIntro = true; WidgetsBinding.instance.addPostFrameCallback((_) { _prepareIntro(); }); } _loadFollowUpEligibility(); } Future _loadFollowUpEligibility() async { if (widget.divinationApi == null || widget.data.threadId == null) { return; } setState(() { _followUpEligibilityLoading = true; }); try { final messages = await widget.divinationApi!.getSessionMessages( threadId: widget.data.threadId!, ); final userCount = messages.where((msg) => msg.role == 'user').length; if (!mounted) { return; } setState(() { _canSendFollowUp = userCount < 2; _followUpEligibilityLoading = false; }); } catch (error, stackTrace) { _logger.error( message: 'Failed to load follow-up eligibility', error: error, stackTrace: stackTrace, ); if (!mounted) { return; } setState(() { _canSendFollowUp = false; _followUpEligibilityLoading = false; }); } } Future _prepareIntro() async { for (int i = 0; i < 12; i++) { if (!mounted) { return; } if (_measureIntroTargetRect()) { break; } await Future.delayed(const Duration(milliseconds: 16)); } if (!mounted) { return; } _playIntro(); } bool _measureIntroTargetRect() { final stackContext = _stackKey.currentContext; final targetContext = _finalSignCardKey.currentContext; if (stackContext == null || targetContext == null) { return false; } final stackRender = stackContext.findRenderObject(); final targetRender = targetContext.findRenderObject(); if (stackRender is! RenderBox || targetRender is! RenderBox) { return false; } final offset = targetRender.localToGlobal( Offset.zero, ancestor: stackRender, ); final targetRect = offset & targetRender.size; if (_introTargetRect == targetRect) { return true; } setState(() { _introTargetRect = targetRect; }); return true; } Future _playIntro() async { await Future.delayed(const Duration(milliseconds: 180)); if (!mounted) { return; } setState(() { _introCollapsed = true; }); await Future.delayed(const Duration(milliseconds: 1450)); if (!mounted) { return; } setState(() { _showIntro = false; }); } Rect _introStartRect(Size size) { const startWidth = 332.0; const startHeight = 234.0; return Rect.fromLTWH( (size.width - startWidth) / 2, (size.height - startHeight) / 2, startWidth, startHeight, ); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final palette = Theme.of(context).extension()!; final l10n = AppLocalizations.of(context)!; return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (didPop) { return; } _backToHome(); }, child: Scaffold( backgroundColor: colors.surface, appBar: AppBar( leading: IconButton( onPressed: _backToHome, icon: const Icon(Icons.arrow_back_ios_new_rounded), ), backgroundColor: colors.surface, surfaceTintColor: colors.surface, title: Text(l10n.resultScreenTitle), centerTitle: true, ), bottomNavigationBar: _buildFollowUpBar(context), body: LayoutBuilder( builder: (context, constraints) { final stackSize = Size(constraints.maxWidth, constraints.maxHeight); final startRect = _introStartRect(stackSize); final targetRect = _introTargetRect ?? startRect; final currentRect = _introCollapsed ? targetRect : startRect; return Stack( key: _stackKey, children: [ AnimatedOpacity( opacity: _showIntro ? 0 : 1, duration: const Duration(milliseconds: 260), child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB( AppSpacing.xl, AppSpacing.lg, AppSpacing.xl, AppSpacing.xl, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _ResultHeader(data: widget.data), const SizedBox(height: AppSpacing.md), _SignCard( key: _finalSignCardKey, signType: widget.data.signType, ), const SizedBox(height: AppSpacing.md), _KeywordCard(keywords: widget.data.keywords), const SizedBox(height: AppSpacing.md), _AnalysisCard( title: l10n.resultConclusion, content: widget.data.conclusion, ), const SizedBox(height: AppSpacing.md), _FocusPointsCard(points: widget.data.focusPoints), const SizedBox(height: AppSpacing.md), _AnalysisCard( title: l10n.resultAnalysis, content: widget.data.analysis, ), const SizedBox(height: AppSpacing.md), _AnalysisCard( title: l10n.resultSuggestion, content: widget.data.suggestion, ), const SizedBox(height: AppSpacing.md), Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: palette.warningContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.warning, color: palette.warning, size: 20, ), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( l10n.resultWarning, style: Theme.of(context).textTheme.bodyMedium ?.copyWith( color: palette.warning, fontWeight: FontWeight.w600, height: 1.35, ), ), ), ], ), ), const SizedBox(height: AppSpacing.xl), Text( l10n.resultBasicInfo, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: AppSpacing.md), _InfoCard(data: widget.data), const SizedBox(height: AppSpacing.xl), Text( l10n.resultHexagramDetail, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: AppSpacing.md), _HexagramDetailCard(data: widget.data), ], ), ), ), if (_showIntro) Positioned.fill( child: IgnorePointer( child: ColoredBox(color: colors.surface), ), ), if (_showIntro) AnimatedPositioned( duration: const Duration(milliseconds: 1450), curve: Curves.easeInOutCubicEmphasized, left: currentRect.left, top: currentRect.top, width: currentRect.width, height: currentRect.height, child: IgnorePointer( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.md), image: DecorationImage( image: AssetImage( _signImageAssetForType( context, widget.data.signType, ), ), fit: BoxFit.cover, ), boxShadow: [ BoxShadow( color: colors.shadow.withValues(alpha: 0.24), blurRadius: 24, offset: const Offset(0, 10), ), ], ), ), ), ), Positioned( left: 0, right: 0, bottom: 0, height: 80, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ colors.surface.withValues(alpha: 0), colors.surface, ], ), ), ), ), ], ); }, ), ), ); } Widget? _buildFollowUpBar(BuildContext context) { if (widget.divinationApi == null || widget.data.threadId == null) { return null; } final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; return SafeArea( top: false, child: Container( padding: const EdgeInsets.fromLTRB( AppSpacing.md, AppSpacing.sm, AppSpacing.md, AppSpacing.md, ), decoration: BoxDecoration(color: colors.surface), child: Row( children: [ Expanded( child: Text( _canSendFollowUp ? l10n.followUpEntryHint : l10n.followUpQuotaUsed, style: Theme.of(context).textTheme.bodyMedium, ), ), const SizedBox(width: AppSpacing.sm), FilledButton( onPressed: _followUpEligibilityLoading ? null : () async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => FollowUpChatScreen( result: widget.data, api: widget.divinationApi!, threadId: widget.data.threadId!, ), ), ); if (!mounted) { return; } await _loadFollowUpEligibility(); }, child: _followUpEligibilityLoading ? SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: colors.onPrimary, ), ) : Text( _canSendFollowUp ? l10n.followUpEntryAction : l10n.followUpViewHistory, ), ), ], ), ), ); } } class _ResultHeader extends StatelessWidget { const _ResultHeader({required this.data}); final DivinationResultData data; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Row( children: [ Text( l10n.resultAIAnalysis, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700), ), const Spacer(), TextButton( onPressed: () { final payload = { 'signType': data.signType, 'question': data.params.question, 'keywords': data.keywords, 'conclusion': data.conclusion, }; Clipboard.setData( ClipboardData( text: const JsonEncoder.withIndent(' ').convert(payload), ), ); Toast.show( context, l10n.toastContentCopied, type: ToastType.success, ); }, style: TextButton.styleFrom(foregroundColor: colors.primary), child: Text(l10n.resultShare), ), ], ); } } class _SignCard extends StatelessWidget { const _SignCard({super.key, required this.signType}); final String signType; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final image = _signImageAssetForType(context, signType); final localizedSignType = _localizedSignTypeLabel(l10n, signType); return Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.lg), child: Column( children: [ Image.asset( image, width: double.infinity, height: 220, fit: BoxFit.cover, ), const SizedBox(height: AppSpacing.sm), Text( localizedSignType, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), ], ), ), ); } } String _localizedSignTypeLabel(AppLocalizations l10n, String signType) { final normalized = signType.trim(); if (normalized.contains('上上')) { return l10n.signTypeShangShang; } if (normalized.contains('中上')) { return l10n.signTypeZhongShang; } if (normalized.contains('下下')) { return l10n.signTypeXiaXia; } return l10n.signTypeZhongXia; } String _signImageAssetForType(BuildContext context, String signType) { final l10n = AppLocalizations.of(context)!; final normalized = _localizedSignTypeLabel(l10n, signType); if (normalized == l10n.signTypeShangShang) { return 'assets/images/qigua/shangshang.jpg'; } if (normalized == l10n.signTypeZhongShang) { return 'assets/images/qigua/zhongshang.jpg'; } if (normalized == l10n.signTypeXiaXia) { return 'assets/images/qigua/xiaxia.jpg'; } return 'assets/images/qigua/zhongxia.jpg'; } class _KeywordCard extends StatelessWidget { const _KeywordCard({required this.keywords}); final String keywords; @override Widget build(BuildContext context) { final palette = Theme.of(context).extension()!; return Card( margin: EdgeInsets.zero, color: palette.warningContainer, elevation: 0, child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Center( child: Text( keywords, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w700, ), ), ), ), ); } } class _FocusPointsCard extends StatelessWidget { const _FocusPointsCard({required this.points}); final List points; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final languageCode = Localizations.localeOf(context).languageCode; final title = languageCode == 'en' ? 'Focus Points' : '断卦要点'; if (points.isEmpty) { return const SizedBox.shrink(); } return Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.sm), ...List.generate(points.length, (index) { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.xs), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${index + 1}. ', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), Expanded( child: Text( points[index], style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(height: 1.55), ), ), ], ), ); }), ], ), ), ); } } class _AnalysisCard extends StatelessWidget { const _AnalysisCard({required this.title, required this.content}); final String title; final String content; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( children: [ Row( children: [ Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const Spacer(), TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: content)); Toast.show( context, l10n.toastContentCopiedWithTitle(title), type: ToastType.success, ); }, child: Text(l10n.resultCopy), ), ], ), const SizedBox(height: AppSpacing.sm), Text( content, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(height: 1.65), ), ], ), ), ); } } class _InfoCard extends StatelessWidget { const _InfoCard({required this.data}); final DivinationResultData data; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.resultDivinationInfo, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.md), _kv( context, l10n.resultDivinationTime, DateFormat.yMd( Localizations.localeOf(context).toString(), ).add_Hm().format(data.params.divinationTime), ), _kv( context, l10n.resultDivinationMethod, data.params.method == DivinationMethod.auto ? l10n.resultAutoMethod : l10n.resultManualMethod, ), _kv( context, l10n.resultQuestionType, _typeLabel(context, data.params.questionType), ), _kv(context, l10n.resultQuestion, data.params.question), ], ), ), ), ); } Widget _kv(BuildContext context, String k, String v) { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: RichText( text: TextSpan( style: Theme.of(context).textTheme.bodyMedium, children: [ TextSpan( text: '$k:', style: TextStyle( color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.75), ), ), TextSpan( text: v, style: TextStyle( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), ], ), ), ); } String _typeLabel(BuildContext context, QuestionType type) { final l10n = AppLocalizations.of(context)!; return switch (type) { QuestionType.career => l10n.questionTypeCareer, QuestionType.love => l10n.questionTypeLove, QuestionType.wealth => l10n.questionTypeWealth, QuestionType.fortune => l10n.questionTypeFortune, QuestionType.dream => l10n.questionTypeDream, QuestionType.health => l10n.questionTypeHealth, QuestionType.study => l10n.questionTypeStudy, QuestionType.search => l10n.questionTypeSearch, QuestionType.other => l10n.questionTypeOther, }; } } class _HexagramDetailCard extends StatelessWidget { const _HexagramDetailCard({required this.data}); final DivinationResultData data; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Column( children: [ Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.ganZhiInfo, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.md), Row( children: [ Expanded( child: _miniKV( context, DivinationTerms.yueJian, data.ganzhi.yueJian, ), ), Expanded( child: _miniKV( context, DivinationTerms.riChen, data.ganzhi.riChen, ), ), ], ), const SizedBox(height: AppSpacing.sm), Row( children: [ Expanded( child: _miniKV( context, DivinationTerms.yuePo, data.ganzhi.yuePo, ), ), Expanded( child: _miniKV( context, DivinationTerms.riChong, data.ganzhi.riChong, ), ), ], ), const SizedBox(height: AppSpacing.md), Text( l10n.wuXingWangShuai, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.sm), _WuXingTable(data: data), const SizedBox(height: AppSpacing.md), Text( l10n.ganZhiKongWang, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.sm), _KongWangTable(data: data), ], ), ), ), const SizedBox(height: AppSpacing.md), Card( margin: EdgeInsets.zero, color: colors.surfaceContainerLow, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( children: [ Row( children: [ Expanded( child: Text( data.guaName, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w700), ), ), if (data.hasChangingYao) Expanded( child: Text( data.targetGuaName, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w700), ), ), ], ), const SizedBox(height: AppSpacing.md), for (int idx = 5; idx >= 0; idx--) _YaoDetailRow( line: data.yaoLines[idx], target: idx < data.targetYaoLines.length ? data.targetYaoLines[idx] : data.yaoLines[idx], showTarget: data.hasChangingYao && idx < data.targetYaoLines.length, ), const SizedBox(height: AppSpacing.sm), const Align( alignment: Alignment.centerLeft, child: YaoLegend(), ), ], ), ), ), ], ); } Widget _miniKV(BuildContext context, String key, String value) { return Row( children: [ Text('$key:'), Text(value, style: const TextStyle(fontWeight: FontWeight.w600)), ], ); } } class _WuXingTable extends StatelessWidget { const _WuXingTable({required this.data}); final DivinationResultData data; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( border: Border.all(color: colors.outline), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Column( children: [ Row( children: DivinationTerms.wuXing .map( (k) => Expanded( child: Container( padding: const EdgeInsets.symmetric( vertical: AppSpacing.sm, ), decoration: BoxDecoration( color: colors.surfaceContainerHigh, border: Border( right: BorderSide( color: k == DivinationTerms.wuXing.last ? colors.surfaceContainerHigh : colors.outline, ), ), ), child: Text(k, textAlign: TextAlign.center), ), ), ) .toList(), ), Row( children: DivinationTerms.wuXing .map( (k) => Expanded( child: Padding( padding: const EdgeInsets.symmetric( vertical: AppSpacing.sm, ), child: Text( data.wuXingStatus[k] ?? '', textAlign: TextAlign.center, ), ), ), ) .toList(), ), ], ), ); } } class _KongWangTable extends StatelessWidget { const _KongWangTable({required this.data}); final DivinationResultData data; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final header = [ l10n.resultPillarColumn, l10n.resultYearPillar, l10n.resultMonthPillar, l10n.resultDayPillar, l10n.resultTimePillar, ]; final rows = >[ [ l10n.resultGanZhiLabel, data.ganzhi.yearGanZhi, data.ganzhi.monthGanZhi, data.ganzhi.dayGanZhi, data.ganzhi.timeGanZhi, ], [ l10n.resultKongWangLabel, data.ganzhi.yearKongWang, data.ganzhi.monthKongWang, data.ganzhi.dayKongWang, data.ganzhi.timeKongWang, ], ]; Widget buildCell( String text, { bool isHeader = false, bool isLast = false, bool isFirst = false, }) { return Expanded( flex: isFirst ? 2 : 3, child: Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xs, vertical: AppSpacing.sm, ), decoration: BoxDecoration( color: isHeader ? colors.surfaceContainerHigh : colors.surface, border: Border( right: isLast ? BorderSide.none : BorderSide(color: colors.outline), ), ), child: Text( text, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: isHeader || isFirst ? FontWeight.w700 : FontWeight.w500, ), ), ), ); } return Container( decoration: BoxDecoration( border: Border.all(color: colors.outline), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Column( children: [ Row( children: [ for (int i = 0; i < header.length; i++) buildCell( header[i], isHeader: true, isFirst: i == 0, isLast: i == header.length - 1, ), ], ), for (int r = 0; r < rows.length; r++) Container( decoration: BoxDecoration( border: Border(top: BorderSide(color: colors.outline)), ), child: Row( children: [ for (int c = 0; c < rows[r].length; c++) buildCell( rows[r][c], isFirst: c == 0, isLast: c == rows[r].length - 1, ), ], ), ), ], ), ); } } class _YaoDetailRow extends StatelessWidget { const _YaoDetailRow({ required this.line, required this.target, required this.showTarget, }); final YaoLineData line; final YaoLineData target; final bool showTarget; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Row( children: [ Expanded(child: _lineCell(context, line, showMark: true)), if (showTarget) Expanded(child: _lineCell(context, target, showMark: false)), ], ), ); } Widget _lineCell( BuildContext context, YaoLineData data, { required bool showMark, }) { return Row( children: [ SizedBox( width: 20, child: Text(data.spirit, textAlign: TextAlign.center), ), SizedBox( width: 28, child: Text(data.relation, textAlign: TextAlign.center), ), SizedBox( width: 18, child: Text(data.branch, textAlign: TextAlign.center), ), SizedBox( width: 18, child: Text(data.element, textAlign: TextAlign.center), ), const SizedBox(width: AppSpacing.xs), Expanded(child: YaoGlyph(type: data.type, height: 6)), SizedBox( width: 18, child: Text(_changeMark(data.type), textAlign: TextAlign.center), ), SizedBox( width: 18, child: Text(showMark ? data.mark : '', textAlign: TextAlign.center), ), ], ); } String _changeMark(YaoType type) { return type.changeMark; } }