182 lines
4.6 KiB
Dart
182 lines
4.6 KiB
Dart
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<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,
|
|
);
|
|
}
|
|
}
|
|
|
|
void showAppToast(BuildContext context, String message) {
|
|
ShadToaster.of(context).show(ShadToast(title: Text(message)));
|
|
}
|