feat(divination): 重构手动起卦教程,支持三硬币交互选择

This commit is contained in:
qzl
2026-04-07 18:41:08 +08:00
parent f904286ba7
commit f394df9362
29 changed files with 873 additions and 326 deletions
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
@@ -130,8 +132,16 @@ class _ManualDivinationScreenState extends State<ManualDivinationScreen>
builder: (_) {
return DivinationGuideDialog(
title: l10n.manualSelectYaoTitle,
guideImages: const ['lc2.jpg', 'lc3.jpg', 'lc4.jpg', 'lc5.jpg'],
instructionText: l10n.manualYaoTipContent,
guideImages: const [
['assets/images/tutorial/tutorial_1.png'],
['assets/images/tutorial/tutorial_2.png'],
['assets/images/tutorial/tutorial_3.png'],
],
instructions: [
l10n.divinationManualGuideStep1,
l10n.divinationManualGuideStep2,
l10n.divinationManualGuideStep3,
],
);
},
);
@@ -409,208 +419,199 @@ class _YaoSelectionCard extends StatelessWidget {
int yaoIndex,
void Function(int, YaoType) onSelect,
) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final options = <(String, YaoType, String)>[
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYang]}${DivinationTerms.youngYangSymbol}',
YaoType.youngYang,
'字字字',
),
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYin]}${DivinationTerms.youngYinSymbol}',
YaoType.youngYin,
'花花花',
),
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYang]}${DivinationTerms.oldYangSymbol}',
YaoType.oldYang,
'字字字',
),
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYin]}${DivinationTerms.oldYinSymbol}',
YaoType.oldYin,
'花花花',
),
];
return showDialog<void>(
context: context,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.manualSelectYaoTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
const SizedBox(height: AppSpacing.md),
...options.map((option) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: _YaoOptionCard(
label: option.$1,
pattern: option.$3,
isSelected: false,
onTap: () {
onSelect(yaoIndex, option.$2);
Navigator.of(context).pop();
},
),
);
}),
const SizedBox(height: AppSpacing.md),
Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.coinFaceGuideTitle,
style: Theme.of(context).textTheme.titleSmall
?.copyWith(
color: colors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppSpacing.sm),
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.sm),
child: Image.asset(
'assets/images/qigua/zihua.jpg',
width: double.infinity,
height: 120,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
Container(
height: 120,
color: colors.errorContainer,
child: Center(
child: Text(
l10n.divinationClose,
style: TextStyle(
color: colors.onErrorContainer,
),
),
),
),
),
),
const SizedBox(height: AppSpacing.xs),
Text(
l10n.coinFaceGuideDescription,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: colors.onSurfaceVariant),
),
],
),
),
],
),
),
),
return _ThreeCoinSelectorDialog(
onConfirm: (yaoType) {
onSelect(yaoIndex, yaoType);
Navigator.of(context).pop();
},
);
},
);
}
}
class _YaoOptionCard extends StatelessWidget {
const _YaoOptionCard({
required this.label,
required this.pattern,
required this.isSelected,
required this.onTap,
});
class _ThreeCoinSelectorDialog extends StatefulWidget {
const _ThreeCoinSelectorDialog({required this.onConfirm});
final String label;
final String pattern;
final bool isSelected;
final VoidCallback onTap;
final void Function(YaoType) onConfirm;
@override
State<_ThreeCoinSelectorDialog> createState() =>
_ThreeCoinSelectorDialogState();
}
class _ThreeCoinSelectorDialogState extends State<_ThreeCoinSelectorDialog> {
final List<bool> _coinStates = [false, false, false];
YaoType get _currentYaoType {
final huaCount = _coinStates.where((isHua) => isHua).length;
return switch (huaCount) {
0 => YaoType.oldYin,
1 => YaoType.youngYang,
2 => YaoType.youngYin,
3 => YaoType.oldYang,
_ => YaoType.undetermined,
};
}
void _toggleCoin(int index) {
setState(() {
_coinStates[index] = !_coinStates[index];
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
return Material(
color: isSelected ? colors.primaryContainer : colors.surface,
borderRadius: BorderRadius.circular(AppRadius.md),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.md),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + 4,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: isSelected ? colors.primary : colors.outlineVariant,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: isSelected
? colors.onPrimaryContainer
: colors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
pattern,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isSelected
? colors.onPrimaryContainer.withValues(alpha: 0.7)
: colors.onSurfaceVariant,
),
),
],
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.manualSelectYaoTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
const SizedBox(height: AppSpacing.lg),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(3, (index) {
return _FlippingCoin(
isHua: _coinStates[index],
onTap: () => _toggleCoin(index),
);
}),
),
const SizedBox(height: AppSpacing.md),
Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.md),
),
Icon(
Icons.chevron_right,
color: isSelected
? colors.onPrimaryContainer
: colors.onSurfaceVariant,
child: Text(
l10n.manualCoinSelectHint,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: AppSpacing.lg),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => widget.onConfirm(_currentYaoType),
child: Text(l10n.confirm),
),
),
],
),
),
);
}
}
class _FlippingCoin extends StatefulWidget {
const _FlippingCoin({required this.isHua, required this.onTap});
final bool isHua;
final VoidCallback onTap;
@override
State<_FlippingCoin> createState() => _FlippingCoinState();
}
class _FlippingCoinState extends State<_FlippingCoin>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool _showHua = false;
@override
void initState() {
super.initState();
_showHua = widget.isHua;
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
void didUpdateWidget(_FlippingCoin oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isHua != widget.isHua) {
if (widget.isHua != _showHua) {
_controller.forward().then((_) {
setState(() {
_showHua = widget.isHua;
});
_controller.reverse();
});
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final angle = _animation.value * pi;
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(angle),
child: ClipOval(
child: Image.asset(
_showHua
? 'assets/images/qigua/hua.jpg'
: 'assets/images/qigua/zi.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
);
},
),
);
}
}