当前位置: 首页 > news >正文

使用 XState 状态机打造英语单词学习界面(demo)

单词学习界面

在现代 Web 应用开发中,管理复杂的用户交互和状态流转是一项极具挑战性的任务。而 XState 作为一个强大的状态机库,为我们提供了一种清晰、可维护的方式来处理这些复杂场景。本文将介绍如何使用 XState 状态机来构建一个英语单词学习界面,通过 studyMachine.ts 状态机文件来驱动 StudyFlow.tsx 组件的交互。

项目概述

我们的项目是一个英语单词学习应用,用户可以学习一系列单词,包括单词的含义、词组和例句。应用通过状态机来管理学习流程,根据用户的输入和操作,在不同的学习阶段之间进行切换。

状态机文件:studyMachine.ts

状态机是整个应用的核心,它定义了学习流程的各个状态和状态之间的转换规则。在 studyMachine.ts 文件中,我们使用 createMachine 函数创建了一个名为 study 的状态机。

状态机结构

状态机包含以下几个主要状态:

  • idle:初始状态,等待用户开始学习。
  • showWordCard:显示单词卡片,用户可以输入单词的含义。
  • chooseMeaning:提供单词含义的选项,用户选择正确答案。
  • showClueMeaning:如果用户回答错误,显示提示信息。
  • choosePhrase:提供词组含义的选项,用户选择正确答案。
  • showCluePhrase:如果用户回答错误,显示提示信息。
  • nextWord:学习下一个单词。
  • sentenceReview:进行例句复习,用户选择正确答案。
  • sentenceReviewNext:判断是否继续复习下一个例句。
  • completed:学习完成状态。

状态转换

状态机通过事件来触发状态转换,例如 START 事件从 idle 状态转换到 showWordCard 状态,ANSWER 事件根据用户的回答在不同的选择状态之间进行转换。

上下文管理

状态机的上下文(context)用于存储学习过程中的相关信息,如当前单词索引、题目索引、聊天记录等。通过 assign 函数,我们可以在状态转换时更新上下文信息。

