在这里插入图片描述

前言

三国杀对新手来说是个挑战。复杂的规则、众多的武将技能、各种卡牌效果,这些都需要时间去理解。一个好的新手入门系统能让玩家快速上手,逐步掌握游戏的核心玩法。

本文将介绍如何实现一个完整的新手入门攻略系统。这不是简单的文字教程,而是一个包含分步引导、交互式学习、进度跟踪和知识测试的完整学习平台。

核心功能设计

分步教程:将复杂规则分解为易懂的步骤,每步都有清晰的目标。

交互式学习:通过选择题、模拟操作等方式加深理解,而不仅仅是被动阅读。

进度跟踪:记录用户的学习进度、得分、成就等,让用户了解自己的学习状态。

个性化推荐:根据用户的学习情况推荐合适的下一个教程,提高学习效率。

教程数据模型

首先定义教程的数据结构。这是整个系统的基础,所有的教程数据都会按照这个结构组织。

// lib/models/tutorial_model.dart
class TutorialModel {
  final String id;
  final String title;
  final String description;
  final String category;
  final int order;
  final List<TutorialStep> steps;
  final List<String> prerequisites;
  final int estimatedTime;
  final String difficulty;
  final bool isCompleted;
  final double progress;

  TutorialModel({
    required this.id,
    required this.title,
    required this.description,
    required this.category,
    required this.order,
    required this.steps,
    required this.prerequisites,
    required this.estimatedTime,
    required this.difficulty,
    required this.isCompleted,
    required this.progress,
  });

  factory TutorialModel.fromJson(Map<String, dynamic> json) {
    return TutorialModel(
      id: json['id'] ?? '',
      title: json['title'] ?? '',
      description: json['description'] ?? '',
      category: json['category'] ?? '',
      order: json['order'] ?? 0,
      steps: (json['steps'] as List?)
          ?.map((e) => TutorialStep.fromJson(e))
          .toList() ?? [],
      prerequisites: List<String>.from(json['prerequisites'] ?? []),
      estimatedTime: json['estimatedTime'] ?? 0,
      difficulty: json['difficulty'] ?? 'beginner',
      isCompleted: json['isCompleted'] ?? false,
      progress: (json['progress'] ?? 0.0).toDouble(),
    );
  }
}

这个模型包含了教程的所有基本信息prerequisites 字段定义了前置条件,比如学习"技能详解"之前要先学"基础规则"。steps 列表包含教程的所有步骤,每个步骤可以是文字、图片、视频或交互式内容。progress 字段记录了用户的学习进度百分比。

教程步骤定义

每个教程由多个步骤组成,每个步骤可以有不同的类型和内容。

class TutorialStep {
  final String id;
  final String title;
  final String content;
  final TutorialStepType type;
  final List<String> images;
  final String? video;
  final TutorialInteraction? interaction;
  final bool isCompleted;

  TutorialStep({
    required this.id,
    required this.title,
    required this.content,
    required this.type,
    required this.images,
    this.video,
    this.interaction,
    required this.isCompleted,
  });

  factory TutorialStep.fromJson(Map<String, dynamic> json) {
    return TutorialStep(
      id: json['id'] ?? '',
      title: json['title'] ?? '',
      content: json['content'] ?? '',
      type: _parseStepType(json['type']),
      images: List<String>.from(json['images'] ?? []),
      video: json['video'],
      interaction: json['interaction'] != null
          ? TutorialInteraction.fromJson(json['interaction'])
          : null,
      isCompleted: json['isCompleted'] ?? false,
    );
  }

  static TutorialStepType _parseStepType(String? type) {
    switch (type) {
      case 'text':
        return TutorialStepType.text;
      case 'image':
        return TutorialStepType.image;
      case 'video':
        return TutorialStepType.video;
      case 'interaction':
        return TutorialStepType.interaction;
      case 'quiz':
        return TutorialStepType.quiz;
      default:
        return TutorialStepType.text;
    }
  }
}

enum TutorialStepType {
  text,        // 纯文字说明
  image,       // 图片展示
  video,       // 视频教程
  interaction, // 交互式内容
  quiz,        // 知识测试
}

