feat: 接入起卦后端流程并完善积分扣减链路

This commit is contained in:
qzl
2026-04-03 19:04:46 +08:00
parent a136e42290
commit d87b2e1e3a
56 changed files with 3310 additions and 809 deletions
@@ -2,20 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import '../../../../core/network/api_problem.dart';
import '../../../../core/network/api_problem_mapper.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/divination/divination_shared_widgets.dart';
import '../../../../shared/widgets/divination/divination_terms.dart';
import '../../../../shared/widgets/divination/yao_legend.dart';
import '../../../../shared/widgets/divination/yao_line_row.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../../../shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart';
import '../../data/models/divination_params.dart';
import '../../data/services/divination_run_service.dart';
import 'divination_result_screen.dart';
import 'divination_processing_screen.dart';
class ManualDivinationScreen extends StatefulWidget {
const ManualDivinationScreen({
@@ -126,12 +122,7 @@ class _ManualDivinationScreenState extends State<ManualDivinationScreen>
builder: (_) {
return DivinationGuideDialog(
title: l10n.manualSelectYaoTitle,
guideImages: const [
'assets/images/qigua/lc2.jpg',
'assets/images/qigua/lc3.jpg',
'assets/images/qigua/lc4.jpg',
'assets/images/qigua/lc5.jpg',
],
guideImages: const ['lc2.jpg', 'lc3.jpg', 'lc4.jpg', 'lc5.jpg'],
instructionText: l10n.manualYaoTipContent,
);
},
@@ -180,35 +171,22 @@ class _ManualDivinationScreenState extends State<ManualDivinationScreen>
}
Future<void> _submitRun() async {
final l10n = AppLocalizations.of(context)!;
setState(() {
_submitting = true;
});
try {
final aggregate = await widget.runService.run(
params: widget.params.copyWith(divinationTime: _selectedTime),
yaoStates: _selectedYaos.cast<YaoType>(),
);
if (!mounted) {
return;
}
Navigator.of(context).push(
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => DivinationResultScreen(
data: aggregate.toViewData(
widget.params.copyWith(divinationTime: _selectedTime),
),
builder: (_) => DivinationProcessingScreen(
params: widget.params.copyWith(divinationTime: _selectedTime),
yaoStates: _selectedYaos.cast<YaoType>(),
runService: widget.runService,
),
),
);
} catch (error) {
if (!mounted) {
return;
}
final message = error is ApiProblem
? mapApiProblemToMessage(error, l10n)
: l10n.errorRequestGeneric;
Toast.show(context, message, type: ToastType.error);
} finally {
if (!mounted) {
return;
@@ -373,70 +351,206 @@ class _YaoSelectionCard extends StatelessWidget {
void Function(int, YaoType) onSelect,
) {
final l10n = AppLocalizations.of(context)!;
final options = <(String, YaoType)>[
final colors = Theme.of(context).colorScheme;
final options = <(String, YaoType, String)>[
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYang]} (\u2014)\uFF1A\u82B1\u5B57\u5B57',
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYang]}${DivinationTerms.youngYangSymbol}',
YaoType.youngYang,
'字字字',
),
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYin]} (--)\uFF1A\u5B57\u82B1\u82B1',
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYin]}${DivinationTerms.youngYinSymbol}',
YaoType.youngYin,
'花花花',
),
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYang]} (\u2014\u25CB)\uFF1A\u82B1\u82B1\u82B1',
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYang]}${DivinationTerms.oldYangSymbol}',
YaoType.oldYang,
'字字字',
),
(
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYin]} (--\u00D7)\uFF1A\u5B57\u5B57\u5B57',
'${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYin]}${DivinationTerms.oldYinSymbol}',
YaoType.oldYin,
'花花花',
),
];
return showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.manualSelectYaoTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final option in options)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: OutlinedButton(
onPressed: () {
onSelect(yaoIndex, option.$2);
Navigator.of(context).pop();
},
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(44),
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,
),
),
child: Text(option.$1),
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: (_, __, ___) => 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),
),
],
),
),
const SizedBox(height: AppSpacing.sm),
const Align(
alignment: Alignment.centerLeft,
child: Text('字花图片说明:'),
),
const SizedBox(height: AppSpacing.sm),
Image.asset(
'assets/images/qigua/zihua.jpg',
width: double.infinity,
height: 180,
fit: BoxFit.contain,
),
],
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.divinationClose),
),
],
);
},
);
}
}
class _YaoOptionCard extends StatelessWidget {
const _YaoOptionCard({
required this.label,
required this.pattern,
required this.isSelected,
required this.onTap,
});
final String label;
final String pattern;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext 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,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: isSelected
? colors.onPrimaryContainer
: colors.onSurfaceVariant,
),
],
),
),
),
);
}
}