This commit is contained in:
Developer
2026-04-03 03:26:06 +08:00
parent 3063deb34c
commit cba04235c8
49 changed files with 3955 additions and 1421 deletions

View File

@@ -0,0 +1,335 @@
# 主页诗词卡片骨架屏实现计划
> **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: 创建骨架屏基础组件**
```dart
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: 提交代码**
```bash
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: 导入骨架屏组件**
在文件顶部添加导入:
```dart
import 'components/skeleton_widgets.dart';
```
- [ ] **Step 2: 修改 _buildTitleSection 方法**
找到 `_buildTitleSection` 方法,修改加载状态的显示逻辑:
```dart
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 方法**
```dart
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 方法**
```dart
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 方法**
```dart
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 方法**
```dart
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: 提交代码**
```bash
git add lib/views/home/home_part.dart
git commit -m "feat: PoetryCard 组件添加骨架屏支持,修复闪白问题"
```
---
## Task 3: 测试验证
**Files:**
- Test: 运行应用验证
- [ ] **Step 1: 运行 Flutter 分析**
```bash
flutter analyze lib/views/home/home_part.dart lib/views/home/components/skeleton_widgets.dart
```
Expected: 无错误
- [ ] **Step 2: 测试功能**
1. 打开主页
2. 点击"下一条"按钮
3. 验证:
- 卡片不出现闪白
- 各区域显示骨架屏动画
- 内容加载完成后平滑显示
- 深色模式下骨架屏颜色正确
- [ ] **Step 3: 提交代码**
```bash
git add .
git commit -m "test: 验证骨架屏功能正常"
```
---
## 实现要点总结
1. **骨架屏动画**:使用渐变动画实现脉冲效果
2. **深色模式支持**:根据 `isDark` 参数调整骨架屏颜色
3. **分区域加载**:每个区域独立控制骨架屏显示
4. **平滑过渡**:骨架屏到实际内容的切换无闪烁

View File

@@ -0,0 +1,663 @@
# 诗词答题页面主题色支持与代码重构实现计划
> **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:** 将诗词答题相关页面重构以支持动态主题色,并将代码分流到 `poetry-page.dart`,实现 UI 和逻辑的分离。
**Architecture:** 采用组件化分流策略,将 UI 组件(选项、标签、布局)提取到 `poetry-page.dart`,主页面保留状态管理和业务逻辑。使用 GetX 的 `Obx` 进行响应式主题色更新。
**Tech Stack:** Flutter, GetX, ThemeController, ThemeColors
---
## 文件结构
**创建文件:**
- `lib/views/profile/level/poetry-page.dart` - UI 组件(选项、标签、布局)
**修改文件:**
- `lib/views/profile/level/poetry.dart` - 主页面(移除 UI 组件方法,使用新组件)
- `lib/views/profile/level/flow-anim.dart` - 流动边框动画(添加主题色支持)
- `lib/views/profile/level/distinguish.dart` - 答题记录页面(添加主题色支持)
---
## Task 1: 创建 poetry-page.dart 文件并实现 PoetryOptionItem 组件
**Files:**
- Create: `lib/views/profile/level/poetry-page.dart`
- [ ] **Step 1: 创建 poetry-page.dart 文件并添加导入**
```dart
/// 诗词答题页面组件
library;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../services/get/theme_controller.dart';
import '../../../models/colors/theme_colors.dart';
import '../../../constants/app_constants.dart';
```
- [ ] **Step 2: 实现 PoetryOptionItem 组件**
```dart
/// 单个选项组件
class PoetryOptionItem extends StatelessWidget {
final dynamic option;
final bool isSelected;
final bool isCorrect;
final bool isWrong;
final bool isSubmitting;
final bool showFeedback;
final bool isAnswerCorrect;
final Function(int) onTap;
const PoetryOptionItem({
super.key,
required this.option,
required this.isSelected,
required this.isCorrect,
required this.isWrong,
required this.isSubmitting,
required this.showFeedback,
required this.isAnswerCorrect,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
return Obx(() {
final isDark = themeController.isDarkModeRx.value;
final primaryColor = ThemeColors.getThemeColor(
themeController.themeColorIndexRx.value,
);
final optionNum = option['index'] ?? option['num'] ?? 0;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: Container(
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
colors: isCorrect
? [Colors.green[400]!, Colors.green[300]!]
: isWrong
? [Colors.red[400]!, Colors.red[300]!]
: [primaryColor, primaryColor.withAlpha(200)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: isSelected
? null
: isDark
? const Color(0xFF2A2A2A)
: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? Colors.transparent
: primaryColor.withAlpha(50),
width: 2,
),
boxShadow: isSelected
? [
BoxShadow(
color: (isCorrect
? Colors.green
: isWrong
? Colors.red
: primaryColor)
.withAlpha(80),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: [
BoxShadow(
color: isDark
? Colors.white.withAlpha(5)
: Colors.black.withAlpha(5),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isSubmitting || (showFeedback && isAnswerCorrect)
? null
: () {
if (showFeedback) {
onTap(-1); // -1 表示重置状态
} else {
onTap(optionNum);
}
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
colors: [
Colors.white,
Colors.white.withAlpha(230),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: isSelected
? null
: primaryColor.withAlpha(20),
shape: BoxShape.circle,
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withAlpha(20),
blurRadius: 4,
offset: const Offset(0, 2),
),
]
: null,
),
child: Center(
child: Text(
'$optionNum',
style: TextStyle(
color: isSelected
? (isCorrect
? Colors.green
: isWrong
? Colors.red
: primaryColor)
: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
option['content'] ?? option['text'] ?? '',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w500,
color: isSelected
? Colors.white
: isDark
? Colors.grey[300]
: Colors.black87,
),
),
),
if (isSelected)
Icon(
isCorrect
? Icons.check_circle
: isWrong
? Icons.cancel
: Icons.radio_button_checked,
color: Colors.white,
size: 28,
),
],
),
),
),
),
),
);
});
}
}
```
- [ ] **Step 3: 保存文件**
---
## Task 2: 实现 PoetryOptionsLayout 组件
**Files:**
- Modify: `lib/views/profile/level/poetry-page.dart`
- [ ] **Step 1: 在 poetry-page.dart 中添加 PoetryOptionsLayout 组件**
```dart
/// 选项布局组件
class PoetryOptionsLayout extends StatelessWidget {
final List<dynamic> options;
final int? selectedAnswer;
final bool showFeedback;
final bool isAnswerCorrect;
final bool isSubmitting;
final Function(int) onTap;
const PoetryOptionsLayout({
super.key,
required this.options,
required this.selectedAnswer,
required this.showFeedback,
required this.isAnswerCorrect,
required this.isSubmitting,
required this.onTap,
});
@override
Widget build(BuildContext context) {
if (options.isEmpty) {
return const SizedBox();
}
// 检查是否所有选项都少于等于4个字
bool allShortOptions = options.every((option) {
final text = option['content'] ?? '';
return text.length <= 4;
});
if (allShortOptions && options.length >= 4) {
// 2*2布局
return Column(
children: [
Row(
children: [
Expanded(
child: _buildOptionItem(options[0]),
),
const SizedBox(width: 12),
Expanded(
child: _buildOptionItem(options[1]),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildOptionItem(options[2]),
),
const SizedBox(width: 12),
Expanded(
child: _buildOptionItem(options[3]),
),
],
),
],
);
} else {
// 1*4布局
final List<Widget> optionWidgets = [];
for (int i = 0; i < options.length; i++) {
optionWidgets.add(_buildOptionItem(options[i]));
if (i < options.length - 1) {
optionWidgets.add(const SizedBox(height: 12));
}
}
return Column(children: optionWidgets);
}
}
Widget _buildOptionItem(dynamic option) {
final optionNum = option['index'] ?? option['num'] ?? 0;
final isSelected = selectedAnswer == optionNum;
final isCorrect = showFeedback && isAnswerCorrect && selectedAnswer == optionNum;
final isWrong = showFeedback && !isAnswerCorrect && selectedAnswer == optionNum;
return PoetryOptionItem(
option: option,
isSelected: isSelected,
isCorrect: isCorrect,
isWrong: isWrong,
isSubmitting: isSubmitting,
showFeedback: showFeedback,
isAnswerCorrect: isAnswerCorrect,
onTap: onTap,
);
}
}
```
- [ ] **Step 2: 保存文件**
---
## Task 3: 实现 PoetryTag 组件
**Files:**
- Modify: `lib/views/profile/level/poetry-page.dart`
- [ ] **Step 1: 在 poetry-page.dart 中添加 PoetryTag 组件**
```dart
/// 标签组件
class PoetryTag extends StatelessWidget {
final String label;
final String value;
const PoetryTag({
super.key,
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
if (value.isEmpty) return const SizedBox();
final themeController = Get.find<ThemeController>();
return Obx(() {
final isDark = themeController.isDarkModeRx.value;
final primaryColor = ThemeColors.getThemeColor(
themeController.themeColorIndexRx.value,
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: primaryColor.withAlpha(20),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
label,
style: TextStyle(
fontSize: 10,
color: primaryColor,
fontWeight: FontWeight.w600,
),
),
Text(
value,
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[300] : Colors.black87,
fontWeight: FontWeight.w500,
),
),
],
),
);
});
}
}
```
- [ ] **Step 2: 保存文件**
---
## Task 4: 修改 poetry.dart 主页面
**Files:**
- Modify: `lib/views/profile/level/poetry.dart`
- [ ] **Step 1: 在 poetry.dart 顶部添加导入**
在文件顶部的导入部分添加:
```dart
import 'poetry-page.dart';
```
- [ ] **Step 2: 移除 _buildOptionItem 方法**
删除 `_PoetryLevelPageState` 类中的 `_buildOptionItem` 方法(约 100 行代码)。
- [ ] **Step 3: 移除 _buildOptionsLayout 方法**
删除 `_PoetryLevelPageState` 类中的 `_buildOptionsLayout` 方法(约 40 行代码)。
- [ ] **Step 4: 移除 _buildTag 方法**
删除 `_PoetryLevelPageState` 类中的 `_buildTag` 方法(约 30 行代码)。
- [ ] **Step 5: 替换 _buildOptionsLayout 调用**
`build` 方法中,找到原来的 `_buildOptionsLayout()` 调用,替换为:
```dart
PoetryOptionsLayout(
options: _currentQuestion!['options'] as List,
selectedAnswer: _selectedAnswer,
showFeedback: _showFeedback,
isAnswerCorrect: _isAnswerCorrect,
isSubmitting: _isSubmitting,
onTap: (optionNum) {
if (optionNum == -1) {
// 重置状态
setState(() {
_showFeedback = false;
_selectedAnswer = null;
_feedbackMessage = null;
});
} else {
_submitAnswer(optionNum);
}
},
)
```
- [ ] **Step 6: 替换 _buildTag 调用**
`build` 方法中,找到原来的 `_buildTag()` 调用,替换为:
```dart
PoetryTag(
label: '类型',
value: _currentQuestion!['type']?.toString() ?? '',
)
```
类似地替换其他标签调用。
- [ ] **Step 7: 添加主题色支持到分数显示**
找到分数显示的 `Container`,将 `AppConstants.primaryColor` 替换为:
```dart
final themeController = Get.find<ThemeController>();
final primaryColor = ThemeColors.getThemeColor(
themeController.themeColorIndexRx.value,
);
```
然后在 `Container``decoration` 中使用 `primaryColor`
- [ ] **Step 8: 保存文件**
---
## Task 5: 修改 flow-anim.dart 添加主题色支持
**Files:**
- Modify: `lib/views/profile/level/flow-anim.dart`
- [ ] **Step 1: 在 flow-anim.dart 顶部添加导入**
```dart
import '../../../models/colors/theme_colors.dart';
```
- [ ] **Step 2: 修改 FlowingBorderContainer 的 build 方法**
`build` 方法修改为:
```dart
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
return Obx(() {
final color = widget.color ?? ThemeColors.getThemeColor(
themeController.themeColorIndexRx.value,
);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
padding: EdgeInsets.all(widget.width),
decoration: FlowingBorderDecoration(
animation: _animation,
color: color,
width: widget.width,
),
child: widget.child,
);
},
);
});
}
```
- [ ] **Step 3: 保存文件**
---
## Task 6: 修改 distinguish.dart 添加主题色支持
**Files:**
- Modify: `lib/views/profile/level/distinguish.dart`
- [ ] **Step 1: 在 distinguish.dart 顶部添加导入**
```dart
import '../../../models/colors/theme_colors.dart';
```
- [ ] **Step 2: 在 _DistinguishPageState 类中添加 ThemeController**
`_DistinguishPageState` 类中添加:
```dart
final ThemeController _themeController = Get.find<ThemeController>();
```
- [ ] **Step 3: 替换所有 AppConstants.primaryColor 为动态主题色**
`build` 方法中,将所有 `AppConstants.primaryColor` 替换为:
```dart
final primaryColor = ThemeColors.getThemeColor(
_themeController.themeColorIndexRx.value,
);
```
然后在需要的地方使用 `primaryColor`
- [ ] **Step 4: 添加深色模式支持**
`build` 方法中添加:
```dart
final isDark = _themeController.isDarkModeRx.value;
```
然后根据 `isDark` 调整背景色、文字颜色等。
- [ ] **Step 5: 保存文件**
---
## Task 7: 测试功能
**Files:**
- Test: 运行应用并测试所有功能
- [ ] **Step 1: 运行应用**
```bash
flutter run
```
- [ ] **Step 2: 测试答题页面**
1. 进入诗词答题页面
2. 点击选项,验证选项点击正常
3. 提交答案,验证反馈显示正常
4. 测试 2x2 和 1x4 布局切换
5. 测试上一题、下一题功能
- [ ] **Step 3: 测试主题色切换**
1. 进入设置页面
2. 切换主题色
3. 返回答题页面,验证所有组件颜色同步更新
- [ ] **Step 4: 测试深色模式**
1. 切换深色模式
2. 验证答题页面颜色正常
3. 验证选项、标签等组件颜色正常
- [ ] **Step 5: 测试关怀模式**
1. 开启关怀模式
2. 进入答题页面
3. 验证底部导航栏不遮挡内容
4. 验证答题功能正常
---
## Task 8: 提交代码
**Files:**
- Commit: 所有修改的文件
- [ ] **Step 1: 检查修改的文件**
```bash
git status
```
- [ ] **Step 2: 添加所有修改的文件**
```bash
git add lib/views/profile/level/poetry.dart
git add lib/views/profile/level/poetry-page.dart
git add lib/views/profile/level/flow-anim.dart
git add lib/views/profile/level/distinguish.dart
```
- [ ] **Step 3: 提交代码**
```bash
git commit -m "feat: 诗词答题页面主题色支持与代码重构
- 创建 poetry-page.dart提取 UI 组件PoetryOptionItem、PoetryOptionsLayout、PoetryTag
- 修改 poetry.dart使用新组件添加主题色支持
- 修改 flow-anim.dart添加主题色支持
- 修改 distinguish.dart添加主题色支持
- 支持动态主题色切换
- 支持深色模式
- 保持页面布局不变"
```
---
## 注意事项
1. **保持布局不变**:重构过程中不修改页面布局
2. **代码平衡**:确保两个文件代码量相近
3. **性能优化**:使用 `Obx` 进行响应式更新,避免不必要的重建
4. **向后兼容**:确保现有功能不受影响
5. **测试充分**:测试所有功能,包括主题色切换、深色模式、关怀模式