import { createMachine, assign } from 'xstate';
import { WORDS } from '../words';export type ChatItem = {type: 'bot' | 'user' | 'feedback' | 'clue';content: any;correct?: boolean;color?: string;
};interface Word {tip: string;
}interface Question {question: string;options: string[];
}interface StudyContext {wordIndex: number;        // 当前单词索引 0-4questionInWord: number;   // 当前单词内题目索引 0-3phase: 'learn' | 'review';completed: boolean;currentWord?: Word;currentQuestion?: Question;chat: ChatItem[];input: string;inputDisabled: boolean;meaningWrongCount: number; // 第二题错误次数phraseWrong: boolean;      // 第三题是否答错过sentenceIndex?: number;    // 当前sentence复习索引
}export const studyMachine = createMachine<StudyContext>({id: 'study',initial: 'idle',context: {wordIndex: 0,questionInWord: 0,phase: 'learn',completed: false,chat: [],input: '',inputDisabled: false,meaningWrongCount: 0,phraseWrong: false,sentenceIndex: 0,},states: {idle: {entry: assign({chat: (_) => [],input: (_) => '',inputDisabled: (_) => false,meaningWrongCount: (_) => 0,phraseWrong: (_) => false}),on: { START: 'showWordCard' }},showWordCard: {entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'bot', content: {type: 'wordCard',word: WORDS[ctx.wordIndex].word,phonetic: WORDS[ctx.wordIndex].phonetic,cet4Count: WORDS[ctx.wordIndex].cet4Count,cet6Count: WORDS[ctx.wordIndex].cet6Count,audio: WORDS[ctx.wordIndex].audio} }],input: (_) => '',inputDisabled: (_) => false,meaningWrongCount: (_) => 0,phraseWrong: (_) => false}),on: {INPUT: {actions: assign({ input: (ctx, e) => e.input })},ANSWER: {actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: ctx.input || '我不知道这个单词意思' }],input: (_) => '',inputDisabled: (_) => true}),target: 'chooseMeaning'},IDK: {actions: assign({chat: (ctx) => [...ctx.chat,{ type: 'user', content: '我不知道这个单词意思' },{ type: 'clue', content: WORDS[ctx.wordIndex].clues[0] },{ type: 'clue', content: WORDS[ctx.wordIndex].clues[1] }],input: (_) => '',inputDisabled: (_) => true}),target: 'chooseMeaning'}}},chooseMeaning: {entry: assign({chat: (ctx) => [...ctx.chat,{type: 'bot',content: {type: 'wordOptions',word: WORDS[ctx.wordIndex].word,phonetic: WORDS[ctx.wordIndex].phonetic,audio: WORDS[ctx.wordIndex].audio,options: WORDS[ctx.wordIndex].options}}]}),on: {ANSWER: [{cond: (ctx, e) => e.answer === WORDS[ctx.wordIndex].meaning,actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }]}),target: 'choosePhrase'},{actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }],meaningWrongCount: (ctx) => ctx.meaningWrongCount + 1}),target: 'showClueMeaning'}]}},showClueMeaning: {entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'clue', content: WORDS[ctx.wordIndex].clues[Math.min(ctx.meaningWrongCount, 2)] }]}),always: 'chooseMeaning'},choosePhrase: {entry: assign({chat: (ctx) => [...ctx.chat,{type: 'bot',content: {type: 'phraseOptions',phrase: WORDS[ctx.wordIndex].phrase,audio: WORDS[ctx.wordIndex].audio,options: WORDS[ctx.wordIndex].phraseOptions}}]}),on: {ANSWER: [{cond: (ctx, e) => e.answer === WORDS[ctx.wordIndex].phraseMeaning,actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }]}),target: 'nextWord'},{actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }],phraseWrong: (_) => true}),target: 'showCluePhrase'}]}},showCluePhrase: {entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'clue', content: WORDS[ctx.wordIndex].clues[3] }]}),always: 'choosePhrase'},nextWord: {entry: assign((ctx) => {let wordIndex = ctx.wordIndex + 1;let completed = false;if (wordIndex >= WORDS.length) {completed = true;wordIndex = WORDS.length - 1;}return {wordIndex,completed,chat: [],input: '',inputDisabled: false,meaningWrongCount: 0,phraseWrong: false,sentenceIndex: ctx.sentenceIndex};}),always: [{ target: 'showWordCard', cond: (ctx) => !ctx.completed },{ target: 'sentenceReview' }]},sentenceReview: {entry: assign({chat: (ctx) => [...ctx.chat,{type: 'bot',content: {type: 'sentenceOptions',sentence: WORDS[ctx.sentenceIndex || 0].sentence,options: WORDS[ctx.sentenceIndex || 0].sentenceOptions,audio: WORDS[ctx.sentenceIndex || 0].audio,phonetic: WORDS[ctx.sentenceIndex || 0].phonetic,cet4Count: WORDS[ctx.sentenceIndex || 0].cet4Count,cet6Count: WORDS[ctx.sentenceIndex || 0].cet6Count}}]}),on: {ANSWER: [{cond: (ctx, e) => e.answer === WORDS[ctx.sentenceIndex || 0].sentenceMeaning,actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer },{ type: 'feedback', content: '回答正确!', correct: true }],sentenceIndex: (ctx) => (ctx.sentenceIndex || 0) + 1}),target: 'sentenceReviewNext'},{actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer },{ type: 'feedback', content: '回答错误,请再试一次。', correct: false }]})}]}},sentenceReviewNext: {always: [{ target: 'sentenceReview', cond: (ctx) => (ctx.sentenceIndex || 0) < WORDS.length },{ target: 'completed' }]},completed: {type: 'final',entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'bot', content: { type: 'end', text: '恭喜你,学习完成!' } }]})}}
});

学习界面组件:StudyFlow.tsx

StudyFlow.tsx 组件是应用的用户界面,它使用 useInterpret 和 useSelector 钩子来与状态机进行交互。

组件结构

组件包含以下几个主要部分:

  • 顶部进度条:显示学习进度。
  • 聊天气泡区:显示聊天记录,包括单词卡片、用户回答、提示信息和反馈信息。
  • 底部输入框:用户输入答案或选择不知道。

事件处理

组件通过事件处理函数来触发状态机的事件,例如 handleStart 函数触发 START 事件,handleAnswer 函数触发 ANSWER 事件。

