| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218 |
- <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, useRoute } 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 route = useRoute()
- 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({
- 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 getHistoryRecordList = async () => {
- try {
- console.log('📋 开始获取移动端考试工坊历史记录列表...')
- isLoadingHistory.value = true
- const startTime = performance.now()
-
- const response = await apis.getHistoryRecord({
- ai_conversation_id: 0,
- business_type: 3
- })
-
- const endTime = performance.now()
- console.log('📋 移动端考试工坊历史记录API调用耗时: ' + (endTime - startTime).toFixed(2) + 'ms')
- console.log('📋 移动端历史记录列表响应:', response)
-
- if (response.statusCode === 200) {
- historyTotal.value = response.total || 0
-
- historyData.value = response.data.map(conversation => ({
- id: conversation.id,
- title: generateConversationTitle(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)
- }
- } 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;
- 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
- currentTime.value = exam.generation_time || currentTime.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())
-
- // 检查URL参数是否有historyId需要加载
- const historyId = route.query.historyId
- if (historyId) {
- // 需要先加载历史记录列表才能找到对应项
- await getHistoryRecordList()
- const targetItem = historyData.value.find(item => String(item.id) === String(historyId))
- if (targetItem) {
- await handleHistoryItem(targetItem)
- }
- }
- } 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>
|