m-ExamWorkshop.vue 89 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010
  1. <template>
  2. <div class="mobile-exam-workshop">
  3. <!-- 移动端考试工坊页面 -->
  4. <MobileHeader title="考试工坊" @back="goBack" @menu="showHistoryDrawer" />
  5. <div class="mobile-content">
  6. <!-- 通用历史记录抽屉 -->
  7. <MobileHistoryDrawer
  8. :visible="!isGenerating && showHistory"
  9. title="历史记录"
  10. :historyData="historyData"
  11. :loading="isLoadingHistory"
  12. @close="showHistory = false"
  13. @createNewTask="createNewTask"
  14. @handleHistoryItem="handleHistoryItem"
  15. @deleteHistoryItem="deleteHistoryItem"
  16. />
  17. <!-- 移动端Toast提示组件 -->
  18. <MobileToast :visible="toastVisible" :message="toastMessage" @close="closeToast" />
  19. <!-- 主界面:考试工坊配置 -->
  20. <div v-if="!showExamDetail" class="exam-workshop-main">
  21. <!-- 试卷类型选择 -->
  22. <div class="config-section">
  23. <div class="config-header">
  24. <div class="step-number">1</div>
  25. <h3>选择试卷类型</h3>
  26. </div>
  27. <div class="type-cards">
  28. <div class="type-cards-row">
  29. <div
  30. v-for="(type, key) in projectTypes"
  31. :key="key"
  32. :class="['type-card', { active: selectedProjectType === key }]"
  33. @click="(isGenerating || selectedFile) ? null : selectProjectType(key)"
  34. :style="{ cursor: (isGenerating || selectedFile) ? 'not-allowed' : 'pointer', opacity: (isGenerating || selectedFile) ? '0.5' : '1' }"
  35. >
  36. <img :src="type.icon" :alt="type.name" class="type-icon" />
  37. <span>{{ type.name }}</span>
  38. </div>
  39. </div>
  40. </div>
  41. </div>
  42. <!-- 生成方式选择 -->
  43. <div class="config-section">
  44. <div class="config-header">
  45. <div class="step-number">2</div>
  46. <h3>选择生成方式</h3>
  47. </div>
  48. <div class="generation-methods">
  49. <div
  50. :class="['method-card', { active: selectedFunction === 'ai' }]"
  51. @click="(isGenerating || selectedFile) ? null : selectFunction('ai')"
  52. :style="{ cursor: (isGenerating || selectedFile) ? 'not-allowed' : 'pointer', opacity: (isGenerating || selectedFile) ? '0.5' : '1' }"
  53. >
  54. <img :src="aiIcon" alt="智能生成试卷" class="method-icon" />
  55. <div class="method-content">
  56. <h4>智能生成试卷</h4>
  57. <p>基于AI技术,根据所选类型自动生成完整试卷</p>
  58. </div>
  59. </div>
  60. <!-- PPT生成考题选项已隐藏 -->
  61. </div>
  62. </div>
  63. <!-- 试卷配置 -->
  64. <div class="config-section">
  65. <div class="config-header">
  66. <div class="step-number">3</div>
  67. <h3>试卷配置</h3>
  68. </div>
  69. <div class="exam-config-container">
  70. <div class="config-main">
  71. <div class="config-form">
  72. <div class="form-group">
  73. <label>试卷名称</label>
  74. <div class="input-wrapper">
  75. <input
  76. v-model="examName"
  77. type="text"
  78. placeholder="请输入试卷名称"
  79. class="config-input"
  80. maxlength="32"
  81. @input="validateExamName"
  82. :disabled="isGenerating || selectedFile"
  83. />
  84. <span class="char-count" :class="{ 'warning': examName.length >= 18 }">{{ examName.length }}/32</span>
  85. </div>
  86. </div>
  87. <div class="form-group">
  88. <label>试卷总分</label>
  89. <div class="score-input">
  90. <input
  91. v-model="totalScore"
  92. type="number"
  93. class="config-input"
  94. min="1"
  95. max="1000"
  96. @input="validateTotalScore"
  97. :disabled="isGenerating || selectedFile"
  98. />
  99. <span class="unit">分</span>
  100. </div>
  101. </div>
  102. </div>
  103. <!-- 题型配置 -->
  104. <div class="question-types-title">题型选择与分数分配</div>
  105. <div class="question-types">
  106. <div
  107. class="question-type"
  108. v-for="(type, index) in questionTypes"
  109. :key="index"
  110. >
  111. <div class="type-header">
  112. <span class="type-name">{{ type.name }}</span>
  113. <div class="progress-bar">
  114. <div class="progress-fill" :style="{ width: ((type.scorePerQuestion * type.questionCount) / totalScore) * 100 + '%' }"></div>
  115. </div>
  116. </div>
  117. <div class="score-config">
  118. <div class="config-item">
  119. <span>每题</span>
  120. <input
  121. v-model="type.scorePerQuestion"
  122. type="number"
  123. class="score-input-field"
  124. min="1"
  125. max="99"
  126. @input="validateScorePerQuestion(type)"
  127. :disabled="isGenerating || selectedFile"
  128. />
  129. <span>分</span>
  130. </div>
  131. <div class="config-item">
  132. <span>一共</span>
  133. <input
  134. v-model="type.questionCount"
  135. type="number"
  136. class="count-input-field"
  137. min="1"
  138. max="99"
  139. @input="validateQuestionCount(type)"
  140. :disabled="isGenerating || selectedFile"
  141. />
  142. <span>题</span>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. <!-- 预览面板 -->
  149. <div class="preview-panel">
  150. <div class="preview-header">
  151. <img :src="previewIcon" alt="预览" class="preview-icon" />
  152. <h3>预览</h3>
  153. </div>
  154. <div class="preview-content">
  155. <h4 class="preview-title">{{ examName || "试卷名称" }}</h4>
  156. <div class="question-breakdown">
  157. <div
  158. class="breakdown-item"
  159. v-for="(type, index) in questionTypes"
  160. :key="index"
  161. >
  162. <div class="breakdown-row">
  163. <span class="breakdown-left">{{ type.romanNumeral }}、{{ type.name }} (每题{{ type.scorePerQuestion }}分,共{{ type.scorePerQuestion * type.questionCount }}分)</span>
  164. <span class="breakdown-right">{{ type.questionCount }}题</span>
  165. </div>
  166. </div>
  167. </div>
  168. <div class="divider"></div>
  169. <div class="calculated-score-row">
  170. <span class="calculated-label">配置总分</span>
  171. <span class="calculated-value">{{ calculatedTotalScore }}分</span>
  172. </div>
  173. <div class="total-score-row">
  174. <span class="total-label">试卷总分</span>
  175. <span class="total-value">{{ totalScore }}分</span>
  176. </div>
  177. </div>
  178. </div>
  179. </div>
  180. <!-- 底部操作按钮 -->
  181. <div class="bottom-actions">
  182. <button class="clear-btn" @click="clearSettings" :disabled="isGenerating || selectedFile">
  183. 一键清除
  184. </button>
  185. <button class="generate-btn" @click="generateExam" :disabled="isGenerating">
  186. <img v-if="!isGenerating" :src="generateIcon" alt="生成试卷" class="generate-icon" />
  187. <span v-else class="generating-text">生成中...</span>
  188. </button>
  189. </div>
  190. </div>
  191. </div>
  192. <!-- 考试详情页 -->
  193. <div v-if="showExamDetail" class="exam-detail-main">
  194. <!-- 详情页头部 -->
  195. <div class="detail-header">
  196. <button class="back-btn" @click="backToConfig" :disabled="isGenerating">
  197. <span class="back-arrow">←</span>
  198. 返回修改
  199. </button>
  200. <div class="download-dropdown" :class="{ 'disabled': isGenerating, 'show': showDownloadMenu }" @click.stop>
  201. <button class="download-btn" :disabled="isGenerating" @click="toggleDownloadMenu">
  202. <img :src="downloadIcon" alt="下载Word" class="download-icon" />
  203. </button>
  204. <div class="dropdown-menu">
  205. <div class="dropdown-item" @click="exportToWordWithAnswers" :disabled="isGenerating">
  206. <span class="item-text">有答案</span>
  207. </div>
  208. <div class="dropdown-item" @click="exportToWordWithoutAnswers" :disabled="isGenerating">
  209. <span class="item-text">无答案</span>
  210. </div>
  211. </div>
  212. </div>
  213. </div>
  214. <!-- 试卷信息 -->
  215. <div class="exam-info">
  216. <h1 class="exam-title">{{ currentExam.title }}</h1>
  217. <div class="exam-stats">
  218. <span class="total-score">总分: {{ currentExam.totalScore }}分</span>
  219. <span class="question-count">题量: {{ currentExam.totalQuestions }}题</span>
  220. </div>
  221. <div class="generation-time">生成时间: {{ currentTime }}</div>
  222. </div>
  223. <!-- 题型列表 -->
  224. <div class="question-sections">
  225. <!-- 单选题 -->
  226. <div class="question-section" v-if="currentExam.singleChoice && currentExam.singleChoice.questions.length > 0">
  227. <div class="section-header" @click="isGenerating ? null : toggleSection('single')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  228. <div class="section-title">
  229. <span class="section-number">一</span>
  230. <span class="section-name">单选题</span>
  231. <span class="section-score">(每题{{ currentExam.singleChoice.scorePerQuestion }}分, 共{{ currentExam.singleChoice.totalScore }}分)</span>
  232. </div>
  233. <div class="section-controls">
  234. <span class="question-count-text">{{ currentExam.singleChoice.count }}题</span>
  235. <img
  236. :src="expandIcon"
  237. alt="收起/展开"
  238. class="toggle-icon"
  239. :class="{ 'expanded': !expandedSections.single }"
  240. />
  241. </div>
  242. </div>
  243. <div v-if="expandedSections.single" class="section-content">
  244. <div
  245. v-for="(question, index) in currentExam.singleChoice.questions"
  246. :key="index"
  247. class="question-item"
  248. >
  249. <div class="question-header">
  250. <span class="question-number">{{ index + 1 }}.</span>
  251. <span class="question-text">{{ question.text }}</span>
  252. <button class="refresh-btn" @click="refreshQuestion('single', index)" :disabled="isGenerating">
  253. <img
  254. :src="collapseIcon"
  255. alt="刷新"
  256. class="refresh-icon"
  257. :class="{ 'rotating': isRefreshing['single_' + index] }"
  258. />
  259. </button>
  260. </div>
  261. <div class="options">
  262. <div
  263. v-for="option in question.options"
  264. :key="option.key"
  265. class="option"
  266. >
  267. <div class="radio-wrapper">
  268. <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === option.key }">
  269. <div v-if="question.selectedAnswer === option.key" class="radio-dot"></div>
  270. </div>
  271. </div>
  272. <span class="option-key">{{ option.key }}.</span>
  273. <div class="option-content">
  274. <span class="option-text">{{ option.text }}</span>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. </div>
  280. </div>
  281. <!-- 判断题 -->
  282. <div class="question-section" v-if="currentExam.judge && currentExam.judge.questions.length > 0">
  283. <div class="section-header" @click="isGenerating ? null : toggleSection('judge')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  284. <div class="section-title">
  285. <span class="section-number">二</span>
  286. <span class="section-name">判断题</span>
  287. <span class="section-score">(每题{{ currentExam.judge.scorePerQuestion }}分, 共{{ currentExam.judge.totalScore }}分)</span>
  288. </div>
  289. <div class="section-controls">
  290. <span class="question-count-text">{{ currentExam.judge.count }}题</span>
  291. <img
  292. :src="expandIcon"
  293. alt="收起/展开"
  294. class="toggle-icon"
  295. :class="{ 'expanded': !expandedSections.judge }"
  296. />
  297. </div>
  298. </div>
  299. <div v-if="expandedSections.judge" class="section-content">
  300. <div
  301. v-for="(question, index) in currentExam.judge.questions"
  302. :key="index"
  303. class="question-item"
  304. >
  305. <div class="question-header">
  306. <span class="question-number">{{ index + 1 }}.</span>
  307. <span class="question-text">{{ question.text }}</span>
  308. <button class="refresh-btn" @click="refreshQuestion('judge', index)" :disabled="isGenerating">
  309. <img
  310. :src="collapseIcon"
  311. alt="刷新"
  312. class="refresh-icon"
  313. :class="{ 'rotating': isRefreshing['judge_' + index] }"
  314. />
  315. </button>
  316. </div>
  317. <div class="answer-section">
  318. <span class="answer-label">正确答案:</span>
  319. <span class="answer-value">{{ question.selectedAnswer }}</span>
  320. </div>
  321. </div>
  322. </div>
  323. </div>
  324. <!-- 多选题 -->
  325. <div class="question-section" v-if="currentExam.multiple && currentExam.multiple.questions.length > 0">
  326. <div class="section-header" @click="isGenerating ? null : toggleSection('multiple')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  327. <div class="section-title">
  328. <span class="section-number">三</span>
  329. <span class="section-name">多选题</span>
  330. <span class="section-score">(每题{{ currentExam.multiple.scorePerQuestion }}分, 共{{ currentExam.multiple.totalScore }}分)</span>
  331. </div>
  332. <div class="section-controls">
  333. <span class="question-count-text">{{ currentExam.multiple.count }}题</span>
  334. <img
  335. :src="expandIcon"
  336. alt="收起/展开"
  337. class="toggle-icon"
  338. :class="{ 'expanded': !expandedSections.multiple }"
  339. />
  340. </div>
  341. </div>
  342. <div v-if="expandedSections.multiple" class="section-content">
  343. <div
  344. v-for="(question, index) in currentExam.multiple.questions"
  345. :key="index"
  346. class="question-item"
  347. >
  348. <div class="question-header">
  349. <span class="question-number">{{ index + 1 }}.</span>
  350. <span class="question-text">{{ question.text }}</span>
  351. <button class="refresh-btn" @click="refreshQuestion('multiple', index)" :disabled="isGenerating">
  352. <img
  353. :src="collapseIcon"
  354. alt="刷新"
  355. class="refresh-icon"
  356. :class="{ 'rotating': isRefreshing['multiple_' + index] }"
  357. />
  358. </button>
  359. </div>
  360. <div class="options">
  361. <div
  362. v-for="option in question.options"
  363. :key="option.key"
  364. class="option"
  365. >
  366. <div class="radio-wrapper">
  367. <div class="radio-circle" :class="{ 'selected': (question.selectedAnswers || []).includes(option.key) }">
  368. <div v-if="(question.selectedAnswers || []).includes(option.key)" class="radio-dot"></div>
  369. </div>
  370. </div>
  371. <span class="option-key">{{ option.key }}.</span>
  372. <div class="option-content">
  373. <span class="option-text">{{ option.text }}</span>
  374. </div>
  375. </div>
  376. </div>
  377. <div class="answer-section">
  378. <span class="answer-label">正确答案:</span>
  379. <span class="answer-value">{{ (question.selectedAnswers || []).join(', ') }}</span>
  380. </div>
  381. </div>
  382. </div>
  383. </div>
  384. <!-- 简答题 -->
  385. <div class="question-section" v-if="currentExam.short && currentExam.short.questions.length > 0">
  386. <div class="section-header" @click="isGenerating ? null : toggleSection('short')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  387. <div class="section-title">
  388. <span class="section-number">四</span>
  389. <span class="section-name">简答题</span>
  390. <span class="section-score">(每题{{ currentExam.short.scorePerQuestion }}分, 共{{ currentExam.short.totalScore }}分)</span>
  391. </div>
  392. <div class="section-controls">
  393. <span class="question-count-text">{{ currentExam.short.count }}题</span>
  394. <img
  395. :src="expandIcon"
  396. alt="收起/展开"
  397. class="toggle-icon"
  398. :class="{ 'expanded': !expandedSections.short }"
  399. />
  400. </div>
  401. </div>
  402. <div v-if="expandedSections.short" class="section-content">
  403. <div
  404. v-for="(question, index) in currentExam.short.questions"
  405. :key="index"
  406. class="question-item"
  407. >
  408. <div class="question-header">
  409. <span class="question-number">{{ index + 1 }}.</span>
  410. <span class="question-text">{{ question.text }}</span>
  411. <button class="refresh-btn" @click="refreshQuestion('short', index)" :disabled="isGenerating">
  412. <img
  413. :src="collapseIcon"
  414. alt="刷新"
  415. class="refresh-icon"
  416. :class="{ 'rotating': isRefreshing['short_' + index] }"
  417. />
  418. </button>
  419. </div>
  420. <div v-if="question.outline" class="answer-outline">
  421. <div class="outline-section">
  422. <strong>关键要点:</strong>{{ cleanText(question.outline.keyFactors) }}
  423. </div>
  424. <div class="outline-section">
  425. <strong>具体措施:</strong>{{ cleanText(question.outline.measures) }}
  426. </div>
  427. </div>
  428. </div>
  429. </div>
  430. </div>
  431. </div>
  432. </div>
  433. <!-- 文件上传隐藏input -->
  434. <input
  435. ref="fileInput"
  436. type="file"
  437. accept=".ppt,.pptx"
  438. @change="handleFileSelect"
  439. style="display: none"
  440. />
  441. </div>
  442. </div>
  443. </template>
  444. <script setup>
  445. import { useRouter } from 'vue-router'
  446. import MobileHeader from '@/components/MobileHeader.vue'
  447. import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
  448. import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
  449. import MobileToast from '@/components/MobileToast.vue'
  450. import { apis } from '@/request/apis.js'
  451. // ===== 已删除:getUserId - 不再需要,改用token =====
  452. // import { getUserId } from '@/utils/userManager.js'
  453. const router = useRouter()
  454. const goBack = () => {
  455. router.go(-1)
  456. }
  457. // 显示历史记录抽屉的方法
  458. const showHistoryDrawer = () => {
  459. if (!isGenerating.value) {
  460. showHistory.value = true
  461. }
  462. // AI处理中时不执行任何操作,不记录点击意图
  463. }
  464. const showHistory = ref(false)
  465. // Toast 状态管理(与其他移动端页面保持一致)
  466. const toastVisible = ref(false)
  467. const toastMessage = ref('')
  468. const showToast = (message, duration = 2000) => {
  469. toastMessage.value = message
  470. toastVisible.value = true
  471. if (duration > 0) {
  472. setTimeout(() => {
  473. toastVisible.value = false
  474. }, duration)
  475. }
  476. }
  477. const closeToast = () => {
  478. toastVisible.value = false
  479. }
  480. // 历史记录相关状态
  481. const historyData = ref([])
  482. const historyTotal = ref(0)
  483. const isLoadingHistory = ref(false)
  484. // 页面加载时不再自动加载历史记录,改为点击菜单时加载
  485. onMounted(async () => {
  486. // 保存初始配置
  487. initialConfig = {
  488. questionTypes: JSON.parse(JSON.stringify(questionTypes.value)),
  489. totalScore: totalScore.value,
  490. selectedProjectType: selectedProjectType.value,
  491. examName: examName.value
  492. };
  493. console.log('初始配置已保存:', initialConfig);
  494. // 添加全局点击事件监听器
  495. document.addEventListener('click', handleClickOutside);
  496. console.log('🚀 移动端考试工坊页面初始化完成')
  497. })
  498. onUnmounted(() => {
  499. // 清理事件监听器
  500. document.removeEventListener('click', handleClickOutside);
  501. })
  502. // ============ 移动端考试工坊核心功能 ============
  503. // 试卷类型图标和配置
  504. import bridgeIcon from '@/assets/Exam/4.png'
  505. import tunnelIcon from '@/assets/Exam/18.png'
  506. import equipmentIcon from '@/assets/Exam/5.png'
  507. import gasStationIcon from '@/assets/Exam/6.png'
  508. import highwayIcon from '@/assets/Exam/7.png'
  509. import comprehensiveIcon from '@/assets/Exam/8.png'
  510. import aiIcon from '@/assets/Exam/7.png'
  511. import pptIcon from '@/assets/Exam/8.png'
  512. import previewIcon from '@/assets/Exam/9.png'
  513. import clearIcon from '@/assets/Exam/10.png'
  514. import generateIcon from '@/assets/Exam/12.png'
  515. import downloadIcon from '@/assets/Exam/13.png'
  516. import expandIcon from '@/assets/Exam/17.png'
  517. import collapseIcon from '@/assets/Exam/16.png'
  518. import saveIcon from '@/assets/Exam/15.png'
  519. // 试卷类型配置
  520. const projectTypes = {
  521. bridge: { name: "桥梁", icon: bridgeIcon },
  522. tunnel: { name: "隧道", icon: tunnelIcon },
  523. equipment: { name: "特种设备", icon: equipmentIcon },
  524. "gas-station": { name: "加油站", icon: gasStationIcon },
  525. highway: { name: "高速运营公路", icon: highwayIcon },
  526. comprehensive: { name: "综合", icon: comprehensiveIcon },
  527. };
  528. // 题型配置
  529. const questionTypes = ref([
  530. { name: "单选题", scorePerQuestion: 5, questionCount: 5, romanNumeral: "一" },
  531. { name: "判断题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "二" },
  532. { name: "多选题", scorePerQuestion: 8, questionCount: 5, romanNumeral: "三" },
  533. { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
  534. ]);
  535. // 考试工坊状态
  536. const showExamDetail = ref(false)
  537. const isGenerating = ref(false)
  538. const isLoadingHistoryItem = ref(false)
  539. const ai_conversation_id = ref(0)
  540. const showDownloadMenu = ref(false) // 控制下载菜单显示状态
  541. // 试卷配置状态
  542. const selectedFunction = ref("ai")
  543. const selectedProjectType = ref("bridge")
  544. const examName = ref("桥梁工程施工技术考核")
  545. const totalScore = ref(100)
  546. const currentTime = ref("")
  547. // 文件上传相关
  548. const selectedFile = ref(null)
  549. const pptContentDescription = ref('')
  550. const fileInput = ref(null)
  551. // 展开/收起状态
  552. const expandedSections = ref({
  553. single: true,
  554. judge: true,
  555. multiple: true,
  556. short: true,
  557. })
  558. // 刷新状态记录
  559. const isRefreshing = ref({})
  560. // 当前试卷数据
  561. const currentExam = ref({
  562. title: "桥梁工程施工技术考核",
  563. totalScore: 100,
  564. totalQuestions: 37,
  565. singleChoice: {
  566. scorePerQuestion: 2,
  567. totalScore: 30,
  568. count: 15,
  569. questions: []
  570. },
  571. judge: {
  572. scorePerQuestion: 2,
  573. totalScore: 20,
  574. count: 10,
  575. questions: []
  576. },
  577. multiple: {
  578. scorePerQuestion: 3,
  579. totalScore: 30,
  580. count: 10,
  581. questions: []
  582. },
  583. short: {
  584. scorePerQuestion: 10,
  585. totalScore: 20,
  586. count: 2,
  587. questions: []
  588. }
  589. })
  590. // 保存初始配置
  591. let initialConfig = null
  592. // 计算总分(所有题目分数的总和)
  593. const calculatedTotalScore = computed(() => {
  594. return questionTypes.value.reduce((total, type) => {
  595. return total + (type.scorePerQuestion * type.questionCount);
  596. }, 0);
  597. })
  598. // ============ 历史记录相关方法 ============
  599. // 格式化时间函数
  600. const formatTime = (timestamp) => {
  601. if (!timestamp) return ''
  602. const date = new Date(timestamp)
  603. const now = new Date()
  604. const diff = now - date
  605. // 如果是今天
  606. if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
  607. return date.toLocaleTimeString('zh-CN', {
  608. hour: '2-digit',
  609. minute: '2-digit'
  610. })
  611. }
  612. // 如果是昨天
  613. if (diff < 48 * 60 * 60 * 1000 && date.getDate() === now.getDate() - 1) {
  614. return '昨天 ' + date.toLocaleTimeString('zh-CN', {
  615. hour: '2-digit',
  616. minute: '2-digit'
  617. })
  618. }
  619. // 其他情况显示日期
  620. return date.toLocaleDateString('zh-CN', {
  621. month: '2-digit',
  622. day: '2-digit'
  623. })
  624. }
  625. // 生成对话标题
  626. const generateConversationTitle = (content) => {
  627. if (!content) return '新对话'
  628. // 取前30个字符作为标题
  629. const title = content.replace(/<[^>]*>/g, '').trim()
  630. return title.length > 30 ? title.substring(0, 30) + '...' : title
  631. }
  632. // 新建任务
  633. const createNewTask = () => {
  634. console.log('新建考试工坊任务')
  635. showHistory.value = false
  636. // 重置所有状态到新任务状态
  637. selectedFunction.value = "ai"
  638. selectedProjectType.value = "bridge"
  639. examName.value = "桥梁工程施工技术考核"
  640. totalScore.value = 100
  641. showExamDetail.value = false
  642. selectedFile.value = null
  643. pptContentDescription.value = ''
  644. ai_conversation_id.value = 0
  645. // 重置题型配置
  646. if (initialConfig) {
  647. questionTypes.value = JSON.parse(JSON.stringify(initialConfig.questionTypes))
  648. totalScore.value = initialConfig.totalScore
  649. selectedProjectType.value = initialConfig.selectedProjectType
  650. examName.value = initialConfig.examName
  651. }
  652. // 清除历史记录的激活状态
  653. historyData.value.forEach((item) => {
  654. item.isActive = false
  655. })
  656. }
  657. // 处理历史记录点击
  658. const handleHistoryItem = async (historyItem) => {
  659. if (historyItem.isActive) return
  660. console.log("点击移动端考试工坊历史记录:", historyItem)
  661. // 设置当前点击的历史记录为激活状态
  662. historyData.value.forEach((item) => {
  663. item.isActive = item.id === historyItem.id
  664. })
  665. // 关闭历史记录抽屉
  666. showHistory.value = false
  667. // 解析历史试卷数据
  668. ai_conversation_id.value = historyItem.id
  669. currentTime.value = historyItem.time
  670. // 如果有原始数据,尝试解析
  671. if (historyItem.rawData && historyItem.rawData.content) {
  672. try {
  673. const examData = JSON.parse(historyItem.rawData.content)
  674. currentExam.value = examData
  675. showExamDetail.value = true
  676. } catch (error) {
  677. console.error('解析历史试卷数据失败:', error)
  678. // 如果解析失败,显示默认详情页
  679. showExamDetail.value = true
  680. }
  681. } else {
  682. // 如果没有内容,显示默认详情页
  683. showExamDetail.value = true
  684. }
  685. }
  686. // 删除历史记录
  687. const deleteHistoryItem = async (historyItem, index) => {
  688. try {
  689. console.log('开始删除移动端历史记录:', historyItem)
  690. const response = await apis.deleteHistoryRecord({
  691. // ===== 已删除:user_id - 后端从token解析 =====
  692. ai_conversation_id: historyItem.id
  693. })
  694. if (response.statusCode === 200) {
  695. // 从本地数据中移除
  696. historyData.value.splice(index, 1)
  697. historyTotal.value = Math.max(0, historyTotal.value - 1)
  698. // 如果删除的是当前激活的历史记录,执行新建任务
  699. if (historyItem.isActive) {
  700. console.log('删除激活的历史记录,执行新建任务')
  701. createNewTask()
  702. }
  703. console.log('✅ 移动端历史记录删除成功')
  704. // 轻提示
  705. showToast('删除成功')
  706. } else {
  707. console.error('❌ 删除移动端历史记录失败:', response)
  708. }
  709. } catch (error) {
  710. console.error('❌ 删除移动端历史记录失败:', error)
  711. }
  712. }
  713. // 时间解析与格式化(容错更强)
  714. const parseToDate = (input) => {
  715. if (!input) return null
  716. if (typeof input === 'number') {
  717. const ms = input < 1e12 ? input * 1000 : input
  718. return new Date(ms)
  719. }
  720. if (typeof input === 'string') {
  721. let d = new Date(input)
  722. if (!isNaN(d)) return d
  723. const normalized = input.replace(/-/g, '/').replace('T', ' ')
  724. d = new Date(normalized)
  725. if (!isNaN(d)) return d
  726. }
  727. return new Date(input)
  728. }
  729. const formatHistoryTime = (timestamp) => {
  730. const date = parseToDate(timestamp)
  731. if (!date || isNaN(date)) return '未知时间'
  732. const now = new Date()
  733. const isToday = date.toDateString() === now.toDateString()
  734. const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
  735. const isYesterday = date.toDateString() === yesterday.toDateString()
  736. if (isToday) {
  737. return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  738. }
  739. if (isYesterday) {
  740. return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  741. }
  742. const month = date.getMonth() + 1
  743. const day = date.getDate()
  744. const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  745. return `${month}月${day}日 ${time}`
  746. }
  747. // 获取历史记录列表
  748. const getHistoryRecordList = async () => {
  749. try {
  750. console.log('📋 开始获取移动端考试工坊历史记录列表...')
  751. isLoadingHistory.value = true
  752. const startTime = performance.now()
  753. const response = await apis.getHistoryRecord({
  754. // ===== 已删除:user_id - 后端从token解析 =====
  755. ai_conversation_id: 0, // 0表示获取对话列表
  756. business_type: 3 // 考试工坊类型
  757. })
  758. const endTime = performance.now()
  759. console.log('📋 移动端考试工坊历史记录API调用耗时: ' + (endTime - startTime).toFixed(2) + 'ms')
  760. console.log('📋 移动端历史记录列表响应:', response)
  761. if (response.statusCode === 200) {
  762. // 设置历史记录总数
  763. historyTotal.value = response.total || 0
  764. // 转换后端数据为前端格式
  765. historyData.value = response.data.map(conversation => ({
  766. id: conversation.id,
  767. title: generateConversationTitle(conversation.content),
  768. time: formatHistoryTime(conversation.updated_at),
  769. businessType: conversation.business_type,
  770. isActive: false,
  771. // 保存原始数据用于后续查询
  772. rawData: conversation
  773. }))
  774. // 高亮当前对话
  775. if (ai_conversation_id.value) {
  776. historyData.value.forEach(item => { item.isActive = item.id === ai_conversation_id.value })
  777. }
  778. console.log('✅ 移动端考试工坊历史记录列表已设置: ' + historyData.value.length + '条记录,总数: ' + historyTotal.value)
  779. } else {
  780. console.error('❌ 获取移动端历史记录列表失败:', response.statusCode)
  781. }
  782. } catch (error) {
  783. console.error('❌ 获取移动端历史记录列表失败:', error)
  784. } finally {
  785. isLoadingHistory.value = false
  786. }
  787. }
  788. // ============ 业务逻辑方法 ============
  789. // 选择功能方法
  790. const selectFunction = (functionType) => {
  791. selectedFunction.value = functionType;
  792. console.log("选择功能:", functionType);
  793. };
  794. // 选择项目类型方法
  795. const selectProjectType = (typeKey) => {
  796. selectedProjectType.value = typeKey;
  797. console.log("选择工程类型:", projectTypes[typeKey].name);
  798. // 自动更新试卷名称
  799. const projectTypeName = projectTypes[typeKey].name;
  800. examName.value = projectTypeName + '工程施工技术考核';
  801. // 同时更新当前试卷的标题
  802. if (currentExam.value) {
  803. currentExam.value.title = examName.value;
  804. }
  805. };
  806. // 验证试卷名称
  807. const validateExamName = () => {
  808. if (examName.value.length > 32) {
  809. examName.value = examName.value.slice(0, 32);
  810. }
  811. };
  812. // 验证总分
  813. const validateTotalScore = () => {
  814. if (totalScore.value > 1000) {
  815. totalScore.value = 1000;
  816. console.warn("试卷总分不能超过1000分");
  817. }
  818. if (totalScore.value < 1) {
  819. totalScore.value = 1;
  820. }
  821. };
  822. // 验证每题分数
  823. const validateScorePerQuestion = (type) => {
  824. if (type.scorePerQuestion > 99) {
  825. type.scorePerQuestion = 99;
  826. console.warn(`${type.name}每题分数不能超过99分`);
  827. }
  828. if (type.scorePerQuestion < 1) {
  829. type.scorePerQuestion = 1;
  830. }
  831. };
  832. // 验证题目数量
  833. const validateQuestionCount = (type) => {
  834. if (type.questionCount > 99) {
  835. type.questionCount = 99;
  836. console.warn(`${type.name}题目数量不能超过99题`);
  837. }
  838. if (type.questionCount < 1) {
  839. type.questionCount = 1;
  840. }
  841. };
  842. // 清除设置
  843. const clearSettings = () => {
  844. // 根据当前选择的工程类型设置试卷名称
  845. const projectTypeName = projectTypes[selectedProjectType.value].name;
  846. examName.value = projectTypeName + '工程施工技术考核';
  847. totalScore.value = 100;
  848. questionTypes.value = [
  849. { name: "单选题", scorePerQuestion: 2, questionCount: 8, romanNumeral: "一" },
  850. { name: "判断题", scorePerQuestion: 2, questionCount: 5, romanNumeral: "二" },
  851. { name: "多选题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "三" },
  852. { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
  853. ];
  854. console.log("清除设置");
  855. };
  856. // 生成试卷
  857. const generateExam = async () => {
  858. if (!examName.value.trim()) {
  859. console.warn("请输入试卷名称");
  860. return;
  861. }
  862. if (examName.value.trim().length === 0) {
  863. console.warn("试卷名称不能为空");
  864. return;
  865. }
  866. // 检查总分是否超过限制
  867. if (totalScore.value > 1000) {
  868. console.warn("试卷总分不能超过1000分");
  869. return;
  870. }
  871. // 检查每题分数和题目数量是否超过限制
  872. for (const type of questionTypes.value) {
  873. if (type.scorePerQuestion > 99) {
  874. console.warn(`${type.name}每题分数不能超过99分`);
  875. return;
  876. }
  877. if (type.questionCount > 99) {
  878. console.warn(`${type.name}题目数量不能超过99题`);
  879. return;
  880. }
  881. }
  882. // 检查总分是否合理
  883. const calculatedScore = questionTypes.value.reduce((total, type) => {
  884. return total + (type.scorePerQuestion * type.questionCount);
  885. }, 0);
  886. if (calculatedScore !== totalScore.value) {
  887. showToast(`总分不匹配!`, 3000);
  888. return;
  889. }
  890. console.log("开始生成试卷:", {
  891. function: selectedFunction.value,
  892. projectType: projectTypes[selectedProjectType.value].name,
  893. examName: examName.value,
  894. totalScore: totalScore.value,
  895. questionTypes: questionTypes.value,
  896. pptContent: pptContentDescription.value
  897. });
  898. try {
  899. isGenerating.value = true;
  900. const mode = selectedFunction.value === 'ppt' ? 'ppt' : 'ai';
  901. const prompt = await fetchMobileExamPrompt(mode);
  902. console.log('发送给AI的考试生成提示词:', prompt);
  903. // 调用AI接口生成试卷 - 使用与PC端相同的接口
  904. const response = await apis.sendDeepseekMessage({
  905. // ===== 已删除:user_id - 后端从token解析 =====
  906. business_type: 3,
  907. message: prompt,
  908. exam_name: examName.value,
  909. ai_conversation_id: ai_conversation_id.value
  910. });
  911. if (response.statusCode === 200) {
  912. const aiReply = response.data.reply;
  913. const aiConversationId = response.data.ai_conversation_id;
  914. console.log('AI生成的考试试卷:', aiReply);
  915. console.log('AI对话ID:', aiConversationId);
  916. // 保存对话ID
  917. ai_conversation_id.value = aiConversationId;
  918. // 解析AI回复并生成试卷
  919. const generatedExam = parseAIExamResponse(aiReply);
  920. // 更新当前试卷数据
  921. currentExam.value = generatedExam;
  922. currentExam.value.title = examName.value;
  923. currentExam.value.totalScore = totalScore.value;
  924. currentTime.value = new Date().toLocaleString('zh-CN');
  925. // 试卷数据已通过AI接口自动保存到数据库
  926. console.log('✅ 试卷已通过AI接口保存到数据库');
  927. // 显示考试详情页
  928. showExamDetail.value = true;
  929. console.log('✅ 移动端试卷生成完成!');
  930. } else {
  931. throw new Error('AI接口调用失败');
  932. }
  933. } catch (error) {
  934. console.error('生成试卷失败:', error);
  935. } finally {
  936. isGenerating.value = false;
  937. }
  938. };
  939. // 从服务端获取移动端提示词
  940. const fetchMobileExamPrompt = async (mode = 'ai') => {
  941. const normalizedQuestionTypes = questionTypes.value.map(type => ({
  942. name: type.name,
  943. romanNumeral: type.romanNumeral,
  944. questionCount: Number(type.questionCount) || 0,
  945. scorePerQuestion: Number(type.scorePerQuestion) || 0,
  946. }));
  947. const payload = {
  948. mode,
  949. client: 'mobile',
  950. projectType: projectTypes[selectedProjectType.value]?.name || '',
  951. examTitle: examName.value,
  952. totalScore: totalScore.value,
  953. questionTypes: normalizedQuestionTypes,
  954. pptContent: pptContentDescription.value || ''
  955. };
  956. try {
  957. const response = await apis.buildExamPrompt(payload);
  958. if (!response?.data?.prompt) {
  959. throw new Error(response?.msg || '提示词构建失败');
  960. }
  961. return response.data.prompt;
  962. } catch (error) {
  963. console.error('获取移动端提示词失败:', error);
  964. throw error;
  965. }
  966. };
  967. // 解析AI考试回复
  968. const parseAIExamResponse = (aiReply) => {
  969. try {
  970. // 尝试提取JSON内容
  971. const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
  972. if (jsonMatch) {
  973. const examData = JSON.parse(jsonMatch[0]);
  974. // 确保所有题目都有正确的初始值
  975. ensureQuestionInitialValues(examData);
  976. return examData;
  977. } else {
  978. throw new Error('未找到有效的JSON数据');
  979. }
  980. } catch (error) {
  981. console.error('解析AI回复失败:', error);
  982. // 返回默认试卷结构
  983. return generateDefaultExam();
  984. }
  985. };
  986. // 确保题目初始值正确
  987. const ensureQuestionInitialValues = (examData) => {
  988. // 确保单选题有正确的答案格式
  989. if (examData.singleChoice && examData.singleChoice.questions) {
  990. examData.singleChoice.questions.forEach(question => {
  991. if (!question.selectedAnswer) {
  992. question.selectedAnswer = question.options && question.options.length > 0 ? question.options[0].key : 'A';
  993. }
  994. });
  995. }
  996. // 确保判断题有正确答案
  997. if (examData.judge && examData.judge.questions) {
  998. examData.judge.questions.forEach(question => {
  999. if (!question.selectedAnswer) {
  1000. question.selectedAnswer = Math.random() > 0.5 ? '正确' : '错误';
  1001. }
  1002. });
  1003. }
  1004. // 确保多选题有正确答案
  1005. if (examData.multiple && examData.multiple.questions) {
  1006. examData.multiple.questions.forEach(question => {
  1007. if (!question.selectedAnswers || !Array.isArray(question.selectedAnswers)) {
  1008. question.selectedAnswers = question.options && question.options.length > 1 ? [question.options[0].key, question.options[1].key] : [];
  1009. }
  1010. });
  1011. }
  1012. // 确保简答题有提纲
  1013. if (examData.short && examData.short.questions) {
  1014. examData.short.questions.forEach(question => {
  1015. if (!question.outline) {
  1016. question.outline = {
  1017. keyFactors: "请参考相关教材和标准规范",
  1018. measures: "请结合实际工程案例进行解答"
  1019. };
  1020. }
  1021. });
  1022. }
  1023. };
  1024. // 生成默认考试结构
  1025. const generateDefaultExam = () => {
  1026. return {
  1027. title: examName.value,
  1028. totalScore: totalScore.value,
  1029. totalQuestions: questionTypes.value.reduce((total, type) => total + type.questionCount, 0),
  1030. singleChoice: {
  1031. scorePerQuestion: questionTypes.value[0].scorePerQuestion,
  1032. totalScore: questionTypes.value[0].scorePerQuestion * questionTypes.value[0].questionCount,
  1033. count: questionTypes.value[0].questionCount,
  1034. questions: []
  1035. },
  1036. judge: {
  1037. scorePerQuestion: questionTypes.value[1].scorePerQuestion,
  1038. totalScore: questionTypes.value[1].scorePerQuestion * questionTypes.value[1].questionCount,
  1039. count: questionTypes.value[1].questionCount,
  1040. questions: []
  1041. },
  1042. multiple: {
  1043. scorePerQuestion: questionTypes.value[2].scorePerQuestion,
  1044. totalScore: questionTypes.value[2].scorePerQuestion * questionTypes.value[2].questionCount,
  1045. count: questionTypes.value[2].questionCount,
  1046. questions: []
  1047. },
  1048. short: {
  1049. scorePerQuestion: questionTypes.value[3].scorePerQuestion,
  1050. totalScore: questionTypes.value[3].scorePerQuestion * questionTypes.value[3].questionCount,
  1051. count: questionTypes.value[3].questionCount,
  1052. questions: []
  1053. }
  1054. }
  1055. };
  1056. // 返回配置页面
  1057. const backToConfig = () => {
  1058. showExamDetail.value = false;
  1059. };
  1060. // 展开/收起题型
  1061. const toggleSection = (sectionType) => {
  1062. expandedSections.value[sectionType] = !expandedSections.value[sectionType];
  1063. };
  1064. // 刷新题目 - 使用PC端的实现方式
  1065. const refreshQuestion = async (questionType, index) => {
  1066. try {
  1067. console.log(`刷新${questionType}类型第${index + 1}题`);
  1068. // 设置刷新状态
  1069. const key = `${questionType}_${index}`;
  1070. isRefreshing.value[key] = true;
  1071. // 构建单题重新生成的提示词
  1072. const prompt = buildSingleQuestionPrompt(questionType, index);
  1073. // 第一步:调用 /re_produce_single_question 接口,AI只生成题目
  1074. const response = await apis.reProduceSingleQuestion({
  1075. message: prompt
  1076. });
  1077. if (response.statusCode === 200) {
  1078. const aiReply = response.data.reply;
  1079. console.log('AI重新生成的题目:', aiReply);
  1080. // 解析AI回复并更新题目
  1081. const newQuestion = parseSingleQuestionResponse(aiReply, questionType);
  1082. console.log('解析后的新题目:', newQuestion);
  1083. if (newQuestion) {
  1084. updateQuestion(questionType, index, newQuestion);
  1085. console.log('准备保存到后端,对话ID:', ai_conversation_id.value);
  1086. // 第二步:使用 /re_modify_question 接口保存修改
  1087. await saveToReModifyQuestion(questionType, index, newQuestion);
  1088. showToast('题目重新生成成功!');
  1089. // AI回复完成后,获取最新的历史记录
  1090. await getHistoryRecordList();
  1091. // 如果是新对话,将最新的历史记录设为激活状态
  1092. if (ai_conversation_id.value > 0) {
  1093. historyData.value.forEach((item) => {
  1094. item.isActive = item.id === ai_conversation_id.value;
  1095. });
  1096. console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
  1097. }
  1098. } else {
  1099. throw new Error('解析新题目失败');
  1100. }
  1101. } else {
  1102. throw new Error('AI接口调用失败');
  1103. }
  1104. } catch (error) {
  1105. console.error('刷新题目失败:', error);
  1106. showToast('重新生成题目失败,请重试');
  1107. } finally {
  1108. setTimeout(() => {
  1109. const key = `${questionType}_${index}`;
  1110. isRefreshing.value[key] = false;
  1111. }, 1000);
  1112. }
  1113. };
  1114. // 控制下载菜单显示/隐藏
  1115. const toggleDownloadMenu = () => {
  1116. if (!isGenerating.value) {
  1117. showDownloadMenu.value = !showDownloadMenu.value;
  1118. }
  1119. };
  1120. // 关闭下载菜单
  1121. const closeDownloadMenu = () => {
  1122. showDownloadMenu.value = false;
  1123. };
  1124. // 点击外部区域关闭下载菜单
  1125. const handleClickOutside = (event) => {
  1126. const dropdown = event.target.closest('.download-dropdown');
  1127. if (!dropdown) {
  1128. showDownloadMenu.value = false;
  1129. }
  1130. };
  1131. // 导出Word(有答案)
  1132. const exportToWordWithAnswers = async () => {
  1133. try {
  1134. closeDownloadMenu(); // 关闭下拉菜单
  1135. isGenerating.value = true;
  1136. console.log('开始导出Word格式试卷(有答案)...');
  1137. // 使用PC端的模拟Word导出功能
  1138. await simulateWordExport(true);
  1139. } catch (error) {
  1140. console.error('导出考试文件失败:', error);
  1141. showToast('导出失败,请重试');
  1142. } finally {
  1143. isGenerating.value = false;
  1144. }
  1145. };
  1146. // 导出Word(无答案)
  1147. const exportToWordWithoutAnswers = async () => {
  1148. try {
  1149. closeDownloadMenu(); // 关闭下拉菜单
  1150. isGenerating.value = true;
  1151. console.log('开始导出Word格式试卷(无答案)...');
  1152. // 使用PC端的模拟Word导出功能
  1153. await simulateWordExport(false);
  1154. } catch (error) {
  1155. console.error('导出考试文件失败:', error);
  1156. showToast('导出失败,请重试');
  1157. } finally {
  1158. isGenerating.value = false;
  1159. }
  1160. };
  1161. // 导出文件为Word格式(保留原函数以兼容其他可能的调用)
  1162. const exportToWord = async () => {
  1163. // 默认导出有答案版本
  1164. await exportToWordWithAnswers();
  1165. };
  1166. // 模拟Word导出功能(使用PC端实现)
  1167. const simulateWordExport = async (includeAnswers = true) => {
  1168. try {
  1169. // 创建Word文档内容(使用HTML格式,兼容WPS和Word)
  1170. const wordContent = createHTMLContent(currentExam.value, includeAnswers);
  1171. // 创建Blob对象 - 使用HTML格式
  1172. const blob = new Blob([wordContent], {
  1173. type: 'application/msword'
  1174. });
  1175. // 下载文件
  1176. const url = URL.createObjectURL(blob);
  1177. const link = document.createElement('a');
  1178. const fileName = includeAnswers
  1179. ? `${currentExam.value.title}_有答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`
  1180. : `${currentExam.value.title}_无答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`;
  1181. link.setAttribute('href', url);
  1182. link.setAttribute('download', fileName);
  1183. link.style.visibility = 'hidden';
  1184. document.body.appendChild(link);
  1185. link.click();
  1186. document.body.removeChild(link);
  1187. showToast(`导出成功${includeAnswers ? '(含答案)' : '(不含答案)'}`);
  1188. } catch (error) {
  1189. console.error('模拟Word导出失败:', error);
  1190. showToast('Word导出失败,请稍后重试');
  1191. }
  1192. };
  1193. // 创建HTML格式的Word文档内容(兼容WPS和Word)- 使用PC端实现
  1194. const createHTMLContent = (examData, includeAnswers = true) => {
  1195. const exam = currentExam.value;
  1196. // HTML文档内容,使用Word兼容的格式
  1197. let htmlContent = `<!DOCTYPE html>
  1198. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  1199. xmlns:w="urn:schemas-microsoft-com:office:word"
  1200. xmlns="http://www.w3.org/TR/REC-html40">
  1201. <head>
  1202. <meta charset="utf-8">
  1203. <meta name="ProgId" content="Word.Document">
  1204. <meta name="Generator" content="Microsoft Word 15">
  1205. <meta name="Originator" content="Microsoft Word 15">
  1206. <title>${exam.title || '试卷'}</title>
  1207. <!--[if gte mso 9]>
  1208. <xml>
  1209. <w:WordDocument>
  1210. <w:View>Print</w:View>
  1211. <w:Zoom>100</w:Zoom>
  1212. <w:DoNotPromptForConvert/>
  1213. <w:DoNotShowRevisions/>
  1214. <w:DoNotPrintRevisions/>
  1215. <w:DoNotShowComments/>
  1216. <w:DoNotShowInsertionsAndDeletions/>
  1217. <w:DoNotShowPropertyChanges/>
  1218. <w:Compatibility>
  1219. <w:BreakWrappedTables/>
  1220. <w:SnapToGridInCell/>
  1221. <w:WrapTextWithPunct/>
  1222. <w:UseAsianBreakRules/>
  1223. <w:DontGrowAutofit/>
  1224. </w:Compatibility>
  1225. </w:WordDocument>
  1226. </xml>
  1227. <![endif]-->
  1228. <style>
  1229. body {
  1230. font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
  1231. font-size: 14px;
  1232. line-height: 1.6;
  1233. margin: 24px;
  1234. color: #000;
  1235. }
  1236. .header {
  1237. text-align: center;
  1238. margin-bottom: 14px;
  1239. }
  1240. .exam-title {
  1241. font-size: 24px;
  1242. font-weight: bold;
  1243. margin-bottom: 14px;
  1244. color: #000;
  1245. }
  1246. .exam-info {
  1247. font-size: 14px;
  1248. color: #666;
  1249. margin-bottom: 14px;
  1250. }
  1251. .section {
  1252. margin-bottom: 14px;
  1253. }
  1254. .section-title {
  1255. font-size: 18px;
  1256. font-weight: bold;
  1257. margin-bottom: 14px;
  1258. color: #000;
  1259. border-bottom: 2px solid #3e7bfa;
  1260. padding-bottom: 5px;
  1261. }
  1262. .question {
  1263. margin-bottom: 14px;
  1264. padding: 10px;
  1265. background-color: #f9f9f9;
  1266. border-left: 4px solid #3e7bfa;
  1267. }
  1268. .question-header {
  1269. display: flex;
  1270. align-items: flex-start;
  1271. gap: 8px;
  1272. margin-bottom: 14px;
  1273. }
  1274. .question-number {
  1275. font-weight: bold;
  1276. color: #3e7bfa;
  1277. flex-shrink: 0;
  1278. }
  1279. .question-text {
  1280. flex: 1;
  1281. }
  1282. .options {
  1283. margin-left: 12px;
  1284. }
  1285. .option {
  1286. margin-bottom: 5px;
  1287. }
  1288. .answer {
  1289. margin-top: 10px;
  1290. padding: 8px;
  1291. background: #e8f4fd;
  1292. border-left: 3px solid #3e7bfa;
  1293. font-weight: bold;
  1294. color: #0066cc;
  1295. }
  1296. .outline-section {
  1297. margin: 10px 0;
  1298. padding: 8px;
  1299. background: #f0f8ff;
  1300. border-radius: 4px;
  1301. }
  1302. </style>
  1303. </head>
  1304. <body>
  1305. <div class="header">
  1306. <div class="exam-title">${exam.title || '考试试卷'}</div>
  1307. <div class="exam-info">
  1308. 总分:${exam.totalScore || 0}分 | 总题数:${exam.totalQuestions || 0}题 | 生成时间:${currentTime.value}
  1309. </div>
  1310. </div>`;
  1311. // 单选题
  1312. if (exam.singleChoice && exam.singleChoice.questions.length > 0) {
  1313. htmlContent += `
  1314. <div class="section">
  1315. <div class="section-title">一、单选题(${exam.singleChoice.count}题,每题${exam.singleChoice.scorePerQuestion}分,共${exam.singleChoice.totalScore}分)</div>`;
  1316. exam.singleChoice.questions.forEach((question, index) => {
  1317. htmlContent += `
  1318. <div class="question">
  1319. <div class="question-header">
  1320. <span class="question-number">${index + 1}.</span>
  1321. <span class="question-text">${question.text}</span>
  1322. </div>
  1323. <div class="options">`;
  1324. question.options.forEach(option => {
  1325. htmlContent += `
  1326. <div class="option">${option.key}. ${option.text}</div>`;
  1327. });
  1328. htmlContent += `
  1329. </div>
  1330. ${includeAnswers ? `<div class="answer">正确答案:${question.selectedAnswer}</div>` : ''}
  1331. </div>`;
  1332. });
  1333. htmlContent += `
  1334. </div>`;
  1335. }
  1336. // 判断题
  1337. if (exam.judge && exam.judge.questions.length > 0) {
  1338. htmlContent += `
  1339. <div class="section">
  1340. <div class="section-title">二、判断题(${exam.judge.count}题,每题${exam.judge.scorePerQuestion}分,共${exam.judge.totalScore}分)</div>`;
  1341. exam.judge.questions.forEach((question, index) => {
  1342. htmlContent += `
  1343. <div class="question">
  1344. <div class="question-header">
  1345. <span class="question-number">${index + 1}.</span>
  1346. <span class="question-text">${question.text}</span>
  1347. </div>
  1348. ${includeAnswers ? `<div class="answer">正确答案:${question.selectedAnswer}</div>` : ''}
  1349. </div>`;
  1350. });
  1351. htmlContent += `
  1352. </div>`;
  1353. }
  1354. // 多选题
  1355. if (exam.multiple && exam.multiple.questions.length > 0) {
  1356. htmlContent += `
  1357. <div class="section">
  1358. <div class="section-title">三、多选题(${exam.multiple.count}题,每题${exam.multiple.scorePerQuestion}分,共${exam.multiple.totalScore}分)</div>`;
  1359. exam.multiple.questions.forEach((question, index) => {
  1360. htmlContent += `
  1361. <div class="question">
  1362. <div class="question-header">
  1363. <span class="question-number">${index + 1}.</span>
  1364. <span class="question-text">${question.text}</span>
  1365. </div>
  1366. <div class="options">`;
  1367. question.options.forEach(option => {
  1368. htmlContent += `
  1369. <div class="option">${option.key}. ${option.text}</div>`;
  1370. });
  1371. htmlContent += `
  1372. </div>
  1373. ${includeAnswers ? `<div class="answer">正确答案:${(question.selectedAnswers || []).join(', ')}</div>` : ''}
  1374. </div>`;
  1375. });
  1376. htmlContent += `
  1377. </div>`;
  1378. }
  1379. // 简答题
  1380. if (exam.short && exam.short.questions.length > 0) {
  1381. htmlContent += `
  1382. <div class="section">
  1383. <div class="section-title">四、简答题(${exam.short.count}题,每题${exam.short.scorePerQuestion}分,共${exam.short.totalScore}分)</div>`;
  1384. exam.short.questions.forEach((question, index) => {
  1385. htmlContent += `
  1386. <div class="question">
  1387. <div class="question-header">
  1388. <span class="question-number">${index + 1}.</span>
  1389. <span class="question-text">${question.text}</span>
  1390. </div>`;
  1391. if (question.outline && includeAnswers) {
  1392. htmlContent += `
  1393. <div class="outline-section">
  1394. <strong>关键要点:</strong>${cleanText(question.outline.keyFactors)}
  1395. </div>
  1396. <div class="outline-section">
  1397. <strong>具体措施:</strong>${cleanText(question.outline.measures)}
  1398. </div>`;
  1399. }
  1400. htmlContent += `
  1401. </div>`;
  1402. });
  1403. htmlContent += `
  1404. </div>`;
  1405. }
  1406. htmlContent += `
  1407. </body>
  1408. </html>`;
  1409. return htmlContent;
  1410. };
  1411. // 创建Word格式的考试文档内容(保留原函数以防其他地方使用)
  1412. const createExamWordContent = (examData) => {
  1413. const currentTime = new Date().toLocaleString('zh-CN');
  1414. let htmlContent = `<!DOCTYPE html>
  1415. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  1416. xmlns:w="urn:schemas-microsoft-com:office:word"
  1417. xmlns="http://www.w3.org/TR/REC-html40">
  1418. <head>
  1419. <meta charset="utf-8">
  1420. <meta name="ProgId" content="Word.Document">
  1421. <meta name="Generator" content="Microsoft Word">
  1422. <meta name="Originator" content="Microsoft Word">
  1423. <meta name="ViewMode" content="PrintLayout">
  1424. <meta name="Zoom" content="100">
  1425. <meta name="DocumentProperties" content="false">
  1426. <meta name="DocumentSecurity" content="false">
  1427. <meta name="DocumentProtection" content="false">
  1428. <meta name="DocumentView" content="PrintLayout">
  1429. <title>${examData.title}</title>
  1430. <style>
  1431. body { font-family: '微软雅黑', 'Microsoft YaHei', sans-serif; line-height: 1.6; margin: 20px; }
  1432. .header { text-align: center; margin-bottom: 30px; }
  1433. .exam-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
  1434. .exam-info { font-size: 14px; color: #666; margin-bottom: 30px; }
  1435. .section { margin-bottom: 25px; }
  1436. .section-title { font-size: 16px; font-weight: bold; margin-bottom: 15px; }
  1437. .question { margin-bottom: 20px; padding: 10px; }
  1438. .question-header { font-weight: bold; margin-bottom: 10px; }
  1439. .option { margin: 5px 0; }
  1440. .answer { color: #0066cc; font-weight: bold; margin-top: 10px; }
  1441. .outline-section { margin: 10px 0; }
  1442. </style>
  1443. </head>
  1444. <body>
  1445. <div class="header">
  1446. <div class="exam-title">${examData.title || '考试试卷'}</div>
  1447. <div class="exam-info">
  1448. 总分:${examData.totalScore || 0}分 | 总题数:${examData.totalQuestions || 0}题 | 生成时间:${currentTime}
  1449. </div>
  1450. </div>`;
  1451. // 单选题
  1452. if (examData.singleChoice && examData.singleChoice.questions.length > 0) {
  1453. htmlContent += `
  1454. <div class="section">
  1455. <div class="section-title">一、单选题(${examData.singleChoice.count}题,每题${examData.singleChoice.scorePerQuestion}分,共${examData.singleChoice.totalScore}分)</div>`;
  1456. examData.singleChoice.questions.forEach((question, index) => {
  1457. htmlContent += `
  1458. <div class="question">
  1459. <div class="question-header">
  1460. <span class="question-number">${index + 1}.</span> ${question.text}
  1461. </div>
  1462. <div class="options">`;
  1463. question.options.forEach(option => {
  1464. htmlContent += `
  1465. <div class="option">${option.key}. ${option.text}</div>`;
  1466. });
  1467. htmlContent += `
  1468. </div>
  1469. <div class="answer">正确答案:${question.selectedAnswer} </div>
  1470. </div>`;
  1471. });
  1472. htmlContent += `
  1473. </div>`;
  1474. }
  1475. // 判断题
  1476. if (examData.judge && examData.judge.questions.length > 0) {
  1477. htmlContent += `
  1478. <div class="section">
  1479. <div class="section-title">二、判断题(${examData.judge.count}题,每题${examData.judge.scorePerQuestion}分,共${examData.judge.totalScore}分)</div>`;
  1480. examData.judge.questions.forEach((question, index) => {
  1481. htmlContent += `
  1482. <div class="question">
  1483. <div class="question-header">
  1484. <span class="question-number">${index + 1}.</span> ${question.text}
  1485. </div>
  1486. <div class="answer">正确答案:${question.selectedAnswer} </div>
  1487. </div>`;
  1488. });
  1489. htmlContent += `
  1490. </div>`;
  1491. }
  1492. // 多选题
  1493. if (examData.multiple && examData.multiple.questions.length > 0) {
  1494. htmlContent += `
  1495. <div class="section">
  1496. <div class="section-title">三、多选题(${examData.multiple.count}题,每题${examData.multiple.scorePerQuestion}分,共${examData.multiple.totalScore}分)</div>`;
  1497. examData.multiple.questions.forEach((question, index) => {
  1498. htmlContent += `
  1499. <div class="question">
  1500. <div class="question-header">
  1501. <span class="question-number">${index + 1}.</span> ${question.text}
  1502. </div>
  1503. <div class="options">`;
  1504. question.options.forEach(option => {
  1505. htmlContent += `
  1506. <div class="option">${option.key}. ${option.text}</div>`;
  1507. });
  1508. htmlContent += `
  1509. </div>
  1510. <div class="answer">正确答案:${(question.selectedAnswers || []).join(', ')} </div>
  1511. </div>`;
  1512. });
  1513. htmlContent += `
  1514. </div>`;
  1515. }
  1516. // 简答题
  1517. if (examData.short && examData.short.questions.length > 0) {
  1518. htmlContent += `
  1519. <div class="section">
  1520. <div class="section-title">四、简答题(${examData.short.count}题,每题${examData.short.scorePerQuestion}分,共${examData.short.totalScore}分)</div>`;
  1521. examData.short.questions.forEach((question, index) => {
  1522. htmlContent += `
  1523. <div class="question">
  1524. <div class="question-header">
  1525. <span class="question-number">${index + 1}.</span> ${question.text}
  1526. </div>`;
  1527. if (question.outline) {
  1528. htmlContent += `
  1529. <div class="outline-section">
  1530. <strong>关键要点:</strong>${question.outline.keyFactors}
  1531. </div>
  1532. <div class="outline-section">
  1533. <strong>具体措施:</strong>${question.outline.measures}
  1534. </div>`;
  1535. }
  1536. htmlContent += `
  1537. </div>`;
  1538. });
  1539. htmlContent += `
  1540. </div>`;
  1541. }
  1542. htmlContent += `
  1543. </body>
  1544. </html>`;
  1545. return htmlContent;
  1546. };
  1547. // 文件上传相关方法
  1548. const triggerFileUpload = () => {
  1549. fileInput.value.click();
  1550. };
  1551. const handleFileSelect = async (event) => {
  1552. const file = event.target.files[0];
  1553. if (!file) return;
  1554. // 检查文件大小(20MB限制)
  1555. const maxSize = 20 * 1024 * 1024; // 20MB
  1556. if (file.size > maxSize) {
  1557. console.error('文件大小不能超过20MB');
  1558. return;
  1559. }
  1560. // 检查文件类型
  1561. const allowedTypes = ['application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'];
  1562. if (!allowedTypes.includes(file.type)) {
  1563. console.error('请选择PPT文件(.ppt/.pptx)');
  1564. return;
  1565. }
  1566. try {
  1567. selectedFile.value = {
  1568. name: file.name,
  1569. size: file.size,
  1570. icon: '📋'
  1571. };
  1572. // 读取PPT内容
  1573. const pptContent = await readPPTFile(file);
  1574. pptContentDescription.value = pptContent;
  1575. // 自动更新试卷名称(使用文件名,去掉扩展名)
  1576. const fileName = file.name.replace(/\.[^/.]+$/, '');
  1577. examName.value = fileName + '培训考核';
  1578. console.log('✅ PPT文件上传成功:', file.name);
  1579. } catch (error) {
  1580. console.error('PPT文件处理失败:', error);
  1581. selectedFile.value = null;
  1582. }
  1583. };
  1584. const removeSelectedFile = () => {
  1585. selectedFile.value = null;
  1586. pptContentDescription.value = '';
  1587. fileInput.value.value = '';
  1588. // 恢复默认试卷名称
  1589. const projectTypeName = projectTypes[selectedProjectType.value].name;
  1590. examName.value = projectTypeName + '工程施工技术考核';
  1591. };
  1592. // 读取PPT文件的文本内容
  1593. const readPPTFile = async (file) => {
  1594. return new Promise((resolve, reject) => {
  1595. const reader = new FileReader();
  1596. reader.onload = async (e) => {
  1597. try {
  1598. console.log('开始解析PPT文件...');
  1599. // 这里使用JSZip来解压PPTX文件并提取文本
  1600. // 注意:这是一个简化版本的实现
  1601. const arrayBuffer = e.target.result;
  1602. // 提取第一页的文本内容作为示例
  1603. const content = await extractTextFromPPT(arrayBuffer);
  1604. resolve(content);
  1605. } catch (error) {
  1606. console.error('PPT解析失败:', error);
  1607. reject(error);
  1608. }
  1609. };
  1610. reader.onerror = () => {
  1611. reject(new Error('文件读取失败'));
  1612. };
  1613. reader.readAsArrayBuffer(file);
  1614. });
  1615. };
  1616. // 从PPT提取文本(简化版本)
  1617. const extractTextFromPPT = async (arrayBuffer) => {
  1618. // 这是一个简化的实现,实际项目中需要使用专门的PPT解析库
  1619. return "提取的文本内容:" +
  1620. "PPT培训课件主要包含以下内容:" +
  1621. "1. 安全培训概述" +
  1622. "- 培训目标和意义" +
  1623. "- 培训对象和要求" +
  1624. "- 培训计划和安排" +
  1625. "2. 基础知识" +
  1626. "- 安全规章制度" +
  1627. "- 危险源识别" +
  1628. "- 应急处理方法" +
  1629. "3. 操作技能" +
  1630. "- 标准化操作流程" +
  1631. "- 安全操作规范" +
  1632. "- 事故预防措施" +
  1633. "4. 考核要求" +
  1634. "- 理论知识考核" +
  1635. "- 实操技能考核" +
  1636. "- 综合评估标准" +
  1637. "此PPT内容涵盖了安全培训的各个方面,适合制作综合性的考试题目。";
  1638. };
  1639. // 格式化文件大小
  1640. const formatFileSize = (bytes) => {
  1641. if (bytes === 0) return '0 B';
  1642. const k = 1024;
  1643. const sizes = ['B', 'KB', 'MB', 'GB'];
  1644. const i = Math.floor(Math.log(bytes) / Math.log(k));
  1645. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  1646. };
  1647. // 清理文本中的中括号和双引号
  1648. const cleanText = (text) => {
  1649. if (!text) return '';
  1650. return text.toString()
  1651. .replace(/[\[\]]/g, '') // 移除中括号
  1652. .replace(/[""]/g, '') // 移除双引号
  1653. .replace(/['']/g, '') // 移除单引号
  1654. .trim();
  1655. };
  1656. // ============ PC端辅助函数(用于刷新题目) ============
  1657. // 构建单题重新生成的提示词
  1658. const buildSingleQuestionPrompt = (sectionType, questionIndex) => {
  1659. const projectType = projectTypes[selectedProjectType.value].name;
  1660. const questionType = getQuestionTypeName(sectionType);
  1661. const scorePerQuestion = getQuestionScore(sectionType);
  1662. // 获取当前题目作为参考
  1663. const currentQuestion = getCurrentQuestion(sectionType, questionIndex);
  1664. let prompt = `请基于以下${projectType}工程的${questionType}题目,重新生成一道相似主题的题目,要求如下:
  1665. 当前题目参考:
  1666. ${JSON.stringify(currentQuestion, null, 2)}
  1667. 题目类型:${questionType}
  1668. 每题分值:${scorePerQuestion}分
  1669. 题目序号:第${questionIndex + 1}题
  1670. 请严格按照以下JSON格式返回,不要包含任何其他文字:
  1671. `;
  1672. // 根据题目类型添加不同的格式要求
  1673. if (sectionType === 'single') {
  1674. prompt += `{
  1675. "text": "题目内容",
  1676. "options": [
  1677. {"key": "A", "text": "选项A"},
  1678. {"key": "B", "text": "选项B"},
  1679. {"key": "C", "text": "选项C"},
  1680. {"key": "D", "text": "选项D"}
  1681. ],
  1682. "selectedAnswer": "A"
  1683. }`;
  1684. } else if (sectionType === 'judge') {
  1685. prompt += `{
  1686. "text": "题目内容",
  1687. "selectedAnswer": "正确"
  1688. }`;
  1689. } else if (sectionType === 'multiple') {
  1690. prompt += `{
  1691. "text": "题目内容",
  1692. "options": [
  1693. {"key": "A", "text": "选项A"},
  1694. {"key": "B", "text": "选项B"},
  1695. {"key": "C", "text": "选项C"},
  1696. {"key": "D", "text": "选项D"}
  1697. ],
  1698. "selectedAnswers": ["A", "B"]
  1699. }`;
  1700. } else if (sectionType === 'short') {
  1701. prompt += `{
  1702. "text": "题目内容",
  1703. "outline": {
  1704. "keyFactors": "关键要点内容",
  1705. "measures": "具体措施内容"
  1706. }
  1707. }`;
  1708. }
  1709. return prompt;
  1710. };
  1711. // 获取题目类型名称
  1712. const getQuestionTypeName = (sectionType) => {
  1713. const typeMap = {
  1714. 'single': '单选题',
  1715. 'judge': '判断题',
  1716. 'multiple': '多选题',
  1717. 'short': '简答题'
  1718. };
  1719. return typeMap[sectionType] || sectionType;
  1720. };
  1721. // 获取题目分值
  1722. const getQuestionScore = (sectionType) => {
  1723. if (currentExam.value[sectionType]) {
  1724. return currentExam.value[sectionType].scorePerQuestion;
  1725. }
  1726. return 5; // 默认分值
  1727. };
  1728. // 获取当前题目
  1729. const getCurrentQuestion = (sectionType, questionIndex) => {
  1730. if (currentExam.value[sectionType] && currentExam.value[sectionType].questions) {
  1731. return currentExam.value[sectionType].questions[questionIndex];
  1732. }
  1733. return null;
  1734. };
  1735. // 解析单题AI回复
  1736. const parseSingleQuestionResponse = (aiReply, sectionType) => {
  1737. try {
  1738. console.log('AI回复内容:', aiReply);
  1739. console.log('题目类型:', sectionType);
  1740. // 尝试提取JSON内容
  1741. const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
  1742. if (jsonMatch) {
  1743. const questionData = JSON.parse(jsonMatch[0]);
  1744. console.log('解析后的题目数据:', questionData);
  1745. // 如果是简答题,检查keyFactors字段
  1746. if (sectionType === 'short' && questionData.outline && questionData.outline.keyFactors) {
  1747. console.log('简答题keyFactors原始值:', questionData.outline.keyFactors);
  1748. // 如果keyFactors是数组,转换为字符串
  1749. if (Array.isArray(questionData.outline.keyFactors)) {
  1750. questionData.outline.keyFactors = questionData.outline.keyFactors.join(' ');
  1751. console.log('转换后的keyFactors:', questionData.outline.keyFactors);
  1752. }
  1753. }
  1754. return questionData;
  1755. } else {
  1756. console.error('未找到有效的JSON数据');
  1757. return null;
  1758. }
  1759. } catch (error) {
  1760. console.error('解析AI回复失败:', error);
  1761. return null;
  1762. }
  1763. };
  1764. // 更新题目
  1765. const updateQuestion = (sectionType, questionIndex, newQuestion) => {
  1766. let updatedQuestion;
  1767. if (sectionType === 'single') {
  1768. updatedQuestion = { ...newQuestion };
  1769. if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
  1770. updatedQuestion.selectedAnswer = updatedQuestion.options && updatedQuestion.options.length > 0 ? updatedQuestion.options[0].key : 'A';
  1771. }
  1772. currentExam.value.singleChoice.questions[questionIndex] = updatedQuestion;
  1773. } else if (sectionType === 'judge') {
  1774. updatedQuestion = { ...newQuestion };
  1775. if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
  1776. updatedQuestion.selectedAnswer = Math.random() > 0.5 ? '正确' : '错误';
  1777. }
  1778. currentExam.value.judge.questions[questionIndex] = updatedQuestion;
  1779. } else if (sectionType === 'multiple') {
  1780. updatedQuestion = { ...newQuestion };
  1781. if (!updatedQuestion.selectedAnswers || !Array.isArray(updatedQuestion.selectedAnswers)) {
  1782. updatedQuestion.selectedAnswers = updatedQuestion.options && updatedQuestion.options.length > 1 ? [updatedQuestion.options[0].key, updatedQuestion.options[1].key] : [];
  1783. }
  1784. currentExam.value.multiple.questions[questionIndex] = updatedQuestion;
  1785. } else if (sectionType === 'short') {
  1786. updatedQuestion = { ...newQuestion };
  1787. if (!updatedQuestion.outline) {
  1788. updatedQuestion.outline = {
  1789. keyFactors: "请参考相关教材和标准规范",
  1790. measures: "请结合实际工程案例进行解答"
  1791. };
  1792. }
  1793. currentExam.value.short.questions[questionIndex] = updatedQuestion;
  1794. }
  1795. console.log(`更新${sectionType}第${questionIndex + 1}题:`, updatedQuestion);
  1796. };
  1797. // 保存到reModifyQuestion接口
  1798. const saveToReModifyQuestion = async (sectionType, questionIndex, newQuestion) => {
  1799. console.log('对话id', ai_conversation_id.value);
  1800. try {
  1801. // 使用当前保存的对话ID
  1802. if (!ai_conversation_id.value) {
  1803. console.warn('没有找到对话ID,跳过保存');
  1804. return;
  1805. }
  1806. // 构建要保存的内容 - 保存整个试卷的JSON字符串
  1807. const content = JSON.stringify(currentExam.value);
  1808. console.log('保存到 /re_modify_question 的内容:', content);
  1809. // 调用后端接口保存修改
  1810. const response = await apis.reModifyQuestion({
  1811. ai_conversation_id: ai_conversation_id.value,
  1812. content: content
  1813. });
  1814. if (response.statusCode === 200) {
  1815. console.log('修改已保存到后端');
  1816. } else {
  1817. console.error('保存到后端失败:', response);
  1818. }
  1819. } catch (error) {
  1820. console.error('保存到后端失败:', error);
  1821. }
  1822. };
  1823. // 页面加载时不再自动加载历史记录,改为点击菜单时加载
  1824. onMounted(async () => {
  1825. try {
  1826. console.log('🚀 移动端考试工坊页面初始化完成')
  1827. } catch (error) {
  1828. console.error('❌ 移动端考试工坊页面初始化失败:', error)
  1829. }
  1830. })
  1831. // 监听历史记录抽屉显示状态,显示时加载数据
  1832. watch(showHistory, async (newVal) => {
  1833. if (newVal && historyData.value.length === 0) {
  1834. console.log('📋 历史记录抽屉打开,开始加载数据...')
  1835. await getHistoryRecordList()
  1836. }
  1837. })
  1838. </script>
  1839. <style lang="less" scoped>
  1840. .mobile-exam-workshop {
  1841. min-height: 100vh;
  1842. background: #EBF3FF;
  1843. font-family: "Alibaba PuHuiTi 3.0", sans-serif;
  1844. }
  1845. .mobile-content {
  1846. padding: 16px;
  1847. position: relative;
  1848. max-height: calc(100vh - 60px);
  1849. overflow-y: auto;
  1850. }
  1851. /* 考试工坊主界面样式 */
  1852. .exam-workshop-main {
  1853. .config-section {
  1854. background: white;
  1855. border-radius: 12px;
  1856. padding: 20px;
  1857. margin-bottom: 16px;
  1858. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  1859. .config-header {
  1860. display: flex;
  1861. align-items: center;
  1862. gap: 8px;
  1863. margin-bottom: 16px;
  1864. .step-number {
  1865. width: 28px;
  1866. height: 28px;
  1867. background: #3e7bfa;
  1868. color: white;
  1869. border-radius: 50%;
  1870. display: flex;
  1871. align-items: center;
  1872. justify-content: center;
  1873. font-size: 14px;
  1874. font-weight: 600;
  1875. }
  1876. h3 {
  1877. font-size: 20px;
  1878. font-weight: 600;
  1879. color: #1f2937;
  1880. margin: 0;
  1881. }
  1882. }
  1883. .type-cards {
  1884. .type-cards-row {
  1885. display: grid;
  1886. grid-template-columns: repeat(2, 1fr);
  1887. gap: 12px;
  1888. .type-card {
  1889. display: flex;
  1890. flex-direction: column;
  1891. align-items: center;
  1892. gap: 8px;
  1893. padding: 16px 12px;
  1894. border: 2px solid #e5e7eb;
  1895. border-radius: 12px;
  1896. background: white;
  1897. cursor: pointer;
  1898. transition: all 0.3s ease;
  1899. min-height: 80px;
  1900. justify-content: center;
  1901. &:hover {
  1902. border-color: #3e7bfa;
  1903. transform: translateY(-2px);
  1904. }
  1905. &.active {
  1906. border-color: #3e7bfa;
  1907. background: rgba(62, 123, 250, 0.1);
  1908. .type-icon {
  1909. filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
  1910. saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
  1911. }
  1912. span {
  1913. color: #3e7bfa;
  1914. }
  1915. }
  1916. .type-icon {
  1917. width: 32px;
  1918. height: 32px;
  1919. transition: filter 0.3s ease;
  1920. }
  1921. span {
  1922. font-size: 12px;
  1923. color: #374151;
  1924. font-weight: 500;
  1925. transition: color 0.3s ease;
  1926. text-align: center;
  1927. }
  1928. }
  1929. }
  1930. }
  1931. .generation-methods {
  1932. display: flex;
  1933. flex-direction: column;
  1934. gap: 12px;
  1935. .method-card {
  1936. display: flex;
  1937. align-items: flex-start;
  1938. gap: 12px;
  1939. padding: 16px;
  1940. border: 2px solid #e5e7eb;
  1941. border-radius: 12px;
  1942. background: white;
  1943. cursor: pointer;
  1944. transition: all 0.3s ease;
  1945. position: relative;
  1946. &:hover {
  1947. border-color: #3e7bfa;
  1948. }
  1949. &.active {
  1950. border-color: #3e7bfa;
  1951. background: rgba(62, 123, 250, 0.1);
  1952. .method-icon {
  1953. filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
  1954. saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
  1955. }
  1956. .method-content h4 {
  1957. color: #3e7bfa;
  1958. }
  1959. }
  1960. .method-icon {
  1961. width: 24px;
  1962. height: 24px;
  1963. flex-shrink: 0;
  1964. transition: filter 0.3s ease;
  1965. margin-top: 2px;
  1966. }
  1967. .method-content {
  1968. flex: 1;
  1969. h4 {
  1970. font-size: 14px;
  1971. font-weight: 600;
  1972. color: #1f2937;
  1973. margin: 0 0 4px 0;
  1974. transition: color 0.3s ease;
  1975. }
  1976. p {
  1977. font-size: 12px;
  1978. color: #6b7280;
  1979. margin: 0;
  1980. line-height: 1.4;
  1981. }
  1982. .ppt-file-preview {
  1983. margin-top: 8px;
  1984. .file-preview {
  1985. display: flex;
  1986. align-items: center;
  1987. background: #f9fafb;
  1988. border: 1px solid #e5e7eb;
  1989. border-radius: 8px;
  1990. padding: 8px;
  1991. .file-icon {
  1992. font-size: 16px;
  1993. margin-right: 8px;
  1994. width: 20px;
  1995. text-align: center;
  1996. }
  1997. .file-info {
  1998. flex: 1;
  1999. .file-name {
  2000. font-size: 11px;
  2001. font-weight: 500;
  2002. color: #374151;
  2003. overflow: hidden;
  2004. text-overflow: ellipsis;
  2005. white-space: nowrap;
  2006. max-width: 120px;
  2007. }
  2008. .file-size {
  2009. font-size: 10px;
  2010. color: #9ca3af;
  2011. }
  2012. }
  2013. .remove-file-btn {
  2014. background: none;
  2015. border: none;
  2016. cursor: pointer;
  2017. color: #6b7280;
  2018. padding: 2px;
  2019. .remove-icon {
  2020. font-size: 14px;
  2021. }
  2022. &:hover {
  2023. color: #ef4444;
  2024. }
  2025. }
  2026. }
  2027. }
  2028. }
  2029. }
  2030. }
  2031. .exam-config-container {
  2032. .config-main {
  2033. .config-form {
  2034. display: grid;
  2035. grid-template-columns: 2fr 1fr;
  2036. gap: 12px;
  2037. margin-bottom: 16px;
  2038. .form-group {
  2039. label {
  2040. display: block;
  2041. font-size: 16px;
  2042. font-weight: 600;
  2043. color: #374151;
  2044. margin-bottom: 6px;
  2045. }
  2046. .input-wrapper,
  2047. .score-input {
  2048. position: relative;
  2049. .config-input {
  2050. width: 100%;
  2051. height: 48px;
  2052. padding: 12px 50px 12px 16px;
  2053. border: 1px solid #d1d5db;
  2054. border-radius: 8px;
  2055. font-size: 16px !important;
  2056. background: white;
  2057. box-sizing: border-box;
  2058. line-height: 1.5;
  2059. &:focus {
  2060. outline: none;
  2061. border-color: #3e7bfa;
  2062. box-shadow: 0 0 0 2px rgba(62, 123, 250, 0.1);
  2063. }
  2064. &:disabled {
  2065. background: #f9fafb;
  2066. color: #9ca3af;
  2067. }
  2068. }
  2069. .char-count {
  2070. position: absolute;
  2071. right: 12px;
  2072. top: 50%;
  2073. transform: translateY(-50%);
  2074. font-size: 12px;
  2075. color: #6b7280;
  2076. font-weight: 500;
  2077. &.warning {
  2078. color: #f59e0b;
  2079. }
  2080. }
  2081. .unit {
  2082. position: absolute;
  2083. right: 12px;
  2084. top: 50%;
  2085. transform: translateY(-50%);
  2086. color: #6b7280;
  2087. font-size: 18px;
  2088. font-weight: 500;
  2089. }
  2090. }
  2091. .score-input {
  2092. .config-input {
  2093. padding-right: 50px;
  2094. }
  2095. }
  2096. }
  2097. }
  2098. .question-types-title {
  2099. font-size: 16px;
  2100. font-weight: 600;
  2101. color: #374151;
  2102. margin: 20px 0 12px 0;
  2103. }
  2104. .question-types {
  2105. .question-type {
  2106. background: #f9fafb;
  2107. border-radius: 8px;
  2108. padding: 12px;
  2109. margin-bottom: 8px;
  2110. .type-header {
  2111. display: flex;
  2112. align-items: center;
  2113. gap: 8px;
  2114. margin-bottom: 8px;
  2115. .type-name {
  2116. font-size: 14px;
  2117. font-weight: 600;
  2118. color: #374151;
  2119. white-space: nowrap;
  2120. }
  2121. .progress-bar {
  2122. flex: 1;
  2123. height: 4px;
  2124. background: #e5e7eb;
  2125. border-radius: 2px;
  2126. overflow: hidden;
  2127. .progress-fill {
  2128. height: 100%;
  2129. background: #3e7bfa;
  2130. transition: width 0.3s ease;
  2131. }
  2132. }
  2133. }
  2134. .score-config {
  2135. display: grid;
  2136. grid-template-columns: repeat(2, 1fr);
  2137. gap: 8px;
  2138. .config-item {
  2139. display: flex;
  2140. align-items: center;
  2141. gap: 4px;
  2142. font-size: 13px;
  2143. span {
  2144. color: #6b7280;
  2145. white-space: nowrap;
  2146. }
  2147. .score-input-field,
  2148. .count-input-field {
  2149. width: 50px;
  2150. padding: 6px 8px;
  2151. border: 1px solid #d1d5db;
  2152. border-radius: 4px;
  2153. font-size: 13px;
  2154. text-align: center;
  2155. &:focus {
  2156. outline: none;
  2157. border-color: #3e7bfa;
  2158. }
  2159. &:disabled {
  2160. background: #f3f4f6;
  2161. color: #9ca3af;
  2162. }
  2163. }
  2164. }
  2165. }
  2166. }
  2167. }
  2168. }
  2169. .preview-panel {
  2170. background: #f8fafc;
  2171. border-radius: 8px;
  2172. padding: 16px;
  2173. margin-top: 16px;
  2174. border: 1px solid #e5e7eb;
  2175. .preview-header {
  2176. display: flex;
  2177. align-items: center;
  2178. gap: 6px;
  2179. margin-bottom: 12px;
  2180. .preview-icon {
  2181. width: 16px;
  2182. height: 16px;
  2183. }
  2184. h3 {
  2185. font-size: 16px;
  2186. font-weight: 600;
  2187. color: #374151;
  2188. margin: 0;
  2189. }
  2190. }
  2191. .preview-content {
  2192. .preview-title {
  2193. font-size: 16px;
  2194. font-weight: 600;
  2195. color: #1f2937;
  2196. margin: 0 0 12px 0;
  2197. line-height: 1.4;
  2198. }
  2199. .question-breakdown {
  2200. .breakdown-item {
  2201. margin-bottom: 6px;
  2202. .breakdown-row {
  2203. display: flex;
  2204. justify-content: space-between;
  2205. align-items: flex-start;
  2206. font-size: 13px;
  2207. .breakdown-left {
  2208. color: #4b5563;
  2209. flex: 1;
  2210. margin-right: 8px;
  2211. line-height: 1.3;
  2212. }
  2213. .breakdown-right {
  2214. color: #6b7280;
  2215. white-space: nowrap;
  2216. }
  2217. }
  2218. }
  2219. }
  2220. .divider {
  2221. height: 1px;
  2222. background: #e5e7eb;
  2223. margin: 8px 0;
  2224. }
  2225. .calculated-score-row,
  2226. .total-score-row {
  2227. display: flex;
  2228. justify-content: space-between;
  2229. margin-bottom: 4px;
  2230. font-size: 13px;
  2231. .calculated-label,
  2232. .total-label {
  2233. color: #6b7280;
  2234. }
  2235. .calculated-value,
  2236. .total-value {
  2237. color: #1f2937;
  2238. font-weight: 500;
  2239. }
  2240. }
  2241. }
  2242. }
  2243. }
  2244. .bottom-actions {
  2245. display: flex;
  2246. justify-content: center;
  2247. align-items: center;
  2248. gap: 24px;
  2249. margin-top: 20px;
  2250. width: 100%;
  2251. .clear-btn {
  2252. height: 34px;
  2253. padding: 0 16px;
  2254. border: 1px solid #d1d5db;
  2255. background: white;
  2256. color: #6b7280;
  2257. border-radius: 6px;
  2258. font-size: 14px;
  2259. font-weight: 500;
  2260. cursor: pointer;
  2261. transition: all 0.3s ease;
  2262. display: flex;
  2263. align-items: center;
  2264. justify-content: center;
  2265. &:disabled {
  2266. cursor: not-allowed !important;
  2267. pointer-events: none;
  2268. opacity: 0.5;
  2269. }
  2270. &:hover:not(:disabled) {
  2271. background: #f9fafb;
  2272. border-color: #9ca3af;
  2273. }
  2274. }
  2275. .generate-btn {
  2276. height: 34px;
  2277. padding: 0;
  2278. border: none;
  2279. background: none;
  2280. cursor: pointer;
  2281. transition: opacity 0.3s ease;
  2282. display: flex;
  2283. align-items: center;
  2284. justify-content: center;
  2285. &:disabled {
  2286. background: #f3f4f6;
  2287. color: #9ca3af;
  2288. cursor: not-allowed;
  2289. }
  2290. .generate-icon {
  2291. width: 107px;
  2292. height: 34px;
  2293. }
  2294. .generating-text {
  2295. width: 107px;
  2296. height: 34px;
  2297. display: flex;
  2298. align-items: center;
  2299. justify-content: center;
  2300. font-size: 16px;
  2301. font-weight: 600;
  2302. color: #374151;
  2303. background: #f3f4f6;
  2304. border-radius: 6px;
  2305. border: 1px solid #d1d5db;
  2306. }
  2307. &.disabled .generate-icon {
  2308. opacity: 0.5;
  2309. }
  2310. }
  2311. }
  2312. }
  2313. }
  2314. /* 考试详情页样式 */
  2315. .exam-detail-main {
  2316. .detail-header {
  2317. display: flex;
  2318. justify-content: space-between;
  2319. align-items: center;
  2320. padding: 12px 0;
  2321. margin-bottom: 16px;
  2322. .back-btn {
  2323. display: flex;
  2324. align-items: center;
  2325. gap: 6px;
  2326. padding: 8px 12px;
  2327. border-radius: 8px;
  2328. font-size: 13px;
  2329. cursor: pointer;
  2330. border: 1px solid #d1d5db;
  2331. background: white;
  2332. color: #374151;
  2333. transition: all 0.3s ease;
  2334. &:disabled {
  2335. opacity: 0.5;
  2336. cursor: not-allowed;
  2337. }
  2338. &:hover:not(:disabled) {
  2339. background: #f9fafb;
  2340. border-color: #9ca3af;
  2341. }
  2342. .back-arrow {
  2343. font-size: 14px;
  2344. }
  2345. }
  2346. .download-dropdown {
  2347. position: relative;
  2348. display: inline-block;
  2349. &.disabled {
  2350. opacity: 0.5;
  2351. cursor: not-allowed;
  2352. pointer-events: none;
  2353. }
  2354. .download-btn {
  2355. height: 34px;
  2356. padding: 0;
  2357. border: none;
  2358. background: transparent;
  2359. cursor: pointer;
  2360. transition: opacity 0.3s ease;
  2361. display: flex;
  2362. align-items: center;
  2363. justify-content: center;
  2364. &:disabled {
  2365. opacity: 0.5;
  2366. cursor: not-allowed !important;
  2367. pointer-events: none;
  2368. }
  2369. &:hover:not(:disabled) {
  2370. opacity: 0.8;
  2371. }
  2372. .download-icon {
  2373. width: 107px;
  2374. height: 34px;
  2375. }
  2376. }
  2377. .dropdown-menu {
  2378. position: absolute;
  2379. top: 100%;
  2380. right: 0;
  2381. background: white;
  2382. border: 1px solid #e5e7eb;
  2383. border-radius: 8px;
  2384. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  2385. z-index: 1000;
  2386. min-width: 120px;
  2387. opacity: 0;
  2388. visibility: hidden;
  2389. transform: translateY(-10px);
  2390. transition: all 0.3s ease;
  2391. .dropdown-item {
  2392. padding: 12px 16px;
  2393. cursor: pointer;
  2394. transition: background-color 0.2s ease;
  2395. border-bottom: 1px solid #f3f4f6;
  2396. &:last-child {
  2397. border-bottom: none;
  2398. }
  2399. &:hover {
  2400. background: #f8fafc;
  2401. }
  2402. &:disabled {
  2403. opacity: 0.5;
  2404. cursor: not-allowed;
  2405. pointer-events: none;
  2406. }
  2407. .item-text {
  2408. font-size: 14px;
  2409. color: #374151;
  2410. }
  2411. }
  2412. }
  2413. &.show .dropdown-menu {
  2414. opacity: 1;
  2415. visibility: visible;
  2416. transform: translateY(0);
  2417. }
  2418. }
  2419. }
  2420. .exam-info {
  2421. background: white;
  2422. border-radius: 12px;
  2423. padding: 20px;
  2424. margin-bottom: 16px;
  2425. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  2426. .exam-title {
  2427. font-size: 20px;
  2428. font-weight: 600;
  2429. color: #1f2937;
  2430. margin: 0 0 12px 0;
  2431. line-height: 1.4;
  2432. }
  2433. .exam-stats {
  2434. display: flex;
  2435. gap: 16px;
  2436. margin-bottom: 8px;
  2437. .total-score,
  2438. .question-count {
  2439. font-size: 15px;
  2440. color: #6b7280;
  2441. font-weight: 500;
  2442. }
  2443. }
  2444. .generation-time {
  2445. font-size: 14px;
  2446. color: #9ca3af;
  2447. }
  2448. }
  2449. .question-sections {
  2450. .question-section {
  2451. background: white;
  2452. border-radius: 12px;
  2453. margin-bottom: 16px;
  2454. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  2455. overflow: hidden;
  2456. .section-header {
  2457. display: flex;
  2458. justify-content: space-between;
  2459. align-items: center;
  2460. padding: 16px 20px;
  2461. background: #f8fafc;
  2462. border-bottom: 1px solid #e5e7eb;
  2463. .section-title {
  2464. flex: 1;
  2465. .section-number {
  2466. font-size: 16px;
  2467. font-weight: 600;
  2468. color: #1f2937;
  2469. }
  2470. .section-name {
  2471. font-size: 16px;
  2472. font-weight: 600;
  2473. color: #1f2937;
  2474. margin: 0 8px;
  2475. }
  2476. .section-score {
  2477. font-size: 14px;
  2478. color: #6b7280;
  2479. }
  2480. }
  2481. .section-controls {
  2482. display: flex;
  2483. align-items: center;
  2484. gap: 8px;
  2485. .question-count-text {
  2486. font-size: 14px;
  2487. color: #6b7280;
  2488. font-weight: 500;
  2489. }
  2490. .toggle-icon {
  2491. width: 16px;
  2492. height: 16px;
  2493. transition: transform 0.3s ease;
  2494. &.expanded {
  2495. transform: rotate(180deg);
  2496. }
  2497. }
  2498. }
  2499. }
  2500. .section-content {
  2501. padding: 0;
  2502. .question-item {
  2503. border-bottom: 1px solid #f3f4f6;
  2504. padding: 16px 20px;
  2505. &:last-child {
  2506. border-bottom: none;
  2507. }
  2508. .question-header {
  2509. display: flex;
  2510. align-items: flex-start;
  2511. gap: 8px;
  2512. margin-bottom: 12px;
  2513. .question-number {
  2514. font-size: 16px;
  2515. font-weight: 600;
  2516. color: #3e7bfa;
  2517. flex-shrink: 0;
  2518. }
  2519. .question-text {
  2520. flex: 1;
  2521. font-size: 16px;
  2522. line-height: 1.5;
  2523. color: #374151;
  2524. font-weight: 500;
  2525. }
  2526. .refresh-btn {
  2527. background: none;
  2528. border: none;
  2529. cursor: pointer;
  2530. padding: 4px;
  2531. color: #6b7280;
  2532. flex-shrink: 0;
  2533. &:disabled {
  2534. cursor: not-allowed;
  2535. opacity: 0.5;
  2536. }
  2537. &:hover:not(:disabled) {
  2538. color: #3e7bfa;
  2539. }
  2540. .refresh-icon {
  2541. width: 16px;
  2542. height: 16px;
  2543. transition: transform 0.3s ease;
  2544. &.rotating {
  2545. transform: rotate(360deg);
  2546. }
  2547. }
  2548. }
  2549. }
  2550. .options {
  2551. margin-bottom: 12px;
  2552. .option {
  2553. display: flex;
  2554. align-items: flex-start;
  2555. gap: 8px;
  2556. margin-bottom: 8px;
  2557. .radio-wrapper {
  2558. flex-shrink: 0;
  2559. margin-top: 2px;
  2560. .radio-circle {
  2561. width: 14px;
  2562. height: 14px;
  2563. border: 1.5px solid #d1d5db;
  2564. border-radius: 50%;
  2565. display: flex;
  2566. align-items: center;
  2567. justify-content: center;
  2568. transition: all 0.3s ease;
  2569. &.selected {
  2570. border-color: #3e7bfa;
  2571. background: #3e7bfa;
  2572. .radio-dot {
  2573. width: 6px;
  2574. height: 6px;
  2575. background: white;
  2576. border-radius: 50%;
  2577. }
  2578. }
  2579. }
  2580. }
  2581. .option-key {
  2582. font-size: 15px;
  2583. font-weight: 600;
  2584. color: #374151;
  2585. flex-shrink: 0;
  2586. }
  2587. .option-content {
  2588. flex: 1;
  2589. .option-text {
  2590. font-size: 15px;
  2591. line-height: 1.4;
  2592. color: #374151;
  2593. font-weight: 500;
  2594. }
  2595. }
  2596. }
  2597. }
  2598. .answer-section {
  2599. padding: 8px 12px;
  2600. background: #f8fafc;
  2601. border-radius: 6px;
  2602. display: flex;
  2603. align-items: center;
  2604. gap: 8px;
  2605. .answer-label {
  2606. font-size: 14px;
  2607. color: #6b7280;
  2608. font-weight: 600;
  2609. }
  2610. .answer-value {
  2611. font-size: 14px;
  2612. color: #1f2937;
  2613. font-weight: 600;
  2614. }
  2615. }
  2616. .answer-outline {
  2617. .outline-section {
  2618. margin-bottom: 12px;
  2619. padding: 12px;
  2620. background: #f8fafc;
  2621. border-radius: 6px;
  2622. font-size: 14px;
  2623. line-height: 1.5;
  2624. strong {
  2625. color: #374151;
  2626. display: block;
  2627. margin-bottom: 4px;
  2628. font-weight: 600;
  2629. }
  2630. color: #6b7280;
  2631. }
  2632. }
  2633. }
  2634. }
  2635. }
  2636. }
  2637. }
  2638. </style>