import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.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/models/divination_params.dart'; import '../../data/models/divination_result.dart'; class DivinationResultScreen extends StatefulWidget { const DivinationResultScreen({super.key, required this.data}); final DivinationResultData data; @override State createState() => _DivinationResultScreenState(); } enum _ResultTransitionStep { preparing, deriving, done } class _DivinationResultScreenState extends State { _ResultTransitionStep _step = _ResultTransitionStep.preparing; bool _showOverlay = true; @override void initState() { super.initState(); _playSequence(); } Future _playSequence() async { await Future.delayed(const Duration(milliseconds: 420)); if (!mounted) { return; } setState(() { _step = _ResultTransitionStep.deriving; }); await Future.delayed(const Duration(milliseconds: 820)); if (!mounted) { return; } setState(() { _step = _ResultTransitionStep.done; }); } void _dismissOverlay() { if (_step != _ResultTransitionStep.done) { return; } setState(() { _showOverlay = false; }); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final palette = Theme.of(context).extension()!; final l10n = AppLocalizations.of(context)!; return Scaffold( backgroundColor: colors.surface, appBar: AppBar( backgroundColor: colors.surface, surfaceTintColor: colors.surface, title: Text(l10n.resultScreenTitle), centerTitle: true, ), body: Stack( children: [ 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(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), _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 (_showOverlay) _ResultTransitionOverlay(step: _step, onTapDone: _dismissOverlay), ], ), ); } } class _ResultTransitionOverlay extends StatelessWidget { const _ResultTransitionOverlay({required this.step, required this.onTapDone}); final _ResultTransitionStep step; final VoidCallback onTapDone; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final cardText = switch (step) { _ResultTransitionStep.preparing => l10n.transitionPreparing, _ResultTransitionStep.deriving => l10n.transitionDeriving, _ResultTransitionStep.done => l10n.transitionDone, }; return Positioned.fill( child: Material( color: colors.surface, child: Center( child: GestureDetector( key: const Key('result_transition_overlay_tap'), onTap: onTapDone, child: TweenAnimationBuilder( key: ValueKey<_ResultTransitionStep>(step), tween: Tween(begin: pi / 2, end: 0), duration: const Duration(milliseconds: 460), curve: Curves.easeOutCubic, builder: (context, angle, child) { final opacity = (1 - angle / (pi / 2)).clamp(0.0, 1.0); return Opacity( opacity: opacity, child: Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateY(angle), child: child, ), ); }, child: Container( width: 220, height: 320, decoration: BoxDecoration( color: colors.surface, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: colors.primary.withValues(alpha: 0.2), ), boxShadow: [ BoxShadow( color: colors.shadow.withValues(alpha: 0.25), blurRadius: 22, offset: const Offset(0, 12), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( step == _ResultTransitionStep.done ? Icons.visibility : Icons.auto_awesome, color: colors.primary, size: 34, ), const SizedBox(height: AppSpacing.md), Text( cardText, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, height: 1.4, ), ), ], ), ), ), ), ), ), ); } } 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({required this.signType}); final String signType; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final image = switch (signType) { '上上签' => 'assets/images/qigua/shangshang.jpg', '中上签' => 'assets/images/qigua/zhongshang.jpg', _ => 'assets/images/qigua/zhongxia.jpg', }; 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( signType, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), ], ), ), ); } } 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 _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; 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, '$title已复制', type: ToastType.success); }, child: const Text('复制'), ), ], ), 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; 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( '起卦信息', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.md), _kv( context, '起卦时间', DateFormat( 'yyyy年MM月dd日 HH:mm', ).format(data.params.divinationTime), ), _kv( context, '起卦方式', data.params.method == DivinationMethod.auto ? '自动起卦' : '手动起卦', ), _kv(context, '问题类型', _typeLabel(data.params.questionType)), _kv(context, '占卜问题', 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(QuestionType type) { return switch (type) { QuestionType.career => '事业', QuestionType.love => '情感', QuestionType.wealth => '财富', QuestionType.fortune => '运势', QuestionType.dream => '解梦', QuestionType.health => '健康', QuestionType.study => '学业', QuestionType.search => '寻物', QuestionType.other => '其他', }; } } class _HexagramDetailCard extends StatelessWidget { const _HexagramDetailCard({required this.data}); final DivinationResultData data; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; 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( '干支信息', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.md), Row( children: [ Expanded( child: _miniKV(context, '月建', data.ganzhi.yueJian), ), Expanded(child: _miniKV(context, '日辰', data.ganzhi.riChen)), ], ), const SizedBox(height: AppSpacing.sm), Row( children: [ Expanded(child: _miniKV(context, '月破', data.ganzhi.yuePo)), Expanded( child: _miniKV(context, '日冲', data.ganzhi.riChong), ), ], ), const SizedBox(height: AppSpacing.md), Text('五行旺衰', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: AppSpacing.sm), _WuXingTable(data: data), const SizedBox(height: AppSpacing.md), Text('干支空亡', style: Theme.of(context).textTheme.bodyMedium), 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: data.targetYaoLines[idx], showTarget: data.hasChangingYao, ), 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 rows = [ ('年', '${data.ganzhi.yearGanZhi}年', data.ganzhi.yearKongWang), ('月', '${data.ganzhi.monthGanZhi}月', data.ganzhi.monthKongWang), ('日', '${data.ganzhi.dayGanZhi}日', data.ganzhi.dayKongWang), ('时', '${data.ganzhi.timeGanZhi}时', data.ganzhi.timeKongWang), ]; return Container( decoration: BoxDecoration( border: Border.all(color: colors.outline), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Column( children: [ for (final row in rows) Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ), child: Row( children: [ SizedBox(width: 28, child: Text(row.$1)), Expanded(child: Text(row.$2, textAlign: TextAlign.center)), SizedBox( width: 64, child: Text(row.$3, textAlign: TextAlign.right), ), ], ), ), ], ), ); } } 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; } }