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

1187 lines
37 KiB
Dart
Raw Normal View History

2026-04-03 16:56:47 +08:00
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import '../../../../core/logging/logger.dart';
2026-04-03 16:56:47 +08:00
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';
2026-04-03 16:56:47 +08:00
import '../../data/models/divination_params.dart';
import '../../data/models/divination_result.dart';
import 'follow_up_chat_screen.dart';
2026-04-03 16:56:47 +08:00
class DivinationResultScreen extends StatefulWidget {
const DivinationResultScreen({
super.key,
required this.data,
this.divinationApi,
this.enableIntroTransition = false,
});
2026-04-03 16:56:47 +08:00
final DivinationResultData data;
final DivinationApi? divinationApi;
final bool enableIntroTransition;
2026-04-03 16:56:47 +08:00
@override
State<DivinationResultScreen> createState() => _DivinationResultScreenState();
}
class _DivinationResultScreenState extends State<DivinationResultScreen> {
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);
}
2026-04-03 16:56:47 +08:00
@override
void initState() {
super.initState();
if (widget.enableIntroTransition) {
_showIntro = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_prepareIntro();
});
}
_loadFollowUpEligibility();
}
Future<void> _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<void> _prepareIntro() async {
for (int i = 0; i < 12; i++) {
if (!mounted) {
return;
}
if (_measureIntroTargetRect()) {
break;
}
await Future<void>.delayed(const Duration(milliseconds: 16));
}
if (!mounted) {
return;
}
_playIntro();
2026-04-03 16:56:47 +08:00
}
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<void> _playIntro() async {
await Future<void>.delayed(const Duration(milliseconds: 180));
2026-04-03 16:56:47 +08:00
if (!mounted) {
return;
}
setState(() {
_introCollapsed = true;
2026-04-03 16:56:47 +08:00
});
await Future<void>.delayed(const Duration(milliseconds: 1450));
2026-04-03 16:56:47 +08:00
if (!mounted) {
return;
}
setState(() {
_showIntro = false;
2026-04-03 16:56:47 +08:00
});
}
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,
);
}
2026-04-03 16:56:47 +08:00
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
final l10n = AppLocalizations.of(context)!;
return PopScope<void>(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (didPop) {
return;
}
_backToHome();
},
child: Scaffold(
2026-04-03 16:56:47 +08:00
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,
2026-04-03 16:56:47 +08:00
),
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),
],
2026-04-03 16:56:47 +08:00
),
),
),
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),
),
],
),
),
2026-04-03 16:56:47 +08:00
),
),
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,
],
),
),
),
),
],
);
},
),
2026-04-03 16:56:47 +08:00
),
);
}
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<void>(
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,
),
),
],
),
),
);
}
2026-04-03 16:56:47 +08:00
}
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({super.key, required this.signType});
2026-04-03 16:56:47 +08:00
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);
2026-04-03 16:56:47 +08:00
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,
2026-04-03 16:56:47 +08:00
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';
}
2026-04-03 16:56:47 +08:00
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 _FocusPointsCard extends StatelessWidget {
const _FocusPointsCard({required this.points});
final List<String> 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<Widget>.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),
),
),
],
),
);
}),
],
),
),
);
}
}
2026-04-03 16:56:47 +08:00
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)!;
2026-04-03 16:56:47 +08:00
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,
);
2026-04-03 16:56:47 +08:00
},
child: Text(l10n.resultCopy),
2026-04-03 16:56:47 +08:00
),
],
),
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)!;
2026-04-03 16:56:47 +08:00
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,
),
2026-04-03 16:56:47 +08:00
),
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),
],
),
2026-04-03 16:56:47 +08:00
),
),
);
}
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)!;
2026-04-03 16:56:47 +08:00
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,
2026-04-03 16:56:47 +08:00
};
}
}
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)!;
2026-04-03 16:56:47 +08:00
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,
2026-04-03 16:56:47 +08:00
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,
),
2026-04-03 16:56:47 +08:00
),
],
),
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,
),
2026-04-03 16:56:47 +08:00
),
],
),
const SizedBox(height: AppSpacing.md),
Text(
l10n.wuXingWangShuai,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
2026-04-03 16:56:47 +08:00
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,
),
),
2026-04-03 16:56:47 +08:00
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,
2026-04-03 16:56:47 +08:00
),
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 = <String>[
l10n.resultPillarColumn,
l10n.resultYearPillar,
l10n.resultMonthPillar,
l10n.resultDayPillar,
l10n.resultTimePillar,
];
final rows = <List<String>>[
<String>[
l10n.resultGanZhiLabel,
data.ganzhi.yearGanZhi,
data.ganzhi.monthGanZhi,
data.ganzhi.dayGanZhi,
data.ganzhi.timeGanZhi,
],
<String>[
l10n.resultKongWangLabel,
data.ganzhi.yearKongWang,
data.ganzhi.monthKongWang,
data.ganzhi.dayKongWang,
data.ganzhi.timeKongWang,
],
2026-04-03 16:56:47 +08:00
];
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,
),
),
),
);
}
2026-04-03 16:56:47 +08:00
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)),
2026-04-03 16:56:47 +08:00
),
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,
),
2026-04-03 16:56:47 +08:00
],
),
),
],
),
);
}
}
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;
}
}