TutorialStepType 枚举定义了不同类型的步骤。每种类型都有不同的展示方式。比如 text 类型就是简单的文字说明,interaction 类型是一个选择题,quiz 类型是知识测试。这种灵活的设计让教程可以包含各种类型的内容。

交互式内容模型

交互式步骤需要定义选项和答案,这样系统才能判断用户的回答是否正确。

class TutorialInteraction {
  final String type;
  final String question;
  final List<InteractionOption> options;
  final String? correctAnswer;

  TutorialInteraction({
    required this.type,
    required this.question,
    required this.options,
    this.correctAnswer,
  });

  factory TutorialInteraction.fromJson(Map<String, dynamic> json) {
    return TutorialInteraction(
      type: json['type'] ?? 'choice',
      question: json['question'] ?? '',
      options: (json['options'] as List?)
          ?.map((e) => InteractionOption.fromJson(e))
          .toList() ?? [],
      correctAnswer: json['correctAnswer'],
    );
  }
}

class InteractionOption {
  final String id;
  final String text;
  final String? image;
  final bool isCorrect;
  final String? explanation;

  InteractionOption({
    required this.id,
    required this.text,
    this.image,
    required this.isCorrect,
    this.explanation,
  });

  factory InteractionOption.fromJson(Map<String, dynamic> json) {
    return InteractionOption(
      id: json['id'] ?? '',
      text: json['text'] ?? '',
      image: json['image'],
      isCorrect: json['isCorrect'] ?? false,
      explanation: json['explanation'],
    );
  }
}

交互选项包含了答案、是否正确、以及解释。当用户选择一个选项时,系统会检查 isCorrect 字段,然后显示相应的反馈和解释。这样用户不仅知道答案是什么,还能理解为什么。

学习进度跟踪

学习进度的跟踪对于个性化推荐和用户激励很重要。系统需要记录用户学过哪些教程、得了多少分、解锁了哪些成就。

class LearningProgress {
  final String userId;
  final Map<String, TutorialProgress> tutorials;
  final int totalScore;
  final int level;
  final List<String> achievements;
  final DateTime lastUpdated;

  LearningProgress({
    required this.userId,
    required this.tutorials,
    required this.totalScore,
    required this.level,
    required this.achievements,
    required this.lastUpdated,
  });

  factory LearningProgress.fromJson(Map<String, dynamic> json) {
    final tutorialsMap = <String, TutorialProgress>{};
    if (json['tutorials'] is Map) {
      (json['tutorials'] as Map).forEach((key, value) {
        tutorialsMap[key] = TutorialProgress.fromJson(value);
      });
    }
    
    return LearningProgress(
      userId: json['userId'] ?? '',
      tutorials: tutorialsMap,
      totalScore: json['totalScore'] ?? 0,
      level: json['level'] ?? 1,
      achievements: List<String>.from(json['achievements'] ?? []),
      lastUpdated: DateTime.parse(json['lastUpdated'] ?? DateTime.now().toIso8601String()),
    );
  }
}

class TutorialProgress {
  final String tutorialId;
  final double progress;
  final int currentStep;
  final bool isCompleted;
  final int score;
  final DateTime startedAt;
  final DateTime? completedAt;

  TutorialProgress({
    required this.tutorialId,
    required this.progress,
    required this.currentStep,
    required this.isCompleted,
    required this.score,
    required this.startedAt,
    this.completedAt,
  });

  factory TutorialProgress.fromJson(Map<String, dynamic> json) {
    return TutorialProgress(
      tutorialId: json['tutorialId'] ?? '',
      progress: (json['progress'] ?? 0.0).toDouble(),
      currentStep: json['currentStep'] ?? 0,
      isCompleted: json['isCompleted'] ?? false,
      score: json['score'] ?? 0,
      startedAt: DateTime.parse(json['startedAt'] ?? DateTime.now().toIso8601String()),
      completedAt: json['completedAt'] != null 
          ? DateTime.parse(json['completedAt'])
          : null,
    );
  }
}

