import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../theme/yanting_tokens.dart'; import 'app_buttons.dart'; import 'app_card.dart'; class LoadingState extends StatelessWidget { const LoadingState({this.label = '正在加载研报解读', super.key}); final String label; @override Widget build(BuildContext context) { return ListView.separated( padding: const EdgeInsets.all(YantingSpacing.screenX), itemCount: 4, separatorBuilder: (_, _) => const SizedBox(height: YantingSpacing.cardGap), itemBuilder: (context, index) => const SkeletonCard(), ); } } class SkeletonCard extends StatelessWidget { const SkeletonCard({super.key}); @override Widget build(BuildContext context) { return AppCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ SkeletonLine(width: 96), SizedBox(height: YantingSpacing.cardGap), SkeletonLine(width: double.infinity, height: 18), SizedBox(height: YantingSpacing.x2), SkeletonLine(width: 240), SizedBox(height: YantingSpacing.cardGap), SkeletonLine(width: 160), ], ), ); } } class SkeletonLine extends StatelessWidget { const SkeletonLine({required this.width, this.height = 12, super.key}); final double width; final double height; @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return _PulsingSkeleton( width: width, height: height, color: theme.colorScheme.muted, ); } } class _PulsingSkeleton extends StatefulWidget { const _PulsingSkeleton({ required this.color, required this.width, required this.height, }); final Color color; final double width; final double height; @override State<_PulsingSkeleton> createState() => _PulsingSkeletonState(); } class _PulsingSkeletonState extends State<_PulsingSkeleton> with SingleTickerProviderStateMixin { late final AnimationController _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(reverse: true); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return FadeTransition( opacity: Tween( begin: 0.4, end: 1, ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)), child: Container( width: widget.width, height: widget.height, decoration: BoxDecoration( color: widget.color, borderRadius: theme.radius, ), ), ); } } class EmptyState extends StatelessWidget { const EmptyState({ required this.title, required this.message, this.icon = Icons.search_off, this.actionLabel, this.onAction, super.key, }); final String title; final String message; final IconData icon; final String? actionLabel; final VoidCallback? onAction; @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(YantingSpacing.x6), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 42, color: theme.colorScheme.foreground), const SizedBox(height: YantingSpacing.cardGap), Text(title, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: YantingSpacing.x2), Text( message, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), if (actionLabel != null) ...[ const SizedBox(height: YantingSpacing.x4), AppButton( label: actionLabel!, onPressed: onAction, kind: AppButtonKind.ghost, ), ], ], ), ), ); } } class ErrorState extends StatelessWidget { const ErrorState({required this.message, this.onRetry, super.key}); final String message; final VoidCallback? onRetry; @override Widget build(BuildContext context) { return EmptyState( icon: Icons.cloud_off_outlined, title: '内容暂时加载失败', message: message, actionLabel: onRetry == null ? null : '重试', onAction: onRetry, ); } } void showAppToast(BuildContext context, String message) { ShadToaster.of(context).show(ShadToast(title: Text(message))); }