| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010 |
- <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>
- <input
- v-model="type.questionCount"
- type="number"
- class="count-input-field"
- min="1"
- max="99"
- @input="validateQuestionCount(type)"
- :disabled="isGenerating || selectedFile"
- />
- <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'
- // ===== 已删除: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 (historyItem.isActive) return
-
- console.log("点击移动端考试工坊历史记录:", historyItem)
-
- // 设置当前点击的历史记录为激活状态
- historyData.value.forEach((item) => {
- item.isActive = item.id === historyItem.id
- })
-
- // 关闭历史记录抽屉
- showHistory.value = false
-
- // 解析历史试卷数据
- ai_conversation_id.value = historyItem.id
- currentTime.value = historyItem.time
-
- // 如果有原始数据,尝试解析
- if (historyItem.rawData && historyItem.rawData.content) {
- try {
- const examData = JSON.parse(historyItem.rawData.content)
- currentExam.value = examData
- showExamDetail.value = true
- } catch (error) {
- console.error('解析历史试卷数据失败:', error)
- // 如果解析失败,显示默认详情页
- showExamDetail.value = true
- }
- } else {
- // 如果没有内容,显示默认详情页
- showExamDetail.value = true
- }
- }
- // 删除历史记录
- 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 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) {
- // 设置历史记录总数
- 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)
- }
- } 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 < 1) {
- type.questionCount = 1;
- }
- };
- // 清除设置
- 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 parseAIExamResponse = (aiReply) => {
- try {
- // 尝试提取JSON内容
- const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const examData = JSON.parse(jsonMatch[0]);
- // 确保所有题目都有正确的初始值
- ensureQuestionInitialValues(examData);
- return examData;
- } else {
- throw new Error('未找到有效的JSON数据');
- }
- } catch (error) {
- console.error('解析AI回复失败:', error);
- // 返回默认试卷结构
- return generateDefaultExam();
- }
- };
- // 确保题目初始值正确
- 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 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('🚀 移动端考试工坊页面初始化完成')
- } catch (error) {
- console.error('❌ 移动端考试工坊页面初始化失败:', error)
- }
- })
- // 监听历史记录抽屉显示状态,显示时加载数据
- watch(showHistory, async (newVal) => {
- if (newVal && historyData.value.length === 0) {
- 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;
- }
- }
- }
- }
- }
- }
- }
- .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>
|