Files
eryao/apps/lib/features/divination/presentation/screens/divination_processing_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

341 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../core/logging/logger.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/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 '../../data/services/divination_run_service.dart';
import 'divination_result_screen.dart';
enum _ProcessingStep { preparing, deriving, done }
class DivinationProcessingScreen extends StatefulWidget {
const DivinationProcessingScreen({
super.key,
required this.params,
required this.yaoStates,
required this.runService,
this.divinationApi,
required this.onCompleted,
});
final DivinationParams params;
final List<YaoType> yaoStates;
final DivinationRunService runService;
final DivinationApi? divinationApi;
final Future<void> Function(DivinationResultData result) onCompleted;
@override
State<DivinationProcessingScreen> createState() =>
_DivinationProcessingScreenState();
}
class _DivinationProcessingScreenState extends State<DivinationProcessingScreen>
with TickerProviderStateMixin {
static final Logger _logger = getLogger(
'features.divination.processing_screen',
);
static const int _iChingCardCount = 8;
_ProcessingStep _step = _ProcessingStep.preparing;
DivinationResultData? _resultData;
String? _errorMessage;
late final AnimationController _cardRotationController;
int _currentCardIndex = 0;
@override
void initState() {
super.initState();
_cardRotationController =
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
)..addStatusListener((status) {
if (status != AnimationStatus.completed || !mounted) {
return;
}
setState(() {
_currentCardIndex = (_currentCardIndex + 1) % _iChingCardCount;
});
_cardRotationController.forward(from: 0);
});
_cardRotationController.forward();
_startRun();
}
@override
void dispose() {
_cardRotationController.dispose();
super.dispose();
}
Future<void> _startRun() async {
try {
final aggregate = await widget.runService.run(
params: widget.params,
yaoStates: widget.yaoStates,
onDerived: () {
if (!mounted) {
return;
}
setState(() {
_step = _ProcessingStep.deriving;
});
},
onTextMessageEnd: () {
if (!mounted) {
return;
}
setState(() {
_step = _ProcessingStep.done;
});
},
);
if (!mounted) {
return;
}
setState(() {
_resultData = aggregate.toViewData(widget.params);
_step = _ProcessingStep.done;
});
_cardRotationController.stop();
final data = _resultData;
if (data != null) {
try {
await widget.onCompleted(data);
} catch (error, stackTrace) {
_logger.warning(
message: 'Failed to persist post-run side effects',
extra: <String, dynamic>{'error': error.toString()},
);
_logger.debug(
message: 'Post-run side effect stack trace',
extra: <String, dynamic>{'stackTrace': stackTrace.toString()},
);
}
}
} catch (error, stackTrace) {
_logger.error(
message: 'Divination processing failed while waiting result events',
error: error,
stackTrace: stackTrace,
extra: <String, dynamic>{
'step': _step.name,
'method': widget.params.method.name,
'questionType': widget.params.questionType.name,
},
);
if (!mounted) {
return;
}
final l10n = AppLocalizations.of(context)!;
final message = error is ApiProblem
? mapApiProblemToMessage(error, l10n)
: l10n.errorRequestGeneric;
setState(() {
_errorMessage = message;
});
Toast.show(context, message, type: ToastType.error);
}
}
void _openResult() {
final data = _resultData;
if (data == null) {
return;
}
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (_) => DivinationResultScreen(
data: data,
divinationApi: data.threadId == null ? null : widget.divinationApi,
enableIntroTransition: true,
),
),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final statusText = switch (_step) {
_ProcessingStep.preparing => l10n.transitionPreparing,
_ProcessingStep.deriving => l10n.transitionDeriving,
_ProcessingStep.done => l10n.transitionDone,
};
final cardDataList = _iChingCardData(l10n);
final canContinue = _step == _ProcessingStep.done && _resultData != null;
return Scaffold(
backgroundColor: colors.surface,
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
child: _errorMessage == null
? GestureDetector(
onTap: canContinue ? _openResult : null,
child: AnimatedBuilder(
animation: _cardRotationController,
builder: (context, _) {
final angle = canContinue
? 0.0
: _rotationForProgress(
_cardRotationController.value,
);
final card = cardDataList[_currentCardIndex];
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.0011)
..rotateY(angle),
child: Container(
width: 220,
height: 320,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
colors.primaryContainer.withValues(
alpha: 0.55,
),
colors.secondaryContainer.withValues(
alpha: 0.38,
),
colors.surface,
],
),
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colors.primary.withValues(alpha: 0.3),
),
boxShadow: [
BoxShadow(
color: colors.shadow.withValues(alpha: 0.18),
blurRadius: 26,
offset: const Offset(0, 14),
),
],
),
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (canContinue)
Icon(
Icons.visibility,
color: colors.primary,
size: 34,
)
else ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: colors.surface.withValues(
alpha: 0.75,
),
borderRadius: BorderRadius.circular(
AppRadius.full,
),
),
child: Text(
l10n.iChingTitle,
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: colors.primary,
letterSpacing: 0.3,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(height: AppSpacing.md),
Text(
card.$1,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: AppSpacing.md),
Text(
card.$2,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
height: 1.5,
color: colors.onSurface.withValues(
alpha: 0.86,
),
),
),
],
const SizedBox(height: AppSpacing.lg),
Text(
statusText,
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.titleMedium,
),
],
),
),
);
},
),
)
: Text(
_errorMessage!,
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(color: colors.error),
),
),
),
),
);
}
double _rotationForProgress(double progress) {
if (progress < 0.25) {
return (1 - progress / 0.25) * (3.1415926 / 2);
}
if (progress < 0.75) {
return 0;
}
return ((progress - 0.75) / 0.25) * (3.1415926 / 2);
}
List<(String, String)> _iChingCardData(AppLocalizations l10n) {
return <(String, String)>[
(l10n.processingCardQianTitle, l10n.processingCardQianQuote),
(l10n.processingCardDuiTitle, l10n.processingCardDuiQuote),
(l10n.processingCardLiTitle, l10n.processingCardLiQuote),
(l10n.processingCardZhenTitle, l10n.processingCardZhenQuote),
(l10n.processingCardXunTitle, l10n.processingCardXunQuote),
(l10n.processingCardKanTitle, l10n.processingCardKanQuote),
(l10n.processingCardGenTitle, l10n.processingCardGenQuote),
(l10n.processingCardKunTitle, l10n.processingCardKunQuote),
];
}
}