| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261 |
- <template>
- <div class="mobile-exam-workshop">
- <!-- 移动端考试工坊页面 -->
- <MobileHeader title="考试工坊" @back="goBack" @menu="showHistoryDrawer" />
-
- <div class="mobile-content">
-
- <!-- 通用历史记录抽屉 -->
- <MobileHistoryDrawer
- :visible="!isGenerating && showHistory"
- title="历史记录"
- :historyData="historyData"
- :loading="isLoadingHistory"
- @close="showHistory = false"
- @createNewTask="createNewTask"
- @handleHistoryItem="handleHistoryItem"
- @deleteHistoryItem="deleteHistoryItem"
- />
- <!-- 移动端Toast提示组件 -->
- <MobileToast :visible="toastVisible" :message="toastMessage" @close="closeToast" />
- <!-- 主界面:考试工坊配置 -->
- <div v-if="!showExamDetail" class="exam-workshop-main">
- <!-- 试卷类型选择 -->
- <div class="config-section">
- <div class="config-header">
- <div class="step-number">1</div>
- <h3>选择试卷类型</h3>
- </div>
- <div class="type-cards">
- <div class="type-cards-row">
- <div
- v-for="(type, key) in projectTypes"
- :key="key"
- :class="['type-card', { active: selectedProjectType === key }]"
- @click="(isGenerating || selectedFile) ? null : selectProjectType(key)"
- :style="{ cursor: (isGenerating || selectedFile) ? 'not-allowed' : 'pointer', opacity: (isGenerating || selectedFile) ? '0.5' : '1' }"
- >
- <img :src="type.icon" :alt="type.name" class="type-icon" />
- <span>{{ type.name }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 生成方式选择 -->
- <div class="config-section">
- <div class="config-header">
- <div class="step-number">2</div>
- <h3>选择生成方式</h3>
- </div>
- <div class="generation-methods">
- <div
- :class="['method-card', { active: selectedFunction === 'ai' }]"
- @click="(isGenerating || selectedFile) ? null : selectFunction('ai')"
- :style="{ cursor: (isGenerating || selectedFile) ? 'not-allowed' : 'pointer', opacity: (isGenerating || selectedFile) ? '0.5' : '1' }"
- >
- <img :src="aiIcon" alt="智能生成试卷" class="method-icon" />
- <div class="method-content">
- <h4>智能生成试卷</h4>
- <p>基于AI技术,根据所选类型自动生成完整试卷</p>
- </div>
- </div>
- <!-- PPT生成考题选项已隐藏 -->
- </div>
- </div>
- <!-- 试卷配置 -->
- <div class="config-section">
- <div class="config-header">
- <div class="step-number">3</div>
- <h3>试卷配置</h3>
- </div>
- <div class="exam-config-container">
- <div class="config-main">
- <div class="config-form">
- <div class="form-group">
- <label>试卷名称</label>
- <div class="input-wrapper">
- <input
- v-model="examName"
- type="text"
- placeholder="请输入试卷名称"
- class="config-input"
- maxlength="32"
- @input="validateExamName"
- :disabled="isGenerating || selectedFile"
- />
- <span class="char-count" :class="{ 'warning': examName.length >= 18 }">{{ examName.length }}/32</span>
- </div>
- </div>
- <div class="form-group">
- <label>试卷总分</label>
- <div class="score-input">
- <input
- v-model="totalScore"
- type="number"
- class="config-input"
- min="1"
- max="1000"
- @input="validateTotalScore"
- :disabled="isGenerating || selectedFile"
- />
- <span class="unit">分</span>
- </div>
- </div>
- </div>
- <!-- 题型配置 -->
- <div class="question-types-title">题型选择与分数分配</div>
- <div class="question-types">
- <div
- class="question-type"
- v-for="(type, index) in questionTypes"
- :key="index"
- >
- <div class="type-header">
- <span class="type-name">{{ type.name }}</span>
- <div class="progress-bar">
- <div class="progress-fill" :style="{ width: ((type.scorePerQuestion * type.questionCount) / totalScore) * 100 + '%' }"></div>
- </div>
- </div>
- <div class="score-config">
- <div class="config-item">
- <span>每题</span>
- <input
- v-model="type.scorePerQuestion"
- type="number"
- class="score-input-field"
- min="1"
- max="99"
- @input="validateScorePerQuestion(type)"
- :disabled="isGenerating || selectedFile"
- />
- <span>分</span>
- </div>
- <div class="config-item">
- <span>一共</span>
- <div class="count-stepper">
- <input
- v-model="type.questionCount"
- type="number"
- class="count-input-field"
- min="0"
- max="99"
- @input="validateQuestionCount(type)"
- :disabled="isGenerating || selectedFile"
- />
- <div class="stepper-buttons">
- <button
- class="stepper-btn stepper-btn-up"
- type="button"
- @click="adjustQuestionCount(type, 1)"
- :disabled="isGenerating || selectedFile || type.questionCount >= 99"
- aria-label="增加题目数量"
- ></button>
- <button
- class="stepper-btn stepper-btn-down"
- type="button"
- @click="adjustQuestionCount(type, -1)"
- :disabled="isGenerating || selectedFile || type.questionCount <= 0"
- aria-label="减少题目数量"
- ></button>
- </div>
- </div>
- <span>题</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 预览面板 -->
- <div class="preview-panel">
- <div class="preview-header">
- <img :src="previewIcon" alt="预览" class="preview-icon" />
- <h3>预览</h3>
- </div>
- <div class="preview-content">
- <h4 class="preview-title">{{ examName || "试卷名称" }}</h4>
- <div class="question-breakdown">
- <div
- class="breakdown-item"
- v-for="(type, index) in questionTypes"
- :key="index"
- >
- <div class="breakdown-row">
- <span class="breakdown-left">{{ type.romanNumeral }}、{{ type.name }} (每题{{ type.scorePerQuestion }}分,共{{ type.scorePerQuestion * type.questionCount }}分)</span>
- <span class="breakdown-right">{{ type.questionCount }}题</span>
- </div>
- </div>
- </div>
- <div class="divider"></div>
- <div class="calculated-score-row">
- <span class="calculated-label">配置总分</span>
- <span class="calculated-value">{{ calculatedTotalScore }}分</span>
- </div>
- <div class="total-score-row">
- <span class="total-label">试卷总分</span>
- <span class="total-value">{{ totalScore }}分</span>
- </div>
-
- </div>
- </div>
- </div>
- <!-- 底部操作按钮 -->
- <div class="bottom-actions">
- <button class="clear-btn" @click="clearSettings" :disabled="isGenerating || selectedFile">
- 一键清除
- </button>
- <button class="generate-btn" @click="generateExam" :disabled="isGenerating">
- <img v-if="!isGenerating" :src="generateIcon" alt="生成试卷" class="generate-icon" />
- <span v-else class="generating-text">生成中...</span>
- </button>
- </div>
- </div>
- </div>
- <!-- 考试详情页 -->
- <div v-if="showExamDetail" class="exam-detail-main">
- <!-- 详情页头部 -->
- <div class="detail-header">
- <button class="back-btn" @click="backToConfig" :disabled="isGenerating">
- <span class="back-arrow">←</span>
- 返回修改
- </button>
- <div class="download-dropdown" :class="{ 'disabled': isGenerating, 'show': showDownloadMenu }" @click.stop>
- <button class="download-btn" :disabled="isGenerating" @click="toggleDownloadMenu">
- <img :src="downloadIcon" alt="下载Word" class="download-icon" />
- </button>
- <div class="dropdown-menu">
- <div class="dropdown-item" @click="exportToWordWithAnswers" :disabled="isGenerating">
- <span class="item-text">有答案</span>
- </div>
- <div class="dropdown-item" @click="exportToWordWithoutAnswers" :disabled="isGenerating">
- <span class="item-text">无答案</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 试卷信息 -->
- <div class="exam-info">
- <h1 class="exam-title">{{ currentExam.title }}</h1>
- <div class="exam-stats">
- <span class="total-score">总分: {{ currentExam.totalScore }}分</span>
- <span class="question-count">题量: {{ currentExam.totalQuestions }}题</span>
- </div>
- <div class="generation-time">生成时间: {{ currentTime }}</div>
- </div>
- <!-- 题型列表 -->
- <div class="question-sections">
- <!-- 单选题 -->
- <div class="question-section" v-if="currentExam.singleChoice && currentExam.singleChoice.questions.length > 0">
- <div class="section-header" @click="isGenerating ? null : toggleSection('single')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">一</span>
- <span class="section-name">单选题</span>
- <span class="section-score">(每题{{ currentExam.singleChoice.scorePerQuestion }}分, 共{{ currentExam.singleChoice.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.singleChoice.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.single }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.single" class="section-content">
- <div
- v-for="(question, index) in currentExam.singleChoice.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('single', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing['single_' + index] }"
- />
- </button>
- </div>
- <div class="options">
- <div
- v-for="option in question.options"
- :key="option.key"
- class="option"
- >
- <div class="radio-wrapper">
- <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === option.key }">
- <div v-if="question.selectedAnswer === option.key" class="radio-dot"></div>
- </div>
- </div>
- <span class="option-key">{{ option.key }}.</span>
- <div class="option-content">
- <span class="option-text">{{ option.text }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 判断题 -->
- <div class="question-section" v-if="currentExam.judge && currentExam.judge.questions.length > 0">
- <div class="section-header" @click="isGenerating ? null : toggleSection('judge')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">二</span>
- <span class="section-name">判断题</span>
- <span class="section-score">(每题{{ currentExam.judge.scorePerQuestion }}分, 共{{ currentExam.judge.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.judge.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.judge }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.judge" class="section-content">
- <div
- v-for="(question, index) in currentExam.judge.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('judge', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing['judge_' + index] }"
- />
- </button>
- </div>
- <div class="answer-section">
- <span class="answer-label">正确答案:</span>
- <span class="answer-value">{{ question.selectedAnswer }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 多选题 -->
- <div class="question-section" v-if="currentExam.multiple && currentExam.multiple.questions.length > 0">
- <div class="section-header" @click="isGenerating ? null : toggleSection('multiple')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">三</span>
- <span class="section-name">多选题</span>
- <span class="section-score">(每题{{ currentExam.multiple.scorePerQuestion }}分, 共{{ currentExam.multiple.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.multiple.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.multiple }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.multiple" class="section-content">
- <div
- v-for="(question, index) in currentExam.multiple.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('multiple', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing['multiple_' + index] }"
- />
- </button>
- </div>
- <div class="options">
- <div
- v-for="option in question.options"
- :key="option.key"
- class="option"
- >
- <div class="radio-wrapper">
- <div class="radio-circle" :class="{ 'selected': (question.selectedAnswers || []).includes(option.key) }">
- <div v-if="(question.selectedAnswers || []).includes(option.key)" class="radio-dot"></div>
- </div>
- </div>
- <span class="option-key">{{ option.key }}.</span>
- <div class="option-content">
- <span class="option-text">{{ option.text }}</span>
- </div>
- </div>
- </div>
- <div class="answer-section">
- <span class="answer-label">正确答案:</span>
- <span class="answer-value">{{ (question.selectedAnswers || []).join(', ') }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 简答题 -->
- <div class="question-section" v-if="currentExam.short && currentExam.short.questions.length > 0">
- <div class="section-header" @click="isGenerating ? null : toggleSection('short')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">四</span>
- <span class="section-name">简答题</span>
- <span class="section-score">(每题{{ currentExam.short.scorePerQuestion }}分, 共{{ currentExam.short.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.short.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.short }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.short" class="section-content">
- <div
- v-for="(question, index) in currentExam.short.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('short', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing['short_' + index] }"
- />
- </button>
- </div>
- <div v-if="question.outline" class="answer-outline">
- <div class="outline-section">
- <strong>关键要点:</strong>{{ cleanText(question.outline.keyFactors) }}
- </div>
- <div class="outline-section">
- <strong>具体措施:</strong>{{ cleanText(question.outline.measures) }}
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 文件上传隐藏input -->
- <input
- ref="fileInput"
- type="file"
- accept=".ppt,.pptx"
- @change="handleFileSelect"
- style="display: none"
- />
- </div>
- </div>
- </template>
- <script setup>
- import { useRouter } from 'vue-router'
- import MobileHeader from '@/components/MobileHeader.vue'
- import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
- import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
- import MobileToast from '@/components/MobileToast.vue'
- import { apis } from '@/request/apis.js'
- import { initNativeNavForSubPage } from '@/utils/nativeBridge.js'
- // ===== 已删除:getUserId - 不再需要,改用token =====
- // import { getUserId } from '@/utils/userManager.js'
- const router = useRouter()
- const goBack = () => {
- router.go(-1)
- }
- // 显示历史记录抽屉的方法
- const showHistoryDrawer = () => {
- if (!isGenerating.value) {
- showHistory.value = true
- }
- // AI处理中时不执行任何操作,不记录点击意图
- }
- const showHistory = ref(false)
- // Toast 状态管理(与其他移动端页面保持一致)
- const toastVisible = ref(false)
- const toastMessage = ref('')
- const showToast = (message, duration = 2000) => {
- toastMessage.value = message
- toastVisible.value = true
- if (duration > 0) {
- setTimeout(() => {
- toastVisible.value = false
- }, duration)
- }
- }
- const closeToast = () => {
- toastVisible.value = false
- }
- // 历史记录相关状态
- const historyData = ref([])
- const historyTotal = ref(0)
- const isLoadingHistory = ref(false)
- // 页面加载时不再自动加载历史记录,改为点击菜单时加载
- onMounted(async () => {
- // 保存初始配置
- initialConfig = {
- questionTypes: JSON.parse(JSON.stringify(questionTypes.value)),
- totalScore: totalScore.value,
- selectedProjectType: selectedProjectType.value,
- examName: examName.value
- };
- console.log('初始配置已保存:', initialConfig);
-
- // 添加全局点击事件监听器
- document.addEventListener('click', handleClickOutside);
-
- console.log('🚀 移动端考试工坊页面初始化完成')
- })
- onUnmounted(() => {
- // 清理事件监听器
- document.removeEventListener('click', handleClickOutside);
- })
- // ============ 移动端考试工坊核心功能 ============
- // 试卷类型图标和配置
- import bridgeIcon from '@/assets/Exam/4.png'
- import tunnelIcon from '@/assets/Exam/18.png'
- import equipmentIcon from '@/assets/Exam/5.png'
- import gasStationIcon from '@/assets/Exam/6.png'
- import highwayIcon from '@/assets/Exam/7.png'
- import comprehensiveIcon from '@/assets/Exam/8.png'
- import aiIcon from '@/assets/Exam/7.png'
- import pptIcon from '@/assets/Exam/8.png'
- import previewIcon from '@/assets/Exam/9.png'
- import clearIcon from '@/assets/Exam/10.png'
- import generateIcon from '@/assets/Exam/12.png'
- import downloadIcon from '@/assets/Exam/13.png'
- import expandIcon from '@/assets/Exam/17.png'
- import collapseIcon from '@/assets/Exam/16.png'
- import saveIcon from '@/assets/Exam/15.png'
- // 试卷类型配置
- const projectTypes = {
- bridge: { name: "桥梁", icon: bridgeIcon },
- tunnel: { name: "隧道", icon: tunnelIcon },
- equipment: { name: "特种设备", icon: equipmentIcon },
- "gas-station": { name: "加油站", icon: gasStationIcon },
- highway: { name: "高速运营公路", icon: highwayIcon },
- comprehensive: { name: "综合", icon: comprehensiveIcon },
- };
- // 题型配置
- const questionTypes = ref([
- { name: "单选题", scorePerQuestion: 5, questionCount: 5, romanNumeral: "一" },
- { name: "判断题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "二" },
- { name: "多选题", scorePerQuestion: 8, questionCount: 5, romanNumeral: "三" },
- { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
- ]);
- // 考试工坊状态
- const showExamDetail = ref(false)
- const isGenerating = ref(false)
- const isLoadingHistoryItem = ref(false)
- const ai_conversation_id = ref(0)
- const showDownloadMenu = ref(false) // 控制下载菜单显示状态
- // 试卷配置状态
- const selectedFunction = ref("ai")
- const selectedProjectType = ref("bridge")
- const examName = ref("桥梁工程施工技术考核")
- const totalScore = ref(100)
- const currentTime = ref("")
- // 文件上传相关
- const selectedFile = ref(null)
- const pptContentDescription = ref('')
- const fileInput = ref(null)
- // 展开/收起状态
- const expandedSections = ref({
- single: true,
- judge: true,
- multiple: true,
- short: true,
- })
- // 刷新状态记录
- const isRefreshing = ref({})
- // 当前试卷数据
- const currentExam = ref({
- title: "桥梁工程施工技术考核",
- totalScore: 100,
- totalQuestions: 37,
- singleChoice: {
- scorePerQuestion: 2,
- totalScore: 30,
- count: 15,
- questions: []
- },
- judge: {
- scorePerQuestion: 2,
- totalScore: 20,
- count: 10,
- questions: []
- },
- multiple: {
- scorePerQuestion: 3,
- totalScore: 30,
- count: 10,
- questions: []
- },
- short: {
- scorePerQuestion: 10,
- totalScore: 20,
- count: 2,
- questions: []
- }
- })
- // 保存初始配置
- let initialConfig = null
- // 计算总分(所有题目分数的总和)
- const calculatedTotalScore = computed(() => {
- return questionTypes.value.reduce((total, type) => {
- return total + (type.scorePerQuestion * type.questionCount);
- }, 0);
- })
- // ============ 历史记录相关方法 ============
- // 格式化时间函数
- const formatTime = (timestamp) => {
- if (!timestamp) return ''
- const date = new Date(timestamp)
- const now = new Date()
- const diff = now - date
-
- // 如果是今天
- if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
- return date.toLocaleTimeString('zh-CN', {
- hour: '2-digit',
- minute: '2-digit'
- })
- }
-
- // 如果是昨天
- if (diff < 48 * 60 * 60 * 1000 && date.getDate() === now.getDate() - 1) {
- return '昨天 ' + date.toLocaleTimeString('zh-CN', {
- hour: '2-digit',
- minute: '2-digit'
- })
- }
-
- // 其他情况显示日期
- return date.toLocaleDateString('zh-CN', {
- month: '2-digit',
- day: '2-digit'
- })
- }
- // 生成对话标题
- const generateConversationTitle = (content) => {
- if (!content) return '新对话'
- // 取前30个字符作为标题
- const title = content.replace(/<[^>]*>/g, '').trim()
- return title.length > 30 ? title.substring(0, 30) + '...' : title
- }
- // 新建任务
- const createNewTask = () => {
- console.log('新建考试工坊任务')
- showHistory.value = false
- // 重置所有状态到新任务状态
- selectedFunction.value = "ai"
- selectedProjectType.value = "bridge"
- examName.value = "桥梁工程施工技术考核"
- totalScore.value = 100
- showExamDetail.value = false
- selectedFile.value = null
- pptContentDescription.value = ''
- ai_conversation_id.value = 0
-
- // 重置题型配置
- if (initialConfig) {
- questionTypes.value = JSON.parse(JSON.stringify(initialConfig.questionTypes))
- totalScore.value = initialConfig.totalScore
- selectedProjectType.value = initialConfig.selectedProjectType
- examName.value = initialConfig.examName
- }
-
- // 清除历史记录的激活状态
- historyData.value.forEach((item) => {
- item.isActive = false
- })
- }
- // 处理历史记录点击
- const handleHistoryItem = async (historyItem) => {
- if (isGenerating.value || isLoadingHistoryItem.value) return
-
- console.log("点击移动端考试工坊历史记录:", historyItem)
- ai_conversation_id.value = historyItem.id
- isLoadingHistoryItem.value = true
- try {
- historyData.value.forEach((item) => {
- item.isActive = item.id === historyItem.id
- })
- showHistory.value = false
- const response = await apis.getHistoryRecord({
- ai_conversation_id: historyItem.id,
- business_type: 3
- })
- if (response.statusCode === 200 && response.data && response.data.length > 0) {
- const latestAiRecord = [...response.data].reverse().find(record => record.type === 'ai' && record.content)
- if (latestAiRecord?.content) {
- try {
- const examData = extractExamDataFromContent(latestAiRecord.content)
- restoreExamFromHistory(examData)
- currentTime.value = historyItem.time
- showExamDetail.value = true
- return
- } catch (error) {
- console.error('解析移动端历史试卷数据失败:', error)
- }
- }
- }
- console.error('移动端历史记录缺少可恢复的试卷详情:', response)
- showToast('该历史记录暂无可恢复的试卷内容')
- } catch (error) {
- console.error('获取移动端历史记录详情失败:', error)
- showToast('获取历史记录详情失败,请重试')
- } finally {
- isLoadingHistoryItem.value = false
- }
- }
- // 删除历史记录
- const deleteHistoryItem = async (historyItem, index) => {
- try {
- console.log('开始删除移动端历史记录:', historyItem)
-
- const response = await apis.deleteHistoryRecord({
- // ===== 已删除:user_id - 后端从token解析 =====
- ai_conversation_id: historyItem.id
- })
-
- if (response.statusCode === 200) {
- // 从本地数据中移除
- historyData.value.splice(index, 1)
- historyTotal.value = Math.max(0, historyTotal.value - 1)
-
- // 如果删除的是当前激活的历史记录,执行新建任务
- if (historyItem.isActive) {
- console.log('删除激活的历史记录,执行新建任务')
- createNewTask()
- }
-
- console.log('✅ 移动端历史记录删除成功')
- // 轻提示
- showToast('删除成功')
- } else {
- console.error('❌ 删除移动端历史记录失败:', response)
- }
- } catch (error) {
- console.error('❌ 删除移动端历史记录失败:', error)
- }
- }
- // 时间解析与格式化(容错更强)
- const parseToDate = (input) => {
- if (!input) return null
- if (typeof input === 'number') {
- const ms = input < 1e12 ? input * 1000 : input
- return new Date(ms)
- }
- if (typeof input === 'string') {
- let d = new Date(input)
- if (!isNaN(d)) return d
- const normalized = input.replace(/-/g, '/').replace('T', ' ')
- d = new Date(normalized)
- if (!isNaN(d)) return d
- }
- return new Date(input)
- }
- const formatHistoryTime = (timestamp) => {
- const date = parseToDate(timestamp)
- if (!date || isNaN(date)) return '未知时间'
- const now = new Date()
- const isToday = date.toDateString() === now.toDateString()
- const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
- const isYesterday = date.toDateString() === yesterday.toDateString()
- if (isToday) {
- return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
- if (isYesterday) {
- return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
- const month = date.getMonth() + 1
- const day = date.getDate()
- const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- return `${month}月${day}日 ${time}`
- }
- // 获取历史记录列表
- const isExamWorkshopConversation = (conversation = {}) => {
- const content = String(conversation.content || '')
- const title = String(conversation.title || '')
- const examName = String(conversation.exam_name || '')
- return (
- Number(conversation.business_type) === 3 ||
- !!examName.trim() ||
- title.includes('技术考核') ||
- content.includes('请根据以下要求直接生成一份完整试卷') ||
- content.includes('"singleChoice"') ||
- content.includes('"totalQuestions"')
- )
- }
- const getHistoryRecordList = async () => {
- try {
- console.log('📋 开始获取移动端考试工坊历史记录列表...')
- isLoadingHistory.value = true
- const startTime = performance.now()
-
- const response = await apis.getHistoryRecord({
- // ===== 已删除:user_id - 后端从token解析 =====
- ai_conversation_id: 0, // 0表示获取对话列表
- business_type: 3 // 考试工坊类型
- })
-
- const endTime = performance.now()
- console.log('📋 移动端考试工坊历史记录API调用耗时: ' + (endTime - startTime).toFixed(2) + 'ms')
- console.log('📋 移动端历史记录列表响应:', response)
-
- if (response.statusCode === 200) {
- const directConversations = Array.isArray(response.data) ? response.data : []
- let conversations = [...directConversations]
- const fallbackResponse = await apis.getHistoryRecord({
- ai_conversation_id: 0
- })
- if (fallbackResponse.statusCode === 200 && Array.isArray(fallbackResponse.data)) {
- const inferredExamConversations = fallbackResponse.data.filter(isExamWorkshopConversation)
- const conversationMap = new Map()
- directConversations.concat(inferredExamConversations).forEach((conversation) => {
- if (!conversation?.id) return
- conversationMap.set(conversation.id, conversation)
- })
- conversations = Array.from(conversationMap.values()).sort((a, b) => {
- return Number(b.updated_at || 0) - Number(a.updated_at || 0)
- })
- }
- // 设置历史记录总数
- historyTotal.value = conversations.length
-
- // 转换后端数据为前端格式
- historyData.value = conversations.map(conversation => ({
- id: conversation.id,
- title: generateConversationTitle(conversation.exam_name || conversation.title || conversation.content),
- time: formatHistoryTime(conversation.updated_at),
- businessType: conversation.business_type,
- isActive: false,
- // 保存原始数据用于后续查询
- rawData: conversation
- }))
- // 高亮当前对话
- if (ai_conversation_id.value) {
- historyData.value.forEach(item => { item.isActive = item.id === ai_conversation_id.value })
- }
- console.log('✅ 移动端考试工坊历史记录列表已设置: ' + historyData.value.length + '条记录,总数: ' + historyTotal.value)
- } else {
- console.error('❌ 获取移动端历史记录列表失败:', response.statusCode)
- }
- } catch (error) {
- console.error('❌ 获取移动端历史记录列表失败:', error)
- } finally {
- isLoadingHistory.value = false
- }
- }
- // ============ 业务逻辑方法 ============
- // 选择功能方法
- const selectFunction = (functionType) => {
- selectedFunction.value = functionType;
- console.log("选择功能:", functionType);
- };
- // 选择项目类型方法
- const selectProjectType = (typeKey) => {
- selectedProjectType.value = typeKey;
- console.log("选择工程类型:", projectTypes[typeKey].name);
-
- // 自动更新试卷名称
- const projectTypeName = projectTypes[typeKey].name;
- examName.value = projectTypeName + '工程施工技术考核';
-
- // 同时更新当前试卷的标题
- if (currentExam.value) {
- currentExam.value.title = examName.value;
- }
- };
- // 验证试卷名称
- const validateExamName = () => {
- if (examName.value.length > 32) {
- examName.value = examName.value.slice(0, 32);
- }
- };
- // 验证总分
- const validateTotalScore = () => {
- if (totalScore.value > 1000) {
- totalScore.value = 1000;
- console.warn("试卷总分不能超过1000分");
- }
- if (totalScore.value < 1) {
- totalScore.value = 1;
- }
- };
- // 验证每题分数
- const validateScorePerQuestion = (type) => {
- if (type.scorePerQuestion > 99) {
- type.scorePerQuestion = 99;
- console.warn(`${type.name}每题分数不能超过99分`);
- }
- if (type.scorePerQuestion < 1) {
- type.scorePerQuestion = 1;
- }
- };
- // 验证题目数量
- const validateQuestionCount = (type) => {
- if (type.questionCount > 99) {
- type.questionCount = 99;
- console.warn(`${type.name}题目数量不能超过99题`);
- }
- if (type.questionCount < 0) {
- type.questionCount = 0;
- }
- };
- const adjustQuestionCount = (type, delta) => {
- type.questionCount = Number(type.questionCount || 0) + delta;
- validateQuestionCount(type);
- };
- // 清除设置
- const clearSettings = () => {
- // 根据当前选择的工程类型设置试卷名称
- const projectTypeName = projectTypes[selectedProjectType.value].name;
- examName.value = projectTypeName + '工程施工技术考核';
- totalScore.value = 100;
- questionTypes.value = [
- { name: "单选题", scorePerQuestion: 2, questionCount: 8, romanNumeral: "一" },
- { name: "判断题", scorePerQuestion: 2, questionCount: 5, romanNumeral: "二" },
- { name: "多选题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "三" },
- { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
- ];
- console.log("清除设置");
- };
- // 生成试卷
- const generateExam = async () => {
- if (!examName.value.trim()) {
- console.warn("请输入试卷名称");
- return;
- }
- if (examName.value.trim().length === 0) {
- console.warn("试卷名称不能为空");
- return;
- }
- // 检查总分是否超过限制
- if (totalScore.value > 1000) {
- console.warn("试卷总分不能超过1000分");
- return;
- }
- // 检查每题分数和题目数量是否超过限制
- for (const type of questionTypes.value) {
- if (type.scorePerQuestion > 99) {
- console.warn(`${type.name}每题分数不能超过99分`);
- return;
- }
- if (type.questionCount > 99) {
- console.warn(`${type.name}题目数量不能超过99题`);
- return;
- }
- }
- // 检查总分是否合理
- const calculatedScore = questionTypes.value.reduce((total, type) => {
- return total + (type.scorePerQuestion * type.questionCount);
- }, 0);
-
- if (calculatedScore !== totalScore.value) {
- showToast(`总分不匹配!`, 3000);
- return;
- }
- console.log("开始生成试卷:", {
- function: selectedFunction.value,
- projectType: projectTypes[selectedProjectType.value].name,
- examName: examName.value,
- totalScore: totalScore.value,
- questionTypes: questionTypes.value,
- pptContent: pptContentDescription.value
- });
- try {
- isGenerating.value = true;
- const mode = selectedFunction.value === 'ppt' ? 'ppt' : 'ai';
- const prompt = await fetchMobileExamPrompt(mode);
- console.log('发送给AI的考试生成提示词:', prompt);
- // 调用AI接口生成试卷 - 使用与PC端相同的接口
- const response = await apis.sendDeepseekMessage({
- // ===== 已删除:user_id - 后端从token解析 =====
- business_type: 3,
- message: prompt,
- exam_name: examName.value,
- ai_conversation_id: ai_conversation_id.value
- });
- if (response.statusCode === 200) {
- const aiReply = response.data.reply;
- const aiConversationId = response.data.ai_conversation_id;
- console.log('AI生成的考试试卷:', aiReply);
- console.log('AI对话ID:', aiConversationId);
- // 保存对话ID
- ai_conversation_id.value = aiConversationId;
- // 解析AI回复并生成试卷
- const generatedExam = parseAIExamResponse(aiReply);
- // 更新当前试卷数据
- currentExam.value = generatedExam;
- currentExam.value.title = examName.value;
- currentExam.value.totalScore = totalScore.value;
- currentTime.value = new Date().toLocaleString('zh-CN');
- // 试卷数据已通过AI接口自动保存到数据库
- console.log('✅ 试卷已通过AI接口保存到数据库');
- // 显示考试详情页
- showExamDetail.value = true;
- // 刷新历史记录,确保新生成的试卷详情可被立即查看
- await getHistoryRecordList();
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- }
- console.log('✅ 移动端试卷生成完成!');
- } else {
- throw new Error('AI接口调用失败');
- }
- } catch (error) {
- console.error('生成试卷失败:', error);
- } finally {
- isGenerating.value = false;
- }
- };
- // 从服务端获取移动端提示词
- const fetchMobileExamPrompt = async (mode = 'ai') => {
- const normalizedQuestionTypes = questionTypes.value.map(type => ({
- name: type.name,
- romanNumeral: type.romanNumeral,
- questionCount: Number(type.questionCount) || 0,
- scorePerQuestion: Number(type.scorePerQuestion) || 0,
- }));
- const payload = {
- mode,
- client: 'mobile',
- projectType: projectTypes[selectedProjectType.value]?.name || '',
- examTitle: examName.value,
- totalScore: totalScore.value,
- questionTypes: normalizedQuestionTypes,
- pptContent: pptContentDescription.value || ''
- };
- try {
- const response = await apis.buildExamPrompt(payload);
- if (!response?.data?.prompt) {
- throw new Error(response?.msg || '提示词构建失败');
- }
- return response.data.prompt;
- } catch (error) {
- console.error('获取移动端提示词失败:', error);
- throw error;
- }
- };
- // 解析AI考试回复
- const extractExamDataFromContent = (content) => {
- if (!content || typeof content !== 'string') {
- throw new Error('历史内容为空');
- }
- const directMatch = content.match(/\{[\s\S]*\}/);
- if (!directMatch) {
- throw new Error('未找到可解析的试卷JSON');
- }
- return JSON.parse(directMatch[0]);
- };
- const parseAIExamResponse = (aiReply) => {
- try {
- const examData = extractExamDataFromContent(aiReply);
- const normalizedExam = normalizeGeneratedExam(examData);
- ensureQuestionInitialValues(normalizedExam);
- return normalizedExam;
- } catch (error) {
- console.error('解析AI回复失败:', error);
- // 返回默认试卷结构
- return generateDefaultExam();
- }
- };
- const getQuestionTypeConfig = (index, fallbackScore = 0) => ({
- scorePerQuestion: Number(questionTypes.value[index]?.scorePerQuestion) || fallbackScore,
- questionCount: Number(questionTypes.value[index]?.questionCount) || 0,
- });
- const normalizeOptions = (options = []) => {
- if (!Array.isArray(options)) {
- return [];
- }
- return options.map((option, index) => {
- if (typeof option === 'string') {
- return {
- key: String.fromCharCode(65 + index),
- text: option,
- };
- }
- return {
- key: option?.key || String.fromCharCode(65 + index),
- text: option?.text || option?.content || option?.label || "",
- };
- });
- };
- const normalizeQuestions = (questions = [], sectionKey) => {
- if (!Array.isArray(questions)) {
- return [];
- }
- return questions.map((question = {}) => {
- if (sectionKey === 'singleChoice') {
- return {
- text: question.text || question.question_text || "",
- options: normalizeOptions(question.options),
- selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
- };
- }
- if (sectionKey === 'judge') {
- return {
- text: question.text || question.question_text || "",
- selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
- };
- }
- if (sectionKey === 'multiple') {
- const selectedAnswers = question.selectedAnswers || question.correct_answers || question.answers || [];
- return {
- text: question.text || question.question_text || "",
- options: normalizeOptions(question.options),
- selectedAnswers: Array.isArray(selectedAnswers) ? selectedAnswers : [selectedAnswers].filter(Boolean),
- };
- }
- return {
- text: question.text || question.question_text || "",
- outline: question.outline || question.answer_outline || {
- keyFactors: question.answer || "请参考相关教材和标准规范",
- measures: "请结合实际工程案例进行解答"
- },
- };
- });
- };
- const normalizeSection = (rawSection, sectionKey, index, fallbackScore = 0) => {
- const section = rawSection || {};
- const config = getQuestionTypeConfig(index, fallbackScore);
- const sourceQuestions = Array.isArray(section)
- ? section
- : (section.questions || section.items || section.question_list || []);
- const normalizedQuestions = normalizeQuestions(sourceQuestions, sectionKey);
- const count = Number(section.count ?? section.question_count ?? normalizedQuestions.length ?? config.questionCount) || 0;
- const scorePerQuestion = Number(section.scorePerQuestion ?? section.score_per_question ?? config.scorePerQuestion) || 0;
- const totalScore = Number(section.totalScore ?? section.total_score ?? (scorePerQuestion * count)) || 0;
- return {
- scorePerQuestion,
- totalScore,
- count,
- questions: normalizedQuestions,
- };
- };
- const normalizeGeneratedExam = (examData = {}) => {
- const normalizedExam = {
- title: examData.title || examData.exam_name || examName.value,
- totalScore: Number(examData.totalScore ?? examData.total_score ?? totalScore.value) || 0,
- totalQuestions: Number(examData.totalQuestions ?? examData.total_questions) || 0,
- singleChoice: normalizeSection(examData.singleChoice || examData.questions?.single_choice || examData.single_choice, 'singleChoice', 0, 2),
- judge: normalizeSection(examData.judge || examData.questions?.judge, 'judge', 1, 2),
- multiple: normalizeSection(examData.multiple || examData.questions?.multiple, 'multiple', 2, 3),
- short: normalizeSection(examData.short || examData.questions?.short, 'short', 3, 10),
- };
- if (!normalizedExam.totalQuestions) {
- normalizedExam.totalQuestions =
- normalizedExam.singleChoice.count +
- normalizedExam.judge.count +
- normalizedExam.multiple.count +
- normalizedExam.short.count;
- }
- return normalizedExam;
- };
- // 确保题目初始值正确
- const ensureQuestionInitialValues = (examData) => {
- // 确保单选题有正确的答案格式
- if (examData.singleChoice && examData.singleChoice.questions) {
- examData.singleChoice.questions.forEach(question => {
- if (!question.selectedAnswer) {
- question.selectedAnswer = question.options && question.options.length > 0 ? question.options[0].key : 'A';
- }
- });
- }
- // 确保判断题有正确答案
- if (examData.judge && examData.judge.questions) {
- examData.judge.questions.forEach(question => {
- if (!question.selectedAnswer) {
- question.selectedAnswer = Math.random() > 0.5 ? '正确' : '错误';
- }
- });
- }
- // 确保多选题有正确答案
- if (examData.multiple && examData.multiple.questions) {
- examData.multiple.questions.forEach(question => {
- if (!question.selectedAnswers || !Array.isArray(question.selectedAnswers)) {
- question.selectedAnswers = question.options && question.options.length > 1 ? [question.options[0].key, question.options[1].key] : [];
- }
- });
- }
- // 确保简答题有提纲
- if (examData.short && examData.short.questions) {
- examData.short.questions.forEach(question => {
- if (!question.outline) {
- question.outline = {
- keyFactors: "请参考相关教材和标准规范",
- measures: "请结合实际工程案例进行解答"
- };
- }
- });
- }
- };
- // 生成默认考试结构
- const generateDefaultExam = () => {
- return {
- title: examName.value,
- totalScore: totalScore.value,
- totalQuestions: questionTypes.value.reduce((total, type) => total + type.questionCount, 0),
- singleChoice: {
- scorePerQuestion: questionTypes.value[0].scorePerQuestion,
- totalScore: questionTypes.value[0].scorePerQuestion * questionTypes.value[0].questionCount,
- count: questionTypes.value[0].questionCount,
- questions: []
- },
- judge: {
- scorePerQuestion: questionTypes.value[1].scorePerQuestion,
- totalScore: questionTypes.value[1].scorePerQuestion * questionTypes.value[1].questionCount,
- count: questionTypes.value[1].questionCount,
- questions: []
- },
- multiple: {
- scorePerQuestion: questionTypes.value[2].scorePerQuestion,
- totalScore: questionTypes.value[2].scorePerQuestion * questionTypes.value[2].questionCount,
- count: questionTypes.value[2].questionCount,
- questions: []
- },
- short: {
- scorePerQuestion: questionTypes.value[3].scorePerQuestion,
- totalScore: questionTypes.value[3].scorePerQuestion * questionTypes.value[3].questionCount,
- count: questionTypes.value[3].questionCount,
- questions: []
- }
- }
- };
- const restoreExamFromHistory = (examData) => {
- const exam = examData?.exam || examData || {}
- const normalizedExam = normalizeGeneratedExam(exam)
- examName.value = normalizedExam.title || examName.value
- totalScore.value = normalizedExam.totalScore || totalScore.value
- questionTypes.value = [
- { name: "单选题", scorePerQuestion: normalizedExam.singleChoice.scorePerQuestion, questionCount: normalizedExam.singleChoice.count, romanNumeral: "一" },
- { name: "判断题", scorePerQuestion: normalizedExam.judge.scorePerQuestion, questionCount: normalizedExam.judge.count, romanNumeral: "二" },
- { name: "多选题", scorePerQuestion: normalizedExam.multiple.scorePerQuestion, questionCount: normalizedExam.multiple.count, romanNumeral: "三" },
- { name: "简答题", scorePerQuestion: normalizedExam.short.scorePerQuestion, questionCount: normalizedExam.short.count, romanNumeral: "四" },
- ]
- currentExam.value = normalizedExam
- }
- // 返回配置页面
- const backToConfig = () => {
- showExamDetail.value = false;
- };
- // 展开/收起题型
- const toggleSection = (sectionType) => {
- expandedSections.value[sectionType] = !expandedSections.value[sectionType];
- };
- // 刷新题目 - 使用PC端的实现方式
- const refreshQuestion = async (questionType, index) => {
- try {
- console.log(`刷新${questionType}类型第${index + 1}题`);
-
- // 设置刷新状态
- const key = `${questionType}_${index}`;
- isRefreshing.value[key] = true;
-
- // 构建单题重新生成的提示词
- const prompt = buildSingleQuestionPrompt(questionType, index);
-
- // 第一步:调用 /re_produce_single_question 接口,AI只生成题目
- const response = await apis.reProduceSingleQuestion({
- message: prompt
- });
-
- if (response.statusCode === 200) {
- const aiReply = response.data.reply;
- console.log('AI重新生成的题目:', aiReply);
-
- // 解析AI回复并更新题目
- const newQuestion = parseSingleQuestionResponse(aiReply, questionType);
- console.log('解析后的新题目:', newQuestion);
-
- if (newQuestion) {
- updateQuestion(questionType, index, newQuestion);
-
- console.log('准备保存到后端,对话ID:', ai_conversation_id.value);
-
- // 第二步:使用 /re_modify_question 接口保存修改
- await saveToReModifyQuestion(questionType, index, newQuestion);
-
- showToast('题目重新生成成功!');
-
- // AI回复完成后,获取最新的历史记录
- await getHistoryRecordList();
-
- // 如果是新对话,将最新的历史记录设为激活状态
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
- }
- } else {
- throw new Error('解析新题目失败');
- }
- } else {
- throw new Error('AI接口调用失败');
- }
- } catch (error) {
- console.error('刷新题目失败:', error);
- showToast('重新生成题目失败,请重试');
- } finally {
- setTimeout(() => {
- const key = `${questionType}_${index}`;
- isRefreshing.value[key] = false;
- }, 1000);
- }
- };
- // 控制下载菜单显示/隐藏
- const toggleDownloadMenu = () => {
- if (!isGenerating.value) {
- showDownloadMenu.value = !showDownloadMenu.value;
- }
- };
- // 关闭下载菜单
- const closeDownloadMenu = () => {
- showDownloadMenu.value = false;
- };
- // 点击外部区域关闭下载菜单
- const handleClickOutside = (event) => {
- const dropdown = event.target.closest('.download-dropdown');
- if (!dropdown) {
- showDownloadMenu.value = false;
- }
- };
- // 导出Word(有答案)
- const exportToWordWithAnswers = async () => {
- try {
- closeDownloadMenu(); // 关闭下拉菜单
- isGenerating.value = true;
-
- console.log('开始导出Word格式试卷(有答案)...');
-
- // 使用PC端的模拟Word导出功能
- await simulateWordExport(true);
- } catch (error) {
- console.error('导出考试文件失败:', error);
- showToast('导出失败,请重试');
- } finally {
- isGenerating.value = false;
- }
- };
- // 导出Word(无答案)
- const exportToWordWithoutAnswers = async () => {
- try {
- closeDownloadMenu(); // 关闭下拉菜单
- isGenerating.value = true;
-
- console.log('开始导出Word格式试卷(无答案)...');
-
- // 使用PC端的模拟Word导出功能
- await simulateWordExport(false);
- } catch (error) {
- console.error('导出考试文件失败:', error);
- showToast('导出失败,请重试');
- } finally {
- isGenerating.value = false;
- }
- };
- // 导出文件为Word格式(保留原函数以兼容其他可能的调用)
- const exportToWord = async () => {
- // 默认导出有答案版本
- await exportToWordWithAnswers();
- };
- // 模拟Word导出功能(使用PC端实现)
- const simulateWordExport = async (includeAnswers = true) => {
- try {
- // 创建Word文档内容(使用HTML格式,兼容WPS和Word)
- const wordContent = createHTMLContent(currentExam.value, includeAnswers);
-
- // 创建Blob对象 - 使用HTML格式
- const blob = new Blob([wordContent], {
- type: 'application/msword'
- });
-
- // 下载文件
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- const fileName = includeAnswers
- ? `${currentExam.value.title}_有答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`
- : `${currentExam.value.title}_无答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`;
- link.setAttribute('href', url);
- link.setAttribute('download', fileName);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
-
- showToast(`导出成功${includeAnswers ? '(含答案)' : '(不含答案)'}`);
-
- } catch (error) {
- console.error('模拟Word导出失败:', error);
- showToast('Word导出失败,请稍后重试');
- }
- };
- // 创建HTML格式的Word文档内容(兼容WPS和Word)- 使用PC端实现
- const createHTMLContent = (examData, includeAnswers = true) => {
- const exam = currentExam.value;
-
- // HTML文档内容,使用Word兼容的格式
- let htmlContent = `<!DOCTYPE html>
- <html xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:w="urn:schemas-microsoft-com:office:word"
- xmlns="http://www.w3.org/TR/REC-html40">
- <head>
- <meta charset="utf-8">
- <meta name="ProgId" content="Word.Document">
- <meta name="Generator" content="Microsoft Word 15">
- <meta name="Originator" content="Microsoft Word 15">
- <title>${exam.title || '试卷'}</title>
- <!--[if gte mso 9]>
- <xml>
- <w:WordDocument>
- <w:View>Print</w:View>
- <w:Zoom>100</w:Zoom>
- <w:DoNotPromptForConvert/>
- <w:DoNotShowRevisions/>
- <w:DoNotPrintRevisions/>
- <w:DoNotShowComments/>
- <w:DoNotShowInsertionsAndDeletions/>
- <w:DoNotShowPropertyChanges/>
- <w:Compatibility>
- <w:BreakWrappedTables/>
- <w:SnapToGridInCell/>
- <w:WrapTextWithPunct/>
- <w:UseAsianBreakRules/>
- <w:DontGrowAutofit/>
- </w:Compatibility>
- </w:WordDocument>
- </xml>
- <![endif]-->
- <style>
- body {
- font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
- font-size: 14px;
- line-height: 1.6;
- margin: 24px;
- color: #000;
- }
- .header {
- text-align: center;
- margin-bottom: 14px;
- }
- .exam-title {
- font-size: 24px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- }
- .exam-info {
- font-size: 14px;
- color: #666;
- margin-bottom: 14px;
- }
- .section {
- margin-bottom: 14px;
- }
- .section-title {
- font-size: 18px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- border-bottom: 2px solid #3e7bfa;
- padding-bottom: 5px;
- }
- .question {
- margin-bottom: 14px;
- padding: 10px;
- background-color: #f9f9f9;
- border-left: 4px solid #3e7bfa;
- }
- .question-header {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- margin-bottom: 14px;
- }
- .question-number {
- font-weight: bold;
- color: #3e7bfa;
- flex-shrink: 0;
- }
- .question-text {
- flex: 1;
- }
- .options {
- margin-left: 12px;
- }
- .option {
- margin-bottom: 5px;
- }
- .answer {
- margin-top: 10px;
- padding: 8px;
- background: #e8f4fd;
- border-left: 3px solid #3e7bfa;
- font-weight: bold;
- color: #0066cc;
- }
- .outline-section {
- margin: 10px 0;
- padding: 8px;
- background: #f0f8ff;
- border-radius: 4px;
- }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="exam-title">${exam.title || '考试试卷'}</div>
- <div class="exam-info">
- 总分:${exam.totalScore || 0}分 | 总题数:${exam.totalQuestions || 0}题 | 生成时间:${currentTime.value}
- </div>
- </div>`;
- // 单选题
- if (exam.singleChoice && exam.singleChoice.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">一、单选题(${exam.singleChoice.count}题,每题${exam.singleChoice.scorePerQuestion}分,共${exam.singleChoice.totalScore}分)</div>`;
- exam.singleChoice.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text}</span>
- </div>
- <div class="options">`;
- question.options.forEach(option => {
- htmlContent += `
- <div class="option">${option.key}. ${option.text}</div>`;
- });
- htmlContent += `
- </div>
- ${includeAnswers ? `<div class="answer">正确答案:${question.selectedAnswer}</div>` : ''}
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- // 判断题
- if (exam.judge && exam.judge.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">二、判断题(${exam.judge.count}题,每题${exam.judge.scorePerQuestion}分,共${exam.judge.totalScore}分)</div>`;
- exam.judge.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text}</span>
- </div>
- ${includeAnswers ? `<div class="answer">正确答案:${question.selectedAnswer}</div>` : ''}
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- // 多选题
- if (exam.multiple && exam.multiple.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">三、多选题(${exam.multiple.count}题,每题${exam.multiple.scorePerQuestion}分,共${exam.multiple.totalScore}分)</div>`;
- exam.multiple.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text}</span>
- </div>
- <div class="options">`;
- question.options.forEach(option => {
- htmlContent += `
- <div class="option">${option.key}. ${option.text}</div>`;
- });
- htmlContent += `
- </div>
- ${includeAnswers ? `<div class="answer">正确答案:${(question.selectedAnswers || []).join(', ')}</div>` : ''}
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- // 简答题
- if (exam.short && exam.short.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">四、简答题(${exam.short.count}题,每题${exam.short.scorePerQuestion}分,共${exam.short.totalScore}分)</div>`;
- exam.short.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text}</span>
- </div>`;
-
- if (question.outline && includeAnswers) {
- htmlContent += `
- <div class="outline-section">
- <strong>关键要点:</strong>${cleanText(question.outline.keyFactors)}
- </div>
- <div class="outline-section">
- <strong>具体措施:</strong>${cleanText(question.outline.measures)}
- </div>`;
- }
- htmlContent += `
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- htmlContent += `
- </body>
- </html>`;
- return htmlContent;
- };
- // 创建Word格式的考试文档内容(保留原函数以防其他地方使用)
- const createExamWordContent = (examData) => {
- const currentTime = new Date().toLocaleString('zh-CN');
- let htmlContent = `<!DOCTYPE html>
- <html xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:w="urn:schemas-microsoft-com:office:word"
- xmlns="http://www.w3.org/TR/REC-html40">
- <head>
- <meta charset="utf-8">
- <meta name="ProgId" content="Word.Document">
- <meta name="Generator" content="Microsoft Word">
- <meta name="Originator" content="Microsoft Word">
- <meta name="ViewMode" content="PrintLayout">
- <meta name="Zoom" content="100">
- <meta name="DocumentProperties" content="false">
- <meta name="DocumentSecurity" content="false">
- <meta name="DocumentProtection" content="false">
- <meta name="DocumentView" content="PrintLayout">
- <title>${examData.title}</title>
- <style>
- body { font-family: '微软雅黑', 'Microsoft YaHei', sans-serif; line-height: 1.6; margin: 20px; }
- .header { text-align: center; margin-bottom: 30px; }
- .exam-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
- .exam-info { font-size: 14px; color: #666; margin-bottom: 30px; }
- .section { margin-bottom: 25px; }
- .section-title { font-size: 16px; font-weight: bold; margin-bottom: 15px; }
- .question { margin-bottom: 20px; padding: 10px; }
- .question-header { font-weight: bold; margin-bottom: 10px; }
- .option { margin: 5px 0; }
- .answer { color: #0066cc; font-weight: bold; margin-top: 10px; }
- .outline-section { margin: 10px 0; }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="exam-title">${examData.title || '考试试卷'}</div>
- <div class="exam-info">
- 总分:${examData.totalScore || 0}分 | 总题数:${examData.totalQuestions || 0}题 | 生成时间:${currentTime}
- </div>
- </div>`;
- // 单选题
- if (examData.singleChoice && examData.singleChoice.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">一、单选题(${examData.singleChoice.count}题,每题${examData.singleChoice.scorePerQuestion}分,共${examData.singleChoice.totalScore}分)</div>`;
- examData.singleChoice.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>
- <div class="options">`;
- question.options.forEach(option => {
- htmlContent += `
- <div class="option">${option.key}. ${option.text}</div>`;
- });
- htmlContent += `
- </div>
- <div class="answer">正确答案:${question.selectedAnswer} </div>
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- // 判断题
- if (examData.judge && examData.judge.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">二、判断题(${examData.judge.count}题,每题${examData.judge.scorePerQuestion}分,共${examData.judge.totalScore}分)</div>`;
- examData.judge.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>
- <div class="answer">正确答案:${question.selectedAnswer} </div>
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- // 多选题
- if (examData.multiple && examData.multiple.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">三、多选题(${examData.multiple.count}题,每题${examData.multiple.scorePerQuestion}分,共${examData.multiple.totalScore}分)</div>`;
- examData.multiple.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>
- <div class="options">`;
- question.options.forEach(option => {
- htmlContent += `
- <div class="option">${option.key}. ${option.text}</div>`;
- });
- htmlContent += `
- </div>
- <div class="answer">正确答案:${(question.selectedAnswers || []).join(', ')} </div>
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- // 简答题
- if (examData.short && examData.short.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">四、简答题(${examData.short.count}题,每题${examData.short.scorePerQuestion}分,共${examData.short.totalScore}分)</div>`;
- examData.short.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span> ${question.text}
- </div>`;
-
- if (question.outline) {
- htmlContent += `
- <div class="outline-section">
- <strong>关键要点:</strong>${question.outline.keyFactors}
- </div>
- <div class="outline-section">
- <strong>具体措施:</strong>${question.outline.measures}
- </div>`;
- }
- htmlContent += `
- </div>`;
- });
- htmlContent += `
- </div>`;
- }
- htmlContent += `
- </body>
- </html>`;
- return htmlContent;
- };
- // 文件上传相关方法
- const triggerFileUpload = () => {
- fileInput.value.click();
- };
- const handleFileSelect = async (event) => {
- const file = event.target.files[0];
- if (!file) return;
- // 检查文件大小(20MB限制)
- const maxSize = 20 * 1024 * 1024; // 20MB
- if (file.size > maxSize) {
- console.error('文件大小不能超过20MB');
- return;
- }
- // 检查文件类型
- const allowedTypes = ['application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'];
- if (!allowedTypes.includes(file.type)) {
- console.error('请选择PPT文件(.ppt/.pptx)');
- return;
- }
- try {
- selectedFile.value = {
- name: file.name,
- size: file.size,
- icon: '📋'
- };
- // 读取PPT内容
- const pptContent = await readPPTFile(file);
- pptContentDescription.value = pptContent;
- // 自动更新试卷名称(使用文件名,去掉扩展名)
- const fileName = file.name.replace(/\.[^/.]+$/, '');
- examName.value = fileName + '培训考核';
- console.log('✅ PPT文件上传成功:', file.name);
- } catch (error) {
- console.error('PPT文件处理失败:', error);
- selectedFile.value = null;
- }
- };
- const removeSelectedFile = () => {
- selectedFile.value = null;
- pptContentDescription.value = '';
- fileInput.value.value = '';
-
- // 恢复默认试卷名称
- const projectTypeName = projectTypes[selectedProjectType.value].name;
- examName.value = projectTypeName + '工程施工技术考核';
- };
- // 读取PPT文件的文本内容
- const readPPTFile = async (file) => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
-
- reader.onload = async (e) => {
- try {
- console.log('开始解析PPT文件...');
-
- // 这里使用JSZip来解压PPTX文件并提取文本
- // 注意:这是一个简化版本的实现
- const arrayBuffer = e.target.result;
-
- // 提取第一页的文本内容作为示例
- const content = await extractTextFromPPT(arrayBuffer);
-
- resolve(content);
- } catch (error) {
- console.error('PPT解析失败:', error);
- reject(error);
- }
- };
-
- reader.onerror = () => {
- reject(new Error('文件读取失败'));
- };
-
- reader.readAsArrayBuffer(file);
- });
- };
- // 从PPT提取文本(简化版本)
- const extractTextFromPPT = async (arrayBuffer) => {
- // 这是一个简化的实现,实际项目中需要使用专门的PPT解析库
- return "提取的文本内容:" +
- "PPT培训课件主要包含以下内容:" +
- "1. 安全培训概述" +
- "- 培训目标和意义" +
- "- 培训对象和要求" +
- "- 培训计划和安排" +
- "2. 基础知识" +
- "- 安全规章制度" +
- "- 危险源识别" +
- "- 应急处理方法" +
- "3. 操作技能" +
- "- 标准化操作流程" +
- "- 安全操作规范" +
- "- 事故预防措施" +
- "4. 考核要求" +
- "- 理论知识考核" +
- "- 实操技能考核" +
- "- 综合评估标准" +
- "此PPT内容涵盖了安全培训的各个方面,适合制作综合性的考试题目。";
- };
- // 格式化文件大小
- const formatFileSize = (bytes) => {
- if (bytes === 0) return '0 B';
-
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
-
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- };
- // 清理文本中的中括号和双引号
- const cleanText = (text) => {
- if (!text) return '';
- return text.toString()
- .replace(/[\[\]]/g, '') // 移除中括号
- .replace(/[""]/g, '') // 移除双引号
- .replace(/['']/g, '') // 移除单引号
- .trim();
- };
- // ============ PC端辅助函数(用于刷新题目) ============
- // 构建单题重新生成的提示词
- const buildSingleQuestionPrompt = (sectionType, questionIndex) => {
- const projectType = projectTypes[selectedProjectType.value].name;
- const questionType = getQuestionTypeName(sectionType);
- const scorePerQuestion = getQuestionScore(sectionType);
-
- // 获取当前题目作为参考
- const currentQuestion = getCurrentQuestion(sectionType, questionIndex);
-
- let prompt = `请基于以下${projectType}工程的${questionType}题目,重新生成一道相似主题的题目,要求如下:
- 当前题目参考:
- ${JSON.stringify(currentQuestion, null, 2)}
- 题目类型:${questionType}
- 每题分值:${scorePerQuestion}分
- 题目序号:第${questionIndex + 1}题
- 请严格按照以下JSON格式返回,不要包含任何其他文字:
- `;
- // 根据题目类型添加不同的格式要求
- if (sectionType === 'single') {
- prompt += `{
- "text": "题目内容",
- "options": [
- {"key": "A", "text": "选项A"},
- {"key": "B", "text": "选项B"},
- {"key": "C", "text": "选项C"},
- {"key": "D", "text": "选项D"}
- ],
- "selectedAnswer": "A"
- }`;
- } else if (sectionType === 'judge') {
- prompt += `{
- "text": "题目内容",
- "selectedAnswer": "正确"
- }`;
- } else if (sectionType === 'multiple') {
- prompt += `{
- "text": "题目内容",
- "options": [
- {"key": "A", "text": "选项A"},
- {"key": "B", "text": "选项B"},
- {"key": "C", "text": "选项C"},
- {"key": "D", "text": "选项D"}
- ],
- "selectedAnswers": ["A", "B"]
- }`;
- } else if (sectionType === 'short') {
- prompt += `{
- "text": "题目内容",
- "outline": {
- "keyFactors": "关键要点内容",
- "measures": "具体措施内容"
- }
- }`;
- }
- return prompt;
- };
- // 获取题目类型名称
- const getQuestionTypeName = (sectionType) => {
- const typeMap = {
- 'single': '单选题',
- 'judge': '判断题',
- 'multiple': '多选题',
- 'short': '简答题'
- };
- return typeMap[sectionType] || sectionType;
- };
- // 获取题目分值
- const getQuestionScore = (sectionType) => {
- if (currentExam.value[sectionType]) {
- return currentExam.value[sectionType].scorePerQuestion;
- }
- return 5; // 默认分值
- };
- // 获取当前题目
- const getCurrentQuestion = (sectionType, questionIndex) => {
- if (currentExam.value[sectionType] && currentExam.value[sectionType].questions) {
- return currentExam.value[sectionType].questions[questionIndex];
- }
- return null;
- };
- // 解析单题AI回复
- const parseSingleQuestionResponse = (aiReply, sectionType) => {
- try {
- console.log('AI回复内容:', aiReply);
- console.log('题目类型:', sectionType);
-
- // 尝试提取JSON内容
- const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const questionData = JSON.parse(jsonMatch[0]);
- console.log('解析后的题目数据:', questionData);
-
- // 如果是简答题,检查keyFactors字段
- if (sectionType === 'short' && questionData.outline && questionData.outline.keyFactors) {
- console.log('简答题keyFactors原始值:', questionData.outline.keyFactors);
-
- // 如果keyFactors是数组,转换为字符串
- if (Array.isArray(questionData.outline.keyFactors)) {
- questionData.outline.keyFactors = questionData.outline.keyFactors.join(' ');
- console.log('转换后的keyFactors:', questionData.outline.keyFactors);
- }
- }
-
- return questionData;
- } else {
- console.error('未找到有效的JSON数据');
- return null;
- }
- } catch (error) {
- console.error('解析AI回复失败:', error);
- return null;
- }
- };
- // 更新题目
- const updateQuestion = (sectionType, questionIndex, newQuestion) => {
- let updatedQuestion;
-
- if (sectionType === 'single') {
- updatedQuestion = { ...newQuestion };
- if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
- updatedQuestion.selectedAnswer = updatedQuestion.options && updatedQuestion.options.length > 0 ? updatedQuestion.options[0].key : 'A';
- }
- currentExam.value.singleChoice.questions[questionIndex] = updatedQuestion;
- } else if (sectionType === 'judge') {
- updatedQuestion = { ...newQuestion };
- if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
- updatedQuestion.selectedAnswer = Math.random() > 0.5 ? '正确' : '错误';
- }
- currentExam.value.judge.questions[questionIndex] = updatedQuestion;
- } else if (sectionType === 'multiple') {
- updatedQuestion = { ...newQuestion };
- if (!updatedQuestion.selectedAnswers || !Array.isArray(updatedQuestion.selectedAnswers)) {
- updatedQuestion.selectedAnswers = updatedQuestion.options && updatedQuestion.options.length > 1 ? [updatedQuestion.options[0].key, updatedQuestion.options[1].key] : [];
- }
- currentExam.value.multiple.questions[questionIndex] = updatedQuestion;
- } else if (sectionType === 'short') {
- updatedQuestion = { ...newQuestion };
- if (!updatedQuestion.outline) {
- updatedQuestion.outline = {
- keyFactors: "请参考相关教材和标准规范",
- measures: "请结合实际工程案例进行解答"
- };
- }
- currentExam.value.short.questions[questionIndex] = updatedQuestion;
- }
-
- console.log(`更新${sectionType}第${questionIndex + 1}题:`, updatedQuestion);
- };
- // 保存到reModifyQuestion接口
- const saveToReModifyQuestion = async (sectionType, questionIndex, newQuestion) => {
- console.log('对话id', ai_conversation_id.value);
- try {
- // 使用当前保存的对话ID
- if (!ai_conversation_id.value) {
- console.warn('没有找到对话ID,跳过保存');
- return;
- }
-
- // 构建要保存的内容 - 保存整个试卷的JSON字符串
- const content = JSON.stringify(currentExam.value);
-
- console.log('保存到 /re_modify_question 的内容:', content);
-
- // 调用后端接口保存修改
- const response = await apis.reModifyQuestion({
- ai_conversation_id: ai_conversation_id.value,
- content: content
- });
-
- if (response.statusCode === 200) {
- console.log('修改已保存到后端');
- } else {
- console.error('保存到后端失败:', response);
- }
- } catch (error) {
- console.error('保存到后端失败:', error);
- }
- };
- // 页面加载时不再自动加载历史记录,改为点击菜单时加载
- onMounted(async () => {
- try {
- console.log('🚀 移动端考试工坊页面初始化完成')
-
- // 初始化原生导航栏(子页面模式:返回按钮执行路由后退)
- initNativeNavForSubPage(() => router.back())
- } catch (error) {
- console.error('❌ 移动端考试工坊页面初始化失败:', error)
- }
- })
- // 监听历史记录抽屉显示状态,显示时加载数据
- watch(showHistory, async (newVal) => {
- if (newVal) {
- console.log('📋 历史记录抽屉打开,开始刷新数据...')
- await getHistoryRecordList()
- }
- })
- </script>
- <style lang="less" scoped>
- .mobile-exam-workshop {
- min-height: 100vh;
- background: #EBF3FF;
- font-family: "Alibaba PuHuiTi 3.0", sans-serif;
- }
- .mobile-content {
- padding: 16px;
- position: relative;
- max-height: calc(100vh - 60px);
- overflow-y: auto;
- }
- /* 考试工坊主界面样式 */
- .exam-workshop-main {
- .config-section {
- background: white;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
- .config-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 16px;
- .step-number {
- width: 28px;
- height: 28px;
- background: #3e7bfa;
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- font-weight: 600;
- }
- h3 {
- font-size: 20px;
- font-weight: 600;
- color: #1f2937;
- margin: 0;
- }
- }
- .type-cards {
- .type-cards-row {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- .type-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- padding: 16px 12px;
- border: 2px solid #e5e7eb;
- border-radius: 12px;
- background: white;
- cursor: pointer;
- transition: all 0.3s ease;
- min-height: 80px;
- justify-content: center;
- &:hover {
- border-color: #3e7bfa;
- transform: translateY(-2px);
- }
- &.active {
- border-color: #3e7bfa;
- background: rgba(62, 123, 250, 0.1);
- .type-icon {
- filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
- saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
- }
- span {
- color: #3e7bfa;
- }
- }
- .type-icon {
- width: 32px;
- height: 32px;
- transition: filter 0.3s ease;
- }
- span {
- font-size: 12px;
- color: #374151;
- font-weight: 500;
- transition: color 0.3s ease;
- text-align: center;
- }
- }
- }
- }
- .generation-methods {
- display: flex;
- flex-direction: column;
- gap: 12px;
- .method-card {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 16px;
- border: 2px solid #e5e7eb;
- border-radius: 12px;
- background: white;
- cursor: pointer;
- transition: all 0.3s ease;
- position: relative;
- &:hover {
- border-color: #3e7bfa;
- }
- &.active {
- border-color: #3e7bfa;
- background: rgba(62, 123, 250, 0.1);
- .method-icon {
- filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
- saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
- }
- .method-content h4 {
- color: #3e7bfa;
- }
- }
- .method-icon {
- width: 24px;
- height: 24px;
- flex-shrink: 0;
- transition: filter 0.3s ease;
- margin-top: 2px;
- }
- .method-content {
- flex: 1;
-
- h4 {
- font-size: 14px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 4px 0;
- transition: color 0.3s ease;
- }
-
- p {
- font-size: 12px;
- color: #6b7280;
- margin: 0;
- line-height: 1.4;
- }
- .ppt-file-preview {
- margin-top: 8px;
-
- .file-preview {
- display: flex;
- align-items: center;
- background: #f9fafb;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- padding: 8px;
-
- .file-icon {
- font-size: 16px;
- margin-right: 8px;
- width: 20px;
- text-align: center;
- }
-
- .file-info {
- flex: 1;
-
- .file-name {
- font-size: 11px;
- font-weight: 500;
- color: #374151;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 120px;
- }
-
- .file-size {
- font-size: 10px;
- color: #9ca3af;
- }
- }
-
- .remove-file-btn {
- background: none;
- border: none;
- cursor: pointer;
- color: #6b7280;
- padding: 2px;
-
- .remove-icon {
- font-size: 14px;
- }
-
- &:hover {
- color: #ef4444;
- }
- }
- }
- }
- }
- }
- }
- .exam-config-container {
- .config-main {
- .config-form {
- display: grid;
- grid-template-columns: 2fr 1fr;
- gap: 12px;
- margin-bottom: 16px;
- .form-group {
- label {
- display: block;
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 6px;
- }
- .input-wrapper,
- .score-input {
- position: relative;
- .config-input {
- width: 100%;
- height: 48px;
- padding: 12px 50px 12px 16px;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- font-size: 16px !important;
- background: white;
- box-sizing: border-box;
- line-height: 1.5;
- &:focus {
- outline: none;
- border-color: #3e7bfa;
- box-shadow: 0 0 0 2px rgba(62, 123, 250, 0.1);
- }
- &:disabled {
- background: #f9fafb;
- color: #9ca3af;
- }
- }
- .char-count {
- position: absolute;
- right: 12px;
- top: 50%;
- transform: translateY(-50%);
- font-size: 12px;
- color: #6b7280;
- font-weight: 500;
- &.warning {
- color: #f59e0b;
- }
- }
- .unit {
- position: absolute;
- right: 12px;
- top: 50%;
- transform: translateY(-50%);
- color: #6b7280;
- font-size: 18px;
- font-weight: 500;
- }
- }
- .score-input {
- .config-input {
- padding-right: 50px;
- }
- }
- }
- }
- .question-types-title {
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- margin: 20px 0 12px 0;
- }
- .question-types {
- .question-type {
- background: #f9fafb;
- border-radius: 8px;
- padding: 12px;
- margin-bottom: 8px;
- .type-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- .type-name {
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- white-space: nowrap;
- }
- .progress-bar {
- flex: 1;
- height: 4px;
- background: #e5e7eb;
- border-radius: 2px;
- overflow: hidden;
- .progress-fill {
- height: 100%;
- background: #3e7bfa;
- transition: width 0.3s ease;
- }
- }
- }
- .score-config {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 8px;
- .config-item {
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 13px;
- span {
- color: #6b7280;
- white-space: nowrap;
- }
- .score-input-field,
- .count-input-field {
- width: 50px;
- padding: 6px 8px;
- border: 1px solid #d1d5db;
- border-radius: 4px;
- font-size: 13px;
- text-align: center;
- &:focus {
- outline: none;
- border-color: #3e7bfa;
- }
- &:disabled {
- background: #f3f4f6;
- color: #9ca3af;
- }
- }
- .count-stepper {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .stepper-buttons {
- display: flex;
- flex-direction: column;
- gap: 3px;
- }
- .stepper-btn {
- width: 10px;
- height: 7px;
- padding: 0;
- border: none;
- outline: none;
- appearance: none;
- -webkit-appearance: none;
- background: #3e7bfa;
- display: block;
- cursor: pointer;
- transition: opacity 0.2s ease, transform 0.2s ease;
- &:disabled {
- opacity: 0.35;
- cursor: not-allowed;
- }
- }
- .stepper-btn-up {
- clip-path: polygon(50% 0, 0 100%, 100% 100%);
- }
- .stepper-btn-down {
- clip-path: polygon(0 0, 100% 0, 50% 100%);
- }
- }
- }
- }
- }
- }
- .preview-panel {
- background: #f8fafc;
- border-radius: 8px;
- padding: 16px;
- margin-top: 16px;
- border: 1px solid #e5e7eb;
- .preview-header {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 12px;
- .preview-icon {
- width: 16px;
- height: 16px;
- }
- h3 {
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- margin: 0;
- }
- }
- .preview-content {
- .preview-title {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 12px 0;
- line-height: 1.4;
- }
- .question-breakdown {
- .breakdown-item {
- margin-bottom: 6px;
- .breakdown-row {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- font-size: 13px;
- .breakdown-left {
- color: #4b5563;
- flex: 1;
- margin-right: 8px;
- line-height: 1.3;
- }
- .breakdown-right {
- color: #6b7280;
- white-space: nowrap;
- }
- }
- }
- }
- .divider {
- height: 1px;
- background: #e5e7eb;
- margin: 8px 0;
- }
- .calculated-score-row,
- .total-score-row {
- display: flex;
- justify-content: space-between;
- margin-bottom: 4px;
- font-size: 13px;
- .calculated-label,
- .total-label {
- color: #6b7280;
- }
- .calculated-value,
- .total-value {
- color: #1f2937;
- font-weight: 500;
- }
- }
- }
- }
- }
- .bottom-actions {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 24px;
- margin-top: 20px;
- width: 100%;
- .clear-btn {
- height: 34px;
- padding: 0 16px;
- border: 1px solid #d1d5db;
- background: white;
- color: #6b7280;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- &:disabled {
- cursor: not-allowed !important;
- pointer-events: none;
- opacity: 0.5;
- }
- &:hover:not(:disabled) {
- background: #f9fafb;
- border-color: #9ca3af;
- }
- }
- .generate-btn {
- height: 34px;
- padding: 0;
- border: none;
- background: none;
- cursor: pointer;
- transition: opacity 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- &:disabled {
- background: #f3f4f6;
- color: #9ca3af;
- cursor: not-allowed;
- }
- .generate-icon {
- width: 107px;
- height: 34px;
- }
- .generating-text {
- width: 107px;
- height: 34px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- background: #f3f4f6;
- border-radius: 6px;
- border: 1px solid #d1d5db;
- }
- &.disabled .generate-icon {
- opacity: 0.5;
- }
- }
- }
- }
- }
- /* 考试详情页样式 */
- .exam-detail-main {
- .detail-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 0;
- margin-bottom: 16px;
- .back-btn {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 12px;
- border-radius: 8px;
- font-size: 13px;
- cursor: pointer;
- border: 1px solid #d1d5db;
- background: white;
- color: #374151;
- transition: all 0.3s ease;
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- &:hover:not(:disabled) {
- background: #f9fafb;
- border-color: #9ca3af;
- }
- .back-arrow {
- font-size: 14px;
- }
- }
- .download-dropdown {
- position: relative;
- display: inline-block;
- &.disabled {
- opacity: 0.5;
- cursor: not-allowed;
- pointer-events: none;
- }
- .download-btn {
- height: 34px;
- padding: 0;
- border: none;
- background: transparent;
- cursor: pointer;
- transition: opacity 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed !important;
- pointer-events: none;
- }
- &:hover:not(:disabled) {
- opacity: 0.8;
- }
- .download-icon {
- width: 107px;
- height: 34px;
- }
- }
- .dropdown-menu {
- position: absolute;
- top: 100%;
- right: 0;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- min-width: 120px;
- opacity: 0;
- visibility: hidden;
- transform: translateY(-10px);
- transition: all 0.3s ease;
- .dropdown-item {
- padding: 12px 16px;
- cursor: pointer;
- transition: background-color 0.2s ease;
- border-bottom: 1px solid #f3f4f6;
- &:last-child {
- border-bottom: none;
- }
- &:hover {
- background: #f8fafc;
- }
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- pointer-events: none;
- }
- .item-text {
- font-size: 14px;
- color: #374151;
- }
- }
- }
- &.show .dropdown-menu {
- opacity: 1;
- visibility: visible;
- transform: translateY(0);
- }
- }
- }
- .exam-info {
- background: white;
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
- .exam-title {
- font-size: 20px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 12px 0;
- line-height: 1.4;
- }
- .exam-stats {
- display: flex;
- gap: 16px;
- margin-bottom: 8px;
- .total-score,
- .question-count {
- font-size: 15px;
- color: #6b7280;
- font-weight: 500;
- }
- }
- .generation-time {
- font-size: 14px;
- color: #9ca3af;
- }
- }
- .question-sections {
- .question-section {
- background: white;
- border-radius: 12px;
- margin-bottom: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
- overflow: hidden;
- .section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 20px;
- background: #f8fafc;
- border-bottom: 1px solid #e5e7eb;
- .section-title {
- flex: 1;
- .section-number {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- }
- .section-name {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 8px;
- }
- .section-score {
- font-size: 14px;
- color: #6b7280;
- }
- }
- .section-controls {
- display: flex;
- align-items: center;
- gap: 8px;
- .question-count-text {
- font-size: 14px;
- color: #6b7280;
- font-weight: 500;
- }
- .toggle-icon {
- width: 16px;
- height: 16px;
- transition: transform 0.3s ease;
- &.expanded {
- transform: rotate(180deg);
- }
- }
- }
- }
- .section-content {
- padding: 0;
- .question-item {
- border-bottom: 1px solid #f3f4f6;
- padding: 16px 20px;
- &:last-child {
- border-bottom: none;
- }
- .question-header {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- margin-bottom: 12px;
- .question-number {
- font-size: 16px;
- font-weight: 600;
- color: #3e7bfa;
- flex-shrink: 0;
- }
- .question-text {
- flex: 1;
- font-size: 16px;
- line-height: 1.5;
- color: #374151;
- font-weight: 500;
- }
- .refresh-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px;
- color: #6b7280;
- flex-shrink: 0;
- &:disabled {
- cursor: not-allowed;
- opacity: 0.5;
- }
- &:hover:not(:disabled) {
- color: #3e7bfa;
- }
- .refresh-icon {
- width: 16px;
- height: 16px;
- transition: transform 0.3s ease;
- &.rotating {
- transform: rotate(360deg);
- }
- }
- }
- }
- .options {
- margin-bottom: 12px;
- .option {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- margin-bottom: 8px;
- .radio-wrapper {
- flex-shrink: 0;
- margin-top: 2px;
- .radio-circle {
- width: 14px;
- height: 14px;
- border: 1.5px solid #d1d5db;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- &.selected {
- border-color: #3e7bfa;
- background: #3e7bfa;
- .radio-dot {
- width: 6px;
- height: 6px;
- background: white;
- border-radius: 50%;
- }
- }
- }
- }
- .option-key {
- font-size: 15px;
- font-weight: 600;
- color: #374151;
- flex-shrink: 0;
- }
- .option-content {
- flex: 1;
- .option-text {
- font-size: 15px;
- line-height: 1.4;
- color: #374151;
- font-weight: 500;
- }
- }
- }
- }
- .answer-section {
- padding: 8px 12px;
- background: #f8fafc;
- border-radius: 6px;
- display: flex;
- align-items: center;
- gap: 8px;
- .answer-label {
- font-size: 14px;
- color: #6b7280;
- font-weight: 600;
- }
- .answer-value {
- font-size: 14px;
- color: #1f2937;
- font-weight: 600;
- }
- }
- .answer-outline {
- .outline-section {
- margin-bottom: 12px;
- padding: 12px;
- background: #f8fafc;
- border-radius: 6px;
- font-size: 14px;
- line-height: 1.5;
- strong {
- color: #374151;
- display: block;
- margin-bottom: 4px;
- font-weight: 600;
- }
- color: #6b7280;
- }
- }
- }
- }
- }
- }
- }
- </style>
|