Files
eryao/apps/lib/features/divination/presentation/screens/divination_result_screen.dart
T
qzl e80a82bef4 docs: 更新协议文档,删除废弃计划文档
- 更新 http-error-codes, user-points-chat-data-protocol
- 更新 divination-run-protocol, profile-protocol
- 删除废弃的后端和前端设计计划文档
2026-04-08 17:23:02 +08:00

1187 lines
37 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 '../../../../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<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);
}
@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();
}
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));
if (!mounted) {
return;
}
setState(() {
_introCollapsed = true;
});
await Future<void>.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<AppColorPalette>()!;
final l10n = AppLocalizations.of(context)!;
return PopScope<void>(
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<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,
),
),
],
),
),
);
}
}
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});
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<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),
),
),
],
),
);
}),
],
),
),
);
}
}
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 = <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,
],
];
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;
}
}