Files
eryao/apps/lib/features/divination/presentation/screens/divination_result_screen.dart
T

809 lines
25 KiB
Dart
Raw Normal View History

2026-04-03 16:56:47 +08:00
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<DivinationResultScreen> createState() => _DivinationResultScreenState();
}
enum _ResultTransitionStep { preparing, deriving, done }
class _DivinationResultScreenState extends State<DivinationResultScreen> {
_ResultTransitionStep _step = _ResultTransitionStep.preparing;
bool _showOverlay = true;
@override
void initState() {
super.initState();
_playSequence();
}
Future<void> _playSequence() async {
await Future<void>.delayed(const Duration(milliseconds: 420));
if (!mounted) {
return;
}
setState(() {
_step = _ResultTransitionStep.deriving;
});
await Future<void>.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<AppColorPalette>()!;
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<double>(
key: ValueKey<_ResultTransitionStep>(step),
tween: Tween<double>(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 = <String, dynamic>{
'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<AppColorPalette>()!;
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.yMd(
Localizations.localeOf(context).toString(),
).add_Hm().format(data.params.divinationTime),
2026-04-03 16:56:47 +08:00
),
_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;
}
}