Files
2026-06-07 12:03:23 +08:00

193 lines
4.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.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<double>(
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,
);
}
}
Future<bool?> showAppToast(BuildContext context, String message) {
// ShadToaster.of(context).show(ShadToast(title: Text(message)));
return Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: const Color(0xCC111111),
textColor: Colors.white,
fontSize: 16,
);
}