Phase 0 — 共享组件: - EmptyStateWidget: 统一空状态 (icon + title + subtitle + CTA) - ErrorStateWidget: 统一错误状态 (message + retry) - SkeletonBox + SkeletonList: 统一骨架屏加载 (shimmer 动画) Phase 1 — Bug 修复: - 班级评论按 journalId 过滤,避免显示在错误日记卡片下 - moodCellColors key 修正: love/tired → angry/thinking - 日历非 CalendarLoaded 状态改为加载指示器 (不再 SizedBox.shrink) - 贴纸数统计改为 '--' 占位 (之前错误显示日记总数)
157 lines
3.6 KiB
Dart
157 lines
3.6 KiB
Dart
// 共享骨架屏加载组件 — 替代 CircularProgressIndicator
|
||
//
|
||
// 使用: SkeletonBox(height: 80) 或 SkeletonBox(height: 48, shimmer: true)
|
||
// 样式: 灰色圆角矩形 + 可选 shimmer 微光动画
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../core/theme/app_radius.dart';
|
||
|
||
/// 骨架屏加载占位块
|
||
class SkeletonBox extends StatelessWidget {
|
||
const SkeletonBox({
|
||
super.key,
|
||
required this.height,
|
||
this.width,
|
||
this.shimmer = false,
|
||
this.borderRadius,
|
||
});
|
||
|
||
/// 高度
|
||
final double height;
|
||
|
||
/// 宽度(默认撑满父容器)
|
||
final double? width;
|
||
|
||
/// 是否启用 shimmer 微光动画
|
||
final bool shimmer;
|
||
|
||
/// 自定义圆角(默认 AppRadius.md)
|
||
final BorderRadius? borderRadius;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final bgColor = theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3);
|
||
final radius = borderRadius ?? AppRadius.mdBorder;
|
||
|
||
if (shimmer) {
|
||
return _ShimmerBox(
|
||
height: height,
|
||
width: width,
|
||
bgColor: bgColor,
|
||
borderRadius: radius,
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
height: height,
|
||
width: width,
|
||
decoration: BoxDecoration(
|
||
color: bgColor,
|
||
borderRadius: radius,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 带微光动画的骨架屏
|
||
class _ShimmerBox extends StatefulWidget {
|
||
const _ShimmerBox({
|
||
required this.height,
|
||
this.width,
|
||
required this.bgColor,
|
||
required this.borderRadius,
|
||
});
|
||
|
||
final double height;
|
||
final double? width;
|
||
final Color bgColor;
|
||
final BorderRadius borderRadius;
|
||
|
||
@override
|
||
State<_ShimmerBox> createState() => _ShimmerBoxState();
|
||
}
|
||
|
||
class _ShimmerBoxState extends State<_ShimmerBox>
|
||
with SingleTickerProviderStateMixin {
|
||
late final AnimationController _controller;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(milliseconds: 1500),
|
||
)..repeat();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return AnimatedBuilder(
|
||
animation: _controller,
|
||
builder: (context, child) {
|
||
return Container(
|
||
height: widget.height,
|
||
width: widget.width,
|
||
decoration: BoxDecoration(
|
||
borderRadius: widget.borderRadius,
|
||
gradient: LinearGradient(
|
||
begin: Alignment(-1.0 + 2.0 * _controller.value, 0),
|
||
end: Alignment(1.0 + 2.0 * _controller.value, 0),
|
||
colors: [
|
||
widget.bgColor,
|
||
theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.15),
|
||
widget.bgColor,
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 通用列表骨架屏 — 显示 N 行占位
|
||
class SkeletonList extends StatelessWidget {
|
||
const SkeletonList({
|
||
super.key,
|
||
this.itemCount = 5,
|
||
this.itemHeight = 72,
|
||
this.spacing = 12,
|
||
this.shimmer = false,
|
||
});
|
||
|
||
/// 骨架行数
|
||
final int itemCount;
|
||
|
||
/// 每行高度
|
||
final double itemHeight;
|
||
|
||
/// 行间距
|
||
final double spacing;
|
||
|
||
/// 是否 shimmer
|
||
final bool shimmer;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ListView.builder(
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
shrinkWrap: true,
|
||
itemCount: itemCount,
|
||
itemBuilder: (_, _) => Padding(
|
||
padding: EdgeInsets.only(bottom: spacing),
|
||
child: SkeletonBox(height: itemHeight, shimmer: shimmer),
|
||
),
|
||
);
|
||
}
|
||
}
|