import React, { useRef, useEffect } from 'react';
import { useInterpret, useSelector } from '@xstate/react';
import { studyMachine, ChatItem } from '../machines/studyMachine';
import {Box, Card, Typography, Button, Avatar, LinearProgress, Stack, IconButton, TextField, InputAdornment, Fade, Grid
} from '@mui/material';
import SchoolIcon from '@mui/icons-material/School';
import VolumeUpIcon from '@mui/icons-material/VolumeUp';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import PersonIcon from '@mui/icons-material/Person';// 定义气泡样式
const bubbleStyles = {bot: { bgcolor: '#eaf4ff', color: '#222', borderRadius: 3, border: '1px solid #b6d6f6', alignSelf: 'flex-start', maxWidth: 500 },user: { bgcolor: '#1976d2', color: '#fff', borderRadius: 3, border: '1px solid #1976d2', alignSelf: 'flex-end', maxWidth: 400 },feedbackRight: { bgcolor: '#e6ffed', color: '#1a7f37', borderRadius: 3, border: '1px solid #7ee787', alignSelf: 'flex-start', maxWidth: 500 },feedbackWrong: { bgcolor: '#ffeaea', color: '#d32f2f', borderRadius: 3, border: '1px solid #f7bdbd', alignSelf: 'flex-start', maxWidth: 500 },clue: { bgcolor: '#fffbe6', color: '#b08800', borderRadius: 3, border: '1px solid #ffe58f', alignSelf: 'flex-start', maxWidth: 500 },
};// 其他组件定义...const StudyFlow: React.FC = () => {const service = useInterpret(studyMachine);const state = useSelector(service, (s) => s);const chat = state.context.chat;const isCompleted = state.done || state.context.completed;const progress = isCompleted ? 100 : ((state.context.wordIndex + 1) / 5) * 100;const chatEndRef = useRef<HTMLDivElement>(null);useEffect(() => {if (chatEndRef.current) {chatEndRef.current.scrollIntoView({ behavior: 'smooth' });}}, [chat]);// 事件派发const handleStart = () => service.send('START');const handleAnswer = (answer: string) => service.send({ type: 'ANSWER', answer });const handleNext = () => service.send('NEXT');const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => service.send({ type: 'INPUT', input: e.target.value });const handleIDK = () => service.send('IDK');// 判断当前是否可选(选项按钮可用)const canSelect = state.matches('chooseMeaning') || state.matches('choosePhrase') || state.matches('chooseSentence') || state.matches('sentenceReview');// 首页卡片if (state.matches('idle') && chat.length === 0) {return (<Box sx={{ minHeight: '100vh', bgcolor: '#f6f9fe', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Card sx={{ minWidth: 350, p: 4, borderRadius: 3, boxShadow: 6, textAlign: 'center' }}><Avatar sx={{ bgcolor: '#e3f0ff', width: 64, height: 64, mx: 'auto', mb: 2 }}><SchoolIcon sx={{ color: '#1976d2', fontSize: 40 }} /></Avatar><Typography variant="h5" fontWeight={700} mb={1}>英语词汇学习</Typography><Box sx={{ bgcolor: '#f5f7fa', color: '#1976d2', borderRadius: 2, px: 2, py: 0.5, display: 'inline-block', mb: 3, fontSize: 16 }}>今日新词 5</Box><Buttonvariant="contained"size="large"endIcon={<ArrowForwardIcon />}sx={{ width: '100%', fontSize: 18, py: 1.5 }}onClick={handleStart}>Start</Button></Card></Box>);}return (<Box sx={{ minHeight: '100vh', bgcolor: '#f6f9fe', width: '100vw', maxWidth: '100vw', px: 0 }}>{/* 顶部进度条和标题固定 */}<Box sx={{ position: 'fixed', top: 0, left: 0, right: 0, zIndex: 100, bgcolor: '#f6f9fe', maxWidth: '100vw', width: '100vw' }}><Box sx={{ maxWidth: 1700, mx: 'auto', pt: 3, pb: 1 }}><Stack direction="row" alignItems="center" spacing={1} mb={1}><SchoolIcon color="primary" /><Typography variant="h6" fontWeight={700}>学习进度</Typography><Box flex={1} /><Typography variant="body2" color="text.secondary">{state.context.wordIndex + 1}/5</Typography></Stack><LinearProgress variant="determinate" value={progress} sx={{ height: 8, borderRadius: 4 }} /></Box></Box>{/* 聊天气泡区 */}<Box sx={{ width: '100vw', maxWidth: 1700, pt: 12, px: 0, minHeight: '70vh', pb: 10, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', ml: 5 }}><Box sx={{ width: '100%', maxWidth: '100%', mr: 0 }}>{chat.map((item: ChatItem, idx: number) => {if (item.type === 'bot') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-start' }}><BotBubble>{renderBotContent(item.content, handleAnswer, handleNext, canSelect)}</BotBubble></Box>);if (item.type === 'user') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-end'}}><UserBubble>{item.content}</UserBubble></Box>);if (item.type === 'clue') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-start' }}><ClueBubble>{item.content}</ClueBubble></Box>);if (item.type === 'feedback') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-start' }}><FeedbackBubble correct={!!item.correct}>{item.content}</FeedbackBubble></Box>);return null;})}<div ref={chatEndRef} /></Box></Box>{/* 底部输入框,始终显示 */}<Box sx={{ position: 'fixed', left: 0, right: 0, bottom: 0, bgcolor: '#fff', borderTop: '1px solid #eee', px: 2, py: 1.5, display: 'flex', alignItems: 'center', zIndex: 10, width: '100vw', maxWidth: '100vw' }}><TextFieldplaceholder="请输入中文含义..."variant="outlined"size="small"sx={{ flex: 1, bgcolor: '#f6f9fe', borderRadius: 2, mr: 2 }}value={state.context.input}onChange={handleInput}onKeyDown={e => {if (e.key === 'Enter' && state.context.input.trim() && !state.context.inputDisabled) {handleAnswer(state.context.input.trim());}}}InputProps={{endAdornment: (<InputAdornment position="end"><IconButton edge="end" color="primary" disabled={!state.context.input.trim() || state.context.inputDisabled} onClick={() => state.context.input.trim() && !state.context.inputDisabled && handleAnswer(state.context.input.trim())}><ArrowForwardIcon /></IconButton></InputAdornment>)}}disabled={state.context.inputDisabled}/>{!state.context.inputDisabled && (<Buttonvariant="outlined"sx={{ ml: 1, px: 3, fontWeight: 700, borderRadius: 2 }}onClick={handleIDK}>我不知道</Button>)}</Box></Box>);
};export default StudyFlow;

