Flutter for OpenHarmony三国杀攻略App实战 - 新手入门攻略实现
对于三国杀新手玩家来说,游戏的复杂规则和众多武将技能往往让人望而却步。一个优秀的新手入门攻略系统能够帮助玩家快速上手,逐步掌握游戏精髓。本文将详细介绍如何在Flutter中实现一个完整的新手入门攻略系统,包括交互式教程、进度跟踪、知识点测试等功能。我们将使用分步引导、动画演示和游戏化设计等技术,结合GetX状态管理和精美的UI动效,打造一个既有趣又实用的学习系统。同时针对OpenHarmony系统
前言
三国杀对新手来说是个挑战。复杂的规则、众多的武将技能、各种卡牌效果,这些都需要时间去理解。一个好的新手入门系统能让玩家快速上手,逐步掌握游戏的核心玩法。
本文将介绍如何实现一个完整的新手入门攻略系统。这不是简单的文字教程,而是一个包含分步引导、交互式学习、进度跟踪和知识测试的完整学习平台。
核心功能设计
分步教程:将复杂规则分解为易懂的步骤,每步都有清晰的目标。
交互式学习:通过选择题、模拟操作等方式加深理解,而不仅仅是被动阅读。
进度跟踪:记录用户的学习进度、得分、成就等,让用户了解自己的学习状态。
个性化推荐:根据用户的学习情况推荐合适的下一个教程,提高学习效率。
教程数据模型
首先定义教程的数据结构。这是整个系统的基础,所有的教程数据都会按照这个结构组织。
// 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();
}
使用 GetxController 和 GetTickerProviderStateMixin 来管理状态和动画。RxList 和 Rx 是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
更多推荐


所有评论(0)