LearningProgress 记录了用户的全局学习数据,包括总积分、等级、成就等。TutorialProgress 记录了单个教程的学习情况,包括当前进度、完成状态、得分等。这样系统可以根据这些数据来推荐下一个教程。

教程控制器实现

控制器负责管理教程的逻辑,包括加载数据、处理用户交互、更新进度等。使用GetX框架来管理状态,这样UI会自动响应数据变化。

// lib/controllers/beginner_guide_controller.dart
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../models/tutorial_model.dart';
import '../services/tutorial_service.dart';

class BeginnerGuideController extends GetxController with GetTickerProviderStateMixin {
  final TutorialService _tutorialService = Get.find<TutorialService>();
  
  final RxList<TutorialModel> tutorials = <TutorialModel>[].obs;
  final Rx<TutorialModel?> currentTutorial = Rx<TutorialModel?>(null);
  final RxInt currentStepIndex = 0.obs;
  final RxBool isPlaying = false.obs;
  final RxString selectedCategory = 'all'.obs;
  final RxBool isLoading = false.obs;
  
  late AnimationController fadeController;
  late Animation<double> fadeAnimation;
  
  
  void onInit() {
    super.onInit();
    _initAnimations();
    loadTutorials();
  }
  
  void _initAnimations() {
    fadeController = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    );
    
    fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: fadeController, curve: Curves.easeInOut),
    );
  }
  
  
  void onClose() {
    fadeController.dispose();
    super.onClose();
  }

使用 GetxControllerGetTickerProviderStateMixin 来管理状态和动画RxListRx 是GetX提供的响应式变量,当它们的值改变时,UI会自动更新。这样我们不需要手动调用 setState

加载和管理教程

  Future<void> loadTutorials() async {
    try {
      isLoading.value = true;
      final data = await _tutorialService.getAllTutorials();
      tutorials.assignAll(data);
      _sortTutorials();
    } catch (e) {
      Get.snackbar('错误', '加载教程数据失败: $e');
    } finally {
      isLoading.value = false;
    }
  }
  
  void _sortTutorials() {
    tutorials.sort((a, b) => a.order.compareTo(b.order));
  }
  
  void startTutorial(TutorialModel tutorial) {
    currentTutorial.value = tutorial;
    currentStepIndex.value = 0;
    isPlaying.value = true;
    fadeController.forward();
    _recordTutorialStart(tutorial.id);
  }
  
  void exitTutorial() {
    fadeController.reverse().then((_) {
      isPlaying.value = false;
      currentTutorial.value = null;
      currentStepIndex.value = 0;
    });
  }

从服务层获取教程数据,然后按顺序排序。这样教程会按照设计的顺序展示给用户。startTutorial 方法设置当前教程和步骤索引,然后播放进入动画。exitTutorial 方法播放退出动画,然后重置状态。

步骤导航和交互处理

  void nextStep() {
    if (currentTutorial.value == null) return;
    
    final tutorial = currentTutorial.value!;
    if (currentStepIndex.value < tutorial.steps.length - 1) {
      currentStepIndex.value++;
      _updateStepProgress();
    } else {
      _completeTutorial();
    }
  }
  
  void previousStep() {
    if (currentStepIndex.value > 0) {
      currentStepIndex.value--;
      _updateStepProgress();
    }
  }
  
  void handleInteractionAnswer(String stepId, String answerId) {
    final step = currentTutorial.value!.steps[currentStepIndex.value];
    if (step.interaction == null) return;
    
    final option = step.interaction!.options.firstWhereOrNull(
      (opt) => opt.id == answerId,
    );
    
    if (option != null) {
      if (option.isCorrect) {
        _showCorrectAnswer(option.explanation);
        Future.delayed(const Duration(milliseconds: 800), () {
          nextStep();
        });
      } else {
        _showIncorrectAnswer(option.explanation);
      }
    }
  }
  
  void _showCorrectAnswer(String? explanation) {
    Get.snackbar(
      '回答正确!',
      explanation ?? '很好,继续加油!',
      backgroundColor: Colors.green,
      colorText: Colors.white,
      icon: Icon(Icons.check_circle, color: Colors.white),
    );
  }
  
  void _showIncorrectAnswer(String? explanation) {
    Get.snackbar(
      '回答错误',
      explanation ?? '再想想看,你可以的!',
      backgroundColor: Colors.orange,
      colorText: Colors.white,
      icon: Icon(Icons.info, color: Colors.white),
    );
  }

