664 lines
18 KiB
Markdown
664 lines
18 KiB
Markdown
# 诗词答题页面主题色支持与代码重构实现计划
|
||
|
||
> **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. **测试充分**:测试所有功能,包括主题色切换、深色模式、关怀模式
|