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

731 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
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();
}
class _DivinationResultScreenState extends State<DivinationResultScreen> {
bool _showIntro = true;
bool _introCollapsed = false;
@override
void initState() {
super.initState();
_playIntro();
}
Future<void> _playIntro() async {
await Future<void>.delayed(const Duration(milliseconds: 120));
if (!mounted) {
return;
}
setState(() {
_introCollapsed = true;
});
await Future<void>.delayed(const Duration(milliseconds: 760));
if (!mounted) {
return;
}
setState(() {
_showIntro = 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: [
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(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 (_showIntro)
Positioned.fill(
child: Material(
color: colors.surface,
child: SafeArea(
child: AnimatedAlign(
duration: const Duration(milliseconds: 760),
curve: Curves.easeInOutCubic,
alignment: _introCollapsed
? const Alignment(0, -0.86)
: Alignment.center,
child: AnimatedContainer(
duration: const Duration(milliseconds: 760),
curve: Curves.easeInOutCubic,
width: _introCollapsed ? 150 : 290,
child: _SignCard(signType: widget.data.signType),
),
),
),
),
),
],
),
);
}
}
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),
),
_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;
}
}