这些方法处理用户在教程中的导航和交互nextStep 会检查是否还有下一步,如果没有就完成教程。handleInteractionAnswer 检查用户的答案是否正确,然后显示相应的反馈。如果正确,延迟后自动进入下一步,这样用户可以看到反馈信息。

完成教程和推荐

  void _completeTutorial() {
    if (currentTutorial.value == null) return;
    
    final tutorialId = currentTutorial.value!.id;
    _recordTutorialCompletion(tutorialId);
    
    Get.dialog(
      AlertDialog(
        title: Row(
          children: [
            Icon(Icons.celebration, color: Colors.amber, size: 24),
            SizedBox(width: 8),
            Text('恭喜完成!'),
          ],
        ),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('您已成功完成《${currentTutorial.value!.title}》教程'),
            SizedBox(height: 16),
            Text('获得经验值: +50'),
            Text('解锁成就: 学而时习之'),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () {
              Get.back();
              exitTutorial();
            },
            child: Text('继续学习'),
          ),
          ElevatedButton(
            onPressed: () {
              Get.back();
              exitTutorial();
              _showNextRecommendation();
            },
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red[700],
              foregroundColor: Colors.white,
            ),
            child: Text('查看推荐'),
          ),
        ],
      ),
    );
  }
  
  void _showNextRecommendation() {
    // 根据当前完成的教程推荐下一个
    final nextTutorial = _findNextTutorial();
    if (nextTutorial != null) {
      Get.snackbar(
        '推荐教程',
        '我们为你推荐了《${nextTutorial.title}》',
        duration: Duration(seconds: 3),
      );
    }
  }
  
  TutorialModel? _findNextTutorial() {
    if (currentTutorial.value == null) return null;
    
    final currentOrder = currentTutorial.value!.order;
    return tutorials.firstWhereOrNull((t) => t.order == currentOrder + 1);
  }

完成教程时显示一个庆祝对话框。这不仅给用户成就感,还能激励他们继续学习。对话框提供了两个选项:继续学习或查看推荐,让用户可以选择下一步的行动。

构建教程列表界面

现在实现UI部分,展示所有可用的教程。用户可以看到教程的难度、预计时间、完成进度等信息。

// lib/screens/strategy/beginner_guide_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../controllers/beginner_guide_controller.dart';

class BeginnerGuideScreen extends StatelessWidget {
  const BeginnerGuideScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final controller = Get.put(BeginnerGuideController());
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('新手入门'),
        backgroundColor: Colors.red[700],
        foregroundColor: Colors.white,
        elevation: 0,
      ),
      body: Obx(() => controller.isPlaying.value
          ? _buildTutorialPlayer(controller)
          : _buildTutorialList(controller)),
    );
  }

使用 Obx 来响应 isPlaying 的变化。当用户开始教程时,isPlaying 变为 true,界面就会切换到教程播放器。当用户退出教程时,isPlaying 变为 false,界面就会切换回教程列表。

教程列表布局

  Widget _buildTutorialList(BeginnerGuideController controller) {
    return Column(
      children: [
        _buildListHeader(),
        Expanded(
          child: Obx(() {
            if (controller.isLoading.value) {
              return Center(child: CircularProgressIndicator());
            }
            
            return ListView.builder(
              padding: EdgeInsets.all(16.w),
              itemCount: controller.tutorials.length,
              itemBuilder: (context, index) {
                final tutorial = controller.tutorials[index];
                return _buildTutorialCard(tutorial, controller);
              },
            );
          }),
        ),
      ],
    );
  }
  
  Widget _buildListHeader() {
    return Container(
      padding: EdgeInsets.all(16.w),
      color: Colors.red[700],
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '从这里开始你的三国杀之旅',
            style: TextStyle(
              color: Colors.white,
              fontSize: 16.sp,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 4.h),
          Text(
            '按照顺序学习,逐步掌握游戏规则',
            style: TextStyle(
              color: Colors.white70,
              fontSize: 13.sp,
            ),
          ),
        ],
      ),
    );
  }