使用状态机工具进行调试

XState 提供了一个强大的状态机工具 Stately Studiohttps://stately.ai/docs/machines,我们可以将 studyMachine.ts 文件导入到该工具中,可视化地查看状态机的结构和状态转换过程。通过该工具,我们可以模拟用户操作,验证状态机的正确性,并且可以深入了解每个状态和事件的详细信息。

状态机转换过程:

学习界面: 

总结

通过使用 XState 状态机,我们成功地构建了一个功能丰富、交互流畅的英语单词学习界面。状态机的使用使得学习流程的管理变得清晰、可维护,同时也方便了我们进行调试和扩展。如果你也在处理复杂的用户交互和状态管理问题,不妨尝试使用 XState 来提升你的开发效率和代码质量。

相关文章:

  • 对象存储Ozone EC应用和优化
  • 多电流传感器电流检测方法多电流传感器电流检测方法
  • 图片转Latex软件
  • HarmonyOS运动语音开发:如何让运动开始时的语音播报更温暖
  • 中断相关知识
  • C语言的全称:(25/6/6)
  • python模块——tqdm
  • An improved YOLACT algorithm for instance segmentation of stacking parts
  • 双面沉金PCB应用:打造卓越电子设备的黄金工艺
  • 深入浅出:计算机网络体系结构——信息世界的“交通规则”
  • C语言速成15之告别变量碎片化:C 语言结构体如何让数据管理从混乱走向有序
  • MCP协议三种传输机制全解析
  • 在线OJ项目测试
  • C++.OpenGL (7/64)摄像机(Camera)
  • 云服务器厂商机房是什么
  • 玛哈特辊式矫平机:塑造金属平整的精密力量
  • U-Mail邮件加密,保障邮件系统信息安全
  • 5.1 HarmonyOS NEXT系统级性能调优:内核调度、I/O优化与多线程管理实战
  • LlamaIndex 工作流简介以及基础工作流
  • 开源语义分割工具箱mmsegmentation基于Lovedata数据集训练模型
  • 郑州网站公司排名/品牌推广运营策划方案
  • 进一步强化网站建设/艺考培训
  • wordpress 设计/网站seo优化方案策划书
  • 低价网站建设浩森宇特/网站软文推广网站
  • 做网站优化的弊端/中文搜索引擎有哪些
  • 企业网站建设排名推荐/电商平台运营