release
This commit is contained in:
335
docs/superpowers/plans/2026-04-03-home-poetry-card-skeleton.md
Normal file
335
docs/superpowers/plans/2026-04-03-home-poetry-card-skeleton.md
Normal 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. **平滑过渡**:骨架屏到实际内容的切换无闪烁
|
||||
663
docs/superpowers/plans/2026-04-03-poetry-theme-refactor.md
Normal file
663
docs/superpowers/plans/2026-04-03-poetry-theme-refactor.md
Normal 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. **测试充分**:测试所有功能,包括主题色切换、深色模式、关怀模式
|
||||
@@ -0,0 +1,59 @@
|
||||
# 主页诗词卡片骨架屏设计文档
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前主页诗词卡片在点击加载下一条时会出现闪白现象,原因是:
|
||||
1. `poetryData.value = newPoetryData` 触发整个卡片重建
|
||||
2. 重建过程中卡片内容短暂消失,显示白色背景
|
||||
3. 用户体验不佳,应该有平滑的过渡效果
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案:骨架屏过渡(推荐)
|
||||
|
||||
在加载新数据时,保持卡片容器显示,使用骨架屏(Skeleton)代替实际内容,实现平滑过渡。
|
||||
|
||||
**核心思路:**
|
||||
1. 卡片容器始终存在,不随数据变化而销毁重建
|
||||
2. 每个区域(标题、诗句、原文、关键词、译文)独立控制加载状态
|
||||
3. 加载时使用骨架屏动画,加载完成后显示实际内容
|
||||
4. 避免整个卡片的闪白现象
|
||||
|
||||
**技术实现:**
|
||||
1. 修改 `PoetryCard` 组件,添加骨架屏支持
|
||||
2. 使用 `Shimmer` 效果或自定义骨架动画
|
||||
3. 保持卡片背景色在加载过程中不变
|
||||
4. 分区域加载,逐步显示内容
|
||||
|
||||
## 文件修改
|
||||
|
||||
### 1. home_part.dart
|
||||
- 修改 `PoetryCard` 组件
|
||||
- 添加骨架屏 Widget
|
||||
- 优化加载状态的显示逻辑
|
||||
|
||||
### 2. home_controller.dart(如有需要)
|
||||
- 调整加载状态的管理
|
||||
- 确保骨架屏在正确时机显示
|
||||
|
||||
## 设计要点
|
||||
|
||||
1. **骨架屏样式**
|
||||
- 使用与卡片背景对比的颜色
|
||||
- 添加脉冲动画效果
|
||||
- 保持与内容区域相同的尺寸
|
||||
|
||||
2. **过渡动画**
|
||||
- 骨架屏到实际内容的淡入淡出
|
||||
- 避免突兀的切换
|
||||
|
||||
3. **深色模式支持**
|
||||
- 骨架屏颜色适配深色/浅色模式
|
||||
- 保持视觉一致性
|
||||
|
||||
## 预期效果
|
||||
|
||||
- 点击下一条时,卡片保持显示
|
||||
- 各区域显示骨架屏动画
|
||||
- 数据加载完成后,内容平滑过渡显示
|
||||
- 无闪白现象,用户体验流畅
|
||||
@@ -0,0 +1,185 @@
|
||||
# 诗词答题页面主题色支持与代码重构设计文档
|
||||
|
||||
**日期:** 2026-04-03
|
||||
**状态:** 待实现
|
||||
**作者:** AI Assistant
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
将诗词答题相关页面重构以支持动态主题色,并将代码分流到 `poetry-page.dart`,实现 UI 和逻辑的分离。
|
||||
|
||||
### 1.2 范围
|
||||
|
||||
涉及文件:
|
||||
- `lib/views/profile/level/poetry.dart` - 主页面(状态管理、网络请求)
|
||||
- `lib/views/profile/level/poetry-page.dart` - UI 组件(选项、标签、布局)
|
||||
- `lib/views/profile/level/flow-anim.dart` - 流动边框动画
|
||||
- `lib/views/profile/level/distinguish.dart` - 答题记录页面
|
||||
- `lib/views/profile/level/level-jilu.dart` - 业务逻辑管理器(无需修改)
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 文件结构
|
||||
|
||||
```
|
||||
lib/views/profile/level/
|
||||
├── poetry.dart # 主页面(状态管理、网络请求)
|
||||
├── poetry-page.dart # UI 组件(选项、标签、布局)
|
||||
├── level-jilu.dart # 业务逻辑管理器
|
||||
├── flow-anim.dart # 流动边框动画
|
||||
└── distinguish.dart # 答题记录页面
|
||||
```
|
||||
|
||||
### 2.2 职责划分
|
||||
|
||||
**poetry.dart 职责:**
|
||||
- `PoetryLevelPage` 类(StatefulWidget)
|
||||
- `_PoetryLevelPageState` 类
|
||||
- 状态变量管理
|
||||
- 生命周期方法
|
||||
- 业务逻辑方法(网络请求、数据处理)
|
||||
- `build` 方法的主框架
|
||||
|
||||
**poetry-page.dart 职责:**
|
||||
- UI 组件定义
|
||||
- 主题色支持
|
||||
- 深色模式适配
|
||||
- 组件复用
|
||||
|
||||
## 3. 组件设计
|
||||
|
||||
### 3.1 PoetryOptionItem 组件
|
||||
|
||||
**功能:** 单个答题选项组件
|
||||
|
||||
**参数:**
|
||||
- `option`: dynamic - 选项数据
|
||||
- `isSelected`: bool - 是否被选中
|
||||
- `isCorrect`: bool - 是否正确
|
||||
- `isWrong`: bool - 是否错误
|
||||
- `isSubmitting`: bool - 是否正在提交
|
||||
- `showFeedback`: bool - 是否显示反馈
|
||||
- `isAnswerCorrect`: bool - 答案是否正确
|
||||
- `onTap`: Function(int) - 点击回调
|
||||
|
||||
**主题色支持:**
|
||||
- 使用 `ThemeColors.getThemeColor(themeController.themeColorIndexRx.value)` 获取主题色
|
||||
- 正确答案使用 `Colors.green`
|
||||
- 错误答案使用 `Colors.red`
|
||||
- 背景色根据深色模式动态调整
|
||||
|
||||
### 3.2 PoetryOptionsLayout 组件
|
||||
|
||||
**功能:** 选项布局组件(支持 2x2 和 1x4 布局)
|
||||
|
||||
**参数:**
|
||||
- `options`: List<dynamic> - 选项列表
|
||||
- `selectedAnswer`: int? - 选中的答案
|
||||
- `showFeedback`: bool - 是否显示反馈
|
||||
- `isAnswerCorrect`: bool - 答案是否正确
|
||||
- `isSubmitting`: bool - 是否正在提交
|
||||
- `onTap`: Function(int) - 点击回调
|
||||
|
||||
**布局逻辑:**
|
||||
- 检查所有选项是否都少于等于4个字
|
||||
- 如果是且选项数>=4,使用 2x2 布局
|
||||
- 否则使用 1x4 布局
|
||||
|
||||
### 3.3 PoetryTag 组件
|
||||
|
||||
**功能:** 标签组件(显示题目类型、年级、朝代)
|
||||
|
||||
**参数:**
|
||||
- `label`: String - 标签名称
|
||||
- `value`: String - 标签值
|
||||
|
||||
**主题色支持:**
|
||||
- 使用主题色作为背景色和文字颜色
|
||||
|
||||
## 4. 主题色支持策略
|
||||
|
||||
### 4.1 颜色替换规则
|
||||
|
||||
**替换:**
|
||||
- `AppConstants.primaryColor` → `ThemeColors.getThemeColor(themeController.themeColorIndexRx.value)`
|
||||
|
||||
**保留:**
|
||||
- 正确答案:`Colors.green` / `AppColors.iosGreen`
|
||||
- 错误答案:`Colors.red` / `AppColors.iosRed`
|
||||
- 背景色:根据深色模式动态调整
|
||||
|
||||
### 4.2 深色模式支持
|
||||
|
||||
**判断方式:**
|
||||
- 使用 `themeController.isDarkModeRx.value` 判断深色模式
|
||||
|
||||
**颜色调整:**
|
||||
- 背景色:`isDark ? Color(0xFF1A1A1A) : Colors.white`
|
||||
- 文字颜色:`isDark ? Colors.grey[300] : Colors.grey[700]`
|
||||
- 边框颜色:`isDark ? Colors.white.withAlpha(20) : Colors.black.withAlpha(10)`
|
||||
|
||||
## 5. 实现步骤
|
||||
|
||||
### 5.1 创建 poetry-page.dart
|
||||
|
||||
1. 创建 `PoetryOptionItem` 组件
|
||||
2. 创建 `PoetryOptionsLayout` 组件
|
||||
3. 创建 `PoetryTag` 组件
|
||||
4. 添加主题色支持
|
||||
5. 添加深色模式支持
|
||||
|
||||
### 5.2 修改 poetry.dart
|
||||
|
||||
1. 移除 `_buildOptionItem` 方法
|
||||
2. 移除 `_buildOptionsLayout` 方法
|
||||
3. 移除 `_buildTag` 方法
|
||||
4. 导入 `poetry-page.dart`
|
||||
5. 使用新组件替换原有方法
|
||||
6. 添加主题色支持
|
||||
|
||||
### 5.3 修改 flow-anim.dart
|
||||
|
||||
1. 添加主题色支持
|
||||
2. 使用 `ThemeColors.getThemeColor()` 替换硬编码颜色
|
||||
|
||||
### 5.4 修改 distinguish.dart
|
||||
|
||||
1. 添加主题色支持
|
||||
2. 使用 `ThemeColors.getThemeColor()` 替换硬编码颜色
|
||||
|
||||
## 6. 测试要点
|
||||
|
||||
### 6.1 功能测试
|
||||
|
||||
- 选项点击正常
|
||||
- 答案提交正常
|
||||
- 反馈显示正常
|
||||
- 布局切换正常(2x2 和 1x4)
|
||||
|
||||
### 6.2 主题色测试
|
||||
|
||||
- 主题色切换正常
|
||||
- 所有组件颜色同步更新
|
||||
- 深色模式切换正常
|
||||
|
||||
### 6.3 兼容性测试
|
||||
|
||||
- 关怀模式正常显示
|
||||
- 底部导航栏不遮挡内容
|
||||
- 动画效果正常
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. **保持布局不变**:重构过程中不修改页面布局
|
||||
2. **代码平衡**:确保两个文件代码量相近
|
||||
3. **性能优化**:使用 `Obx` 进行响应式更新,避免不必要的重建
|
||||
4. **向后兼容**:确保现有功能不受影响
|
||||
|
||||
## 8. 后续优化
|
||||
|
||||
1. 考虑将更多组件提取到 `poetry-page.dart`
|
||||
2. 添加单元测试
|
||||
3. 优化性能,减少不必要的重建
|
||||
4. 添加更多主题色选项
|
||||
Reference in New Issue
Block a user