Files
wushu/docs/superpowers/plans/2026-04-03-home-poetry-card-skeleton.md
Developer cba04235c8 release
2026-04-03 03:26:06 +08:00

8.7 KiB

主页诗词卡片骨架屏实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 修复主页诗词卡片点击下一条时的闪白问题,使用骨架屏实现平滑过渡

Architecture: 修改 PoetryCard 组件,在加载新数据时显示骨架屏动画,保持卡片容器不销毁重建,分区域逐步显示内容

Tech Stack: Flutter, GetX, Shimmer 效果


文件结构

文件 职责
lib/views/home/home_part.dart 修改 PoetryCard 组件,添加骨架屏支持
lib/views/home/set/home_components.dart 添加骨架屏 Widget 组件
lib/services/get/home_controller.dart 调整加载状态管理(如有需要)

Task 1: 创建骨架屏组件

Files:

  • Create: lib/views/home/components/skeleton_widgets.dart

  • Step 1: 创建骨架屏基础组件

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<SkeletonContainer> createState() => _SkeletonContainerState();
}

class _SkeletonContainerState extends State<SkeletonContainer>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1500),
      vsync: this,
    )..repeat();
    _animation = Tween<double>(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,
              ],
            ),
          ),
        );
      },
    );
  }
}
  • Step 2: 提交代码
git add lib/views/home/components/skeleton_widgets.dart
git commit -m "feat: 添加骨架屏基础组件 SkeletonContainer"

Task 2: 修改 PoetryCard 组件

Files:

  • Modify: lib/views/home/home_part.dart

  • Step 1: 导入骨架屏组件

在文件顶部添加导入:

import 'components/skeleton_widgets.dart';
  • Step 2: 修改 _buildTitleSection 方法

找到 _buildTitleSection 方法,修改加载状态的显示逻辑:

Widget _buildTitleSection() {
  final isLoading = widget.sectionLoadingStates?['title'] ?? false;
  
  if (isLoading) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: SkeletonContainer(
        width: double.infinity,
        height: 24,
        borderRadius: 4,
        baseColor: widget.isDark ? Colors.grey[800] : Colors.grey[300],
        highlightColor: widget.isDark ? Colors.grey[700] : Colors.grey[100],
      ),
    );
  }
  
  // 原有的非加载状态代码保持不变
  // ...
}
  • Step 3: 修改 _buildNameSection 方法
Widget _buildNameSection() {
  final isLoading = widget.sectionLoadingStates?['name'] ?? false;
  
  if (isLoading) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Column(
        children: [
          SkeletonContainer(
            width: 120,
            height: 28,
            borderRadius: 4,
            baseColor: widget.isDark ? Colors.grey[800] : Colors.grey[300],
            highlightColor: widget.isDark ? Colors.grey[700] : Colors.grey[100],
          ),
          const SizedBox(height: 8),
          SkeletonContainer(
            width: 80,
            height: 16,
            borderRadius: 4,
            baseColor: widget.isDark ? Colors.grey[800] : Colors.grey[300],
            highlightColor: widget.isDark ? Colors.grey[700] : Colors.grey[100],
          ),
        ],
      ),
    );
  }
  
  // 原有的非加载状态代码保持不变
  // ...
}
  • Step 4: 修改 _buildContentSection 方法
Widget _buildContentSection() {
  final isLoading = widget.sectionLoadingStates?['content'] ?? false;
  
  if (isLoading) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: List.generate(4, (index) => Padding(
          padding: const EdgeInsets.only(bottom: 8),
          child: SkeletonContainer(
            width: index == 3 ? double.infinity * 0.6 : double.infinity,
            height: 20,
            borderRadius: 4,
            baseColor: widget.isDark ? Colors.grey[800] : Colors.grey[300],
            highlightColor: widget.isDark ? Colors.grey[700] : Colors.grey[100],
          ),
        )),
      ),
    );
  }
  
  // 原有的非加载状态代码保持不变
  // ...
}
  • Step 5: 修改 _buildKeywordsSection 方法
Widget _buildKeywordsSection() {
  final isLoading = widget.sectionLoadingStates?['keywords'] ?? false;
  
  if (isLoading) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Wrap(
        spacing: 8,
        runSpacing: 8,
        children: List.generate(3, (index) => SkeletonContainer(
          width: 60,
          height: 24,
          borderRadius: 12,
          baseColor: widget.isDark ? Colors.grey[800] : Colors.grey[300],
          highlightColor: widget.isDark ? Colors.grey[700] : Colors.grey[100],
        )),
      ),
    );
  }
  
  // 原有的非加载状态代码保持不变
  // ...
}
  • Step 6: 修改 _buildIntroductionSection 方法
Widget _buildIntroductionSection() {
  final isLoading = widget.sectionLoadingStates?['introduction'] ?? false;
  
  if (isLoading) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SkeletonContainer(
            width: 80,
            height: 18,
            borderRadius: 4,
            baseColor: widget.isDark ? Colors.grey[800] : Colors.grey[300],
            highlightColor: widget.isDark ? Colors.grey[700] : Colors.grey[100],
          ),
          const SizedBox(height: 12),
          ...List.generate(3, (index) => Padding(
            padding: const EdgeInsets.only(bottom: 8),
            child: SkeletonContainer(
              width: double.infinity,
              height: 16,
              borderRadius: 4,
              baseColor: widget.isDark ? Colors.grey[800] : Colors.grey[300],
              highlightColor: widget.isDark ? Colors.grey[700] : Colors.grey[100],
            ),
          )),
        ],
      ),
    );
  }
  
  // 原有的非加载状态代码保持不变
  // ...
}
  • Step 7: 提交代码
git add lib/views/home/home_part.dart
git commit -m "feat: PoetryCard 组件添加骨架屏支持,修复闪白问题"

Task 3: 测试验证

Files:

  • Test: 运行应用验证

  • Step 1: 运行 Flutter 分析

flutter analyze lib/views/home/home_part.dart lib/views/home/components/skeleton_widgets.dart

Expected: 无错误

  • Step 2: 测试功能
  1. 打开主页
  2. 点击"下一条"按钮
  3. 验证:
    • 卡片不出现闪白
    • 各区域显示骨架屏动画
    • 内容加载完成后平滑显示
    • 深色模式下骨架屏颜色正确
  • Step 3: 提交代码
git add .
git commit -m "test: 验证骨架屏功能正常"

实现要点总结

  1. 骨架屏动画:使用渐变动画实现脉冲效果
  2. 深色模式支持:根据 isDark 参数调整骨架屏颜色
  3. 分区域加载:每个区域独立控制骨架屏显示
  4. 平滑过渡:骨架屏到实际内容的切换无闪烁