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

18 KiB
Raw Blame History

诗词答题页面主题色支持与代码重构实现计划

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 文件并添加导入

/// 诗词答题页面组件
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 组件
/// 单个选项组件
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 组件

/// 选项布局组件
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 组件

/// 标签组件
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 顶部添加导入

在文件顶部的导入部分添加:

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() 调用,替换为:

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() 调用,替换为:

PoetryTag(
  label: '类型',
  value: _currentQuestion!['type']?.toString() ?? '',
)

类似地替换其他标签调用。

  • Step 7: 添加主题色支持到分数显示

找到分数显示的 Container,将 AppConstants.primaryColor 替换为:

final themeController = Get.find<ThemeController>();
final primaryColor = ThemeColors.getThemeColor(
  themeController.themeColorIndexRx.value,
);

然后在 Containerdecoration 中使用 primaryColor

  • Step 8: 保存文件

Task 5: 修改 flow-anim.dart 添加主题色支持

Files:

  • Modify: lib/views/profile/level/flow-anim.dart

  • Step 1: 在 flow-anim.dart 顶部添加导入

import '../../../models/colors/theme_colors.dart';
  • Step 2: 修改 FlowingBorderContainer 的 build 方法

build 方法修改为:

@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 顶部添加导入

import '../../../models/colors/theme_colors.dart';
  • Step 2: 在 _DistinguishPageState 类中添加 ThemeController

_DistinguishPageState 类中添加:

final ThemeController _themeController = Get.find<ThemeController>();
  • Step 3: 替换所有 AppConstants.primaryColor 为动态主题色

build 方法中,将所有 AppConstants.primaryColor 替换为:

final primaryColor = ThemeColors.getThemeColor(
  _themeController.themeColorIndexRx.value,
);

然后在需要的地方使用 primaryColor

  • Step 4: 添加深色模式支持

build 方法中添加:

final isDark = _themeController.isDarkModeRx.value;

然后根据 isDark 调整背景色、文字颜色等。

  • Step 5: 保存文件

Task 7: 测试功能

Files:

  • Test: 运行应用并测试所有功能

  • Step 1: 运行应用

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: 检查修改的文件

git status
  • Step 2: 添加所有修改的文件
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: 提交代码
git commit -m "feat: 诗词答题页面主题色支持与代码重构

- 创建 poetry-page.dart提取 UI 组件PoetryOptionItem、PoetryOptionsLayout、PoetryTag
- 修改 poetry.dart使用新组件添加主题色支持
- 修改 flow-anim.dart添加主题色支持
- 修改 distinguish.dart添加主题色支持
- 支持动态主题色切换
- 支持深色模式
- 保持页面布局不变"

注意事项

  1. 保持布局不变:重构过程中不修改页面布局
  2. 代码平衡:确保两个文件代码量相近
  3. 性能优化:使用 Obx 进行响应式更新,避免不必要的重建
  4. 向后兼容:确保现有功能不受影响
  5. 测试充分:测试所有功能,包括主题色切换、深色模式、关怀模式