1+ import { useState } from "react" ;
2+ import {
3+ Modal ,
4+ Button ,
5+ Steps ,
6+ Radio ,
7+ Checkbox ,
8+ Space ,
9+ Typography ,
10+ Alert ,
11+ Result ,
12+ Divider ,
13+ } from "antd" ;
14+ import {
15+ BookOutlined ,
16+ FormOutlined ,
17+ ExperimentOutlined ,
18+ TrophyOutlined ,
19+ } from "@ant-design/icons" ;
20+ import {
21+ useNewcomerStore ,
22+ checkFixedPass ,
23+ checkRandomPass ,
24+ } from "../../stores/newcomerStore" ;
25+ import type { QuizQuestion } from "../../data/newcomerQuiz" ;
26+
27+ const { Title, Paragraph, Text, Link } = Typography ;
28+
29+ export function NewcomerGuideModal ( ) {
30+ const {
31+ modalOpen,
32+ step,
33+ fixedQuiz,
34+ randomQuiz,
35+ fixedAnswers,
36+ randomAnswers,
37+ setStep,
38+ setFixedAnswer,
39+ setRandomAnswer,
40+ markPassed,
41+ closeModal,
42+ } = useNewcomerStore ( ) ;
43+
44+ const [ fixedError , setFixedError ] = useState ( false ) ;
45+ const [ randomError , setRandomError ] = useState ( false ) ;
46+
47+ if ( ! modalOpen ) return null ;
48+
49+ const handleSubmitFixed = ( ) => {
50+ if ( checkFixedPass ( fixedQuiz , fixedAnswers ) ) {
51+ setFixedError ( false ) ;
52+ setStep ( 2 ) ;
53+ } else {
54+ setFixedError ( true ) ;
55+ }
56+ } ;
57+
58+ const handleSubmitRandom = ( ) => {
59+ if ( checkRandomPass ( randomQuiz , randomAnswers ) ) {
60+ setRandomError ( false ) ;
61+ setStep ( 3 ) ;
62+ } else {
63+ setRandomError ( true ) ;
64+ }
65+ } ;
66+
67+ const handleFinish = ( ) => {
68+ markPassed ( ) ;
69+ closeModal ( ) ;
70+ } ;
71+
72+ return (
73+ < Modal
74+ open = { modalOpen }
75+ closable = { false }
76+ maskClosable = { false }
77+ keyboard = { false }
78+ footer = { null }
79+ width = { 640 }
80+ centered
81+ destroyOnHidden
82+ >
83+ < Steps
84+ current = { step }
85+ size = "small"
86+ style = { { marginBottom : 24 } }
87+ items = { [
88+ { title : "了解" , icon : < BookOutlined /> } ,
89+ { title : "基础" , icon : < FormOutlined /> } ,
90+ { title : "技巧" , icon : < ExperimentOutlined /> } ,
91+ { title : "通关" , icon : < TrophyOutlined /> } ,
92+ ] }
93+ />
94+
95+ { step === 0 && < IntroPage onNext = { ( ) => setStep ( 1 ) } /> }
96+ { step === 1 && (
97+ < QuizPage
98+ title = "基础知识测试"
99+ description = "以下为 MaaFW 基本常识,必须全部答对才能进入下一步。"
100+ quiz = { fixedQuiz }
101+ answers = { fixedAnswers }
102+ setAnswer = { setFixedAnswer }
103+ error = { fixedError }
104+ errorMessage = "存在错误答案,请全部答对后再提交。"
105+ onSubmit = { handleSubmitFixed }
106+ onBack = { ( ) => setStep ( 0 ) }
107+ />
108+ ) }
109+ { step === 2 && (
110+ < QuizPage
111+ title = "常用技巧测试"
112+ description = "以下为常用小知识,答对 60% 即可通过。"
113+ quiz = { randomQuiz }
114+ answers = { randomAnswers }
115+ setAnswer = { setRandomAnswer }
116+ error = { randomError }
117+ errorMessage = "正确率不足 60%,请重新检查后再提交。"
118+ onSubmit = { handleSubmitRandom }
119+ onBack = { ( ) => setStep ( 1 ) }
120+ />
121+ ) }
122+ { step === 3 && < CertificatePage onFinish = { handleFinish } /> }
123+ </ Modal >
124+ ) ;
125+ }
126+
127+ function IntroPage ( { onNext } : { onNext : ( ) => void } ) {
128+ return (
129+ < div >
130+ < Title level = { 4 } > 欢迎使用 MaaPipelineEditor</ Title >
131+ < Paragraph >
132+ < Text strong > MaaPipelineEditor (MPE)</ Text > 是{ " " }
133+ < Text strong > MaaFramework (MaaFW)</ Text > 的可视化 Pipeline
134+ 编辑器,帮助你以图形化方式编辑任务流程。
135+ </ Paragraph >
136+
137+ < Alert
138+ type = "warning"
139+ showIcon
140+ message = "重要提示"
141+ description = "MPE 是 MaaFW 的辅助工具,不能替代对 MaaFW 本身的学习。请确保你已经了解 MaaFW 的基本概念(如 Node、Task、Pipeline 等)后再使用本编辑器。"
142+ style = { { marginBottom : 16 } }
143+ />
144+
145+ < Paragraph > 如果你还不熟悉 MaaFramework,请先阅读官方文档:</ Paragraph >
146+
147+ < Space direction = "vertical" style = { { marginBottom : 24 } } >
148+ < Link href = "https://maafw.com/docs/1.1-QuickStarted" target = "_blank" >
149+ MaaFramework 官方文档
150+ </ Link >
151+ </ Space >
152+
153+ < Divider />
154+
155+ < div style = { { textAlign : "right" } } >
156+ < Button type = "primary" onClick = { onNext } >
157+ 我已了解,开始答题
158+ </ Button >
159+ </ div >
160+ </ div >
161+ ) ;
162+ }
163+
164+ function QuizPage ( {
165+ title,
166+ description,
167+ quiz,
168+ answers,
169+ setAnswer,
170+ error,
171+ errorMessage,
172+ onSubmit,
173+ onBack,
174+ } : {
175+ title : string ;
176+ description : string ;
177+ quiz : QuizQuestion [ ] ;
178+ answers : Record < number , number | number [ ] > ;
179+ setAnswer : ( qi : number , val : number | number [ ] ) => void ;
180+ error : boolean ;
181+ errorMessage : string ;
182+ onSubmit : ( ) => void ;
183+ onBack : ( ) => void ;
184+ } ) {
185+ const allAnswered = quiz . every ( ( q , i ) => {
186+ const a = answers [ i ] ;
187+ if ( a === undefined ) return false ;
188+ if ( q . type === "multi" ) return Array . isArray ( a ) && a . length > 0 ;
189+ return true ;
190+ } ) ;
191+
192+ return (
193+ < div >
194+ < Title level = { 4 } > { title } </ Title >
195+ < Paragraph type = "secondary" > { description } </ Paragraph >
196+
197+ { error && (
198+ < Alert
199+ type = "error"
200+ showIcon
201+ message = { errorMessage }
202+ style = { { marginBottom : 16 } }
203+ />
204+ ) }
205+
206+ < div style = { { maxHeight : 400 , overflowY : "auto" , paddingRight : 8 } } >
207+ < Space direction = "vertical" size = "large" style = { { width : "100%" } } >
208+ { quiz . map ( ( q , qi ) => (
209+ < QuizItem
210+ key = { qi }
211+ index = { qi }
212+ question = { q }
213+ value = { answers [ qi ] }
214+ onChange = { ( val ) => setAnswer ( qi , val ) }
215+ />
216+ ) ) }
217+ </ Space >
218+ </ div >
219+
220+ < Divider />
221+
222+ < div style = { { display : "flex" , justifyContent : "space-between" } } >
223+ < Button onClick = { onBack } > 上一步</ Button >
224+ < Button type = "primary" disabled = { ! allAnswered } onClick = { onSubmit } >
225+ 提交答案
226+ </ Button >
227+ </ div >
228+ </ div >
229+ ) ;
230+ }
231+
232+ function QuizItem ( {
233+ index,
234+ question,
235+ value,
236+ onChange,
237+ } : {
238+ index : number ;
239+ question : QuizQuestion ;
240+ value : number | number [ ] | undefined ;
241+ onChange : ( val : number | number [ ] ) => void ;
242+ } ) {
243+ const prefixMap = { choice : "单选题" , judge : "判断题" , multi : "多选题" } ;
244+ const prefix = prefixMap [ question . type ] ;
245+
246+ if ( question . type === "multi" ) {
247+ return (
248+ < div >
249+ < Text strong >
250+ { index + 1 } . [{ prefix } ] { question . question }
251+ </ Text >
252+ < Checkbox . Group
253+ value = { Array . isArray ( value ) ? value : [ ] }
254+ onChange = { ( checked ) => onChange ( checked as number [ ] ) }
255+ style = { { display : "flex" , flexDirection : "column" , gap : 8 , marginTop : 8 } }
256+ >
257+ { question . options . map ( ( opt , oi ) => (
258+ < Checkbox key = { oi } value = { oi } >
259+ { String . fromCharCode ( 65 + oi ) } . { opt }
260+ </ Checkbox >
261+ ) ) }
262+ </ Checkbox . Group >
263+ </ div >
264+ ) ;
265+ }
266+
267+ return (
268+ < div >
269+ < Text strong >
270+ { index + 1 } . [{ prefix } ] { question . question }
271+ </ Text >
272+ < Radio . Group
273+ value = { typeof value === "number" ? value : undefined }
274+ onChange = { ( e ) => onChange ( e . target . value ) }
275+ style = { { display : "flex" , flexDirection : "column" , gap : 8 , marginTop : 8 } }
276+ >
277+ { question . options . map ( ( opt , oi ) => (
278+ < Radio key = { oi } value = { oi } >
279+ { question . type === "choice"
280+ ? `${ String . fromCharCode ( 65 + oi ) } . ${ opt } `
281+ : opt }
282+ </ Radio >
283+ ) ) }
284+ </ Radio . Group >
285+ </ div >
286+ ) ;
287+ }
288+
289+ function CertificatePage ( { onFinish } : { onFinish : ( ) => void } ) {
290+ return (
291+ < Result
292+ status = "success"
293+ icon = { < TrophyOutlined style = { { color : "#faad14" } } /> }
294+ title = "恭喜通过测试!"
295+ subTitle = "你已具备使用 MaaPipelineEditor 的基础知识,欢迎开始使用。"
296+ extra = {
297+ < Space direction = "vertical" align = "center" >
298+ < Link href = "https://mpe.maa.plus/" target = "_blank" >
299+ 查看 MaaPipelineEditor 使用文档
300+ </ Link >
301+ < Button type = "primary" size = "large" onClick = { onFinish } >
302+ 开始使用 MPE
303+ </ Button >
304+ </ Space >
305+ }
306+ />
307+ ) ;
308+ }
0 commit comments