列表使用 ListView.builder 来高效地渲染教程卡片。这样即使有很多教程,也只会渲染可见的部分,提高性能。头部提供了一个欢迎信息,让用户知道这是新手入门区。

教程卡片设计

  Widget _buildTutorialCard(TutorialModel tutorial, BeginnerGuideController controller) {
    return Container(
      margin: EdgeInsets.only(bottom: 12.h),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: () => controller.startTutorial(tutorial),
          borderRadius: BorderRadius.circular(12.r),
          child: Padding(
            padding: EdgeInsets.all(16.w),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            tutorial.title,
                            style: TextStyle(
                              fontSize: 16.sp,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          SizedBox(height: 4.h),
                          Text(
                            tutorial.description,
                            style: TextStyle(
                              fontSize: 13.sp,
                              color: Colors.grey.shade600,
                            ),
                            maxLines: 2,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ],
                      ),
                    ),
                    SizedBox(width: 12.w),
                    _buildDifficultyBadge(tutorial.difficulty),
                  ],
                ),
                SizedBox(height: 12.h),
                Row(
                  children: [
                    Icon(Icons.schedule, size: 16.sp, color: Colors.grey),
                    SizedBox(width: 4.w),
                    Text(
                      '${tutorial.estimatedTime}分钟',
                      style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade600),
                    ),
                    SizedBox(width: 16.w),
                    Icon(Icons.layers, size: 16.sp, color: Colors.grey),
                    SizedBox(width: 4.w),
                    Text(
                      '${tutorial.steps.length}步',
                      style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade600),
                    ),
                    Spacer(),
                    if (tutorial.isCompleted)
                      Container(
                        padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
                        decoration: BoxDecoration(
                          color: Colors.green.withOpacity(0.1),
                          borderRadius: BorderRadius.circular(4.r),
                        ),
                        child: Row(
                          children: [
                            Icon(Icons.check, size: 14.sp, color: Colors.green),
                            SizedBox(width: 2.w),
                            Text(
                              '已完成',
                              style: TextStyle(
                                fontSize: 12.sp,
                                color: Colors.green,
                                fontWeight: FontWeight.w500,
                              ),
                            ),
                          ],
                        ),
                      ),
                  ],
                ),
                if (!tutorial.isCompleted) ...[
                  SizedBox(height: 8.h),
                  ClipRRect(
                    borderRadius: BorderRadius.circular(4.r),
                    child: LinearProgressIndicator(
                      value: tutorial.progress,
                      backgroundColor: Colors.grey.shade200,
                      valueColor: AlwaysStoppedAnimation<Color>(Colors.red[700]!),
                      minHeight: 4.h,
                    ),
                  ),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }
  
  Widget _buildDifficultyBadge(String difficulty) {
    final color = _getDifficultyColor(difficulty);
    final label = _getDifficultyLabel(difficulty);
    
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
      decoration: BoxDecoration(
        color: color.withOpacity(0.1),
        borderRadius: BorderRadius.circular(4.r),
        border: Border.all(color: color.withOpacity(0.3)),
      ),
      child: Text(
        label,
        style: TextStyle(
          fontSize: 12.sp,
          color: color,
          fontWeight: FontWeight.w500,
        ),
      ),
    );
  }
  
  Color _getDifficultyColor(String difficulty) {
    switch (difficulty) {
      case 'beginner':
        return Colors.green;
      case 'intermediate':
        return Colors.orange;
      case 'advanced':
        return Colors.red;
      default:
        return Colors.grey;
    }
  }
  
  String _getDifficultyLabel(String difficulty) {
    switch (difficulty) {
      case 'beginner':
        return '入门';
      case 'intermediate':
        return '进阶';
      case 'advanced':
        return '高级';
      default:
        return '未知';
    }
  }

卡片设计包含了教程的关键信息:标题、描述、难度、预计时间、步骤数。对于已完成的教程,显示一个绿色的"已完成"标签。对于未完成的教程,显示一个进度条,让用户知道自己学到了哪里。难度徽章用颜色编码来表示难度等级。绿色表示入门,橙色表示进阶,红色表示高级。这样用户一眼就能看出教程的难度。

