feat(app): 共享 UI 组件 + 4 个关键 UX bug 修复
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

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)
- 贴纸数统计改为 '--' 占位 (之前错误显示日记总数)
This commit is contained in:
iven
2026-06-07 13:36:10 +08:00
parent 1f48a67db5
commit f64355946c
7 changed files with 360 additions and 30 deletions

View File

@@ -0,0 +1,156 @@
// 共享骨架屏加载组件 — 替代 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),
),
);
}
}