diff --git a/lib/views/home/components/skeleton_widgets.dart b/lib/views/home/components/skeleton_widgets.dart new file mode 100644 index 0000000..7c16e46 --- /dev/null +++ b/lib/views/home/components/skeleton_widgets.dart @@ -0,0 +1,258 @@ +/// 时间: 2026-04-03 +/// 功能: 骨架屏组件 +/// 介绍: 用于在数据加载时显示占位动画,避免页面闪白 +/// 最新变化: 新建文件 + +import 'package:flutter/material.dart'; + +/// 骨架屏基础组件 +class SkeletonContainer extends StatefulWidget { + final double width; + final double height; + final double borderRadius; + final Color? baseColor; + final Color? highlightColor; + + const SkeletonContainer({ + super.key, + required this.width, + required this.height, + this.borderRadius = 8, + this.baseColor, + this.highlightColor, + }); + + @override + State createState() => _SkeletonContainerState(); +} + +class _SkeletonContainerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(); + _animation = Tween(begin: -1, end: 2).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + widget.baseColor ?? Colors.grey[300]!, + widget.highlightColor ?? Colors.grey[100]!, + widget.baseColor ?? Colors.grey[300]!, + ], + stops: [ + _animation.value - 0.3, + _animation.value, + _animation.value + 0.3, + ], + ), + ), + ); + }, + ); + } +} + +/// 诗词卡片骨架屏 +class PoetryCardSkeleton extends StatelessWidget { + final bool isDark; + + const PoetryCardSkeleton({super.key, this.isDark = false}); + + @override + Widget build(BuildContext context) { + final baseColor = isDark ? const Color(0xFF3A3A3A) : Colors.grey[300]; + final highlightColor = isDark ? const Color(0xFF4A4A4A) : Colors.grey[100]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(isDark ? 40 : 10), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题区域骨架 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: SkeletonContainer( + width: double.infinity, + height: 24, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + ), + // 诗句名称区域骨架 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + SkeletonContainer( + width: 120, + height: 28, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + const SizedBox(height: 8), + SkeletonContainer( + width: 80, + height: 16, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + ], + ), + ), + // 原文区域骨架 + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + SkeletonContainer( + width: double.infinity, + height: 20, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + const SizedBox(height: 8), + SkeletonContainer( + width: double.infinity, + height: 20, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + const SizedBox(height: 8), + SkeletonContainer( + width: double.infinity, + height: 20, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + const SizedBox(height: 8), + SkeletonContainer( + width: 150, + height: 20, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + ], + ), + ), + // 关键词区域骨架 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + SkeletonContainer( + width: 60, + height: 24, + borderRadius: 12, + baseColor: baseColor, + highlightColor: highlightColor, + ), + SkeletonContainer( + width: 80, + height: 24, + borderRadius: 12, + baseColor: baseColor, + highlightColor: highlightColor, + ), + SkeletonContainer( + width: 70, + height: 24, + borderRadius: 12, + baseColor: baseColor, + highlightColor: highlightColor, + ), + ], + ), + ), + // 译文区域骨架 + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SkeletonContainer( + width: 80, + height: 18, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + const SizedBox(height: 12), + SkeletonContainer( + width: double.infinity, + height: 16, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + const SizedBox(height: 8), + SkeletonContainer( + width: double.infinity, + height: 16, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + const SizedBox(height: 8), + SkeletonContainer( + width: double.infinity, + height: 16, + borderRadius: 4, + baseColor: baseColor, + highlightColor: highlightColor, + ), + ], + ), + ), + ], + ), + ); + } +}