教程播放器界面

当用户开始教程时,需要一个专门的播放器来展示教程内容。播放器分为三部分:头部显示进度、中间显示内容、底部显示控制按钮。

  Widget _buildTutorialPlayer(BeginnerGuideController controller) {
    final tutorial = controller.currentTutorial.value!;
    final step = tutorial.steps[controller.currentStepIndex.value];
    
    return Column(
      children: [
        _buildPlayerHeader(tutorial, controller),
        Expanded(
          child: FadeTransition(
            opacity: controller.fadeAnimation,
            child: _buildStepContent(step, controller),
          ),
        ),
        _buildPlayerControls(tutorial, controller),
      ],
    );
  }
  
  Widget _buildPlayerHeader(TutorialModel tutorial, BeginnerGuideController controller) {
    final currentStep = controller.currentStepIndex.value + 1;
    final totalSteps = tutorial.steps.length;
    final progress = currentStep / totalSteps;
    
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.red[700],
        borderRadius: BorderRadius.only(
          bottomLeft: Radius.circular(12.r),
          bottomRight: Radius.circular(12.r),
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      tutorial.title,
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 18.sp,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(height: 4.h),
                    Text(
                      '第 $currentStep / $totalSteps 步',
                      style: TextStyle(
                        color: Colors.white.withOpacity(0.9),
                        fontSize: 13.sp,
                      ),
                    ),
                  ],
                ),
              ),
              IconButton(
                icon: Icon(Icons.close, color: Colors.white),
                onPressed: controller.exitTutorial,
              ),
            ],
          ),
          SizedBox(height: 12.h),
          ClipRRect(
            borderRadius: BorderRadius.circular(4.r),
            child: LinearProgressIndicator(
              value: progress,
              backgroundColor: Colors.white.withOpacity(0.3),
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
              minHeight: 4.h,
            ),
          ),
        ],
      ),
    );
  }

头部显示教程标题、当前步骤和总步骤数,以及一个进度条。右上角有一个关闭按钮,让用户可以随时退出教程。进度条用白色表示,在红色背景上很醒目。

步骤内容展示

  Widget _buildStepContent(TutorialStep step, BeginnerGuideController controller) {
    return SingleChildScrollView(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            step.title,
            style: TextStyle(
              fontSize: 20.sp,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 16.h),
          Text(
            step.content,
            style: TextStyle(
              fontSize: 14.sp,
              height: 1.6,
              color: Colors.grey.shade700,
            ),
          ),
          if (step.images.isNotEmpty) ...[
            SizedBox(height: 16.h),
            ...step.images.map((image) => Padding(
              padding: EdgeInsets.only(bottom: 12.h),
              child: ClipRRect(
                borderRadius: BorderRadius.circular(8.r),
                child: Image.network(
                  image,
                  fit: BoxFit.cover,
                  errorBuilder: (context, error, stackTrace) {
                    return Container(
                      height: 200.h,
                      color: Colors.grey.shade200,
                      child: Center(
                        child: Icon(Icons.image_not_supported),
                      ),
                    );
                  },
                ),
              ),
            )),
          ],
          if (step.type == TutorialStepType.interaction && step.interaction != null) ...[
            SizedBox(height: 16.h),
            _buildInteractionContent(step, controller),
          ],
        ],
      ),
    );
  }

步骤内容根据类型动态展示。如果有图片,就显示图片。如果是交互式步骤,就显示选择题。这种灵活的设计让教程可以包含各种类型的内容。

交互式选择题

  Widget _buildInteractionContent(TutorialStep step, BeginnerGuideController controller) {
    final interaction = step.interaction!;
    
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '请选择正确答案:',
          style: TextStyle(
            fontSize: 14.sp,
            fontWeight: FontWeight.bold,
            color: Colors.red[700],
          ),
        ),
        SizedBox(height: 12.h),
        ...interaction.options.map((option) => Padding(
          padding: EdgeInsets.only(bottom: 8.h),
          child: Material(
            color: Colors.transparent,
            child: InkWell(
              onTap: () => controller.handleInteractionAnswer(step.id, option.id),
              borderRadius: BorderRadius.circular(8.r),
              child: Container(
                padding: EdgeInsets.all(12.w),
                decoration: BoxDecoration(
                  color: Colors.grey.shade100,
                  borderRadius: BorderRadius.circular(8.r),
                  border: Border.all(color: Colors.grey.shade300),
                ),
                child: Row(
                  children: [
                    Container(
                      width: 24.w,
                      height: 24.w,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        border: Border.all(color: Colors.grey.shade400),
                      ),
                    ),
                    SizedBox(width: 12.w),
                    Expanded(
                      child: Text(
                        option.text,
                        style: TextStyle(fontSize: 14.sp),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        )),
      ],
    );
  }

交互式内容显示为一个选择题。每个选项都是可点击的,点击时会调用 handleInteractionAnswer 方法来检查答案。选项前面有一个圆形的单选框,符合Material Design规范。

播放器控制栏

  Widget _buildPlayerControls(TutorialModel tutorial, BeginnerGuideController controller) {
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(top: BorderSide(color: Colors.grey.shade200)),
      ),
      child: Row(
        children: [
          ElevatedButton.icon(
            onPressed: controller.currentStepIndex.value > 0 
              ? controller.previousStep 
              : null,
            icon: Icon(Icons.arrow_back),
            label: Text('上一步'),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.grey.shade300,
              foregroundColor: Colors.black,
              disabledBackgroundColor: Colors.grey.shade100,
              disabledForegroundColor: Colors.grey.shade400,
            ),
          ),
          Spacer(),
          ElevatedButton.icon(
            onPressed: controller.currentStepIndex.value < tutorial.steps.length - 1
              ? controller.nextStep
              : controller._completeTutorial,
            icon: Icon(controller.currentStepIndex.value < tutorial.steps.length - 1
              ? Icons.arrow_forward
              : Icons.check),
            label: Text(controller.currentStepIndex.value < tutorial.steps.length - 1
              ? '下一步'
              : '完成'),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red[700],
              foregroundColor: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}

控制栏提供了上一步和下一步按钮。当到达最后一步时,"下一步"按钮会变成"完成"按钮。上一步按钮在第一步时会被禁用。这样用户可以灵活地在教程中导航。

教程服务层

服务层负责与后端API通信获取教程数据。

// lib/services/tutorial_service.dart
class TutorialService extends GetxService {
  final ApiClient _apiClient = Get.find<ApiClient>();

  Future<List<TutorialModel>> getAllTutorials() async {
    try {
      final response = await _apiClient.get('/api/tutorials');
      final List<dynamic> data = response.data ?? [];
      return data.map((e) => TutorialModel.fromJson(e)).toList();
    } catch (e) {
      throw Exception('获取教程列表失败: $e');
    }
  }

  Future<TutorialModel> getTutorialById(String id) async {
    try {
      final response = await _apiClient.get('/api/tutorials/$id');
      return TutorialModel.fromJson(response.data);
    } catch (e) {
      throw Exception('获取教程详情失败: $e');
    }
  }

  Future<void> recordTutorialProgress(String tutorialId, int currentStep) async {
    try {
      await _apiClient.post('/api/tutorials/$tutorialId/progress', {
        'currentStep': currentStep,
        'timestamp': DateTime.now().toIso8601String(),
      });
    } catch (e) {
      throw Exception('记录进度失败: $e');
    }
  }
}

服务提供了获取教程列表、获取单个教程、记录学习进度等方法。这样控制器可以通过服务来获取数据,而不需要直接调用API。

总结

本文实现了一个完整的新手入门攻略系统。从数据模型到UI界面,从状态管理到服务层,每个部分都经过精心设计。这个系统不仅能帮助新手快速上手,还能通过游戏化设计激励用户继续学习

用户可以按照自己的节奏学习,系统会记录学习进度,并根据完成情况推荐下一步的学习内容。交互式的选择题让学习变得更加有趣,即时的反馈让用户知道自己是否理解了内容。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