ExamWorkshop.vue 175 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802
  1. <template>
  2. <div class="chat-container">
  3. <!-- 最左侧边栏 -->
  4. <Sidebar v-if="!hideSidebar" />
  5. <!-- 中间历史记录区域 -->
  6. <div class="history-sidebar" v-if="!hideSidebar">
  7. <div class="history-header">
  8. <span class="section-title">历史记录</span>
  9. <img
  10. src="@/assets/Chat/2.png"
  11. alt="新建任务"
  12. class="new-chat-btn"
  13. @click="createNewChat"
  14. />
  15. </div>
  16. <div class="history-list">
  17. <!-- 历史记录加载状态 -->
  18. <div v-if="isLoadingHistory && historyTotal === 0" class="history-loading">
  19. <div class="loading-spinner"></div>
  20. <div class="loading-text">正在加载历史记录...</div>
  21. </div>
  22. <!-- 有历史记录时显示 -->
  23. <div
  24. v-else-if="historyTotal > 0"
  25. v-for="(item, index) in historyData"
  26. :key="index"
  27. :class="['history-item', { active: item.isActive }]"
  28. @click="item.isActive ? null : (isGenerating || isLoadingHistoryItem ? null : handleHistoryItem(item))"
  29. :style="{ cursor: item.isActive ? 'default' : (isGenerating || isLoadingHistoryItem ? 'not-allowed' : 'pointer'), opacity: item.isActive ? '0.8' : '1' }"
  30. >
  31. <div class="history-content">
  32. <div class="history-title">{{ item.title }}</div>
  33. <div class="history-time">{{ item.time }}</div>
  34. </div>
  35. <div
  36. class="delete-btn"
  37. @click.stop="deleteHistoryItem(item, index)"
  38. :class="{ 'always-visible': item.isActive }"
  39. >
  40. <img src="/src/assets/AIWriting/8.png" alt="删除" class="delete-icon" />
  41. </div>
  42. </div>
  43. <!-- 无历史记录时显示空状态 -->
  44. <div v-else class="empty-history">
  45. <img src="@/assets/Chat/22.png" alt="暂无数据" class="empty-icon">
  46. <div class="empty-text">暂无数据</div>
  47. </div>
  48. </div>
  49. </div>
  50. <!-- 右侧工作区域 -->
  51. <div class="main-work" :style="{ background: showExamDetail ? 'transparent' : '#ebf3ff', position: 'relative' }">
  52. <!-- 头部 -->
  53. <div class="work-header" v-if="showExamDetail">
  54. <h2>考试工坊</h2>
  55. </div>
  56. <!-- 工作内容区域 -->
  57. <div class="work-content" :class="{ 'exam-detail-mode': showExamDetail }">
  58. <!-- 加载状态遮罩 -->
  59. <div v-if="isLoadingHistoryItem" class="loading-overlay">
  60. <div class="loading-spinner"></div>
  61. <p>正在加载历史记录...</p>
  62. </div>
  63. <!-- 考试工坊主界面 -->
  64. <div v-if="!showExamDetail" class="exam-workshop-card app-container">
  65. <!-- 中间主操作区 -->
  66. <main class="main-content" style="padding-top: 36px; position: relative;">
  67. <!-- 返回AI问答按钮 -->
  68. <button v-if="!showExamDetail" class="return-ai-btn has-before" @click="handleReturnToAI">
  69. 返回AI问答
  70. </button>
  71. <div class="form-group" style="position: relative;">
  72. <label class="form-label">试卷名称</label>
  73. <input type="text" class="form-control" v-model="examName" maxlength="32" placeholder="请输入试卷名称..." :disabled="isGenerating">
  74. <div class="char-count">{{ examName?.length || 0 }}/32</div>
  75. </div>
  76. <div class="form-group">
  77. <label class="form-label">出题依据内容</label>
  78. <textarea class="form-control" v-model="questionBasis" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || uploadedFiles.length > 0"></textarea>
  79. <div class="ppt-upload-section" style="flex-direction: column; align-items: flex-start;" @click="!isGenerating ? triggerFileUpload() : null">
  80. <div style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
  81. <div class="ppt-upload-content">
  82. <div class="ppt-upload-icon-wrapper">
  83. <el-icon style="font-size: 28px; color: #4b5563;"><UploadFilled /></el-icon>
  84. </div>
  85. <div class="ppt-upload-text-wrapper">
  86. <div class="ppt-upload-title">从PPT生成考题</div>
  87. <div class="ppt-upload-hint">上传培训PPT,智能提取关键内容生成考题(支持多文件,单文件20M内)</div>
  88. </div>
  89. </div>
  90. <el-icon class="ppt-arrow"><ArrowRight /></el-icon>
  91. </div>
  92. <div v-if="uploadedFiles.length > 0" class="files-list" @click.stop style="width: 100%; display: flex; flex-wrap: wrap; gap: 8px;">
  93. <div v-for="(file, index) in uploadedFiles" :key="index" class="file-status-badge">
  94. <span class="file-name truncate">已上传: {{ file.name }}</span>
  95. <span @click.stop="removeSelectedFile(index)" class="remove-btn">×</span>
  96. </div>
  97. </div>
  98. </div>
  99. </div>
  100. <!-- =============== 题型配置区域 开始 =============== -->
  101. <div class="config-section">
  102. <div class="config-header">
  103. <h3>题型配置</h3>
  104. <div class="total-score">试卷总分 {{ calculatedTotalScore }}</div>
  105. </div>
  106. <!-- 动态渲染各题型 -->
  107. <div class="question-types-grid">
  108. <div class="question-type-card" v-for="(type, index) in questionTypes" :key="index">
  109. <div class="question-type-header">
  110. <div class="question-type-title">{{ type.name }}</div>
  111. <div class="question-type-score">
  112. 每题 <input type="number" class="score-input" v-model.number="type.scorePerQuestion" min="1" max="100" :disabled="isGenerating"> 分
  113. </div>
  114. </div>
  115. <div class="slider-container">
  116. <span class="slider-label">数量</span>
  117. <input type="range" class="question-slider" v-model.number="type.questionCount" min="0" :max="type.max || 50" :disabled="isGenerating">
  118. <div class="question-count-stepper">
  119. <span class="question-count">{{ type.questionCount }} 题</span>
  120. <div class="stepper-buttons">
  121. <button
  122. class="stepper-btn stepper-btn-up"
  123. type="button"
  124. @click="adjustQuestionCount(type, 1)"
  125. :disabled="isGenerating || type.questionCount >= 99"
  126. aria-label="增加题目数量"
  127. ></button>
  128. <button
  129. class="stepper-btn stepper-btn-down"
  130. type="button"
  131. @click="adjustQuestionCount(type, -1)"
  132. :disabled="isGenerating || type.questionCount <= 0"
  133. aria-label="减少题目数量"
  134. ></button>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. <div class="action-buttons">
  141. <button class="clear-btn" @click="clearSettings" :disabled="isGenerating">
  142. <el-icon style="font-size: 18px; margin-right: 4px;"><Delete /></el-icon>
  143. 清空当前配置
  144. </button>
  145. <button class="generate-btn" @click="generateExam" :disabled="isGenerating">
  146. <el-icon v-if="!isGenerating" style="margin-right: 6px;"><MagicStick /></el-icon>
  147. <el-icon v-else class="is-loading" style="margin-right: 6px;"><Loading /></el-icon>
  148. {{ isGenerating ? '生成中...' : '开始智能生成试卷' }}
  149. </button>
  150. </div>
  151. </div>
  152. <!-- =============== 题型配置区域 结束 =============== -->
  153. </main>
  154. <!-- =============== 实时预览区域 开始 =============== -->
  155. <aside class="preview-panel">
  156. <div class="preview-header">
  157. <h3>实时预览</h3>
  158. </div>
  159. <div class="preview-name-card">
  160. <div class="preview-name-label">试卷名称</div>
  161. <div class="preview-title" :style="{ fontStyle: examName ? 'normal' : 'italic' }">{{ examName || '未命名试卷...' }}</div>
  162. </div>
  163. <div class="preview-section">
  164. <div class="preview-section-title">结构大纲</div>
  165. <div class="preview-item" v-for="(type, index) in questionTypes" :key="index">
  166. <div class="preview-item-top">
  167. <div class="preview-item-left">
  168. <div class="preview-dot" :style="{ backgroundColor: ['#2563eb', '#60a5fa', '#93c5fd', '#dbeafe'][index % 4] }"></div>
  169. <span class="preview-type-name">{{ type.name }}</span>
  170. </div>
  171. <span class="preview-type-count">{{ type.questionCount }}题</span>
  172. </div>
  173. <div class="preview-item-bottom">
  174. <span class="preview-type-score">{{ type.questionCount * type.scorePerQuestion }} 分</span>
  175. </div>
  176. </div>
  177. </div>
  178. <div class="preview-footer">
  179. <div class="preview-total">
  180. <span>配置总分</span>
  181. <span class="preview-total-score" style="color: #000000; font-size: 24px;">{{ totalScore }}</span>
  182. </div>
  183. <div class="preview-total" style="margin-top: 20px; font-size: 20px; color: #000000;">
  184. <span>试卷总分</span>
  185. <span style="color: var(--primary-color); font-size: 24px;">{{ calculatedTotalScore }}</span>
  186. </div>
  187. </div>
  188. </aside>
  189. <!-- =============== 实时预览区域 结束 =============== -->
  190. </div>
  191. <!-- 考试详情页 -->
  192. <div v-if="showExamDetail" class="exam-detail-card">
  193. <!-- 详情页头部 -->
  194. <div class="detail-header">
  195. <div class="header-left">
  196. </div>
  197. <div class="header-right" style="display: flex; align-items: center; gap: 12px;">
  198. <button class="return-ai-btn has-before" style="position: static;" @click="backToConfig" :disabled="isGenerating">
  199. 返回修改
  200. </button>
  201. <!-- <button class="save-btn" @click="saveExam" :disabled="isGenerating">
  202. <img :src="saveIcon" alt="保存试卷" class="save-icon" />
  203. </button> -->
  204. <div class="download-dropdown" :class="{ 'disabled': isGenerating, 'show': showDownloadMenu }" @click.stop>
  205. <button class="download-btn" :disabled="isGenerating" @click="toggleDownloadMenu">
  206. <img :src="downloadIcon" alt="下载Word" class="download-icon" />
  207. </button>
  208. <div class="dropdown-menu">
  209. <div class="dropdown-item" @click="exportToWordWithAnswers" :disabled="isGenerating">
  210. <span class="item-text">有答案</span>
  211. </div>
  212. <div class="dropdown-item" @click="exportToWordWithoutAnswers" :disabled="isGenerating">
  213. <span class="item-text">无答案</span>
  214. </div>
  215. </div>
  216. </div>
  217. </div>
  218. </div>
  219. <!-- 试卷信息 -->
  220. <div class="exam-info">
  221. <div>
  222. <h1 class="exam-title">{{ currentExam.title }}</h1>
  223. <div class="exam-stats">
  224. <span class="total-score">总分: {{ currentExam.totalScore }}分</span>
  225. <span class="question-count">题量: {{ currentExam.totalQuestions }}题</span>
  226. </div>
  227. </div>
  228. <!-- 生成时间 -->
  229. <div class="generation-time">生成时间: {{ currentTime}}</div>
  230. </div>
  231. <!-- 题型列表 -->
  232. <div class="question-sections">
  233. <!-- 单选题 -->
  234. <div class="question-section">
  235. <div class="section-header" @click="isGenerating ? null : toggleSection('single')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  236. <div class="section-title">
  237. <span class="section-number">一</span>
  238. <span class="section-name">单选题</span>
  239. <span class="section-score">(每题{{ currentExam.singleChoice.scorePerQuestion }}分, 共{{ currentExam.singleChoice.totalScore }}分)</span>
  240. </div>
  241. <div class="section-controls">
  242. <span class="question-count-text">{{ currentExam.singleChoice.count }}题</span>
  243. <img
  244. :src="expandIcon"
  245. alt="收起/展开"
  246. class="toggle-icon"
  247. :class="{ 'expanded': !expandedSections.single }"
  248. />
  249. </div>
  250. </div>
  251. <div v-if="expandedSections.single" class="section-content">
  252. <div
  253. v-for="(question, index) in currentExam.singleChoice.questions"
  254. :key="index"
  255. class="question-item"
  256. >
  257. <div class="question-header">
  258. <span class="question-number">{{ index + 1 }}.</span>
  259. <span class="question-text">{{ question.text }}</span>
  260. <button class="refresh-btn" @click="refreshQuestion('single', index)" :disabled="isGenerating">
  261. <img
  262. :src="collapseIcon"
  263. alt="刷新"
  264. class="refresh-icon"
  265. :class="{ 'rotating': isRefreshing[`single_${index}`] }"
  266. />
  267. </button>
  268. </div>
  269. <div class="options">
  270. <div
  271. v-for="option in question.options"
  272. :key="option.key"
  273. :class="['option', { selected: question.selectedAnswer === option.key }]"
  274. :style="{ cursor: 'default' }"
  275. >
  276. <div class="radio-wrapper">
  277. <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === option.key }">
  278. <div v-if="question.selectedAnswer === option.key" class="radio-dot"></div>
  279. </div>
  280. </div>
  281. <span class="option-key">{{ option.key }}.</span>
  282. <div class="option-content">
  283. <span class="option-text">{{ option.text }}</span>
  284. <!-- 编辑按钮已禁用 -->
  285. <!-- <button
  286. class="edit-option-btn"
  287. @click.stop="openEditModal('single', index, option.key)"
  288. :disabled="isGenerating"
  289. title="编辑选项"
  290. >
  291. <img src="@/assets/Chat/13.png" alt="编辑" class="edit-icon" />
  292. </button> -->
  293. </div>
  294. </div>
  295. </div>
  296. </div>
  297. </div>
  298. </div>
  299. <!-- 判断题 -->
  300. <div class="question-section">
  301. <div class="section-header" @click="isGenerating ? null : toggleSection('judge')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  302. <div class="section-title">
  303. <span class="section-number">二</span>
  304. <span class="section-name">判断题</span>
  305. <span class="section-score">(每题{{ currentExam.judge.scorePerQuestion }}分, 共{{ currentExam.judge.totalScore }}分)</span>
  306. </div>
  307. <div class="section-controls">
  308. <span class="question-count-text">{{ currentExam.judge.count }}题</span>
  309. <img
  310. :src="expandIcon"
  311. alt="收起/展开"
  312. class="toggle-icon"
  313. :class="{ 'expanded': !expandedSections.judge }"
  314. />
  315. </div>
  316. </div>
  317. <div v-if="expandedSections.judge" class="section-content">
  318. <div
  319. v-for="(question, index) in currentExam.judge.questions"
  320. :key="index"
  321. class="question-item"
  322. >
  323. <div class="question-header">
  324. <span class="question-number">{{ index + 1 }}.</span>
  325. <span class="question-text">{{ question.text }}</span>
  326. <button class="refresh-btn" @click="refreshQuestion('judge', index)" :disabled="isGenerating">
  327. <img
  328. :src="collapseIcon"
  329. alt="刷新"
  330. class="refresh-icon"
  331. :class="{ 'rotating': isRefreshing[`judge_${index}`] }"
  332. />
  333. </button>
  334. </div>
  335. <div class="options">
  336. <div
  337. class="option judge-option"
  338. :class="{ selected: question.selectedAnswer === '正确' }"
  339. :style="{ cursor: 'default' }"
  340. >
  341. <div class="radio-wrapper">
  342. <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === '正确' }">
  343. <div v-if="question.selectedAnswer === '正确'" class="radio-dot"></div>
  344. </div>
  345. </div>
  346. <span class="option-text">正确</span>
  347. </div>
  348. <div
  349. class="option judge-option"
  350. :class="{ selected: question.selectedAnswer === '错误' }"
  351. :style="{ cursor: 'default' }"
  352. >
  353. <div class="radio-wrapper">
  354. <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === '错误' }">
  355. <div v-if="question.selectedAnswer === '错误'" class="radio-dot"></div>
  356. </div>
  357. </div>
  358. <span class="option-text">错误</span>
  359. </div>
  360. </div>
  361. </div>
  362. </div>
  363. </div>
  364. <!-- 多选题 -->
  365. <div class="question-section">
  366. <div class="section-header" @click="isGenerating ? null : toggleSection('multiple')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  367. <div class="section-title">
  368. <span class="section-number">三</span>
  369. <span class="section-name">多选题</span>
  370. <span class="section-score">(每题{{ currentExam.multiple.scorePerQuestion }}分, 共{{ currentExam.multiple.totalScore }}分)</span>
  371. </div>
  372. <div class="section-controls">
  373. <span class="question-count-text">{{ currentExam.multiple.count }}题</span>
  374. <img
  375. :src="expandIcon"
  376. alt="收起/展开"
  377. class="toggle-icon"
  378. :class="{ 'expanded': !expandedSections.multiple }"
  379. />
  380. </div>
  381. </div>
  382. <div v-if="expandedSections.multiple" class="section-content">
  383. <div
  384. v-for="(question, index) in currentExam.multiple.questions"
  385. :key="index"
  386. class="question-item"
  387. >
  388. <div class="question-header">
  389. <span class="question-number">{{ index + 1 }}.</span>
  390. <span class="question-text">{{ question.text }}</span>
  391. <button class="refresh-btn" @click="refreshQuestion('multiple', index)" :disabled="isGenerating">
  392. <img
  393. :src="collapseIcon"
  394. alt="刷新"
  395. class="refresh-icon"
  396. :class="{ 'rotating': isRefreshing[`multiple_${index}`] }"
  397. />
  398. </button>
  399. </div>
  400. <div class="options">
  401. <div
  402. v-for="option in question.options"
  403. :key="option.key"
  404. :class="['option', { selected: question.selectedAnswers.includes(option.key) }]"
  405. :style="{ cursor: 'default' }"
  406. >
  407. <div class="radio-wrapper">
  408. <div class="radio-circle" :class="{ 'selected': question.selectedAnswers.includes(option.key) }">
  409. <div v-if="question.selectedAnswers.includes(option.key)" class="radio-dot"></div>
  410. </div>
  411. </div>
  412. <span class="option-key">{{ option.key }}.</span>
  413. <div class="option-content">
  414. <span class="option-text">{{ option.text }}</span>
  415. <!-- 编辑按钮已禁用 -->
  416. <!-- <button
  417. class="edit-option-btn"
  418. @click.stop="openEditModal('multiple', index, option.key)"
  419. :disabled="isGenerating"
  420. title="编辑选项"
  421. >
  422. <img src="@/assets/Chat/13.png" alt="编辑" class="edit-icon" />
  423. </button> -->
  424. </div>
  425. </div>
  426. </div>
  427. </div>
  428. </div>
  429. </div>
  430. <!-- 简答题 -->
  431. <div class="question-section">
  432. <div class="section-header" @click="isGenerating ? null : toggleSection('short')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
  433. <div class="section-title">
  434. <span class="section-number">四</span>
  435. <span class="section-name">简答题</span>
  436. <span class="section-score">(每题{{ currentExam.short.scorePerQuestion }}分, 共{{ currentExam.short.totalScore }}分)</span>
  437. </div>
  438. <div class="section-controls">
  439. <span class="question-count-text">{{ currentExam.short.count }}题</span>
  440. <img
  441. :src="expandIcon"
  442. alt="收起/展开"
  443. class="toggle-icon"
  444. :class="{ 'expanded': !expandedSections.short }"
  445. />
  446. </div>
  447. </div>
  448. <div v-if="expandedSections.short" class="section-content">
  449. <div
  450. v-for="(question, index) in currentExam.short.questions"
  451. :key="index"
  452. class="question-item"
  453. >
  454. <div class="question-header">
  455. <span class="question-number">{{ index + 1 }}.</span>
  456. <span class="question-text">{{ question.text }}</span>
  457. <button class="refresh-btn" @click="refreshQuestion('short', index)" :disabled="isGenerating">
  458. <img
  459. :src="collapseIcon"
  460. alt="刷新"
  461. class="refresh-icon"
  462. :class="{ 'rotating': isRefreshing[`short_${index}`] }"
  463. />
  464. </button>
  465. </div>
  466. <div class="answer-box">
  467. <div class="answer-outline">
  468. <div class="outline-item">
  469. <span class="outline-text">{{ question.outline.keyFactors }}</span>
  470. <!-- 编辑按钮已禁用 -->
  471. <!-- <button
  472. class="edit-option-btn"
  473. @click="openEditModal('short', index, 'keyFactors')"
  474. :disabled="isGenerating"
  475. title="编辑答题要点"
  476. >
  477. <img src="@/assets/Chat/13.png" alt="编辑" class="edit-icon" />
  478. </button> -->
  479. </div>
  480. </div>
  481. </div>
  482. </div>
  483. </div>
  484. </div>
  485. </div>
  486. </div>
  487. </div>
  488. </div>
  489. <!-- 删除确认弹窗 -->
  490. <DeleteConfirmModal
  491. :visible="showDeleteModal"
  492. @close="showDeleteModal = false"
  493. @confirm="handleDeleteConfirm"
  494. @cancel="handleDeleteCancel"
  495. />
  496. <!-- 编辑选项弹窗已禁用 -->
  497. <!-- <div v-if="showEditModal" class="modal-overlay" @click="closeEditModal">
  498. <div class="edit-modal" @click.stop>
  499. <div class="modal-header">
  500. <h3>编辑{{ editModalData.type === 'short' ? '答题要点' : '选项' }}</h3>
  501. <button class="close-btn" @click="closeEditModal">×</button>
  502. </div>
  503. <div class="modal-body">
  504. <div v-if="editModalData.type === 'short'" class="edit-section">
  505. <label>答题要点:</label>
  506. <textarea
  507. v-model="editModalData.keyFactors"
  508. class="edit-textarea"
  509. placeholder="请输入答题要点..."
  510. ></textarea>
  511. </div>
  512. <div v-else class="edit-section">
  513. <label>选项文本:</label>
  514. <input
  515. v-model="editModalData.optionText"
  516. class="edit-input"
  517. placeholder="请输入选项内容..."
  518. maxlength="20"
  519. @input="validateOptionText"
  520. />
  521. <div class="text-validation" v-if="editModalData.optionText.length > 0">
  522. <span class="char-count" :class="{ 'warning': editModalData.optionText.length >= 18 }">
  523. {{ editModalData.optionText.length }}/20
  524. </span>
  525. </div>
  526. </div>
  527. </div>
  528. <div class="modal-footer">
  529. <button class="btn btn-cancel" @click="closeEditModal">取消</button>
  530. <button class="btn btn-confirm" @click="saveEdit">保存</button>
  531. </div>
  532. </div>
  533. </div> -->
  534. <!-- 隐藏的文件输入框 -->
  535. <input
  536. ref="fileInput"
  537. type="file"
  538. accept=".ppt,.pptx"
  539. multiple
  540. style="display: none"
  541. @change="handleFileSelect"
  542. />
  543. <!-- 删除确认弹窗 -->
  544. <DeleteConfirmModal
  545. :visible="showDeleteModal"
  546. title="删除历史记录"
  547. :message="deleteConfirmMessage"
  548. @confirm="confirmDeleteHistory"
  549. @cancel="cancelDeleteHistory"
  550. @close="cancelDeleteHistory"
  551. />
  552. </div>
  553. </template>
  554. <script setup>
  555. import { ref, computed, onMounted, onUnmounted, reactive, watch, defineProps, defineEmits } from "vue";
  556. import { useRoute, useRouter } from "vue-router";
  557. import Sidebar from "@/components/Sidebar.vue";
  558. import DeleteConfirmModal from "@/components/DeleteConfirmModal.vue";
  559. import { UploadFilled, ArrowRight, Delete, MagicStick, Loading } from '@element-plus/icons-vue';
  560. const props = defineProps({
  561. hideSidebar: {
  562. type: Boolean,
  563. default: false
  564. }
  565. });
  566. const emit = defineEmits(['return-to-ai']);
  567. const route = useRoute();
  568. const router = useRouter();
  569. const handleReturnToAI = () => {
  570. emit('return-to-ai');
  571. };
  572. import { apis } from '@/request/apis.js'
  573. import { ElMessage } from 'element-plus'
  574. // ===== 已删除:getUserId - 不再需要,改用token =====
  575. // import { getUserId } from '@/utils/userManager.js'
  576. // 导入图片资源
  577. import bridgeIcon from '@/assets/Exam/4.png'
  578. import tunnelIcon from '@/assets/Exam/18.png'
  579. import equipmentIcon from '@/assets/Exam/5.png'
  580. import gasStationIcon from '@/assets/Exam/6.png'
  581. import highwayIcon from '@/assets/Exam/7.png'
  582. import comprehensiveIcon from '@/assets/Exam/8.png'
  583. import aiIcon from '@/assets/Exam/9.png'
  584. import pptIcon from '@/assets/Exam/10.png'
  585. import previewIcon from '@/assets/Exam/14.png'
  586. import clearIcon from '@/assets/Exam/11.png'
  587. import generateIcon from '@/assets/Exam/12.png'
  588. import saveIcon from '@/assets/Exam/15.png'
  589. import downloadIcon from '@/assets/Exam/13.png'
  590. import expandIcon from '@/assets/Exam/17.png'
  591. import collapseIcon from '@/assets/Exam/16.png'
  592. import attachmentIcon from '@/assets/Chat/9.png'
  593. // 响应式数据
  594. const selectedFunction = ref("ai");
  595. const selectedProjectType = ref("bridge");
  596. const examName = ref("桥梁工程施工技术考核");
  597. const totalScore = ref(100);
  598. const showExamDetail = ref(false);
  599. const currentTime = ref("");
  600. const isGenerating = ref(false); // 控制生成状态
  601. const isRefreshing = ref({}); // 控制单题刷新状态
  602. const showDownloadMenu = ref(false); // 控制下载菜单显示状态
  603. const isLoadingHistory = ref(false); // 是否正在加载历史记录
  604. const isLoadingHistoryItem = ref(false); // 是否正在加载历史记录详情
  605. const showDeleteModal = ref(false); // 控制删除确认弹窗显示
  606. const deleteTargetItem = ref(null); // 要删除的目标项
  607. // 编辑功能已禁用
  608. /*
  609. const showEditModal = ref(false); // 控制编辑选项弹窗显示
  610. const editModalData = ref({
  611. type: '', // 'single', 'multiple', 'short'
  612. questionIndex: -1,
  613. optionKey: '',
  614. optionText: '',
  615. keyFactors: '',
  616. correctAnswer: '',
  617. correctAnswers: []
  618. }); // 编辑模态框数据
  619. */
  620. // PPT文件上传相关
  621. const fileInput = ref(null);
  622. const uploadedFiles = ref([]);
  623. const isUploadingFile = ref(false);
  624. const fileContent = ref(''); // 存储文件内容
  625. const pptContentDescription = ref(''); // 存储用户输入的PPT内容描述
  626. const questionBasis = computed({
  627. get: () => pptContentDescription.value,
  628. set: (value) => {
  629. pptContentDescription.value = value;
  630. }
  631. });
  632. // 文件处理配置
  633. const fileConfig = reactive({
  634. maxSize: 20 * 1024 * 1024, // 20MB
  635. allowedTypes: ['.ppt', '.pptx'] // 只允许PPT文件
  636. });
  637. // 移除原有的Toast相关变量,使用ElMessage替代
  638. // const showToast = ref(false); // 控制轻提示显示
  639. // const toastMessage = ref(""); // 轻提示消息
  640. // const toastType = ref("success"); // 轻提示类型
  641. // 展开/收起状态
  642. const expandedSections = ref({
  643. single: true,
  644. judge: true,
  645. multiple: true,
  646. short: true
  647. });
  648. // 当前试卷数据
  649. const currentExam = ref(null);
  650. // 历史记录数据
  651. const historyData = ref([])
  652. const historyTotal = ref(0) // 历史记录总数
  653. // 获取历史记录列表
  654. const getHistoryRecordList = async () => {
  655. try {
  656. console.log('📋 开始获取考试工坊历史记录列表...')
  657. isLoadingHistory.value = true
  658. const startTime = performance.now()
  659. const response = await apis.getHistoryRecord({
  660. ai_conversation_id: 0,
  661. business_type: 3
  662. })
  663. const endTime = performance.now()
  664. console.log(`📋 考试工坊历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
  665. console.log('📋 考试工坊历史记录列表响应:', response)
  666. if (response.statusCode === 200) {
  667. historyTotal.value = response.total || 0
  668. historyData.value = response.data.map(conversation => ({
  669. id: conversation.id,
  670. title: generateConversationTitle(conversation.exam_name),
  671. time: formatTime(conversation.updated_at),
  672. businessType: conversation.business_type,
  673. isActive: false,
  674. rawData: conversation
  675. }))
  676. console.log(`✅ 考试工坊历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
  677. } else {
  678. console.error('❌ 获取考试工坊历史记录列表失败:', response.statusCode)
  679. }
  680. } finally {
  681. isLoadingHistory.value = false
  682. }
  683. }
  684. // 生成对话标题(从内容中提取)
  685. const generateConversationTitle = (content) => {
  686. if (!content) return '未知对话'
  687. // 清理HTML标签
  688. const cleanContent = content.replace(/<[^>]*>/g, '')
  689. // 提取第一句话作为标题
  690. const firstSentence = cleanContent.split(/[。!?\n]/)[0]
  691. // 限制标题长度
  692. if (firstSentence.length > 30) {
  693. return firstSentence.substring(0, 30) + '...'
  694. }
  695. return firstSentence || '新对话'
  696. }
  697. // 格式化时间
  698. const formatTime = (timestamp) => {
  699. if (!timestamp) return '未知时间'
  700. // 处理时间戳
  701. let date
  702. if (typeof timestamp === 'string') {
  703. // 如果是ISO字符串格式,直接创建Date对象
  704. date = new Date(timestamp)
  705. } else {
  706. // 如果是数字时间戳
  707. let timestampMs = timestamp
  708. if (timestamp.toString().length === 10) {
  709. timestampMs = timestamp * 1000
  710. } else if (timestamp.toString().length === 11) {
  711. timestampMs = timestamp * 1000
  712. } else if (timestamp.toString().length === 13) {
  713. // 13位时间戳,直接使用
  714. } else {
  715. timestampMs = timestamp * 1000
  716. }
  717. date = new Date(timestampMs)
  718. }
  719. const now = new Date()
  720. // 获取今天的开始时间(0点0分0秒)
  721. const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  722. // 获取昨天的开始时间
  723. const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000)
  724. // 今天的对话(日期相同)
  725. if (date >= todayStart) {
  726. return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  727. }
  728. // 昨天的对话(日期是昨天)
  729. if (date >= yesterdayStart && date < todayStart) {
  730. return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  731. }
  732. // 更早的对话:显示为 "8月30日 15:30" 格式
  733. const month = date.getMonth() + 1 // getMonth() 返回 0-11
  734. const day = date.getDate()
  735. const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  736. return `${month}月${day}日 ${time}`
  737. }
  738. // 工程类型配置
  739. const projectTypes = {
  740. bridge: { name: "桥梁", icon: bridgeIcon },
  741. tunnel: { name: "隧道", icon: tunnelIcon },
  742. equipment: { name: "特种设备", icon: equipmentIcon },
  743. "gas-station": { name: "加油站", icon: gasStationIcon },
  744. highway: { name: "高速运营公路", icon: highwayIcon },
  745. comprehensive: { name: "综合", icon: comprehensiveIcon },
  746. };
  747. // 题型配置
  748. const questionTypes = ref([
  749. { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
  750. { name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
  751. { name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
  752. { name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
  753. ]);
  754. // 保存初始配置
  755. let initialConfig = null;
  756. const ai_conversation_id = ref(0)
  757. // 删除确认消息
  758. const deleteConfirmMessage = computed(() => {
  759. const title = deleteTargetItem.value?.item?.title || ''
  760. return `确定要删除历史记录"${title}"吗?删除后将无法恢复。`
  761. })
  762. // 计算总分(所有题目分数的总和)
  763. const calculatedTotalScore = computed(() => {
  764. return questionTypes.value.reduce((total, type) => {
  765. return total + (type.scorePerQuestion * type.questionCount);
  766. }, 0);
  767. })
  768. // 删除历史记录
  769. const deleteHistoryItem = (historyItem, index) => {
  770. console.log('准备删除考试工坊历史记录:', historyItem)
  771. // 设置要删除的项目并显示确认弹窗
  772. deleteTargetItem.value = { item: historyItem, index: index }
  773. showDeleteModal.value = true
  774. }
  775. // 确认删除历史记录
  776. const confirmDeleteHistory = async () => {
  777. if (!deleteTargetItem.value) return
  778. const { item: historyItem, index } = deleteTargetItem.value
  779. try {
  780. // 调用删除接口
  781. const response = await apis.deleteHistoryRecord({
  782. ai_conversation_id: historyItem.id
  783. })
  784. if (response.statusCode === 200) {
  785. // 删除成功,从列表中移除
  786. removeExamWorkshopHistory(historyItem.id)
  787. // 如果删除的是当前激活的历史记录,需要清空界面并调用新建任务
  788. if (historyItem.isActive) {
  789. await createNewChat()
  790. }
  791. console.log('考试工坊历史记录删除成功')
  792. ElMessage.success('删除成功')
  793. } else {
  794. console.error('删除考试工坊历史记录失败:', response.msg)
  795. ElMessage.error(response.msg || '删除失败')
  796. }
  797. } catch (error) {
  798. console.error('删除考试工坊历史记录失败:', error)
  799. ElMessage.error('删除失败,请稍后重试')
  800. } finally {
  801. // 关闭弹窗并清除目标项
  802. showDeleteModal.value = false
  803. deleteTargetItem.value = null
  804. }
  805. }
  806. // 取消删除
  807. const cancelDeleteHistory = () => {
  808. showDeleteModal.value = false
  809. deleteTargetItem.value = null
  810. }
  811. // 方法
  812. const createNewChat = async () => {
  813. if (isGenerating.value) return;
  814. console.log("创建新考试工坊任务");
  815. // 重置所有状态
  816. ai_conversation_id.value = 0
  817. showExamDetail.value = false
  818. // 重置配置到初始状态
  819. selectedFunction.value = "ai";
  820. selectedProjectType.value = "bridge";
  821. examName.value = "桥梁工程施工技术考核";
  822. totalScore.value = 100;
  823. currentTime.value = "";
  824. // 恢复题型配置到初始状态
  825. if (initialConfig) {
  826. questionTypes.value = initialConfig.questionTypes;
  827. totalScore.value = initialConfig.totalScore;
  828. selectedProjectType.value = initialConfig.selectedProjectType;
  829. examName.value = initialConfig.examName;
  830. } else {
  831. // 如果没有初始配置,使用默认配置
  832. questionTypes.value = [
  833. { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
  834. { name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
  835. { name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
  836. { name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
  837. ];
  838. }
  839. // 重置当前试卷数据到初始状态
  840. currentExam.value = null;
  841. // 重置展开/收起状态
  842. expandedSections.value = {
  843. single: true,
  844. judge: true,
  845. multiple: true,
  846. short: true
  847. };
  848. // 重置生成状态
  849. isGenerating.value = false;
  850. isRefreshing.value = {};
  851. // 清理文件
  852. uploadedFiles.value = [];
  853. pptContentDescription.value = '';
  854. // 清除所有历史记录的选中状态
  855. historyData.value.forEach((item) => {
  856. item.isActive = false
  857. })
  858. // 刷新历史记录列表
  859. await getHistoryRecordList()
  860. };
  861. const handleHistoryItem = async (historyItem) => {
  862. if (isGenerating.value || isLoadingHistoryItem.value) return;
  863. console.log("点击考试工坊历史记录:", historyItem);
  864. ai_conversation_id.value = historyItem.id;
  865. isLoadingHistoryItem.value = true;
  866. try {
  867. historyData.value.forEach((item) => {
  868. item.isActive = item.id === historyItem.id;
  869. });
  870. const response = await apis.getHistoryRecord({
  871. ai_conversation_id: historyItem.id,
  872. business_type: 3
  873. });
  874. console.log(response.data)
  875. if (response.statusCode === 200 && response.data && response.data.length > 0) {
  876. const latestRecord = response.data[response.data.length - 1];
  877. console.log('获取到的试卷数据:', latestRecord);
  878. console.log('试卷数据结构:', JSON.stringify(latestRecord, null, 2));
  879. currentTime.value = formatTime(latestRecord.created_at)
  880. if (latestRecord && latestRecord.content) {
  881. try {
  882. const examData = extractExamDataFromContent(latestRecord.content);
  883. restoreExamFromHistory(examData);
  884. showExamDetail.value = true;
  885. } catch (error) {
  886. console.error('解析试卷数据失败:', error);
  887. showExamDetail.value = true;
  888. currentTime.value = historyItem.time;
  889. }
  890. } else {
  891. showExamDetail.value = true;
  892. currentTime.value = historyItem.time;
  893. }
  894. } else {
  895. console.error('获取历史记录详情失败:', response);
  896. showExamDetail.value = true;
  897. currentTime.value = historyItem.time;
  898. }
  899. } catch (error) {
  900. console.error('获取历史记录详情失败:', error);
  901. showExamDetail.value = true;
  902. currentTime.value = historyItem.time;
  903. } finally {
  904. isLoadingHistoryItem.value = false;
  905. }
  906. };
  907. const selectFunction = (functionType) => {
  908. selectedFunction.value = functionType;
  909. console.log("选择功能:", functionType);
  910. };
  911. const selectProjectType = (typeKey) => {
  912. selectedProjectType.value = typeKey;
  913. console.log("选择工程类型:", projectTypes[typeKey].name);
  914. // 自动更新试卷名称
  915. const projectTypeName = projectTypes[typeKey].name;
  916. examName.value = `${projectTypeName}工程施工技术考核`;
  917. // 同时更新当前试卷的标题
  918. if (currentExam.value) {
  919. currentExam.value.title = examName.value;
  920. }
  921. };
  922. const clearSettings = () => {
  923. // 根据当前选择的工程类型设置试卷名称
  924. const projectTypeName = projectTypes[selectedProjectType.value].name;
  925. examName.value = `${projectTypeName}工程施工技术考核`;
  926. totalScore.value = 100; // 清空时配置总分恢复默认值 100
  927. // 保留原数组引用,更新每个对象的属性,避免破坏 Vue 3 响应式绑定
  928. questionTypes.value.forEach(type => {
  929. type.scorePerQuestion = 0;
  930. type.questionCount = 0;
  931. });
  932. console.log("清除设置");
  933. };
  934. const validateExamName = () => {
  935. if (examName.value.length > 32) {
  936. examName.value = examName.value.slice(0, 20);
  937. }
  938. };
  939. // 验证总分
  940. const validateTotalScore = () => {
  941. if (totalScore.value > 1000) {
  942. totalScore.value = 1000;
  943. ElMessage.warning("试卷总分不能超过1000分");
  944. }
  945. if (totalScore.value < 1) {
  946. totalScore.value = 1;
  947. }
  948. };
  949. // 验证每题分数
  950. const validateScorePerQuestion = (type) => {
  951. if (type.scorePerQuestion > 99) {
  952. type.scorePerQuestion = 99;
  953. ElMessage.warning(`${type.name}每题分数不能超过99分`);
  954. }
  955. if (type.scorePerQuestion < 1) {
  956. type.scorePerQuestion = 1;
  957. }
  958. };
  959. // 验证题目数量
  960. const validateQuestionCount = (type) => {
  961. if (type.questionCount > 99) {
  962. type.questionCount = 99;
  963. ElMessage.warning(`${type.name}题目数量不能超过99题`);
  964. }
  965. if (type.questionCount < 0) {
  966. type.questionCount = 0;
  967. }
  968. };
  969. const adjustQuestionCount = (type, delta) => {
  970. type.questionCount = Number(type.questionCount || 0) + delta;
  971. validateQuestionCount(type);
  972. };
  973. const generateExam = async () => {
  974. if (!examName.value.trim()) {
  975. ElMessage.warning("请输入试卷名称");
  976. return;
  977. }
  978. if (examName.value.trim().length === 0) {
  979. ElMessage.warning("试卷名称不能为空");
  980. return;
  981. }
  982. // 检查总分是否超过限制
  983. if (totalScore.value > 1000) {
  984. ElMessage.warning("试卷总分不能超过1000分");
  985. return;
  986. }
  987. // 检查每题分数和题目数量是否超过限制
  988. for (const type of questionTypes.value) {
  989. if (type.scorePerQuestion > 99) {
  990. ElMessage.warning(`${type.name}每题分数不能超过99分`);
  991. return;
  992. }
  993. if (type.questionCount > 99) {
  994. ElMessage.warning(`${type.name}题目数量不能超过99题`);
  995. return;
  996. }
  997. }
  998. // 检查总分是否合理
  999. const calculatedScore = questionTypes.value.reduce((total, type) => {
  1000. return total + (type.scorePerQuestion * type.questionCount);
  1001. }, 0);
  1002. if (calculatedScore !== totalScore.value) {
  1003. ElMessage.warning(`总分不匹配!当前配置总分为${calculatedScore}分,请检查配置`);
  1004. return;
  1005. }
  1006. console.log("生成试卷:", {
  1007. function: selectedFunction.value,
  1008. projectType: projectTypes[selectedProjectType.value].name,
  1009. examName: examName.value,
  1010. totalScore: totalScore.value,
  1011. questionTypes: questionTypes.value,
  1012. });
  1013. // 设置当前时间
  1014. const now = new Date();
  1015. currentTime.value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  1016. if (selectedFunction.value === 'ai') {
  1017. // AI智能生成试卷
  1018. await generateAIExam();
  1019. } else {
  1020. // PPT生成方式
  1021. if (uploadedFiles.value.length === 0) {
  1022. ElMessage.warning("请先上传PPT文件");
  1023. return;
  1024. }
  1025. await generatePPTExam();
  1026. }
  1027. };
  1028. // PPT生成试卷
  1029. const generatePPTExam = async () => {
  1030. try {
  1031. isGenerating.value = true;
  1032. // 构建PPT生成提示词(服务端构建)
  1033. const prompt = await fetchExamPrompt('ppt');
  1034. console.log('发送给AI的PPT生成提示词:', prompt);
  1035. // 调用AI接口
  1036. const response = await apis.sendDeepseekMessage({
  1037. // ===== 已删除:user_id - 后端从token解析 =====
  1038. business_type: 3,
  1039. message: prompt,
  1040. exam_name: examName.value,
  1041. ai_conversation_id: ai_conversation_id.value
  1042. });
  1043. if (response.statusCode === 200) {
  1044. const aiReply = response.data.reply;
  1045. const aiConversationId = response.data.ai_conversation_id; // 保存对话ID
  1046. console.log('AI生成的PPT试卷:', aiReply);
  1047. console.log('AI对话ID:', aiConversationId);
  1048. // 保存对话ID
  1049. ai_conversation_id.value = aiConversationId;
  1050. // 解析AI回复并生成试卷
  1051. const generatedExam = parseAIExamResponse(aiReply);
  1052. // 更新当前试卷数据
  1053. updateCurrentExam(generatedExam);
  1054. // 显示详情页
  1055. showExamDetail.value = true;
  1056. ElMessage.success("PPT试卷生成完成!");
  1057. await getHistoryRecordList();
  1058. if (ai_conversation_id.value > 0) {
  1059. historyData.value.forEach((item) => {
  1060. item.isActive = item.id === ai_conversation_id.value;
  1061. });
  1062. console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
  1063. } else {
  1064. selectLatestHistoryRecord();
  1065. }
  1066. } else {
  1067. throw new Error('AI接口调用失败');
  1068. }
  1069. } catch (error) {
  1070. console.error('PPT生成试卷失败:', error);
  1071. ElMessage.error('PPT生成试卷失败,请稍后重试或检查网络连接');
  1072. // 失败时显示默认试卷
  1073. showExamDetail.value = true;
  1074. } finally {
  1075. // 重置发送状态
  1076. isGenerating.value = false;
  1077. }
  1078. };
  1079. // AI生成试卷
  1080. const generateAIExam = async () => {
  1081. try {
  1082. isGenerating.value = true;
  1083. // 构建AI提示词(服务端构建)
  1084. const prompt = await fetchExamPrompt('ai');
  1085. console.log('发送给AI的提示词:', prompt);
  1086. // 调用AI接口
  1087. const response = await apis.sendDeepseekMessage({
  1088. // ===== 已删除:user_id - 后端从token解析 =====
  1089. business_type: 3,
  1090. message: prompt,
  1091. exam_name: examName.value,
  1092. ai_conversation_id: ai_conversation_id.value
  1093. });
  1094. if (response.statusCode === 200) {
  1095. const aiReply = response.data.reply;
  1096. const aiConversationId = response.data.ai_conversation_id; // 保存对话ID
  1097. console.log('AI生成的试卷:', aiReply);
  1098. console.log('AI对话ID:', aiConversationId);
  1099. // 保存对话ID
  1100. ai_conversation_id.value = aiConversationId;
  1101. // 解析AI回复并生成试卷
  1102. const generatedExam = parseAIExamResponse(aiReply);
  1103. // 更新当前试卷数据
  1104. updateCurrentExam(generatedExam);
  1105. // 显示详情页
  1106. showExamDetail.value = true;
  1107. ElMessage.success("AI试卷生成完成!");
  1108. await getHistoryRecordList();
  1109. if (ai_conversation_id.value > 0) {
  1110. historyData.value.forEach((item) => {
  1111. item.isActive = item.id === ai_conversation_id.value;
  1112. });
  1113. console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
  1114. } else {
  1115. selectLatestHistoryRecord();
  1116. }
  1117. } else {
  1118. throw new Error('AI接口调用失败');
  1119. }
  1120. } catch (error) {
  1121. console.error('AI生成试卷失败:', error);
  1122. ElMessage.error('AI生成试卷失败,请稍后重试或检查网络连接');
  1123. // 失败时显示默认试卷
  1124. showExamDetail.value = true;
  1125. } finally {
  1126. // 重置发送状态
  1127. isGenerating.value = false;
  1128. }
  1129. };
  1130. // 从服务端构建提示词
  1131. const fetchExamPrompt = async (mode = 'ai') => {
  1132. const normalizedQuestionTypes = questionTypes.value.map(type => ({
  1133. name: type.name,
  1134. romanNumeral: type.romanNumeral,
  1135. questionCount: Number(type.questionCount) || 0,
  1136. scorePerQuestion: Number(type.scorePerQuestion) || 0,
  1137. }));
  1138. const pptContents = uploadedFiles.value.map(file => file.content).join('\n\n');
  1139. const finalContentBasis = pptContents || questionBasis.value || '';
  1140. const payload = {
  1141. mode,
  1142. client: 'pc',
  1143. projectType: projectTypes[selectedProjectType.value]?.name || '',
  1144. examTitle: examName.value,
  1145. totalScore: totalScore.value,
  1146. questionTypes: normalizedQuestionTypes,
  1147. pptContent: finalContentBasis
  1148. };
  1149. try {
  1150. const response = await apis.buildExamPrompt(payload);
  1151. if (!response?.data?.prompt) {
  1152. throw new Error(response?.msg || '提示词构建失败');
  1153. }
  1154. return response.data.prompt;
  1155. } catch (error) {
  1156. console.error('获取提示词失败:', error);
  1157. throw error;
  1158. }
  1159. };
  1160. // 解析AI回复
  1161. const extractExamDataFromContent = (content) => {
  1162. if (!content || typeof content !== 'string') {
  1163. throw new Error('试卷内容为空');
  1164. }
  1165. const jsonMatch = content.match(/\{[\s\S]*\}/);
  1166. if (!jsonMatch) {
  1167. throw new Error('未找到有效的JSON数据');
  1168. }
  1169. return JSON.parse(jsonMatch[0]);
  1170. };
  1171. const parseAIExamResponse = (aiReply) => {
  1172. try {
  1173. const examData = extractExamDataFromContent(aiReply);
  1174. const normalizedExam = normalizeGeneratedExam(examData);
  1175. // 确保所有题目都有正确的初始值
  1176. ensureQuestionInitialValues(normalizedExam);
  1177. return normalizedExam;
  1178. } catch (error) {
  1179. console.error('解析AI回复失败:', error);
  1180. // 返回默认试卷结构
  1181. return generateDefaultExam();
  1182. }
  1183. };
  1184. const getQuestionTypeConfig = (name, fallbackScore = 0) => {
  1185. const config = questionTypes.value.find(type => type.name === name);
  1186. return {
  1187. scorePerQuestion: Number(config?.scorePerQuestion) || fallbackScore,
  1188. questionCount: Number(config?.questionCount) || 0,
  1189. };
  1190. };
  1191. const normalizeOptions = (options = []) => {
  1192. if (!Array.isArray(options)) {
  1193. return [];
  1194. }
  1195. return options.map((option, index) => {
  1196. if (typeof option === 'string') {
  1197. return {
  1198. key: String.fromCharCode(65 + index),
  1199. text: option,
  1200. };
  1201. }
  1202. return {
  1203. key: option?.key || String.fromCharCode(65 + index),
  1204. text: option?.text || option?.content || option?.label || "",
  1205. };
  1206. });
  1207. };
  1208. const normalizeQuestions = (questions = [], sectionKey) => {
  1209. if (!Array.isArray(questions)) {
  1210. return [];
  1211. }
  1212. return questions.map((question = {}) => {
  1213. if (sectionKey === 'singleChoice') {
  1214. return {
  1215. text: question.text || question.question_text || "",
  1216. options: normalizeOptions(question.options),
  1217. selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
  1218. analysis: question.analysis || question.explanation || "",
  1219. };
  1220. }
  1221. if (sectionKey === 'judge') {
  1222. return {
  1223. text: question.text || question.question_text || "",
  1224. selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
  1225. analysis: question.analysis || question.explanation || "",
  1226. };
  1227. }
  1228. if (sectionKey === 'multiple') {
  1229. const selectedAnswers = question.selectedAnswers || question.correct_answers || question.answers || [];
  1230. return {
  1231. text: question.text || question.question_text || "",
  1232. options: normalizeOptions(question.options),
  1233. selectedAnswers: Array.isArray(selectedAnswers) ? selectedAnswers : [selectedAnswers].filter(Boolean),
  1234. analysis: question.analysis || question.explanation || "",
  1235. };
  1236. }
  1237. return {
  1238. text: question.text || question.question_text || "",
  1239. outline: question.outline || question.answer_outline || { keyFactors: question.answer || "答题要点、关键因素、示例答案" },
  1240. analysis: question.analysis || question.explanation || "",
  1241. };
  1242. });
  1243. };
  1244. const normalizeSection = (rawSection, sectionKey, fallbackName, fallbackScore = 0) => {
  1245. const section = rawSection || {};
  1246. const config = getQuestionTypeConfig(fallbackName, fallbackScore);
  1247. const sourceQuestions = Array.isArray(section)
  1248. ? section
  1249. : (section.questions || section.items || section.question_list || []);
  1250. const normalizedQuestions = normalizeQuestions(sourceQuestions, sectionKey);
  1251. const count = Number(section.count ?? section.question_count ?? normalizedQuestions.length ?? config.questionCount) || 0;
  1252. const scorePerQuestion = Number(section.scorePerQuestion ?? section.score_per_question ?? config.scorePerQuestion) || 0;
  1253. const totalScore = Number(section.totalScore ?? section.total_score ?? (scorePerQuestion * count)) || 0;
  1254. return {
  1255. scorePerQuestion,
  1256. totalScore,
  1257. count,
  1258. questions: normalizedQuestions,
  1259. };
  1260. };
  1261. const normalizeGeneratedExam = (examData = {}) => {
  1262. const singleSource = examData.singleChoice || examData.questions?.single_choice || examData.single_choice;
  1263. const judgeSource = examData.judge || examData.questions?.judge;
  1264. const multipleSource = examData.multiple || examData.questions?.multiple;
  1265. const shortSource = examData.short || examData.questions?.short;
  1266. const normalizedExam = {
  1267. title: examData.title || examData.exam_name || examName.value,
  1268. totalScore: Number(examData.totalScore ?? examData.total_score ?? totalScore.value) || 0,
  1269. totalQuestions: Number(examData.totalQuestions ?? examData.total_questions) || 0,
  1270. singleChoice: normalizeSection(singleSource, 'singleChoice', '单选题', 2),
  1271. judge: normalizeSection(judgeSource, 'judge', '判断题', 2),
  1272. multiple: normalizeSection(multipleSource, 'multiple', '多选题', 3),
  1273. short: normalizeSection(shortSource, 'short', '简答题', 10),
  1274. };
  1275. if (!normalizedExam.totalQuestions) {
  1276. normalizedExam.totalQuestions =
  1277. normalizedExam.singleChoice.count +
  1278. normalizedExam.judge.count +
  1279. normalizedExam.multiple.count +
  1280. normalizedExam.short.count;
  1281. }
  1282. return normalizedExam;
  1283. };
  1284. // 确保题目初始值正确
  1285. const ensureQuestionInitialValues = (examData) => {
  1286. // 单选题
  1287. if (examData.singleChoice && examData.singleChoice.questions) {
  1288. examData.singleChoice.questions.forEach(question => {
  1289. if (!question.selectedAnswer) {
  1290. question.selectedAnswer = "";
  1291. }
  1292. if (!question.options || question.options.length === 0) {
  1293. question.options = [
  1294. { key: "A", text: "选项A" },
  1295. { key: "B", text: "选项B" },
  1296. { key: "C", text: "选项C" },
  1297. { key: "D", text: "选项D" }
  1298. ];
  1299. }
  1300. });
  1301. }
  1302. // 判断题
  1303. if (examData.judge && examData.judge.questions) {
  1304. examData.judge.questions.forEach(question => {
  1305. if (!question.selectedAnswer) {
  1306. question.selectedAnswer = "";
  1307. }
  1308. });
  1309. }
  1310. // 多选题
  1311. if (examData.multiple && examData.multiple.questions) {
  1312. examData.multiple.questions.forEach(question => {
  1313. if (!question.selectedAnswers) {
  1314. question.selectedAnswers = [];
  1315. }
  1316. if (!question.options || question.options.length === 0) {
  1317. question.options = [
  1318. { key: "A", text: "选项A" },
  1319. { key: "B", text: "选项B" },
  1320. { key: "C", text: "选项C" },
  1321. { key: "D", text: "选项D" }
  1322. ];
  1323. }
  1324. });
  1325. }
  1326. // 简答题
  1327. if (examData.short && examData.short.questions) {
  1328. examData.short.questions.forEach(question => {
  1329. if (!question.outline) {
  1330. question.outline = { keyFactors: "答题要点、关键因素、示例答案" };
  1331. }
  1332. });
  1333. }
  1334. // 不再自动设置默认答案,让AI自己生成正确答案
  1335. // setDefaultAnswers(examData);
  1336. };
  1337. // 设置默认答案
  1338. const setDefaultAnswers = (examData) => {
  1339. // 单选题设置默认答案
  1340. if (examData.singleChoice && examData.singleChoice.questions) {
  1341. examData.singleChoice.questions.forEach((question, index) => {
  1342. // 根据题目内容智能设置答案
  1343. if (question.text.includes('桩身倾斜') || question.text.includes('桩位纠偏')) {
  1344. question.selectedAnswer = "B";
  1345. } else if (question.text.includes('深水河流') || question.text.includes('悬臂浇筑')) {
  1346. question.selectedAnswer = "B";
  1347. } else if (question.text.includes('预应力张拉') || question.text.includes('对称张拉')) {
  1348. question.selectedAnswer = "C";
  1349. } else if (question.text.includes('混凝土裂缝') || question.text.includes('伸缩缝')) {
  1350. question.selectedAnswer = "C";
  1351. } else if (question.text.includes('沉井下沉') || question.text.includes('土体破坏')) {
  1352. question.selectedAnswer = "C";
  1353. } else {
  1354. // 随机设置一个答案
  1355. const answers = ["A", "B", "C", "D"];
  1356. question.selectedAnswer = answers[index % answers.length];
  1357. }
  1358. });
  1359. }
  1360. // 判断题设置默认答案
  1361. if (examData.judge && examData.judge.questions) {
  1362. examData.judge.questions.forEach((question, index) => {
  1363. // 根据题目内容智能设置答案
  1364. if (question.text.includes('水灰比无关')) {
  1365. question.selectedAnswer = "错误";
  1366. } else if (question.text.includes('张拉顺序') || question.text.includes('预应力损失')) {
  1367. question.selectedAnswer = "正确";
  1368. } else if (question.text.includes('养护时间越长越好')) {
  1369. question.selectedAnswer = "错误";
  1370. } else if (question.text.includes('下沉速度应控制')) {
  1371. question.selectedAnswer = "正确";
  1372. } else if (question.text.includes('张拉应力值可以超过设计值')) {
  1373. question.selectedAnswer = "错误";
  1374. } else {
  1375. // 随机设置一个答案
  1376. question.selectedAnswer = index % 2 === 0 ? "正确" : "错误";
  1377. }
  1378. });
  1379. }
  1380. // 多选题设置默认答案
  1381. if (examData.multiple && examData.multiple.questions) {
  1382. examData.multiple.questions.forEach((question, index) => {
  1383. if (!question.selectedAnswers) {
  1384. question.selectedAnswers = [];
  1385. }
  1386. // 根据题目内容智能设置答案
  1387. if (question.text.includes('桥梁基础施工')) {
  1388. question.selectedAnswers = ["A", "C", "D"];
  1389. } else if (question.text.includes('预应力技术')) {
  1390. question.selectedAnswers = ["A", "B"];
  1391. } else if (question.text.includes('质量控制')) {
  1392. question.selectedAnswers = ["A", "B", "C"];
  1393. } else if (question.text.includes('安全措施')) {
  1394. question.selectedAnswers = ["A", "B", "C", "D"];
  1395. } else {
  1396. // 随机设置2-3个答案
  1397. const answers = ["A", "B", "C", "D"];
  1398. const count = Math.floor(Math.random() * 2) + 2; // 2-3个答案
  1399. question.selectedAnswers = answers.slice(0, count);
  1400. }
  1401. });
  1402. }
  1403. };
  1404. // 生成默认试卷(当AI解析失败时)
  1405. const generateDefaultExam = () => {
  1406. return {
  1407. title: examName.value,
  1408. totalScore: totalScore.value,
  1409. totalQuestions: questionTypes.value.reduce((sum, type) => sum + type.questionCount, 0),
  1410. singleChoice: {
  1411. scorePerQuestion: questionTypes.value.find(t => t.name === '单选题')?.scorePerQuestion || 2,
  1412. totalScore: (questionTypes.value.find(t => t.name === '单选题')?.scorePerQuestion || 2) * (questionTypes.value.find(t => t.name === '单选题')?.questionCount || 15),
  1413. count: questionTypes.value.find(t => t.name === '单选题')?.questionCount || 15,
  1414. questions: generateDefaultQuestions('single', questionTypes.value.find(t => t.name === '单选题')?.questionCount || 15)
  1415. },
  1416. judge: {
  1417. scorePerQuestion: questionTypes.value.find(t => t.name === '判断题')?.scorePerQuestion || 2,
  1418. totalScore: (questionTypes.value.find(t => t.name === '判断题')?.scorePerQuestion || 2) * (questionTypes.value.find(t => t.name === '判断题')?.questionCount || 10),
  1419. count: questionTypes.value.find(t => t.name === '判断题')?.questionCount || 10,
  1420. questions: generateDefaultQuestions('judge', questionTypes.value.find(t => t.name === '判断题')?.questionCount || 10)
  1421. },
  1422. multiple: {
  1423. scorePerQuestion: questionTypes.value.find(t => t.name === '多选题')?.scorePerQuestion || 3,
  1424. totalScore: (questionTypes.value.find(t => t.name === '多选题')?.scorePerQuestion || 3) * (questionTypes.value.find(t => t.name === '多选题')?.questionCount || 10),
  1425. count: questionTypes.value.find(t => t.name === '多选题')?.questionCount || 10,
  1426. questions: generateDefaultQuestions('multiple', questionTypes.value.find(t => t.name === '多选题')?.questionCount || 10)
  1427. },
  1428. short: {
  1429. scorePerQuestion: questionTypes.value.find(t => t.name === '简答题')?.scorePerQuestion || 10,
  1430. totalScore: (questionTypes.value.find(t => t.name === '简答题')?.scorePerQuestion || 10) * (questionTypes.value.find(t => t.name === '简答题')?.questionCount || 2),
  1431. count: questionTypes.value.find(t => t.name === '简答题')?.questionCount || 2,
  1432. questions: generateDefaultQuestions('short', questionTypes.value.find(t => t.name === '简答题')?.questionCount || 2)
  1433. }
  1434. };
  1435. };
  1436. // 生成默认题目
  1437. const generateDefaultQuestions = (type, count) => {
  1438. const questions = [];
  1439. const projectType = projectTypes[selectedProjectType.value].name;
  1440. for (let i = 0; i < count; i++) {
  1441. if (type === 'single') {
  1442. questions.push({
  1443. text: `${projectType}工程相关单选题${i + 1}`,
  1444. options: [
  1445. { key: "A", text: "选项A" },
  1446. { key: "B", text: "选项B" },
  1447. { key: "C", text: "选项C" },
  1448. { key: "D", text: "选项D" }
  1449. ],
  1450. selectedAnswer: ""
  1451. });
  1452. } else if (type === 'judge') {
  1453. questions.push({
  1454. text: `${projectType}工程相关判断题${i + 1}`,
  1455. selectedAnswer: ""
  1456. });
  1457. } else if (type === 'multiple') {
  1458. questions.push({
  1459. text: `${projectType}工程相关多选题${i + 1}`,
  1460. options: [
  1461. { key: "A", text: "选项A" },
  1462. { key: "B", text: "选项B" },
  1463. { key: "C", text: "选项C" },
  1464. { key: "D", text: "选项D" }
  1465. ],
  1466. selectedAnswers: []
  1467. });
  1468. } else if (type === 'short') {
  1469. questions.push({
  1470. text: `${projectType}工程相关简答题${i + 1}`,
  1471. outline: {
  1472. keyFactors: "答题要点、关键因素、示例答案"
  1473. }
  1474. });
  1475. }
  1476. }
  1477. return questions;
  1478. };
  1479. // 更新当前试卷数据
  1480. const updateCurrentExam = (generatedExam) => {
  1481. currentExam.value = generatedExam;
  1482. };
  1483. // 返回配置页面
  1484. const backToConfig = () => {
  1485. showExamDetail.value = false;
  1486. };
  1487. // 切换题型展开/收起
  1488. const toggleSection = (sectionType) => {
  1489. expandedSections.value[sectionType] = !expandedSections.value[sectionType];
  1490. };
  1491. // 选中最新历史记录
  1492. const selectLatestHistoryRecord = () => {
  1493. if (historyData.value.length > 0) {
  1494. // 清除所有选中状态
  1495. historyData.value.forEach((item) => {
  1496. item.isActive = false;
  1497. });
  1498. // 选中第一条(最新的)记录
  1499. historyData.value[0].isActive = true;
  1500. }
  1501. };
  1502. // 选择答案功能已禁用
  1503. /*
  1504. // 选择答案(单选题和判断题)
  1505. const selectAnswer = async (sectionType, questionIndex, answer) => {
  1506. if (sectionType === 'single') {
  1507. currentExam.value.singleChoice.questions[questionIndex].selectedAnswer = answer;
  1508. } else if (sectionType === 'judge') {
  1509. currentExam.value.judge.questions[questionIndex].selectedAnswer = answer;
  1510. }
  1511. // 保存答案修改到后端
  1512. await saveToReModifyQuestion(sectionType, questionIndex, null);
  1513. // 给用户提示
  1514. ElMessage.success("答案已保存");
  1515. // AI回复完成后,获取最新的历史记录
  1516. await getHistoryRecordList();
  1517. // 如果是新对话,将最新的历史记录设为激活状态
  1518. if (ai_conversation_id.value > 0) {
  1519. historyData.value.forEach((item) => {
  1520. item.isActive = item.id === ai_conversation_id.value;
  1521. });
  1522. console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
  1523. } else {
  1524. // 如果没有对话ID,选中第一条记录
  1525. selectLatestHistoryRecord();
  1526. }
  1527. };
  1528. */
  1529. // 切换多选题答案功能已禁用
  1530. /*
  1531. // 切换多选题答案
  1532. const toggleMultipleAnswer = async (sectionType, questionIndex, answer) => {
  1533. if (sectionType === 'multiple') {
  1534. const question = currentExam.value.multiple.questions[questionIndex];
  1535. const index = question.selectedAnswers.indexOf(answer);
  1536. if (index > -1) {
  1537. question.selectedAnswers.splice(index, 1);
  1538. } else {
  1539. question.selectedAnswers.push(answer);
  1540. }
  1541. }
  1542. // 保存答案修改到后端
  1543. await saveToReModifyQuestion(sectionType, questionIndex, null);
  1544. // 给用户提示
  1545. ElMessage.success("答案已保存");
  1546. // AI回复完成后,获取最新的历史记录
  1547. await getHistoryRecordList();
  1548. // 如果是新对话,将最新的历史记录设为激活状态
  1549. if (ai_conversation_id.value > 0) {
  1550. historyData.value.forEach((item) => {
  1551. item.isActive = item.id === ai_conversation_id.value;
  1552. });
  1553. console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
  1554. } else {
  1555. // 如果没有对话ID,选中第一条记录
  1556. selectLatestHistoryRecord();
  1557. }
  1558. };
  1559. */
  1560. // 刷新题目
  1561. const refreshQuestion = async (sectionType, questionIndex) => {
  1562. try {
  1563. console.log(`刷新${sectionType}类型第${questionIndex + 1}题`);
  1564. // 设置刷新状态
  1565. const key = `${sectionType}_${questionIndex}`;
  1566. isRefreshing.value[key] = true;
  1567. // 构建单题重新生成的提示词
  1568. const prompt = buildSingleQuestionPrompt(sectionType, questionIndex);
  1569. // 第一步:调用 /re_produce_single_question 接口,AI只生成题目
  1570. const response = await apis.reProduceSingleQuestion({
  1571. message: prompt
  1572. });
  1573. if (response.statusCode === 200) {
  1574. const aiReply = response.data.reply;
  1575. console.log('AI重新生成的题目:', aiReply);
  1576. // 不更新对话ID,使用当前的ai_conversation_id
  1577. console.log('使用当前对话ID:', ai_conversation_id.value);
  1578. // 解析AI回复并更新题目
  1579. const newQuestion = parseSingleQuestionResponse(aiReply, sectionType);
  1580. console.log('解析后的新题目:', newQuestion);
  1581. if (newQuestion) {
  1582. updateQuestion(sectionType, questionIndex, newQuestion);
  1583. console.log('准备保存到后端,对话ID:', ai_conversation_id.value);
  1584. // 第二步:使用 /re_modify_question 接口保存修改
  1585. await saveToReModifyQuestion(sectionType, questionIndex, newQuestion);
  1586. ElMessage.success("题目重新生成成功!");
  1587. // AI回复完成后,获取最新的历史记录
  1588. await getHistoryRecordList();
  1589. // 如果是新对话,将最新的历史记录设为激活状态
  1590. if (ai_conversation_id.value > 0) {
  1591. historyData.value.forEach((item) => {
  1592. item.isActive = item.id === ai_conversation_id.value;
  1593. });
  1594. console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
  1595. } else {
  1596. // 如果没有对话ID,选中第一条记录
  1597. selectLatestHistoryRecord();
  1598. }
  1599. } else {
  1600. ElMessage.error("题目重新生成失败,请稍后重试");
  1601. }
  1602. } else {
  1603. throw new Error('AI接口调用失败');
  1604. }
  1605. } catch (error) {
  1606. console.error('重新生成题目失败:', error);
  1607. ElMessage.error('重新生成题目失败,请稍后重试');
  1608. } finally {
  1609. // 重置刷新状态
  1610. const key = `${sectionType}_${questionIndex}`;
  1611. isRefreshing.value[key] = false;
  1612. }
  1613. };
  1614. // 获取当前题目
  1615. const getCurrentQuestion = (sectionType, questionIndex) => {
  1616. if (sectionType === 'single') {
  1617. return currentExam.value.singleChoice.questions[questionIndex];
  1618. } else if (sectionType === 'judge') {
  1619. return currentExam.value.judge.questions[questionIndex];
  1620. } else if (sectionType === 'multiple') {
  1621. return currentExam.value.multiple.questions[questionIndex];
  1622. } else if (sectionType === 'short') {
  1623. return currentExam.value.short.questions[questionIndex];
  1624. }
  1625. return null;
  1626. };
  1627. // 构建单题重新生成的提示词
  1628. const buildSingleQuestionPrompt = (sectionType, questionIndex) => {
  1629. const projectType = projectTypes[selectedProjectType.value].name;
  1630. const questionType = getQuestionTypeName(sectionType);
  1631. const scorePerQuestion = getQuestionScore(sectionType);
  1632. // 获取当前题目作为参考
  1633. const currentQuestion = getCurrentQuestion(sectionType, questionIndex);
  1634. let prompt = `请基于以下${projectType}工程的${questionType}题目,重新生成一道相似主题的题目,要求如下:
  1635. 当前题目参考:
  1636. ${JSON.stringify(currentQuestion, null, 2)}
  1637. 题目类型:${questionType}
  1638. 每题分值:${scorePerQuestion}分
  1639. 题目序号:第${questionIndex + 1}题
  1640. 请严格按照以下JSON格式返回,不要包含任何其他文字:
  1641. `;
  1642. if (sectionType === 'single') {
  1643. prompt += `{
  1644. "text": "题目内容",
  1645. "options": [
  1646. {"key": "A", "text": "选项A内容"},
  1647. {"key": "B", "text": "选项B内容"},
  1648. {"key": "C", "text": "选项C内容"},
  1649. {"key": "D", "text": "选项D内容"}
  1650. ],
  1651. "selectedAnswer": "正确答案选项(A/B/C/D)"
  1652. }`;
  1653. } else if (sectionType === 'judge') {
  1654. prompt += `{
  1655. "text": "题目内容",
  1656. "selectedAnswer": "正确答案(正确/错误)"
  1657. }`;
  1658. } else if (sectionType === 'multiple') {
  1659. prompt += `{
  1660. "text": "题目内容",
  1661. "options": [
  1662. {"key": "A", "text": "选项A内容"},
  1663. {"key": "B", "text": "选项B内容"},
  1664. {"key": "C", "text": "选项C内容"},
  1665. {"key": "D", "text": "选项D内容"}
  1666. ],
  1667. "selectedAnswers": ["正确答案选项1", "正确答案选项2"]
  1668. }`;
  1669. } else if (sectionType === 'short') {
  1670. prompt += `{
  1671. "text": "题目内容",
  1672. "outline": {
  1673. "keyFactors": "答题要点、关键因素、示例答案"
  1674. }
  1675. }`;
  1676. }
  1677. prompt += `
  1678. 注意:
  1679. 1. 新题目必须与当前题目保持相似的主题和难度
  1680. 2. 题目内容必须与${projectType}工程相关
  1681. 3. 题目难度适中,符合考试要求
  1682. 4. 严格按照JSON格式返回,不要有多余字符
  1683. 5. 单选题和判断题的选项要合理
  1684. 6. 多选题至少要有2个正确答案
  1685. 7. 简答题要提供清晰的答题要点
  1686. 8. 必须为每道题设置正确答案:
  1687. - 单选题:selectedAnswer字段填写正确的选项(A/B/C/D)
  1688. - 判断题:selectedAnswer字段填写"正确"或"错误"
  1689. - 多选题:selectedAnswers数组包含所有正确答案选项
  1690. 9. 简答题答案字数不超过500字
  1691. 10. 新题目应该是当前题目的变体,保持主题一致性但内容要有所变化`;
  1692. return prompt;
  1693. };
  1694. // 获取题型名称
  1695. const getQuestionTypeName = (sectionType) => {
  1696. const typeMap = {
  1697. 'single': '单选题',
  1698. 'judge': '判断题',
  1699. 'multiple': '多选题',
  1700. 'short': '简答题'
  1701. };
  1702. return typeMap[sectionType] || '题目';
  1703. };
  1704. // 获取题型分值
  1705. const getQuestionScore = (sectionType) => {
  1706. const scoreMap = {
  1707. 'single': currentExam.value.singleChoice.scorePerQuestion,
  1708. 'judge': currentExam.value.judge.scorePerQuestion,
  1709. 'multiple': currentExam.value.multiple.scorePerQuestion,
  1710. 'short': currentExam.value.short.scorePerQuestion
  1711. };
  1712. return scoreMap[sectionType] || 2;
  1713. };
  1714. // 解析单题AI回复
  1715. const parseSingleQuestionResponse = (aiReply, sectionType) => {
  1716. try {
  1717. console.log('AI回复内容:', aiReply);
  1718. console.log('题目类型:', sectionType);
  1719. // 尝试提取JSON内容
  1720. const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
  1721. if (jsonMatch) {
  1722. const questionData = JSON.parse(jsonMatch[0]);
  1723. console.log('解析后的题目数据:', questionData);
  1724. // 如果是简答题,检查keyFactors字段
  1725. if (sectionType === 'short' && questionData.outline && questionData.outline.keyFactors) {
  1726. console.log('简答题keyFactors原始值:', questionData.outline.keyFactors);
  1727. // 如果keyFactors是数组,转换为字符串
  1728. if (Array.isArray(questionData.outline.keyFactors)) {
  1729. questionData.outline.keyFactors = questionData.outline.keyFactors.join(' ');
  1730. console.log('转换后的keyFactors:', questionData.outline.keyFactors);
  1731. }
  1732. }
  1733. return questionData;
  1734. } else {
  1735. throw new Error('未找到有效的JSON数据');
  1736. }
  1737. } catch (error) {
  1738. console.error('解析单题AI回复失败:', error);
  1739. return null;
  1740. }
  1741. };
  1742. // 更新题目
  1743. const updateQuestion = (sectionType, questionIndex, newQuestion) => {
  1744. let updatedQuestion;
  1745. if (sectionType === 'single') {
  1746. // 直接使用AI生成的题目和答案,不重置
  1747. updatedQuestion = {
  1748. ...newQuestion
  1749. };
  1750. // 如果AI没有生成答案,才设置默认答案
  1751. if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
  1752. setDefaultAnswerForQuestion(sectionType, questionIndex, updatedQuestion);
  1753. }
  1754. currentExam.value.singleChoice.questions[questionIndex] = updatedQuestion;
  1755. } else if (sectionType === 'judge') {
  1756. // 直接使用AI生成的题目和答案,不重置
  1757. updatedQuestion = {
  1758. ...newQuestion
  1759. };
  1760. // 如果AI没有生成答案,才设置默认答案
  1761. if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
  1762. setDefaultAnswerForQuestion(sectionType, questionIndex, updatedQuestion);
  1763. }
  1764. currentExam.value.judge.questions[questionIndex] = updatedQuestion;
  1765. } else if (sectionType === 'multiple') {
  1766. // 直接使用AI生成的题目和答案,不重置
  1767. updatedQuestion = {
  1768. ...newQuestion
  1769. };
  1770. // 如果AI没有生成答案,才设置默认答案
  1771. if (!updatedQuestion.selectedAnswers || updatedQuestion.selectedAnswers.length === 0) {
  1772. setDefaultAnswerForQuestion(sectionType, questionIndex, updatedQuestion);
  1773. }
  1774. currentExam.value.multiple.questions[questionIndex] = updatedQuestion;
  1775. } else if (sectionType === 'short') {
  1776. currentExam.value.short.questions[questionIndex] = newQuestion;
  1777. }
  1778. // 强制触发Vue响应式更新
  1779. currentExam.value = { ...currentExam.value };
  1780. // 调试输出
  1781. console.log(`更新后的题目${sectionType}_${questionIndex}:`, updatedQuestion);
  1782. console.log('当前试卷数据:', currentExam.value);
  1783. };
  1784. // 为新生成的题目设置默认答案
  1785. const setDefaultAnswerForQuestion = (sectionType, questionIndex, question) => {
  1786. if (sectionType === 'single') {
  1787. // 单选题:根据题目内容智能设置答案
  1788. if (question.text.includes('桩身倾斜') || question.text.includes('桩位纠偏')) {
  1789. question.selectedAnswer = "B";
  1790. } else if (question.text.includes('深水河流') || question.text.includes('悬臂浇筑')) {
  1791. question.selectedAnswer = "B";
  1792. } else if (question.text.includes('预应力张拉') || question.text.includes('对称张拉')) {
  1793. question.selectedAnswer = "C";
  1794. } else if (question.text.includes('混凝土裂缝') || question.text.includes('伸缩缝')) {
  1795. question.selectedAnswer = "C";
  1796. } else if (question.text.includes('沉井下沉') || question.text.includes('土体破坏')) {
  1797. question.selectedAnswer = "C";
  1798. } else {
  1799. // 随机设置一个答案
  1800. const answers = ["A", "B", "C", "D"];
  1801. question.selectedAnswer = answers[questionIndex % answers.length];
  1802. }
  1803. } else if (sectionType === 'judge') {
  1804. // 判断题:根据题目内容智能设置答案
  1805. if (question.text.includes('水灰比无关')) {
  1806. question.selectedAnswer = "错误";
  1807. } else if (question.text.includes('张拉顺序') || question.text.includes('预应力损失')) {
  1808. question.selectedAnswer = "正确";
  1809. } else if (question.text.includes('养护时间越长越好')) {
  1810. question.selectedAnswer = "错误";
  1811. } else if (question.text.includes('下沉速度应控制')) {
  1812. question.selectedAnswer = "正确";
  1813. } else if (question.text.includes('张拉应力值可以超过设计值')) {
  1814. question.selectedAnswer = "错误";
  1815. } else {
  1816. // 随机设置一个答案
  1817. question.selectedAnswer = questionIndex % 2 === 0 ? "正确" : "错误";
  1818. }
  1819. } else if (sectionType === 'multiple') {
  1820. // 多选题:根据题目内容智能设置答案
  1821. if (!question.selectedAnswers) {
  1822. question.selectedAnswers = [];
  1823. }
  1824. if (question.text.includes('桥梁基础施工')) {
  1825. question.selectedAnswers = ["A", "C", "D"];
  1826. } else if (question.text.includes('预应力技术')) {
  1827. question.selectedAnswers = ["A", "B"];
  1828. } else if (question.text.includes('质量控制')) {
  1829. question.selectedAnswers = ["A", "B", "C"];
  1830. } else if (question.text.includes('安全措施')) {
  1831. question.selectedAnswers = ["A", "B", "C", "D"];
  1832. } else {
  1833. // 随机设置2-3个答案
  1834. const answers = ["A", "B", "C", "D"];
  1835. const count = Math.floor(Math.random() * 2) + 2; // 2-3个答案
  1836. question.selectedAnswers = answers.slice(0, count);
  1837. }
  1838. }
  1839. // 简答题不需要设置答案,保持原有逻辑
  1840. };
  1841. // 使用 /re_modify_question 接口保存修改
  1842. const saveToReModifyQuestion = async (sectionType, questionIndex, newQuestion) => {
  1843. console.log('对话id',ai_conversation_id.value);
  1844. try {
  1845. // 使用当前保存的对话ID
  1846. if (!ai_conversation_id.value) {
  1847. console.warn('没有找到对话ID,跳过保存');
  1848. return;
  1849. }
  1850. // 构建要保存的内容 - 保存整个试卷的JSON字符串
  1851. const content = JSON.stringify(currentExam.value);
  1852. console.log('保存到 /re_modify_question 的内容:', content);
  1853. // 调用后端接口保存修改
  1854. const response = await apis.reModifyQuestion({
  1855. ai_conversation_id: ai_conversation_id.value,
  1856. content: content
  1857. });
  1858. if (response.statusCode === 200) {
  1859. console.log('修改已保存到后端');
  1860. // 如果是重新生成题目,不显示额外提示,因为已经有成功提示了
  1861. if (newQuestion) {
  1862. console.log('题目重新生成并保存成功');
  1863. }
  1864. } else {
  1865. console.error('保存修改失败:', response);
  1866. }
  1867. } catch (error) {
  1868. console.error('保存修改失败:', error);
  1869. // 不抛出错误,避免影响用户体验
  1870. }
  1871. };
  1872. // 控制下载菜单显示/隐藏
  1873. const toggleDownloadMenu = () => {
  1874. if (!isGenerating.value) {
  1875. showDownloadMenu.value = !showDownloadMenu.value;
  1876. }
  1877. };
  1878. // 关闭下载菜单
  1879. const closeDownloadMenu = () => {
  1880. showDownloadMenu.value = false;
  1881. };
  1882. // 点击外部区域关闭下载菜单
  1883. const handleClickOutside = (event) => {
  1884. const dropdown = event.target.closest('.download-dropdown');
  1885. if (!dropdown) {
  1886. showDownloadMenu.value = false;
  1887. }
  1888. };
  1889. // 导出Word(有答案)
  1890. const exportToWordWithAnswers = async () => {
  1891. try {
  1892. closeDownloadMenu(); // 关闭下拉菜单
  1893. // 准备导出数据
  1894. const exportData = prepareExamDataForExport();
  1895. console.log('准备导出的数据(有答案):', exportData);
  1896. // 直接使用模拟导出功能
  1897. console.log('使用模拟Word导出功能(有答案)');
  1898. await simulateWordExport(true);
  1899. } catch (error) {
  1900. console.error('Word导出失败:', error);
  1901. ElMessage.error('Word导出失败,请稍后重试');
  1902. }
  1903. };
  1904. // 导出Word(无答案)
  1905. const exportToWordWithoutAnswers = async () => {
  1906. try {
  1907. closeDownloadMenu(); // 关闭下拉菜单
  1908. // 准备导出数据
  1909. const exportData = prepareExamDataForExport();
  1910. console.log('准备导出的数据(无答案):', exportData);
  1911. // 直接使用模拟导出功能
  1912. console.log('使用模拟Word导出功能(无答案)');
  1913. await simulateWordExport(false);
  1914. } catch (error) {
  1915. console.error('Word导出失败:', error);
  1916. ElMessage.error('Word导出失败,请稍后重试');
  1917. }
  1918. };
  1919. // 导出Word(保留原函数以兼容其他可能的调用)
  1920. const exportToWord = async () => {
  1921. // 默认导出有答案版本
  1922. await exportToWordWithAnswers();
  1923. };
  1924. // 模拟Word导出功能(临时使用)
  1925. const simulateWordExport = async (includeAnswers = true) => {
  1926. try {
  1927. // 准备导出数据
  1928. const exportData = prepareExamDataForExport();
  1929. // 创建Word文档内容(使用HTML格式,兼容WPS和Word)
  1930. const wordContent = createHTMLContent(exportData, includeAnswers);
  1931. // 创建Blob对象 - 使用HTML格式
  1932. const blob = new Blob([wordContent], {
  1933. type: 'application/msword'
  1934. });
  1935. // 下载文件
  1936. const url = URL.createObjectURL(blob);
  1937. const link = document.createElement('a');
  1938. const fileName = includeAnswers
  1939. ? `${currentExam.value.title}_有答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`
  1940. : `${currentExam.value.title}_无答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`;
  1941. link.setAttribute('href', url);
  1942. link.setAttribute('download', fileName);
  1943. link.style.visibility = 'hidden';
  1944. document.body.appendChild(link);
  1945. link.click();
  1946. document.body.removeChild(link);
  1947. ElMessage.success(`导出成功${includeAnswers ? '(含答案)' : '(不含答案)'}`);
  1948. } catch (error) {
  1949. console.error('模拟Word导出失败:', error);
  1950. ElMessage.error('Word导出失败,请稍后重试');
  1951. }
  1952. };
  1953. // 创建HTML格式的Word文档内容(兼容WPS和Word)
  1954. const createHTMLContent = (exportData, includeAnswers = true) => {
  1955. const exam = currentExam.value;
  1956. // HTML文档内容,使用Word兼容的格式
  1957. let htmlContent = `<!DOCTYPE html>
  1958. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  1959. xmlns:w="urn:schemas-microsoft-com:office:word"
  1960. xmlns="http://www.w3.org/TR/REC-html40">
  1961. <head>
  1962. <meta charset="utf-8">
  1963. <meta name="ProgId" content="Word.Document">
  1964. <meta name="Generator" content="Microsoft Word 15">
  1965. <meta name="Originator" content="Microsoft Word 15">
  1966. <title>${exam.title || '试卷'}</title>
  1967. <!--[if gte mso 9]>
  1968. <xml>
  1969. <w:WordDocument>
  1970. <w:View>Print</w:View>
  1971. <w:Zoom>100</w:Zoom>
  1972. <w:DoNotPromptForConvert/>
  1973. <w:DoNotShowRevisions/>
  1974. <w:DoNotPrintRevisions/>
  1975. <w:DoNotShowComments/>
  1976. <w:DoNotShowInsertionsAndDeletions/>
  1977. <w:DoNotShowPropertyChanges/>
  1978. <w:Compatibility>
  1979. <w:BreakWrappedTables/>
  1980. <w:SnapToGridInCell/>
  1981. <w:WrapTextWithPunct/>
  1982. <w:UseAsianBreakRules/>
  1983. <w:DontGrowAutofit/>
  1984. </w:Compatibility>
  1985. </w:WordDocument>
  1986. </xml>
  1987. <![endif]-->
  1988. <style>
  1989. body {
  1990. font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
  1991. font-size: 14px;
  1992. line-height: 1.6;
  1993. margin: 24px;
  1994. color: #000;
  1995. }
  1996. .header {
  1997. text-align: center;
  1998. margin-bottom: 14px;
  1999. }
  2000. .exam-title {
  2001. font-size: 24px;
  2002. font-weight: bold;
  2003. margin-bottom: 14px;
  2004. color: #000;
  2005. }
  2006. .exam-info {
  2007. font-size: 14px;
  2008. color: #666;
  2009. margin-bottom: 14px;
  2010. }
  2011. .section {
  2012. margin-bottom: 14px;
  2013. }
  2014. .section-title {
  2015. font-size: 18px;
  2016. font-weight: bold;
  2017. margin-bottom: 14px;
  2018. color: #000;
  2019. border-bottom: 2px solid #3e7bfa;
  2020. padding-bottom: 5px;
  2021. }
  2022. .question {
  2023. margin-bottom: 14px;
  2024. padding: 10px;
  2025. background-color: #f9f9f9;
  2026. border-left: 4px solid #3e7bfa;
  2027. }
  2028. .question-header {
  2029. display: flex;
  2030. align-items: flex-start;
  2031. gap: 8px;
  2032. margin-bottom: 14px;
  2033. }
  2034. .question-number {
  2035. font-weight: bold;
  2036. color: #3e7bfa;
  2037. flex-shrink: 0;
  2038. }
  2039. .question-text {
  2040. flex: 1;
  2041. }
  2042. .options {
  2043. margin-left: 12px;
  2044. }
  2045. .option {
  2046. margin-bottom: 5px;
  2047. }
  2048. .answer {
  2049. margin-top: 10px;
  2050. padding: 8px;
  2051. background: #e8f4fd;
  2052. border-radius: 4px;
  2053. font-weight: bold;
  2054. color: #2c5aa0;
  2055. }
  2056. </style>
  2057. </head>
  2058. <body>
  2059. <div class="header">
  2060. <div class="exam-title">${exam.title || '试卷'}</div>
  2061. <div class="exam-info">
  2062. 总分:${exam.totalScore || 0}分 | 总题数:${exam.totalQuestions || 0}题 | 生成时间:${currentTime.value}
  2063. </div>
  2064. </div>`;
  2065. // 单选题
  2066. if (exam.singleChoice && exam.singleChoice.questions && exam.singleChoice.questions.length > 0) {
  2067. htmlContent += `
  2068. <div class="section">
  2069. <div class="section-title">一、单选题(每题${exam.singleChoice.scorePerQuestion || 0}分,共${exam.singleChoice.totalScore || 0}分)</div>`;
  2070. exam.singleChoice.questions.forEach((question, index) => {
  2071. htmlContent += `
  2072. <div class="question">
  2073. <div class="question-header">
  2074. <span class="question-number">${index + 1}.</span>
  2075. <span class="question-text">${question.text || '题目内容'}</span>
  2076. </div>`;
  2077. if (question.options && question.options.length > 0) {
  2078. htmlContent += `<div class="options">`;
  2079. question.options.forEach((option, optIndex) => {
  2080. const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
  2081. htmlContent += `<div class="option">${optionLabel}. ${option.text || '选项内容'}</div>`;
  2082. });
  2083. htmlContent += `</div>`;
  2084. }
  2085. htmlContent += `${includeAnswers ? `<div class="answer">答案:${question.selectedAnswer || '未设置'}</div>` : ''}
  2086. </div>`;
  2087. });
  2088. htmlContent += `</div>`;
  2089. }
  2090. // 判断题
  2091. if (exam.judge && exam.judge.questions && exam.judge.questions.length > 0) {
  2092. htmlContent += `
  2093. <div class="section">
  2094. <div class="section-title">二、判断题(每题${exam.judge.scorePerQuestion || 0}分,共${exam.judge.totalScore || 0}分)</div>`;
  2095. exam.judge.questions.forEach((question, index) => {
  2096. htmlContent += `
  2097. <div class="question">
  2098. <div class="question-header">
  2099. <span class="question-number">${index + 1}.</span>
  2100. <span class="question-text">${question.text || '题目内容'}</span>
  2101. </div>
  2102. ${includeAnswers ? `<div class="answer">答案:${question.selectedAnswer || '未设置'}</div>` : ''}
  2103. </div>`;
  2104. });
  2105. htmlContent += `</div>`;
  2106. }
  2107. // 多选题
  2108. if (exam.multipleChoice && exam.multipleChoice.questions && exam.multipleChoice.questions.length > 0) {
  2109. htmlContent += `
  2110. <div class="section">
  2111. <div class="section-title">三、多选题(每题${exam.multipleChoice.scorePerQuestion || 0}分,共${exam.multipleChoice.totalScore || 0}分)</div>`;
  2112. exam.multipleChoice.questions.forEach((question, index) => {
  2113. htmlContent += `
  2114. <div class="question">
  2115. <div class="question-header">
  2116. <span class="question-number">${index + 1}.</span>
  2117. <span class="question-text">${question.text || '题目内容'}</span>
  2118. </div>`;
  2119. if (question.options && question.options.length > 0) {
  2120. htmlContent += `<div class="options">`;
  2121. question.options.forEach((option, optIndex) => {
  2122. const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
  2123. htmlContent += `<div class="option">${optionLabel}. ${option.text || '选项内容'}</div>`;
  2124. });
  2125. htmlContent += `</div>`;
  2126. }
  2127. htmlContent += `${includeAnswers ? `<div class="answer">答案:${question.selectedAnswer || question.selectedAnswers?.join(', ') || '未设置'}</div>` : ''}
  2128. </div>`;
  2129. });
  2130. htmlContent += `</div>`;
  2131. }
  2132. // 简答题
  2133. if (exam.short && exam.short.questions && exam.short.questions.length > 0) {
  2134. htmlContent += `
  2135. <div class="section">
  2136. <div class="section-title">四、简答题(每题${exam.short.scorePerQuestion || 0}分,共${exam.short.totalScore || 0}分)</div>`;
  2137. exam.short.questions.forEach((question, index) => {
  2138. htmlContent += `
  2139. <div class="question">
  2140. <div class="question-header">
  2141. <span class="question-number">${index + 1}.</span>
  2142. <span class="question-text">${question.text || '题目内容'}</span>
  2143. </div>
  2144. ${includeAnswers ? `<div class="answer">答题要点:${question.outline?.keyFactors || '未设置'}</div>` : ''}
  2145. </div>`;
  2146. });
  2147. htmlContent += `</div>`;
  2148. }
  2149. htmlContent += `
  2150. </body>
  2151. </html>`;
  2152. return htmlContent;
  2153. };
  2154. // 创建纯文本格式的文档内容(避免编码问题)
  2155. const createTextContent = (exportData, includeAnswers = true) => {
  2156. const exam = currentExam.value;
  2157. let textContent = '';
  2158. // 试卷标题
  2159. textContent += `${exam.title || '试卷'}\n`;
  2160. textContent += '='.repeat(50) + '\n\n';
  2161. // 试卷信息
  2162. textContent += `总分:${exam.totalScore || 0}分\n`;
  2163. textContent += `总题数:${exam.totalQuestions || 0}题\n`;
  2164. textContent += `生成时间:${currentTime.value}\n\n`;
  2165. // 单选题
  2166. if (exam.singleChoice && exam.singleChoice.questions && exam.singleChoice.questions.length > 0) {
  2167. textContent += `一、单选题(每题${exam.singleChoice.scorePerQuestion || 0}分,共${exam.singleChoice.totalScore || 0}分)\n`;
  2168. textContent += '-'.repeat(30) + '\n';
  2169. exam.singleChoice.questions.forEach((question, index) => {
  2170. textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
  2171. if (question.options && question.options.length > 0) {
  2172. question.options.forEach((option, optIndex) => {
  2173. const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
  2174. textContent += ` ${optionLabel}. ${option.text || '选项内容'}\n`;
  2175. });
  2176. }
  2177. if (includeAnswers) {
  2178. textContent += ` 答案:${question.selectedAnswer || '未设置'}\n\n`;
  2179. } else {
  2180. textContent += `\n`;
  2181. }
  2182. });
  2183. }
  2184. // 判断题
  2185. if (exam.judge && exam.judge.questions && exam.judge.questions.length > 0) {
  2186. textContent += `二、判断题(每题${exam.judge.scorePerQuestion || 0}分,共${exam.judge.totalScore || 0}分)\n`;
  2187. textContent += '-'.repeat(30) + '\n';
  2188. exam.judge.questions.forEach((question, index) => {
  2189. textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
  2190. if (includeAnswers) {
  2191. textContent += ` 答案:${question.selectedAnswer || '未设置'}\n\n`;
  2192. } else {
  2193. textContent += `\n`;
  2194. }
  2195. });
  2196. }
  2197. // 多选题
  2198. if (exam.multipleChoice && exam.multipleChoice.questions && exam.multipleChoice.questions.length > 0) {
  2199. textContent += `三、多选题(每题${exam.multipleChoice.scorePerQuestion || 0}分,共${exam.multipleChoice.totalScore || 0}分)\n`;
  2200. textContent += '-'.repeat(30) + '\n';
  2201. exam.multipleChoice.questions.forEach((question, index) => {
  2202. textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
  2203. if (question.options && question.options.length > 0) {
  2204. question.options.forEach((option, optIndex) => {
  2205. const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
  2206. textContent += ` ${optionLabel}. ${option.text || '选项内容'}\n`;
  2207. });
  2208. }
  2209. if (includeAnswers) {
  2210. textContent += ` 答案:${question.selectedAnswer || question.selectedAnswers?.join(', ') || '未设置'}\n\n`;
  2211. } else {
  2212. textContent += `\n`;
  2213. }
  2214. });
  2215. }
  2216. // 简答题
  2217. if (exam.short && exam.short.questions && exam.short.questions.length > 0) {
  2218. textContent += `四、简答题(每题${exam.short.scorePerQuestion || 0}分,共${exam.short.totalScore || 0}分)\n`;
  2219. textContent += '-'.repeat(30) + '\n';
  2220. exam.short.questions.forEach((question, index) => {
  2221. textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
  2222. if (includeAnswers) {
  2223. textContent += ` 答题要点:${question.outline?.keyFactors || '未设置'}\n\n`;
  2224. } else {
  2225. textContent += `\n`;
  2226. }
  2227. });
  2228. }
  2229. return textContent;
  2230. };
  2231. // 创建RTF格式的Word文档内容(兼容性更好)
  2232. const createRTFContent = (exportData, includeAnswers = true) => {
  2233. const exam = currentExam.value;
  2234. // RTF文档头部
  2235. let rtfContent = `{\\rtf1\\ansi\\deff0 {\\fonttbl {\\f0 Times New Roman;}{\\f1 Microsoft YaHei;}}
  2236. {\\colortbl;\\red0\\green0\\blue0;\\red62\\green123\\blue250;}
  2237. {\\*\\generator Microsoft Word;}\\f1\\fs24\\par`;
  2238. // 试卷标题
  2239. rtfContent += `{\\qc\\b\\fs32 ${exam.title || '试卷'}\\par}\\par`;
  2240. // 试卷信息
  2241. rtfContent += `{\\qc 总分:${exam.totalScore || 0}分 | 总题数:${exam.totalQuestions || 0}题 | 生成时间:${currentTime.value}\\par}\\par`;
  2242. // 单选题
  2243. if (exam.singleChoice && exam.singleChoice.questions && exam.singleChoice.questions.length > 0) {
  2244. rtfContent += `{\\b\\fs28 一、单选题(每题${exam.singleChoice.scorePerQuestion || 0}分,共${exam.singleChoice.totalScore || 0}分)\\par}\\par`;
  2245. exam.singleChoice.questions.forEach((question, index) => {
  2246. rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
  2247. if (question.options && question.options.length > 0) {
  2248. question.options.forEach((option, optIndex) => {
  2249. const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
  2250. rtfContent += ` ${optionLabel}. ${option.text || '选项内容'}\\par`;
  2251. });
  2252. }
  2253. if (includeAnswers) {
  2254. rtfContent += `{\\b 答案:}${question.selectedAnswer || '未设置'}\\par\\par`;
  2255. } else {
  2256. rtfContent += `\\par\\par`;
  2257. }
  2258. });
  2259. }
  2260. // 判断题
  2261. if (exam.judge && exam.judge.questions && exam.judge.questions.length > 0) {
  2262. rtfContent += `{\\b\\fs28 二、判断题(每题${exam.judge.scorePerQuestion || 0}分,共${exam.judge.totalScore || 0}分)\\par}\\par`;
  2263. exam.judge.questions.forEach((question, index) => {
  2264. rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
  2265. if (includeAnswers) {
  2266. rtfContent += `{\\b 答案:}${question.selectedAnswer || '未设置'}\\par\\par`;
  2267. } else {
  2268. rtfContent += `\\par\\par`;
  2269. }
  2270. });
  2271. }
  2272. // 多选题
  2273. if (exam.multipleChoice && exam.multipleChoice.questions && exam.multipleChoice.questions.length > 0) {
  2274. rtfContent += `{\\b\\fs28 三、多选题(每题${exam.multipleChoice.scorePerQuestion || 0}分,共${exam.multipleChoice.totalScore || 0}分)\\par}\\par`;
  2275. exam.multipleChoice.questions.forEach((question, index) => {
  2276. rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
  2277. if (question.options && question.options.length > 0) {
  2278. question.options.forEach((option, optIndex) => {
  2279. const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
  2280. rtfContent += ` ${optionLabel}. ${option.text || '选项内容'}\\par`;
  2281. });
  2282. }
  2283. if (includeAnswers) {
  2284. rtfContent += `{\\b 答案:}${question.selectedAnswer || question.selectedAnswers?.join(', ') || '未设置'}\\par\\par`;
  2285. } else {
  2286. rtfContent += `\\par\\par`;
  2287. }
  2288. });
  2289. }
  2290. // 简答题
  2291. if (exam.short && exam.short.questions && exam.short.questions.length > 0) {
  2292. rtfContent += `{\\b\\fs28 四、简答题(每题${exam.short.scorePerQuestion || 0}分,共${exam.short.totalScore || 0}分)\\par}\\par`;
  2293. exam.short.questions.forEach((question, index) => {
  2294. rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
  2295. if (includeAnswers) {
  2296. rtfContent += `{\\b 答题要点:}${question.outline?.keyFactors || '未设置'}\\par\\par`;
  2297. } else {
  2298. rtfContent += `\\par\\par`;
  2299. }
  2300. });
  2301. }
  2302. // RTF文档结尾
  2303. rtfContent += `}`;
  2304. return rtfContent;
  2305. };
  2306. // 创建Word文档内容(保留原函数以防需要)
  2307. const createWordContent = (exportData) => {
  2308. const exam = currentExam.value;
  2309. // 创建HTML格式的文档内容
  2310. let htmlContent = `
  2311. <html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
  2312. <head>
  2313. <meta charset='utf-8'>
  2314. <title>${exam.title}</title>
  2315. <style>
  2316. body { font-family: 'Microsoft YaHei', Arial, sans-serif; font-size: 14px; line-height: 1.6; }
  2317. .header { text-align: center; margin-bottom: 30px; }
  2318. .exam-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
  2319. .exam-info { margin-bottom: 12px; }
  2320. .section { margin-bottom: 30px; }
  2321. .section-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; color: #333; }
  2322. .question { margin-bottom: 12px; }
  2323. .question-header { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 12px; }
  2324. .question-number { font-weight: bold; color: #3e7bfa; flex-shrink: 0; }
  2325. .question-text { flex: 1; }
  2326. .options { margin-left: 20px; }
  2327. .option { margin-bottom: 5px; }
  2328. .answer { margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 5px; }
  2329. </style>
  2330. </head>
  2331. <body>
  2332. <div class='header'>
  2333. <div class='exam-title'>${exam.title}</div>
  2334. <div class='exam-info'>
  2335. <p>总分:${exam.totalScore}分 | 总题数:${exam.totalQuestions}题 | 生成时间:${currentTime.value}</p>
  2336. </div>
  2337. </div>
  2338. `;
  2339. // 单选题
  2340. htmlContent += `
  2341. <div class='section'>
  2342. <div class='section-title'>一、单选题(每题${exam.singleChoice.scorePerQuestion}分,共${exam.singleChoice.totalScore}分)</div>
  2343. `;
  2344. exam.singleChoice.questions.forEach((question, index) => {
  2345. htmlContent += `
  2346. <div class='question'>
  2347. <div class='question-header'>
  2348. <span class='question-number'>${index + 1}.</span>
  2349. <span class='question-text'>${question.text}</span>
  2350. </div>
  2351. <div class='options'>
  2352. ${question.options.map(option => `<div class='option'>${option.key}. ${option.text}</div>`).join('')}
  2353. </div>
  2354. <div class='answer'>正确答案:${question.selectedAnswer || '未设置'}</div>
  2355. </div>
  2356. `;
  2357. });
  2358. htmlContent += '</div>';
  2359. // 判断题
  2360. htmlContent += `
  2361. <div class='section'>
  2362. <div class='section-title'>二、判断题(每题${exam.judge.scorePerQuestion}分,共${exam.judge.totalScore}分)</div>
  2363. `;
  2364. exam.judge.questions.forEach((question, index) => {
  2365. htmlContent += `
  2366. <div class='question'>
  2367. <div class='question-header'>
  2368. <span class='question-number'>${index + 1}.</span>
  2369. <span class='question-text'>${question.text}</span>
  2370. </div>
  2371. <div class='answer'>正确答案:${question.selectedAnswer || '未设置'}</div>
  2372. </div>
  2373. `;
  2374. });
  2375. htmlContent += '</div>';
  2376. // 多选题
  2377. htmlContent += `
  2378. <div class='section'>
  2379. <div class='section-title'>三、多选题(每题${exam.multiple.scorePerQuestion}分,共${exam.multiple.totalScore}分)</div>
  2380. `;
  2381. exam.multiple.questions.forEach((question, index) => {
  2382. htmlContent += `
  2383. <div class='question'>
  2384. <div class='question-header'>
  2385. <span class='question-number'>${index + 1}.</span>
  2386. <span class='question-text'>${question.text}</span>
  2387. </div>
  2388. <div class='options'>
  2389. ${question.options.map(option => `<div class='option'>${option.key}. ${option.text}</div>`).join('')}
  2390. </div>
  2391. <div class='answer'>正确答案:${question.selectedAnswers.join(', ') || '未设置'}</div>
  2392. </div>
  2393. `;
  2394. });
  2395. htmlContent += '</div>';
  2396. // 简答题
  2397. htmlContent += `
  2398. <div class='section'>
  2399. <div class='section-title'>四、简答题(每题${exam.short.scorePerQuestion}分,共${exam.short.totalScore}分)</div>
  2400. `;
  2401. exam.short.questions.forEach((question, index) => {
  2402. htmlContent += `
  2403. <div class='question'>
  2404. <div class='question-header'>
  2405. <span class='question-number'>${index + 1}.</span>
  2406. <span class='question-text'>${question.text}</span>
  2407. </div>
  2408. <div class='answer'>答题要点:${question.outline?.keyFactors || '未设置'}</div>
  2409. </div>
  2410. `;
  2411. });
  2412. htmlContent += '</div>';
  2413. htmlContent += '</body></html>';
  2414. return htmlContent;
  2415. };
  2416. // 准备Word数据
  2417. const prepareExamDataForExport = () => {
  2418. const exam = currentExam.value;
  2419. // 试卷基本信息
  2420. const basicInfo = [
  2421. ['试卷名称', exam.title],
  2422. ['总分', `${exam.totalScore}分`],
  2423. ['总题数', `${exam.totalQuestions}题`],
  2424. ['生成时间', currentTime.value],
  2425. ['', ''], // 空行
  2426. ];
  2427. // 单选题数据
  2428. const singleChoiceData = [
  2429. ['一、单选题', `(每题${exam.singleChoice.scorePerQuestion}分,共${exam.singleChoice.totalScore}分)`],
  2430. ['题号', '题目内容', '选项A', '选项B', '选项C', '选项D', '正确答案'],
  2431. ];
  2432. exam.singleChoice.questions.forEach((question, index) => {
  2433. singleChoiceData.push([
  2434. `${index + 1}`,
  2435. question.text,
  2436. question.options[0]?.text || '',
  2437. question.options[1]?.text || '',
  2438. question.options[2]?.text || '',
  2439. question.options[3]?.text || '',
  2440. question.selectedAnswer || ''
  2441. ]);
  2442. });
  2443. // 判断题数据
  2444. const judgeData = [
  2445. ['', ''], // 空行
  2446. ['二、判断题', `(每题${exam.judge.scorePerQuestion}分,共${exam.judge.totalScore}分)`],
  2447. ['题号', '题目内容', '正确答案'],
  2448. ];
  2449. exam.judge.questions.forEach((question, index) => {
  2450. judgeData.push([
  2451. `${index + 1}`,
  2452. question.text,
  2453. question.selectedAnswer || ''
  2454. ]);
  2455. });
  2456. // 多选题数据
  2457. const multipleData = [
  2458. ['', ''], // 空行
  2459. ['三、多选题', `(每题${exam.multiple.scorePerQuestion}分,共${exam.multiple.totalScore}分)`],
  2460. ['题号', '题目内容', '选项A', '选项B', '选项C', '选项D', '正确答案'],
  2461. ];
  2462. exam.multiple.questions.forEach((question, index) => {
  2463. multipleData.push([
  2464. `${index + 1}`,
  2465. question.text,
  2466. question.options[0]?.text || '',
  2467. question.options[1]?.text || '',
  2468. question.options[2]?.text || '',
  2469. question.options[3]?.text || '',
  2470. question.selectedAnswers.join(', ') || ''
  2471. ]);
  2472. });
  2473. // 简答题数据
  2474. const shortData = [
  2475. ['', ''], // 空行
  2476. ['四、简答题', `(每题${exam.short.scorePerQuestion}分,共${exam.short.totalScore}分)`],
  2477. ['题号', '题目内容', '答题要点'],
  2478. ];
  2479. exam.short.questions.forEach((question, index) => {
  2480. shortData.push([
  2481. `${index + 1}`,
  2482. question.text,
  2483. question.outline?.keyFactors || ''
  2484. ]);
  2485. });
  2486. // 合并所有数据
  2487. return [
  2488. ...basicInfo,
  2489. ...singleChoiceData,
  2490. ...judgeData,
  2491. ...multipleData,
  2492. ...shortData
  2493. ];
  2494. };
  2495. const saveExam = async () => {
  2496. try {
  2497. // 准备保存的试卷数据
  2498. const examData = prepareExamDataForSave();
  2499. console.log('准备保存的试卷数据:', examData);
  2500. const response = await apis.saveExam(examData);
  2501. if (response.statusCode === 200) {
  2502. ElMessage.success("试卷保存成功!");
  2503. updateHistoryData(examData);
  2504. console.log('试卷已保存到历史记录');
  2505. } else {
  2506. throw new Error('保存失败');
  2507. }
  2508. } catch (error) {
  2509. console.error('保存试卷失败:', error);
  2510. ElMessage.error('保存试卷失败,请稍后重试');
  2511. }
  2512. };
  2513. // 准备保存的试卷数据
  2514. const prepareExamDataForSave = () => {
  2515. const exam = currentExam.value;
  2516. return {
  2517. // 基本信息
  2518. exam_name: exam.title,
  2519. exam_type: selectedProjectType.value,
  2520. total_score: exam.totalScore,
  2521. total_questions: exam.totalQuestions,
  2522. generation_method: selectedFunction.value,
  2523. generation_time: currentTime.value,
  2524. created_at: new Date().toISOString(),
  2525. // 题型配置
  2526. question_config: {
  2527. single_choice: {
  2528. score_per_question: exam.singleChoice.scorePerQuestion,
  2529. total_score: exam.singleChoice.totalScore,
  2530. count: exam.singleChoice.count
  2531. },
  2532. judge: {
  2533. score_per_question: exam.judge.scorePerQuestion,
  2534. total_score: exam.judge.totalScore,
  2535. count: exam.judge.count
  2536. },
  2537. multiple: {
  2538. score_per_question: exam.multiple.scorePerQuestion,
  2539. total_score: exam.multiple.totalScore,
  2540. count: exam.multiple.count
  2541. },
  2542. short: {
  2543. score_per_question: exam.short.scorePerQuestion,
  2544. total_score: exam.short.totalScore,
  2545. count: exam.short.count
  2546. }
  2547. },
  2548. // 题目内容
  2549. questions: {
  2550. single_choice: exam.singleChoice.questions.map(q => ({
  2551. question_text: q.text,
  2552. options: q.options,
  2553. correct_answer: q.selectedAnswer
  2554. })),
  2555. judge: exam.judge.questions.map(q => ({
  2556. question_text: q.text,
  2557. correct_answer: q.selectedAnswer
  2558. })),
  2559. multiple: exam.multiple.questions.map(q => ({
  2560. question_text: q.text,
  2561. options: q.options,
  2562. correct_answers: q.selectedAnswers
  2563. })),
  2564. short: exam.short.questions.map(q => ({
  2565. question_text: q.text,
  2566. answer_outline: q.outline
  2567. }))
  2568. },
  2569. // 用户答案(可选,用于保存用户的答题状态)
  2570. user_answers: {
  2571. single_choice: exam.singleChoice.questions.map(q => q.selectedAnswer),
  2572. judge: exam.judge.questions.map(q => q.selectedAnswer),
  2573. multiple: exam.multiple.questions.map(q => q.selectedAnswers),
  2574. short: exam.short.questions.map(q => null) // 简答题不保存用户答案
  2575. }
  2576. };
  2577. };
  2578. // 更新历史记录数据
  2579. const updateHistoryData = (examData) => {
  2580. const newHistoryItem = {
  2581. id: Date.now(),
  2582. title: examData.exam_name,
  2583. time: examData.generation_time,
  2584. isActive: false,
  2585. examData: examData
  2586. };
  2587. historyData.value.unshift(newHistoryItem);
  2588. if (historyData.value.length > 20) {
  2589. historyData.value = historyData.value.slice(0, 20);
  2590. }
  2591. };
  2592. // 从历史记录恢复试卷
  2593. const restoreExamFromHistory = (examData) => {
  2594. try {
  2595. console.log('开始恢复试卷数据:', examData);
  2596. console.log('试卷数据详细结构:', JSON.stringify(examData, null, 2));
  2597. // 检查数据格式,支持多种格式
  2598. let exam = examData;
  2599. // 如果数据是嵌套在exam字段中
  2600. if (examData.exam) {
  2601. exam = examData.exam;
  2602. }
  2603. // 恢复基本信息
  2604. if (exam.title) {
  2605. examName.value = exam.title;
  2606. } else if (exam.exam_name) {
  2607. examName.value = exam.exam_name;
  2608. }
  2609. if (exam.exam_type) {
  2610. selectedProjectType.value = exam.exam_type;
  2611. }
  2612. if (exam.totalScore) {
  2613. totalScore.value = exam.totalScore;
  2614. } else if (exam.total_score) {
  2615. totalScore.value = exam.total_score;
  2616. }
  2617. if (exam.generation_method) {
  2618. selectedFunction.value = exam.generation_method;
  2619. }
  2620. if (exam.generationTime) {
  2621. currentTime.value = exam.generationTime;
  2622. } else if (exam.generation_time) {
  2623. currentTime.value = exam.generation_time;
  2624. }
  2625. // 恢复题型配置
  2626. if (exam.question_config) {
  2627. questionTypes.value = [
  2628. {
  2629. name: "单选题",
  2630. scorePerQuestion: exam.question_config.single_choice.score_per_question,
  2631. questionCount: exam.question_config.single_choice.count,
  2632. romanNumeral: "一"
  2633. },
  2634. {
  2635. name: "判断题",
  2636. scorePerQuestion: exam.question_config.judge.score_per_question,
  2637. questionCount: exam.question_config.judge.count,
  2638. romanNumeral: "二"
  2639. },
  2640. {
  2641. name: "多选题",
  2642. scorePerQuestion: exam.question_config.multiple.score_per_question,
  2643. questionCount: exam.question_config.multiple.count,
  2644. romanNumeral: "三"
  2645. },
  2646. {
  2647. name: "简答题",
  2648. scorePerQuestion: exam.question_config.short.score_per_question,
  2649. questionCount: exam.question_config.short.count,
  2650. romanNumeral: "四"
  2651. }
  2652. ];
  2653. }
  2654. // 恢复题目内容
  2655. if (exam.singleChoice || exam.questions?.single_choice) {
  2656. const singleChoice = exam.singleChoice || exam.questions.single_choice;
  2657. const judge = exam.judge || exam.questions.judge;
  2658. const multiple = exam.multiple || exam.questions.multiple;
  2659. const short = exam.short || exam.questions.short;
  2660. console.log('单选题数据:', singleChoice);
  2661. console.log('判断题数据:', judge);
  2662. console.log('多选题数据:', multiple);
  2663. console.log('简答题数据:', short);
  2664. currentExam.value = {
  2665. title: examName.value,
  2666. totalScore: totalScore.value,
  2667. totalQuestions: exam.totalQuestions || exam.total_questions,
  2668. singleChoice: {
  2669. scorePerQuestion: singleChoice.scorePerQuestion || singleChoice.score_per_question,
  2670. totalScore: singleChoice.totalScore || singleChoice.total_score,
  2671. count: singleChoice.count,
  2672. questions: singleChoice.questions.map(q => ({
  2673. text: q.text || q.question_text,
  2674. options: q.options || [],
  2675. selectedAnswer: q.selectedAnswer || q.correct_answer || q.answer || ""
  2676. }))
  2677. },
  2678. judge: {
  2679. scorePerQuestion: judge.scorePerQuestion || judge.score_per_question,
  2680. totalScore: judge.totalScore || judge.total_score,
  2681. count: judge.count,
  2682. questions: judge.questions.map(q => ({
  2683. text: q.text || q.question_text,
  2684. selectedAnswer: q.selectedAnswer || q.correct_answer || q.answer || ""
  2685. }))
  2686. },
  2687. multiple: {
  2688. scorePerQuestion: multiple.scorePerQuestion || multiple.score_per_question,
  2689. totalScore: multiple.totalScore || multiple.total_score,
  2690. count: multiple.count,
  2691. questions: multiple.questions.map(q => ({
  2692. text: q.text || q.question_text,
  2693. options: q.options || [],
  2694. selectedAnswers: q.selectedAnswers || q.correct_answers || q.answers || []
  2695. }))
  2696. },
  2697. short: {
  2698. scorePerQuestion: short.scorePerQuestion || short.score_per_question,
  2699. totalScore: short.totalScore || short.total_score,
  2700. count: short.count,
  2701. questions: short.questions.map(q => ({
  2702. text: q.text || q.question_text,
  2703. outline: q.outline || q.answer_outline || { keyFactors: "答题要点、关键因素、示例答案" }
  2704. }))
  2705. }
  2706. };
  2707. }
  2708. // 恢复用户答案(如果有)
  2709. if (exam.user_answers) {
  2710. if (exam.user_answers.single_choice) {
  2711. exam.user_answers.single_choice.forEach((answer, index) => {
  2712. if (currentExam.value.singleChoice.questions[index]) {
  2713. currentExam.value.singleChoice.questions[index].selectedAnswer = answer || "";
  2714. }
  2715. });
  2716. }
  2717. if (exam.user_answers.judge) {
  2718. exam.user_answers.judge.forEach((answer, index) => {
  2719. if (currentExam.value.judge.questions[index]) {
  2720. currentExam.value.judge.questions[index].selectedAnswer = answer || "";
  2721. }
  2722. });
  2723. }
  2724. if (exam.user_answers.multiple) {
  2725. exam.user_answers.multiple.forEach((answers, index) => {
  2726. if (currentExam.value.multiple.questions[index]) {
  2727. currentExam.value.multiple.questions[index].selectedAnswers = answers || [];
  2728. }
  2729. });
  2730. }
  2731. }
  2732. console.log('恢复完成后的试卷数据:', currentExam.value);
  2733. console.log('单选题答案:', currentExam.value.singleChoice?.questions?.map(q => q.selectedAnswer));
  2734. console.log('判断题答案:', currentExam.value.judge?.questions?.map(q => q.selectedAnswer));
  2735. console.log('多选题答案:', currentExam.value.multiple?.questions?.map(q => q.selectedAnswers));
  2736. // 显示详情页
  2737. showExamDetail.value = true;
  2738. // ElMessage.success("试卷已从历史记录恢复");
  2739. } catch (error) {
  2740. console.error('恢复试卷失败:', error);
  2741. ElMessage.error('恢复试卷失败,请稍后重试');
  2742. }
  2743. };
  2744. // 文件处理工具函数
  2745. const validateFile = (file) => {
  2746. // 检查文件大小
  2747. if (file.size > fileConfig.maxSize) {
  2748. throw new Error('文件大小不能超过20MB')
  2749. }
  2750. // 检查文件类型
  2751. const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
  2752. if (!fileConfig.allowedTypes.includes(fileExtension)) {
  2753. throw new Error('只支持PPT格式文件(.ppt/.pptx)')
  2754. }
  2755. return fileExtension
  2756. }
  2757. // 获取文件图标
  2758. const getFileIcon = (fileType) => {
  2759. switch (fileType) {
  2760. case '.ppt':
  2761. case '.pptx':
  2762. return '📊'
  2763. default:
  2764. return '📎'
  2765. }
  2766. }
  2767. // 格式化文件大小
  2768. const formatFileSize = (bytes) => {
  2769. if (bytes === 0) return '0 B'
  2770. const k = 1024
  2771. const sizes = ['B', 'KB', 'MB', 'GB']
  2772. const i = Math.floor(Math.log(bytes) / Math.log(k))
  2773. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  2774. }
  2775. // 从PPT文件中提取文本内容
  2776. const extractTextFromPPT = async (uint8Array) => {
  2777. try {
  2778. // 尝试使用JSZip解析PPTX文件
  2779. const JSZip = await import('jszip')
  2780. const zip = new JSZip.default()
  2781. // 加载PPTX文件
  2782. const zipContent = await zip.loadAsync(uint8Array)
  2783. // 查找幻灯片文件
  2784. const slideFiles = Object.keys(zipContent.files).filter(name =>
  2785. name.startsWith('ppt/slides/slide') && name.endsWith('.xml')
  2786. )
  2787. let extractedText = ''
  2788. // 读取第一张幻灯片的内容
  2789. if (slideFiles.length > 0) {
  2790. const firstSlide = slideFiles[0]
  2791. const slideContent = await zipContent.file(firstSlide).async('text')
  2792. // 提取XML中的文本内容
  2793. const textMatches = slideContent.match(/<a:t[^>]*>([^<]+)<\/a:t>/g)
  2794. if (textMatches) {
  2795. textMatches.forEach(match => {
  2796. const text = match.replace(/<[^>]*>/g, '').trim()
  2797. if (text.length > 0) {
  2798. extractedText += text + ' '
  2799. }
  2800. })
  2801. }
  2802. // 如果没有找到a:t标签,尝试其他方式
  2803. if (extractedText.length === 0) {
  2804. const generalTextMatches = slideContent.match(/>([^<]{2,})</g)
  2805. if (generalTextMatches) {
  2806. generalTextMatches.forEach(match => {
  2807. const text = match.replace(/[<>]/g, '').trim()
  2808. if (text.length > 2 && /[\u4e00-\u9fa5a-zA-Z]/.test(text)) {
  2809. extractedText += text + ' '
  2810. }
  2811. })
  2812. }
  2813. }
  2814. }
  2815. // 如果还是没有内容,尝试读取其他XML文件
  2816. if (extractedText.length === 0) {
  2817. const xmlFiles = Object.keys(zipContent.files).filter(name =>
  2818. name.endsWith('.xml') && !name.includes('_rels')
  2819. )
  2820. for (const xmlFile of xmlFiles.slice(0, 3)) { // 只检查前3个文件
  2821. try {
  2822. const xmlContent = await zipContent.file(xmlFile).async('text')
  2823. const textMatches = xmlContent.match(/>([^<]{2,})</g)
  2824. if (textMatches) {
  2825. textMatches.forEach(match => {
  2826. const text = match.replace(/[<>]/g, '').trim()
  2827. if (text.length > 2 && /[\u4e00-\u9fa5a-zA-Z]/.test(text)) {
  2828. extractedText += text + ' '
  2829. }
  2830. })
  2831. }
  2832. } catch (error) {
  2833. console.log('读取XML文件失败:', xmlFile, error)
  2834. }
  2835. }
  2836. }
  2837. // 清理文本
  2838. if (extractedText.length > 0) {
  2839. extractedText = extractedText
  2840. .replace(/\s+/g, ' ')
  2841. .replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '')
  2842. .trim()
  2843. return extractedText.substring(0, 800)
  2844. }
  2845. return ''
  2846. } catch (error) {
  2847. console.error('PPT文本提取失败:', error)
  2848. // 如果JSZip方法失败,回退到简单文本提取
  2849. try {
  2850. const decoder = new TextDecoder('utf-8')
  2851. const content = decoder.decode(uint8Array)
  2852. const chineseMatches = content.match(/[\u4e00-\u9fa5]{2,}/g)
  2853. const englishMatches = content.match(/[a-zA-Z]{3,}/g)
  2854. let extractedText = ''
  2855. if (chineseMatches && chineseMatches.length > 0) {
  2856. extractedText += chineseMatches.join(' ')
  2857. }
  2858. if (englishMatches && englishMatches.length > 0) {
  2859. extractedText += ' ' + englishMatches.join(' ')
  2860. }
  2861. if (extractedText.length > 0) {
  2862. extractedText = extractedText
  2863. .replace(/\s+/g, ' ')
  2864. .replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '')
  2865. .trim()
  2866. return extractedText.substring(0, 800)
  2867. }
  2868. } catch (fallbackError) {
  2869. console.error('回退文本提取也失败:', fallbackError)
  2870. }
  2871. return ''
  2872. }
  2873. }
  2874. // 读取PPT文件内容
  2875. const readPPTFile = async (file) => {
  2876. try {
  2877. console.log('开始读取PPT文件:', file.name, '文件大小:', file.size)
  2878. // 检查文件是否为空
  2879. if (file.size === 0) {
  2880. throw new Error('PPT文件为空')
  2881. }
  2882. // 使用FileReader读取文件内容
  2883. console.log('开始读取PPT文件内容...')
  2884. return new Promise((resolve, reject) => {
  2885. const reader = new FileReader()
  2886. reader.onload = async (event) => {
  2887. try {
  2888. const arrayBuffer = event.target.result
  2889. console.log('文件读取成功,大小:', arrayBuffer.byteLength)
  2890. // 使用简单的方法读取PPT文件第一页内容
  2891. console.log('开始读取PPT文件第一页内容...')
  2892. try {
  2893. // 将ArrayBuffer转换为Uint8Array
  2894. const uint8Array = new Uint8Array(arrayBuffer)
  2895. // 查找PPT文件中的文本内容
  2896. // PPT文件是ZIP格式,包含XML文件
  2897. let extractedText = `PPT文件信息:
  2898. 文件名:${file.name}
  2899. 文件大小:${formatFileSize(file.size)}
  2900. 文件类型:${file.type}
  2901. 修改时间:${new Date(file.lastModified).toLocaleString('zh-CN')}
  2902. PPT第一页内容提取结果:
  2903. `
  2904. // 尝试从PPT文件中提取文本内容
  2905. const textContent = await extractTextFromPPT(uint8Array)
  2906. if (textContent && textContent.length > 0) {
  2907. extractedText += `\n提取的文本内容:\n${textContent}`
  2908. console.log('PPT文本提取成功,长度:', textContent.length)
  2909. } else {
  2910. extractedText += `\n无法自动提取PPT文本内容。\n`
  2911. }
  2912. extractedText += `\n\n请在下方文本框中补充或修正PPT内容描述,AI将基于这些信息生成相关考题。`
  2913. console.log('PPT内容提取完成,长度:', extractedText.length)
  2914. console.log('提取的内容预览:', extractedText.substring(0, 500))
  2915. resolve(extractedText)
  2916. } catch (parseError) {
  2917. console.error('PPT解析失败:', parseError)
  2918. // 如果解析失败,返回文件信息
  2919. const fallbackText = `PPT文件:${file.name}
  2920. 文件大小:${formatFileSize(file.size)}
  2921. 文件类型:${file.type}
  2922. 修改时间:${new Date(file.lastModified).toLocaleString('zh-CN')}
  2923. PPT内容提取失败,请手动描述PPT的主要内容、关键知识点、培训目标等信息,AI将基于您的描述生成相关考题。
  2924. 您可以描述:
  2925. 1. PPT的主要主题和内容
  2926. 2. 关键知识点和重点
  2927. 3. 培训目标和学习要求
  2928. 4. 相关的技术要点和注意事项`
  2929. resolve(fallbackText)
  2930. }
  2931. } catch (error) {
  2932. console.error('PPT处理失败:', error)
  2933. // 如果处理失败,返回文件信息
  2934. const fallbackText = `PPT文件:${file.name}
  2935. 文件大小:${formatFileSize(file.size)}
  2936. 文件类型:${file.type}
  2937. 修改时间:${new Date(file.lastModified).toLocaleString('zh-CN')}
  2938. PPT文件处理失败,请手动描述PPT的主要内容、关键知识点、培训目标等信息,AI将基于您的描述生成相关考题。
  2939. 您可以描述:
  2940. 1. PPT的主要主题和内容
  2941. 2. 关键知识点和重点
  2942. 3. 培训目标和学习要求
  2943. 4. 相关的技术要点和注意事项`
  2944. resolve(fallbackText)
  2945. }
  2946. }
  2947. reader.onerror = () => {
  2948. reject(new Error('文件读取失败'))
  2949. }
  2950. // 读取文件为ArrayBuffer
  2951. reader.readAsArrayBuffer(file)
  2952. })
  2953. } catch (error) {
  2954. console.error('PPT文件读取失败,详细错误:', error)
  2955. console.error('错误堆栈:', error.stack)
  2956. // 提供更具体的错误信息
  2957. if (error.message.includes('Invalid file format')) {
  2958. throw new Error('PPT文件格式无效或已损坏')
  2959. } else if (error.message.includes('File is empty')) {
  2960. throw new Error('PPT文件为空')
  2961. } else {
  2962. throw new Error(`PPT文件读取失败: ${error.message}`)
  2963. }
  2964. }
  2965. }
  2966. // 处理文件选择
  2967. const handleFileSelect = async (event) => {
  2968. const files = Array.from(event.target.files)
  2969. if (!files || files.length === 0) return
  2970. isUploadingFile.value = true
  2971. let successCount = 0;
  2972. for (const file of files) {
  2973. try {
  2974. // 验证文件
  2975. const fileExtension = validateFile(file)
  2976. console.log('开始读取文件内容:', file.name)
  2977. // 处理PPT文档
  2978. const extractedContent = await readPPTFile(file)
  2979. // 创建文件信息对象
  2980. uploadedFiles.value.push({
  2981. file,
  2982. name: file.name,
  2983. size: file.size,
  2984. type: fileExtension,
  2985. icon: getFileIcon(fileExtension),
  2986. content: extractedContent // 存储提取的内容
  2987. })
  2988. successCount++;
  2989. // 如果是第一个上传的文件,且当前试卷名称还是默认状态或为空,使用该文件名作为试卷名称
  2990. if (uploadedFiles.value.length === 1 && (!examName.value || examName.value.includes('工程施工技术考核'))) {
  2991. const fileNameWithoutExt = file.name.replace(/\.(ppt|pptx)$/i, '')
  2992. examName.value = `${fileNameWithoutExt}考试试卷`
  2993. }
  2994. } catch (error) {
  2995. console.error(`文件 ${file.name} 读取失败:`, error)
  2996. ElMessage.error(`${file.name}读取失败: ${error.message || '请重试'}`)
  2997. }
  2998. }
  2999. if (successCount > 0) {
  3000. ElMessage.success(`成功读取了 ${successCount} 个文件`)
  3001. }
  3002. isUploadingFile.value = false
  3003. event.target.value = ''
  3004. }
  3005. // 删除选中的文件
  3006. const removeSelectedFile = (index) => {
  3007. if (index >= 0 && index < uploadedFiles.value.length) {
  3008. uploadedFiles.value.splice(index, 1)
  3009. // 如果全部删除了,重置相关状态
  3010. if (uploadedFiles.value.length === 0) {
  3011. pptContentDescription.value = ''
  3012. const projectTypeName = projectTypes[selectedProjectType.value]?.name || '桥梁'
  3013. examName.value = `${projectTypeName}工程施工技术考核`
  3014. }
  3015. }
  3016. }
  3017. // 触发文件上传
  3018. const triggerFileUpload = () => {
  3019. fileInput.value?.click()
  3020. }
  3021. // 删除确认弹窗处理函数
  3022. const handleDeleteConfirm = () => {
  3023. // 处理删除确认逻辑
  3024. showDeleteModal.value = false;
  3025. ElMessage.success('删除成功');
  3026. };
  3027. const handleDeleteCancel = () => {
  3028. showDeleteModal.value = false;
  3029. };
  3030. // 生命周期钩子
  3031. onMounted(async () => {
  3032. // 保存初始配置
  3033. initialConfig = {
  3034. questionTypes: JSON.parse(JSON.stringify(questionTypes.value)),
  3035. totalScore: totalScore.value,
  3036. selectedProjectType: selectedProjectType.value,
  3037. examName: examName.value
  3038. };
  3039. console.log('初始配置已保存:', initialConfig);
  3040. // 获取历史记录列表
  3041. await getHistoryRecordList()
  3042. // 检查URL参数是否有historyId需要加载
  3043. const historyId = route.query.historyId
  3044. if (historyId) {
  3045. const targetItem = historyData.value.find(item => String(item.id) === String(historyId))
  3046. if (targetItem) {
  3047. await handleHistoryItem(targetItem)
  3048. }
  3049. }
  3050. // 添加全局点击事件监听器
  3051. document.addEventListener('click', handleClickOutside);
  3052. })
  3053. onUnmounted(() => {
  3054. // 清理事件监听器
  3055. document.removeEventListener('click', handleClickOutside);
  3056. })
  3057. </script>
  3058. <style lang="less" scoped>
  3059. // 删除图标样式
  3060. .delete-icon {
  3061. width: 16px;
  3062. height: 16px;
  3063. }
  3064. .chat-container {
  3065. display: flex;
  3066. height: 100vh;
  3067. font-family: "Alibaba PuHuiTi 3.0", sans-serif;
  3068. }
  3069. /* 中间历史记录栏 */
  3070. .history-sidebar {
  3071. width: 280px;
  3072. background: #e8f0ff;
  3073. display: flex;
  3074. flex-direction: column;
  3075. }
  3076. /* 中间历史记录栏样式 */
  3077. .history-sidebar {
  3078. padding: 24px 16px 0 16px;
  3079. .history-header {
  3080. background: transparent;
  3081. .section-title {
  3082. font-size: 16px;
  3083. font-weight: 600;
  3084. color: #2c3e50;
  3085. }
  3086. .new-chat-btn {
  3087. width: 248px;
  3088. height: 40px;
  3089. cursor: pointer;
  3090. transition: opacity 0.3s ease;
  3091. object-fit: contain;
  3092. display: block;
  3093. margin-top: 16px;
  3094. margin-bottom: 14px;
  3095. &:hover {
  3096. opacity: 0.8;
  3097. }
  3098. }
  3099. }
  3100. .history-list {
  3101. flex: 1;
  3102. overflow-y: auto;
  3103. width: 248px;
  3104. height: 64px;
  3105. /* 隐藏滚动条 */
  3106. &::-webkit-scrollbar {
  3107. display: none;
  3108. }
  3109. -ms-overflow-style: none; /* IE and Edge */
  3110. scrollbar-width: none; /* Firefox */
  3111. .history-item {
  3112. background: white;
  3113. border-radius: 8px;
  3114. padding: 15px;
  3115. margin-bottom: 14px;
  3116. cursor: pointer;
  3117. transition: all 0.3s ease;
  3118. border-left: 3px solid transparent;
  3119. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  3120. display: flex;
  3121. align-items: center;
  3122. justify-content: space-between;
  3123. position: relative;
  3124. &:hover {
  3125. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  3126. .delete-btn {
  3127. opacity: 1;
  3128. visibility: visible;
  3129. }
  3130. }
  3131. &.active {
  3132. border-left-color: #3e7bfa;
  3133. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
  3134. cursor: default;
  3135. opacity: 0.8;
  3136. &:hover {
  3137. transform: none;
  3138. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
  3139. }
  3140. }
  3141. .history-content {
  3142. flex: 1;
  3143. min-width: 0; // 允许内容收缩
  3144. }
  3145. .history-title {
  3146. font-size: 14px;
  3147. line-height: 1.4;
  3148. margin-bottom: 14px;
  3149. color: #2c3e50;
  3150. overflow: hidden;
  3151. text-overflow: ellipsis;
  3152. white-space: nowrap;
  3153. }
  3154. .history-time {
  3155. font-size: 12px;
  3156. color: #7c8db5;
  3157. }
  3158. .delete-btn {
  3159. opacity: 0;
  3160. visibility: hidden;
  3161. transition: all 0.2s ease;
  3162. cursor: pointer;
  3163. padding: 4px;
  3164. border-radius: 4px;
  3165. color: #7c8db5;
  3166. display: flex;
  3167. align-items: center;
  3168. justify-content: center;
  3169. margin-left: 8px;
  3170. flex-shrink: 0;
  3171. &:hover {
  3172. color: #ff4757;
  3173. background-color: rgba(255, 71, 87, 0.1);
  3174. }
  3175. &.always-visible {
  3176. opacity: 1;
  3177. visibility: visible;
  3178. }
  3179. }
  3180. }
  3181. .empty-history {
  3182. display: flex;
  3183. flex-direction: column;
  3184. align-items: center;
  3185. justify-content: center;
  3186. margin-top: 236px;
  3187. .empty-icon {
  3188. width: 147px;
  3189. height: 148px;
  3190. object-fit: contain;
  3191. margin-bottom: 14px;
  3192. }
  3193. .empty-text {
  3194. font-size: 16px;
  3195. color: #9C9FA7;
  3196. text-align: center;
  3197. }
  3198. }
  3199. }
  3200. }
  3201. /* 主工作区域 */
  3202. .main-work {
  3203. flex: 1;
  3204. background: #ebf3ff;
  3205. display: flex;
  3206. flex-direction: column;
  3207. &.exam-detail-mode {
  3208. background: transparent;
  3209. }
  3210. }
  3211. /* 工作头部 */
  3212. .work-header {
  3213. background: transparent;
  3214. padding: 40px 0px 0px 18px;
  3215. h2 {
  3216. margin: 0;
  3217. font-size: 25px;
  3218. font-weight: 600;
  3219. color: #2c3e50;
  3220. }
  3221. }
  3222. /* 工作内容区域 */
  3223. .work-content {
  3224. flex: 1;
  3225. padding: 0;
  3226. // overflow-y: auto;
  3227. display: flex;
  3228. flex-direction: column;
  3229. align-items: stretch;
  3230. height: 100%;
  3231. /* 隐藏滚动条样式 */
  3232. &::-webkit-scrollbar {
  3233. width: 0;
  3234. background: transparent;
  3235. }
  3236. &::-webkit-scrollbar-track {
  3237. background: transparent;
  3238. }
  3239. &::-webkit-scrollbar-thumb {
  3240. background: transparent;
  3241. }
  3242. }
  3243. .app-container {
  3244. --primary-color: #0d6efd;
  3245. --danger-color: #dc3545;
  3246. --warning-color: #ffc107;
  3247. --border-color: #dee2e6;
  3248. --bg-light: #f8f9fa;
  3249. --text-dark: #212529;
  3250. --text-muted: #6c757d;
  3251. display: flex;
  3252. height: 100%;
  3253. width: 100%;
  3254. padding: 0 !important;
  3255. background-color: #f5f5f5;
  3256. overflow: hidden;
  3257. margin: 0 !important;
  3258. max-width: 100% !important;
  3259. border-radius: 0 !important;
  3260. box-shadow: none !important;
  3261. /* 中间主操作区 */
  3262. .main-content {
  3263. flex: 1;
  3264. padding: 20px;
  3265. overflow-y: hidden;
  3266. background: white;
  3267. scrollbar-width: none; /* Firefox */
  3268. -ms-overflow-style: none; /* IE and Edge */
  3269. }
  3270. .main-content::-webkit-scrollbar {
  3271. display: none; /* Chrome, Safari and Opera */
  3272. }
  3273. .form-group {
  3274. margin-bottom: 12px;
  3275. max-width: 1150px; /* 限制输入框模块的最大宽度 */
  3276. margin-left: auto;
  3277. margin-right: auto; /* 使其在工作区居中 */
  3278. }
  3279. .form-label {
  3280. font-weight: 600;
  3281. margin-bottom: 6px;
  3282. display: block;
  3283. font-size: 14px;
  3284. }
  3285. .form-control {
  3286. width: 100%;
  3287. border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
  3288. border-radius: 12px; /* 统一圆角 */
  3289. padding: 12px 16px; /* 稍微增加内边距让它看起来更像卡片 */
  3290. font-size: 14px;
  3291. transition: all 0.3s;
  3292. box-sizing: border-box;
  3293. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
  3294. }
  3295. .form-control:focus {
  3296. outline: none;
  3297. border-color: var(--primary-color);
  3298. box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
  3299. }
  3300. textarea.form-control {
  3301. resize: none;
  3302. height: 250px;
  3303. }
  3304. .char-count {
  3305. text-align: right;
  3306. font-size: 12px;
  3307. color: var(--text-muted);
  3308. margin-top: 4px;
  3309. }
  3310. /* PPT上传区域 */
  3311. .ppt-upload-section {
  3312. background: white;
  3313. border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
  3314. border-radius: 12px; /* 统一圆角 */
  3315. padding: 16px 20px;
  3316. margin-top: 40px;
  3317. cursor: pointer;
  3318. transition: all 0.3s;
  3319. display: flex;
  3320. align-items: center;
  3321. justify-content: space-between;
  3322. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
  3323. position: relative;
  3324. }
  3325. .ppt-upload-section:hover {
  3326. border-color: var(--primary-color);
  3327. box-shadow: 0 8px 24px rgba(13, 110, 253, 0.12); /* 悬浮时加深发光阴影 */
  3328. }
  3329. .ppt-upload-content {
  3330. display: flex;
  3331. align-items: center;
  3332. gap: 16px;
  3333. }
  3334. .ppt-upload-icon-wrapper {
  3335. width: 48px;
  3336. height: 48px;
  3337. background: #f3f4f6;
  3338. border-radius: 12px;
  3339. display: flex;
  3340. align-items: center;
  3341. justify-content: center;
  3342. flex-shrink: 0;
  3343. }
  3344. .ppt-upload-text-wrapper {
  3345. display: flex;
  3346. flex-direction: column;
  3347. justify-content: center;
  3348. }
  3349. .ppt-upload-title {
  3350. font-size: 15px;
  3351. font-weight: 600;
  3352. color: #1f2937;
  3353. margin-bottom: 4px;
  3354. }
  3355. .ppt-upload-hint {
  3356. font-size: 13px;
  3357. color: #6b7280;
  3358. }
  3359. .ppt-arrow {
  3360. color: #9ca3af;
  3361. font-size: 24px;
  3362. transition: transform 0.3s;
  3363. }
  3364. .ppt-upload-section:hover .ppt-arrow {
  3365. color: var(--primary-color);
  3366. transform: translateX(2px);
  3367. }
  3368. .file-status-badge {
  3369. background: #ebf3ff;
  3370. color: var(--primary-color);
  3371. padding: 5px 12px;
  3372. border-radius: 8px;
  3373. font-size: 10px;
  3374. display: flex;
  3375. align-items: center;
  3376. gap: 8px;
  3377. border: 1px solid rgba(13, 110, 253, 0.1);
  3378. max-width: 100%;
  3379. margin-top: 12px;
  3380. }
  3381. .file-name {
  3382. font-weight: 500;
  3383. }
  3384. .remove-btn {
  3385. color: #ef4444;
  3386. font-size: 16px;
  3387. font-weight: bold;
  3388. cursor: pointer;
  3389. padding: 0 4px;
  3390. }
  3391. .remove-btn:hover {
  3392. color: #b91c1c;
  3393. }
  3394. /* =============== 题型配置区域 样式开始 =============== */
  3395. .config-section {
  3396. margin-top: 16px;
  3397. }
  3398. .config-header {
  3399. display: flex;
  3400. justify-content: center; /* 改为靠左对齐,而不是两端对齐 */
  3401. align-items: center;
  3402. gap: 960px; /* 控制“题型配置”和“试卷总分”之间的固定间距 */
  3403. margin-bottom: 6px;
  3404. }
  3405. .config-header h3 {
  3406. font-size: 18px;
  3407. font-weight: 600;
  3408. }
  3409. .total-score {
  3410. background: var(--bg-light);
  3411. padding: 8px 16px;
  3412. border-radius: 20px;
  3413. font-size: 14px;
  3414. font-weight: 600;
  3415. color: var(--primary-color);
  3416. }
  3417. .question-types-grid {
  3418. display: grid;
  3419. /* 为了减小卡片宽度,我们不再让它们自动拉伸占满,而是指定最大宽度并居中,或者留出更大的列间距 */
  3420. grid-template-columns: repeat(2, minmax(0, 500px));
  3421. justify-content: center; /* 让网格居中,而不是两端拉伸 */
  3422. gap: 20px 150px; /* 行间距(高度)20px,列间距(宽度)40px */
  3423. margin-bottom: 12px;
  3424. }
  3425. .question-type-card {
  3426. background: white; /* 改为白色背景 */
  3427. border-radius: 12px;
  3428. padding: 16px 20px; /* 根据截图稍微调大内边距以容纳阴影内容 */
  3429. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 加深阴影 */
  3430. border: 1px solid rgba(0, 0, 0, 0.06); /* 稍微加深边框线 */
  3431. }
  3432. .question-type-header {
  3433. display: flex;
  3434. justify-content: space-between;
  3435. align-items: center;
  3436. margin-bottom: 20px; /* 稍微增加与滑动条的间距 */
  3437. }
  3438. .question-type-title {
  3439. font-weight: 600;
  3440. font-size: 16px; /* 字体稍微调大 */
  3441. color: #1f2937;
  3442. }
  3443. .question-type-score {
  3444. font-size: 13px;
  3445. color: #3b82f6; /* 改变字体颜色为蓝色系 */
  3446. background: #eff6ff; /* 添加淡蓝色背景 */
  3447. padding: 4px 12px;
  3448. border-radius: 20px; /* 胶囊形状 */
  3449. display: flex;
  3450. align-items: center;
  3451. gap: 6px;
  3452. }
  3453. .score-input {
  3454. width: 32px;
  3455. text-align: center;
  3456. border: none; /* 移除输入框边框 */
  3457. border-radius: 4px;
  3458. padding: 0;
  3459. font-size: 14px;
  3460. font-weight: 600;
  3461. color: #2563eb; /* 加深数字颜色 */
  3462. background: transparent; /* 背景透明,融入胶囊 */
  3463. transition: all 0.3s;
  3464. -webkit-appearance: textfield;
  3465. -moz-appearance: textfield;
  3466. appearance: textfield;
  3467. }
  3468. .score-input::-webkit-outer-spin-button,
  3469. .score-input::-webkit-inner-spin-button {
  3470. -webkit-appearance: none;
  3471. margin: 0;
  3472. }
  3473. .score-input:focus {
  3474. outline: none;
  3475. background: white; /* 聚焦时背景变白 */
  3476. box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
  3477. }
  3478. .slider-container {
  3479. display: flex;
  3480. align-items: center;
  3481. gap: 12px;
  3482. }
  3483. .slider-label {
  3484. font-size: 14px;
  3485. color: #4b5563;
  3486. font-weight: 500;
  3487. min-width: 40px;
  3488. }
  3489. .question-slider {
  3490. flex: 1;
  3491. height: 6px;
  3492. -webkit-appearance: none;
  3493. appearance: none;
  3494. background: #e5e7eb; /* 滑动条底色调浅 */
  3495. border-radius: 3px;
  3496. outline: none;
  3497. }
  3498. .question-slider::-webkit-slider-thumb {
  3499. -webkit-appearance: none;
  3500. appearance: none;
  3501. width: 18px; /* 滑块调大一点 */
  3502. height: 18px;
  3503. background: #2563eb; /* 蓝色滑块 */
  3504. border: 2px solid white; /* 添加白色边框 */
  3505. box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 滑块阴影 */
  3506. border-radius: 50%;
  3507. cursor: pointer;
  3508. transition: all 0.3s;
  3509. }
  3510. .question-slider::-webkit-slider-thumb:hover {
  3511. background: #1d4ed8;
  3512. transform: scale(1.1);
  3513. }
  3514. .question-count {
  3515. font-weight: bold; /* 数字加粗 */
  3516. font-size: 15px;
  3517. color: #1f2937;
  3518. min-width: 40px;
  3519. text-align: right;
  3520. }
  3521. .question-count-stepper {
  3522. display: flex;
  3523. align-items: center;
  3524. justify-content: flex-end;
  3525. gap: 8px;
  3526. min-width: 76px;
  3527. }
  3528. .stepper-buttons {
  3529. display: flex;
  3530. flex-direction: column;
  3531. gap: 3px;
  3532. }
  3533. .stepper-btn {
  3534. width: 10px;
  3535. height: 7px;
  3536. padding: 0;
  3537. border: none;
  3538. outline: none;
  3539. appearance: none;
  3540. -webkit-appearance: none;
  3541. background: #2563eb;
  3542. display: block;
  3543. cursor: pointer;
  3544. transition: opacity 0.2s ease, transform 0.2s ease;
  3545. }
  3546. .stepper-btn-up {
  3547. clip-path: polygon(50% 0, 0 100%, 100% 100%);
  3548. }
  3549. .stepper-btn-down {
  3550. clip-path: polygon(0 0, 100% 0, 50% 100%);
  3551. }
  3552. .stepper-btn:hover:not(:disabled) {
  3553. transform: scale(1.08);
  3554. }
  3555. .stepper-btn:disabled {
  3556. opacity: 0.35;
  3557. cursor: not-allowed;
  3558. }
  3559. .action-buttons {
  3560. display: flex;
  3561. justify-content: space-between;
  3562. align-items: center;
  3563. margin-top: 16px;
  3564. padding-top: 16px;
  3565. border-top: 1px solid var(--border-color);
  3566. }
  3567. .clear-btn {
  3568. background: white;
  3569. border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
  3570. border-radius: 8px; /* 添加圆角 */
  3571. color: var(--text-muted);
  3572. font-size: 14px;
  3573. cursor: pointer;
  3574. padding: 8px 16px;
  3575. transition: all 0.3s;
  3576. display: flex;
  3577. align-items: center;
  3578. gap: 6px;
  3579. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
  3580. }
  3581. .clear-btn:hover {
  3582. color: var(--danger-color);
  3583. border-color: rgba(220, 53, 69, 0.2); /* 悬浮时边框微红 */
  3584. box-shadow: 0 6px 20px rgba(220, 53, 69, 0.1); /* 悬浮时带红色的发光阴影 */
  3585. }
  3586. .generate-btn {
  3587. background: var(--primary-color);
  3588. color: white;
  3589. border: none;
  3590. padding: 10px 24px;
  3591. border-radius: 8px;
  3592. font-size: 14px;
  3593. font-weight: 600;
  3594. cursor: pointer;
  3595. transition: all 0.3s;
  3596. display: flex;
  3597. align-items: center;
  3598. gap: 8px;
  3599. }
  3600. .generate-btn:hover:not(:disabled) {
  3601. background: #0b5ed7;
  3602. transform: translateY(-2px);
  3603. box-shadow: 0 4px 12px rgba(13, 110, 253, 0.3);
  3604. }
  3605. .generate-btn:disabled {
  3606. background: #93c5fd; /* 浅蓝色背景 */
  3607. cursor: not-allowed;
  3608. box-shadow: 0 4px 12px rgba(147, 197, 253, 0.4); /* 浅蓝色阴影 */
  3609. opacity: 0.9;
  3610. }
  3611. /* =============== 题型配置区域 样式结束 =============== */
  3612. /* =============== 实时预览区域 样式开始 =============== */
  3613. .preview-panel {
  3614. width: 320px;
  3615. background: #f7f9fb; /* 匹配截图背景 */
  3616. border-left: 1px solid var(--border-color);
  3617. padding: 24px;
  3618. overflow-y: hidden;
  3619. flex-shrink: 0;
  3620. scrollbar-width: none;
  3621. -ms-overflow-style: none;
  3622. }
  3623. .preview-panel::-webkit-scrollbar {
  3624. display: none;
  3625. }
  3626. .preview-header {
  3627. margin-bottom: 24px;
  3628. }
  3629. .preview-header h3 {
  3630. font-size: 18px;
  3631. font-weight: bold;
  3632. color: #1f2937;
  3633. margin: 0;
  3634. }
  3635. .preview-name-card {
  3636. background: white;
  3637. border-radius: 16px;
  3638. padding: 16px 20px;
  3639. margin-bottom: 24px;
  3640. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 加深阴影 */
  3641. border: 1px solid rgba(0, 0, 0, 0.06); /* 稍微加深边框线 */
  3642. }
  3643. .preview-name-label {
  3644. font-size: 13px;
  3645. font-weight: 600;
  3646. color: #4b5563;
  3647. margin-bottom: 12px;
  3648. }
  3649. .preview-title {
  3650. font-size: 15px;
  3651. color: #9ca3af;
  3652. line-height: 1.4;
  3653. }
  3654. .preview-section {
  3655. margin-bottom: 24px;
  3656. }
  3657. .preview-section-title {
  3658. font-size: 14px;
  3659. font-weight: bold;
  3660. color: #4b5563;
  3661. margin-bottom: 16px;
  3662. }
  3663. .preview-item {
  3664. margin-bottom: 16px;
  3665. display: flex;
  3666. flex-direction: column;
  3667. gap: 4px;
  3668. }
  3669. .preview-item-top {
  3670. display: flex;
  3671. justify-content: space-between;
  3672. align-items: center;
  3673. }
  3674. .preview-item-left {
  3675. display: flex;
  3676. align-items: center;
  3677. gap: 8px;
  3678. }
  3679. .preview-dot {
  3680. width: 6px;
  3681. height: 6px;
  3682. border-radius: 50%;
  3683. }
  3684. .preview-type-name {
  3685. font-size: 14px;
  3686. color: #1f2937;
  3687. }
  3688. .preview-type-count {
  3689. font-size: 14px;
  3690. font-weight: 600;
  3691. color: #1f2937;
  3692. }
  3693. .preview-item-bottom {
  3694. padding-left: 14px; /* Align with text (6px dot + 8px gap) */
  3695. }
  3696. .preview-type-score {
  3697. font-size: 12px;
  3698. color: #9ca3af;
  3699. }
  3700. .preview-footer {
  3701. margin-top: 24px;
  3702. padding-top: 20px;
  3703. border-top: 1px solid #e5e7eb;
  3704. }
  3705. .preview-total {
  3706. display: flex;
  3707. justify-content: space-between;
  3708. align-items: center;
  3709. font-size: 15px;
  3710. font-weight: 600;
  3711. color: #000000;
  3712. padding: 0 10px; /* 左右各加16px的内边距,使文字向中间靠拢 */
  3713. }
  3714. .preview-total-score {
  3715. font-weight: bold;
  3716. }
  3717. /* =============== 实时预览区域 样式结束 =============== */
  3718. }
  3719. .exam-workshop-card {
  3720. background: white;
  3721. width: 100%;
  3722. height: 100%;
  3723. padding: 0;
  3724. border-radius: 0;
  3725. box-shadow: none;
  3726. max-width: 100%;
  3727. .config-section {
  3728. flex: 1;
  3729. display: flex;
  3730. flex-direction: column;
  3731. gap: 40px;
  3732. .config-item {
  3733. .config-header {
  3734. display: flex;
  3735. align-items: center;
  3736. gap: 8px;
  3737. margin-bottom: 14px;
  3738. .step-number {
  3739. width: 24px;
  3740. height: 24px;
  3741. background: #3e7bfa;
  3742. color: white;
  3743. border-radius: 50%;
  3744. display: flex;
  3745. align-items: center;
  3746. justify-content: center;
  3747. font-size: 16px;
  3748. font-weight: 600;
  3749. }
  3750. h3 {
  3751. font-size: 18px;
  3752. font-weight: 600;
  3753. color: #1f2937;
  3754. margin: 0;
  3755. }
  3756. }
  3757. .type-cards {
  3758. display: flex;
  3759. flex-wrap: wrap;
  3760. gap: 16px;
  3761. justify-content: space-between;
  3762. .type-card {
  3763. display: flex;
  3764. flex-direction: column;
  3765. align-items: center;
  3766. gap: 14px;
  3767. padding: 14px;
  3768. border: 2px solid #e5e7eb;
  3769. border-radius: 12px;
  3770. background: white;
  3771. cursor: pointer;
  3772. transition: all 0.3s ease;
  3773. flex: 1;
  3774. min-width: 180px;
  3775. &:hover {
  3776. border-color: #3e7bfa;
  3777. transform: translateY(-2px);
  3778. }
  3779. &.active {
  3780. border-color: #3e7bfa;
  3781. background: rgba(62, 123, 250, 0.1);
  3782. .type-icon {
  3783. filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
  3784. saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
  3785. }
  3786. span {
  3787. color: #3e7bfa;
  3788. }
  3789. }
  3790. .type-icon {
  3791. width: 40px;
  3792. height: 40px;
  3793. transition: filter 0.3s ease;
  3794. }
  3795. span {
  3796. font-size: 14px;
  3797. color: #374151;
  3798. font-weight: 500;
  3799. transition: color 0.3s ease;
  3800. }
  3801. }
  3802. }
  3803. .generation-methods {
  3804. display: flex;
  3805. gap: 16px;
  3806. .method-card {
  3807. flex: 1;
  3808. display: flex;
  3809. align-items: center;
  3810. gap: 14px;
  3811. padding: 18px;
  3812. border: 2px solid #e5e7eb;
  3813. border-radius: 12px;
  3814. background: white;
  3815. cursor: pointer;
  3816. transition: all 0.3s ease;
  3817. position: relative;
  3818. &:hover {
  3819. border-color: #3e7bfa;
  3820. }
  3821. &.active {
  3822. border-color: #3e7bfa;
  3823. background: rgba(62, 123, 250, 0.1);
  3824. .method-icon {
  3825. filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
  3826. saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
  3827. }
  3828. .method-content {
  3829. h4 {
  3830. color: #3e7bfa;
  3831. }
  3832. }
  3833. }
  3834. .method-icon {
  3835. width: 32px;
  3836. height: 32px;
  3837. flex-shrink: 0;
  3838. transition: filter 0.3s ease;
  3839. }
  3840. .method-content {
  3841. h4 {
  3842. font-size: 16px;
  3843. font-weight: 600;
  3844. color: #1f2937;
  3845. margin: 0 0 6px 0;
  3846. transition: color 0.3s ease;
  3847. }
  3848. p {
  3849. font-size: 14px;
  3850. color: #6b7280;
  3851. margin: 0;
  3852. line-height: 1.5;
  3853. }
  3854. // PPT文件预览样式(绝对定位)
  3855. .ppt-file-preview {
  3856. position: absolute;
  3857. top: 18px;
  3858. right: 10px;
  3859. width: 200px;
  3860. z-index: 10;
  3861. .file-preview {
  3862. display: flex;
  3863. align-items: center;
  3864. background: white;
  3865. border: 1px solid #E5E7EB;
  3866. border-radius: 8px;
  3867. padding: 12px;
  3868. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  3869. .file-icon {
  3870. font-size: 24px;
  3871. margin-right: 12px;
  3872. width: 32px;
  3873. text-align: center;
  3874. }
  3875. .file-info {
  3876. flex: 1;
  3877. .file-name {
  3878. font-size: 12px;
  3879. font-weight: 500;
  3880. color: #374151;
  3881. margin-bottom: 2px;
  3882. overflow: hidden;
  3883. text-overflow: ellipsis;
  3884. white-space: nowrap;
  3885. max-width: 150px;
  3886. }
  3887. .file-size {
  3888. font-size: 10px;
  3889. color: #6B7280;
  3890. }
  3891. }
  3892. .remove-file-btn {
  3893. width: 20px;
  3894. height: 20px;
  3895. border: none;
  3896. background: rgba(239, 68, 68, 0.1);
  3897. color: #DC2626;
  3898. border-radius: 50%;
  3899. cursor: pointer;
  3900. display: flex;
  3901. align-items: center;
  3902. justify-content: center;
  3903. transition: all 0.2s ease;
  3904. &:hover {
  3905. background: rgba(239, 68, 68, 0.2);
  3906. transform: scale(1.1);
  3907. }
  3908. .remove-icon {
  3909. font-size: 12px;
  3910. font-weight: bold;
  3911. }
  3912. }
  3913. }
  3914. }
  3915. }
  3916. }
  3917. }
  3918. .exam-config-container {
  3919. display: flex;
  3920. gap: 20px;
  3921. align-items: flex-start;
  3922. .config-left {
  3923. flex: 1;
  3924. display: flex;
  3925. flex-direction: column;
  3926. gap: 14px;
  3927. position: relative;
  3928. .section-title {
  3929. font-size: 14px;
  3930. color: #374151;
  3931. font-weight: 500;
  3932. }
  3933. .config-row {
  3934. display: flex;
  3935. align-items: flex-start;
  3936. gap: 40px;
  3937. margin-bottom: 14px;
  3938. .config-group {
  3939. flex: 1;
  3940. display: flex;
  3941. flex-direction: column;
  3942. gap: 8px;
  3943. label {
  3944. font-size: 14px;
  3945. font-weight: 500;
  3946. color: #374151;
  3947. }
  3948. .input-wrapper {
  3949. position: relative;
  3950. width: 100%;
  3951. }
  3952. .config-input {
  3953. width: 100%;
  3954. padding: 12px 16px;
  3955. padding-right: 60px;
  3956. border: 2px solid #e5e7eb;
  3957. border-radius: 8px;
  3958. font-size: 14px;
  3959. color: #374151;
  3960. height: 42px;
  3961. &:focus {
  3962. outline: none;
  3963. border-color: #3e7bfa;
  3964. }
  3965. }
  3966. .char-count-inline {
  3967. position: absolute;
  3968. right: 16px;
  3969. top: 50%;
  3970. transform: translateY(-50%);
  3971. font-size: 12px;
  3972. color: #6b7280;
  3973. pointer-events: none;
  3974. &.warning {
  3975. color: #f59e0b;
  3976. }
  3977. }
  3978. .score-input {
  3979. display: flex;
  3980. align-items: center;
  3981. gap: 8px;
  3982. width: 180px;
  3983. height: 42px;
  3984. .config-input {
  3985. width: 110px;
  3986. padding: 8px 12px;
  3987. height: 42px;
  3988. }
  3989. .unit {
  3990. font-size: 14px;
  3991. color: #374151;
  3992. }
  3993. }
  3994. }
  3995. }
  3996. .question-types {
  3997. .question-type {
  3998. margin-bottom: 14px;
  3999. .type-row {
  4000. display: flex;
  4001. align-items: center;
  4002. gap: 16px;
  4003. .type-name {
  4004. font-size: 14px;
  4005. font-weight: 500;
  4006. color: #374151;
  4007. min-width: 60px;
  4008. }
  4009. .progress-bar {
  4010. flex: 1;
  4011. height: 8px;
  4012. background: #e5e7eb;
  4013. border-radius: 4px;
  4014. overflow: hidden;
  4015. .progress-fill {
  4016. height: 100%;
  4017. background: #3e7bfa;
  4018. transition: width 0.3s ease;
  4019. border-radius: 4px;
  4020. }
  4021. }
  4022. .score-config {
  4023. display: flex;
  4024. align-items: center;
  4025. gap: 8px;
  4026. font-size: 14px;
  4027. color: #6b7280;
  4028. white-space: nowrap;
  4029. .score-input-field,
  4030. .count-input-field {
  4031. width: 50px;
  4032. padding: 4px 8px;
  4033. border: 1px solid #d1d5db;
  4034. border-radius: 4px;
  4035. text-align: center;
  4036. font-size: 14px;
  4037. &:focus {
  4038. outline: none;
  4039. border-color: #3e7bfa;
  4040. }
  4041. }
  4042. }
  4043. }
  4044. }
  4045. }
  4046. }
  4047. }
  4048. }
  4049. }
  4050. .preview-panel {
  4051. width: 320px;
  4052. background: #f8faff;
  4053. border-radius: 12px;
  4054. padding: 24px;
  4055. flex-shrink: 0;
  4056. .preview-header {
  4057. display: flex;
  4058. align-items: center;
  4059. gap: 12px;
  4060. margin-bottom: 24px;
  4061. .preview-icon {
  4062. width: 20px;
  4063. height: 16px;
  4064. }
  4065. h3 {
  4066. font-size: 16px;
  4067. font-weight: 600;
  4068. color: #374151;
  4069. margin: 0;
  4070. }
  4071. }
  4072. .preview-content {
  4073. background-color: #ffffff;
  4074. border-radius: 8px;
  4075. border: 1px solid #e5e7eb;
  4076. padding: 10px 10px 16px 10px;
  4077. .preview-title {
  4078. font-size: 14px;
  4079. font-weight: 600;
  4080. color: #1f2937;
  4081. margin: 0 0 10px 0;
  4082. text-align: center;
  4083. }
  4084. .question-breakdown {
  4085. .breakdown-item {
  4086. margin-bottom: 14px;
  4087. .breakdown-row {
  4088. display: flex;
  4089. justify-content: space-between;
  4090. align-items: center;
  4091. font-size: 12px;
  4092. color: #374151;
  4093. line-height: 1.5;
  4094. .breakdown-left {
  4095. flex: 1;
  4096. }
  4097. .breakdown-right {
  4098. font-weight: 500;
  4099. }
  4100. }
  4101. }
  4102. }
  4103. .divider {
  4104. height: 1px;
  4105. background: #e5e7eb;
  4106. margin: 12px 0 8px 0;
  4107. }
  4108. .calculated-score-row {
  4109. display: flex;
  4110. justify-content: space-between;
  4111. align-items: center;
  4112. font-size: 12px;
  4113. font-weight: 500;
  4114. color: #6b7280;
  4115. margin-bottom: 14px;
  4116. .calculated-label {
  4117. color: #6b7280;
  4118. }
  4119. .calculated-value {
  4120. color: #6b7280;
  4121. }
  4122. }
  4123. .total-score-row {
  4124. display: flex;
  4125. justify-content: space-between;
  4126. align-items: center;
  4127. font-size: 12px;
  4128. font-weight: 600;
  4129. color: #4b5563;
  4130. .total-label {
  4131. color: #374151;
  4132. }
  4133. .total-value {
  4134. color: #4b5563;
  4135. }
  4136. }
  4137. }
  4138. }
  4139. }
  4140. .bottom-actions {
  4141. display: flex;
  4142. justify-content: center;
  4143. gap: 24px;
  4144. margin-top: 20px;
  4145. width: 100%;
  4146. .clear-btn {
  4147. padding: 0;
  4148. border: none;
  4149. background: none;
  4150. cursor: pointer;
  4151. transition: opacity 0.3s ease;
  4152. &:hover {
  4153. opacity: 0.8;
  4154. }
  4155. .clear-icon {
  4156. width: 128px;
  4157. height: 50px;
  4158. }
  4159. }
  4160. .generate-btn {
  4161. padding: 0;
  4162. border: none;
  4163. background: none;
  4164. cursor: pointer;
  4165. transition: opacity 0.3s ease;
  4166. &:hover:not(:disabled) {
  4167. opacity: 0.8;
  4168. }
  4169. &:disabled {
  4170. cursor: not-allowed;
  4171. opacity: 0.6;
  4172. }
  4173. .generate-icon {
  4174. width: 154px;
  4175. height: 50px;
  4176. }
  4177. .generating-text {
  4178. display: flex;
  4179. align-items: center;
  4180. justify-content: center;
  4181. width: 154px;
  4182. height: 50px;
  4183. text-align: center;
  4184. background: #f3f4f6;
  4185. color: #6b7280;
  4186. border-radius: 8px;
  4187. // border: 1px solid #e5e7eb;
  4188. font-size: 16px;
  4189. font-weight: 500;
  4190. .loading-dots {
  4191. display: inline-flex;
  4192. align-items: center;
  4193. gap: 4px;
  4194. margin-left: 2px;
  4195. .dot {
  4196. display: inline-block;
  4197. width: 4px;
  4198. height: 4px;
  4199. border-radius: 50%;
  4200. background: #6b7280;
  4201. animation: loading-dot 1.4s infinite ease-in-out;
  4202. &:nth-child(1) {
  4203. animation-delay: 0s;
  4204. }
  4205. &:nth-child(2) {
  4206. animation-delay: 0.2s;
  4207. }
  4208. &:nth-child(3) {
  4209. animation-delay: 0.4s;
  4210. }
  4211. }
  4212. }
  4213. }
  4214. @keyframes loading-dot {
  4215. 0%, 40%, 100% {
  4216. opacity: 0.3;
  4217. transform: scale(0.8);
  4218. }
  4219. 20% {
  4220. opacity: 1;
  4221. transform: scale(1);
  4222. }
  4223. }
  4224. }
  4225. }
  4226. /* 固定布局,支持横向滚动 */
  4227. .chat-container {
  4228. min-width: 1428px;
  4229. }
  4230. .work-content {
  4231. min-width: 1428px;
  4232. overflow-x: auto;
  4233. &.exam-detail-mode {
  4234. background: transparent;
  4235. }
  4236. }
  4237. .exam-workshop-card {
  4238. min-width: 1428px; /* 与AI写作保持一致 */
  4239. }
  4240. /* 考试详情页样式 */
  4241. .exam-detail-card {
  4242. // background: white;
  4243. width: 100%;
  4244. min-height: 800px;
  4245. // padding: 32px;
  4246. border-radius: 16px;
  4247. // box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  4248. max-width: 1528px;
  4249. min-width: 1428px;
  4250. .detail-header {
  4251. display: flex;
  4252. justify-content: space-between;
  4253. align-items: center;
  4254. margin-bottom: 24px;
  4255. .header-left {
  4256. .back-btn {
  4257. border: none;
  4258. background: transparent;
  4259. color: #3E7BFA;
  4260. font-size: 16px;
  4261. cursor: pointer;
  4262. transition: all 0.3s ease;
  4263. &:hover {
  4264. background: transparent;
  4265. color: #3e7bfa;
  4266. }
  4267. .back-arrow {
  4268. font-size: 16px;
  4269. font-weight: bold;
  4270. }
  4271. }
  4272. }
  4273. .header-right {
  4274. display: flex;
  4275. align-items: center;
  4276. gap: 24px;
  4277. .save-btn {
  4278. // padding: 8px;
  4279. border: none;
  4280. background: transparent;
  4281. cursor: pointer;
  4282. transition: opacity 0.3s ease;
  4283. &:hover {
  4284. opacity: 0.8;
  4285. }
  4286. .save-icon {
  4287. width: 134px;
  4288. height: 42px;
  4289. }
  4290. }
  4291. .download-dropdown {
  4292. position: relative;
  4293. display: inline-block;
  4294. &.disabled {
  4295. opacity: 0.5;
  4296. cursor: not-allowed;
  4297. pointer-events: none;
  4298. }
  4299. .download-btn {
  4300. // padding: 8px;
  4301. border: none;
  4302. background: transparent;
  4303. cursor: pointer;
  4304. transition: opacity 0.3s ease;
  4305. &:hover {
  4306. opacity: 0.8;
  4307. }
  4308. .download-icon {
  4309. width: 107px;
  4310. height: 34px;
  4311. }
  4312. }
  4313. .dropdown-menu {
  4314. position: absolute;
  4315. top: 100%;
  4316. right: 0;
  4317. background: white;
  4318. border: 1px solid #e5e7eb;
  4319. border-radius: 8px;
  4320. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  4321. z-index: 1000;
  4322. min-width: 180px;
  4323. opacity: 0;
  4324. visibility: hidden;
  4325. transform: translateY(-10px);
  4326. transition: all 0.3s ease;
  4327. .dropdown-item {
  4328. padding: 12px 16px;
  4329. cursor: pointer;
  4330. transition: background-color 0.2s ease;
  4331. border-bottom: 1px solid #f3f4f6;
  4332. &:last-child {
  4333. border-bottom: none;
  4334. }
  4335. &:hover {
  4336. background: #f8fafc;
  4337. }
  4338. &:disabled {
  4339. opacity: 0.5;
  4340. cursor: not-allowed;
  4341. pointer-events: none;
  4342. }
  4343. .item-text {
  4344. font-size: 14px;
  4345. color: #374151;
  4346. }
  4347. }
  4348. }
  4349. &.show .dropdown-menu {
  4350. opacity: 1;
  4351. visibility: visible;
  4352. transform: translateY(0);
  4353. }
  4354. }
  4355. }
  4356. }
  4357. .generation-time {
  4358. font-size: 14px;
  4359. color: #6B7280;
  4360. }
  4361. .exam-info {
  4362. display: flex;
  4363. justify-content: space-between;
  4364. align-items: center;
  4365. background: white;
  4366. padding: 24px;
  4367. border-radius: 12px;
  4368. margin-bottom: 24px;
  4369. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  4370. .exam-title {
  4371. font-size: 24px;
  4372. font-weight: 600;
  4373. color: #1f2937;
  4374. margin: 0 0 16px 0;
  4375. text-align: left;
  4376. }
  4377. .exam-stats {
  4378. display: flex;
  4379. // justify-content: space-between;
  4380. gap: 24px;
  4381. align-items: center;
  4382. font-size: 16px;
  4383. color: #6b7280;
  4384. .left-stats {
  4385. display: flex;
  4386. gap: 24px;
  4387. align-items: center;
  4388. }
  4389. .total-score,
  4390. .question-count {
  4391. font-weight: 500;
  4392. font-size: 14px;
  4393. }
  4394. }
  4395. }
  4396. .question-sections {
  4397. display: flex;
  4398. flex-direction: column;
  4399. gap: 24px;
  4400. .question-section {
  4401. border: 1px solid #e5e7eb;
  4402. border-radius: 12px;
  4403. overflow: hidden;
  4404. .section-header {
  4405. display: flex;
  4406. justify-content: space-between;
  4407. align-items: center;
  4408. padding: 14px 18px;
  4409. background: #F9FAFB;
  4410. cursor: pointer;
  4411. transition: background-color 0.3s ease;
  4412. &:hover {
  4413. background: #e8f0ff;
  4414. }
  4415. .section-title {
  4416. display: flex;
  4417. align-items: center;
  4418. gap: 12px;
  4419. font-size: 16px;
  4420. font-weight: 600;
  4421. color: #1f2937;
  4422. .section-number {
  4423. width: 24px;
  4424. height: 24px;
  4425. background: #3e7bfa;
  4426. color: white;
  4427. border-radius: 50%;
  4428. display: flex;
  4429. align-items: center;
  4430. justify-content: center;
  4431. font-size: 14px;
  4432. font-weight: bold;
  4433. }
  4434. .section-name {
  4435. color: #1f2937;
  4436. }
  4437. .section-score {
  4438. font-size: 14px;
  4439. font-weight: 400;
  4440. color: #6b7280;
  4441. }
  4442. }
  4443. .section-controls {
  4444. display: flex;
  4445. align-items: center;
  4446. gap: 16px;
  4447. .question-count-text {
  4448. font-size: 14px;
  4449. color: #6b7280;
  4450. }
  4451. .toggle-icon {
  4452. width: 24px;
  4453. height: 24px;
  4454. transition: transform 0.3s ease;
  4455. &.expanded {
  4456. transform: rotate(180deg);
  4457. }
  4458. }
  4459. }
  4460. }
  4461. .section-content {
  4462. padding: 14px;
  4463. background: white;
  4464. .question-item {
  4465. margin-bottom: 14px;
  4466. padding: 14px;
  4467. border: 1px solid #f3f4f6;
  4468. border-radius: 8px;
  4469. background: #fafbfc;
  4470. &:last-child {
  4471. margin-bottom: 0;
  4472. }
  4473. .question-header {
  4474. display: flex;
  4475. align-items: flex-start;
  4476. gap: 12px;
  4477. margin-bottom: 14px;
  4478. .question-number {
  4479. font-size: 16px;
  4480. font-weight: 600;
  4481. color: #3e7bfa;
  4482. min-width: 24px;
  4483. }
  4484. .question-text {
  4485. flex: 1;
  4486. font-size: 16px;
  4487. line-height: 1.6;
  4488. color: #1f2937;
  4489. }
  4490. .refresh-btn {
  4491. border: none;
  4492. background: transparent;
  4493. cursor: pointer;
  4494. transition: all 0.3s ease;
  4495. &:hover {
  4496. background: transparent;
  4497. opacity: 0.8;
  4498. }
  4499. flex-shrink: 0;
  4500. &:hover {
  4501. border-color: #3e7bfa;
  4502. color: #3e7bfa;
  4503. }
  4504. .refresh-icon {
  4505. width: 24px;
  4506. height: 24px;
  4507. }
  4508. }
  4509. }
  4510. .options {
  4511. display: grid;
  4512. grid-template-columns: 1fr 1fr;
  4513. gap: 12px 24px;
  4514. .option {
  4515. display: flex;
  4516. align-items: center;
  4517. gap: 16px;
  4518. padding: 8px 0;
  4519. border: none;
  4520. background: transparent;
  4521. cursor: pointer;
  4522. transition: all 0.3s ease;
  4523. &:hover {
  4524. opacity: 0.8;
  4525. }
  4526. &.selected {
  4527. color: #3e7bfa;
  4528. .option-key {
  4529. color: #3e7bfa;
  4530. }
  4531. }
  4532. .radio-wrapper,
  4533. .checkbox-wrapper {
  4534. flex-shrink: 0;
  4535. /* 重置浏览器默认样式 */
  4536. input[type="radio"],
  4537. input[type="checkbox"] {
  4538. display: none !important;
  4539. }
  4540. }
  4541. .radio-circle {
  4542. width: 20px !important;
  4543. height: 20px !important;
  4544. border: 1px solid #d1d5db !important;
  4545. border-radius: 50% !important;
  4546. display: flex !important;
  4547. align-items: center !important;
  4548. justify-content: center !important;
  4549. transition: all 0.3s ease;
  4550. background: white !important;
  4551. flex-shrink: 0;
  4552. position: relative !important;
  4553. z-index: 1 !important;
  4554. &.selected {
  4555. border-color: #d1d5db !important;
  4556. background: white !important;
  4557. }
  4558. .radio-dot {
  4559. width: 12px !important;
  4560. height: 12px !important;
  4561. background: #3e7bfa !important;
  4562. border-radius: 50% !important;
  4563. }
  4564. }
  4565. .checkbox-square {
  4566. width: 20px !important;
  4567. height: 20px !important;
  4568. border: 1px solid #d1d5db !important;
  4569. border-radius: 4px !important;
  4570. display: flex !important;
  4571. align-items: center !important;
  4572. justify-content: center !important;
  4573. transition: all 0.3s ease;
  4574. background: white !important;
  4575. flex-shrink: 0;
  4576. &.selected {
  4577. border-color: #3e7bfa !important;
  4578. background: #3e7bfa !important;
  4579. }
  4580. .checkbox-check {
  4581. color: white !important;
  4582. font-size: 12px;
  4583. font-weight: bold;
  4584. }
  4585. }
  4586. .option-key {
  4587. font-weight: 600;
  4588. color: #6b7280;
  4589. font-size: 16px;
  4590. min-width: 24px;
  4591. margin-right: 8px;
  4592. font-family: "Alibaba PuHuiTi 3.0", sans-serif;
  4593. transition: color 0.3s ease;
  4594. }
  4595. .option.selected .option-key {
  4596. color: #3e7bfa;
  4597. }
  4598. .option-content {
  4599. flex: 1;
  4600. display: flex;
  4601. align-items: center;
  4602. gap: 4px;
  4603. }
  4604. .option-text {
  4605. font-size: 16px;
  4606. line-height: 1.5;
  4607. color: #6b7280;
  4608. font-family: "Alibaba PuHuiTi 3.0", sans-serif;
  4609. transition: color 0.3s ease;
  4610. }
  4611. .option.selected .option-text {
  4612. color: #374151;
  4613. }
  4614. .edit-option-btn {
  4615. display: flex;
  4616. align-items: center;
  4617. justify-content: center;
  4618. width: 20px;
  4619. height: 20px;
  4620. border: none;
  4621. background: transparent;
  4622. cursor: pointer;
  4623. border-radius: 4px;
  4624. transition: all 0.2s ease;
  4625. opacity: 0.8;
  4626. flex-shrink: 0;
  4627. &:hover {
  4628. opacity: 1;
  4629. background: #f3f4f6;
  4630. }
  4631. &:disabled {
  4632. opacity: 0.3;
  4633. cursor: not-allowed;
  4634. }
  4635. .edit-icon {
  4636. width: 14px;
  4637. height: 14px;
  4638. }
  4639. }
  4640. }
  4641. }
  4642. .answer-box {
  4643. .answer-outline {
  4644. display: flex;
  4645. flex-direction: column;
  4646. gap: 16px;
  4647. .outline-item {
  4648. display: flex;
  4649. align-items: flex-start;
  4650. gap: 8px;
  4651. .outline-text {
  4652. flex: 1;
  4653. font-size: 16px;
  4654. line-height: 1.6;
  4655. color: #374151;
  4656. padding: 12px;
  4657. background: white;
  4658. border: 1px solid #e5e7eb;
  4659. border-radius: 6px;
  4660. margin-right: 0;
  4661. }
  4662. .edit-option-btn {
  4663. display: flex;
  4664. align-items: center;
  4665. justify-content: center;
  4666. width: 20px;
  4667. height: 20px;
  4668. border: none;
  4669. background: transparent;
  4670. cursor: pointer;
  4671. border-radius: 4px;
  4672. transition: all 0.2s ease;
  4673. opacity: 0.8;
  4674. flex-shrink: 0;
  4675. align-self: flex-start;
  4676. margin-top: 12px;
  4677. margin-left: 4px;
  4678. &:hover {
  4679. opacity: 1;
  4680. background: #f3f4f6;
  4681. }
  4682. &:disabled {
  4683. opacity: 0.3;
  4684. cursor: not-allowed;
  4685. }
  4686. .edit-icon {
  4687. width: 14px;
  4688. height: 14px;
  4689. }
  4690. }
  4691. }
  4692. }
  4693. }
  4694. }
  4695. }
  4696. }
  4697. }
  4698. }
  4699. /* 旋转动效样式 */
  4700. .refresh-icon.rotating {
  4701. animation: rotate 1s linear infinite;
  4702. }
  4703. @keyframes rotate {
  4704. from {
  4705. transform: rotate(0deg);
  4706. }
  4707. to {
  4708. transform: rotate(360deg);
  4709. }
  4710. }
  4711. /* 禁用状态样式 */
  4712. .type-card:disabled,
  4713. .method-card:disabled,
  4714. .config-input:disabled,
  4715. .score-input-field:disabled,
  4716. .count-input-field:disabled,
  4717. .generate-btn:disabled,
  4718. .back-btn:disabled,
  4719. .save-btn:disabled,
  4720. .download-btn:disabled,
  4721. .refresh-btn:disabled {
  4722. opacity: 0.5;
  4723. cursor: not-allowed !important;
  4724. pointer-events: none;
  4725. }
  4726. /* 一键清除按钮禁用状态 - 保持原色,只改变鼠标状态 */
  4727. .clear-btn:disabled {
  4728. cursor: not-allowed !important;
  4729. pointer-events: none;
  4730. /* 不改变opacity,保持原色 */
  4731. }
  4732. /* 生成中状态的按钮样式 */
  4733. .generate-btn:disabled {
  4734. background: #f3f4f6;
  4735. color: #9ca3af;
  4736. }
  4737. .generate-btn:disabled .generate-icon {
  4738. opacity: 0.5;
  4739. }
  4740. /* 禁用状态的输入框样式 */
  4741. .config-input:disabled,
  4742. .score-input-field:disabled,
  4743. .count-input-field:disabled {
  4744. background: #f9fafb;
  4745. color: #9ca3af;
  4746. border-color: #d1d5db;
  4747. }
  4748. /* 禁用状态的选项样式 */
  4749. .option[style*="cursor: not-allowed"] {
  4750. opacity: 0.5;
  4751. pointer-events: none;
  4752. }
  4753. /* 禁用状态的section header样式 */
  4754. .section-header[style*="cursor: not-allowed"] {
  4755. opacity: 0.7;
  4756. }
  4757. /* 生成中状态的视觉反馈 */
  4758. .generating-text {
  4759. color: #6b7280;
  4760. font-weight: 500;
  4761. }
  4762. /* 编辑模态框样式 */
  4763. .modal-overlay {
  4764. position: fixed;
  4765. top: 0;
  4766. left: 0;
  4767. right: 0;
  4768. bottom: 0;
  4769. background: rgba(0, 0, 0, 0.5);
  4770. display: flex;
  4771. align-items: center;
  4772. justify-content: center;
  4773. z-index: 1000;
  4774. }
  4775. .edit-modal {
  4776. background: white;
  4777. border-radius: 8px;
  4778. width: 90%;
  4779. max-width: 500px;
  4780. max-height: 80vh;
  4781. overflow: hidden;
  4782. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  4783. }
  4784. .modal-header {
  4785. display: flex;
  4786. align-items: center;
  4787. justify-content: space-between;
  4788. padding: 16px 24px;
  4789. border-bottom: 1px solid #e5e7eb;
  4790. h3 {
  4791. margin: 0;
  4792. font-size: 18px;
  4793. font-weight: 600;
  4794. color: #111827;
  4795. }
  4796. .close-btn {
  4797. background: none;
  4798. border: none;
  4799. font-size: 24px;
  4800. cursor: pointer;
  4801. color: #6b7280;
  4802. padding: 0;
  4803. width: 32px;
  4804. height: 32px;
  4805. display: flex;
  4806. align-items: center;
  4807. justify-content: center;
  4808. border-radius: 4px;
  4809. &:hover {
  4810. background: #f3f4f6;
  4811. color: #374151;
  4812. }
  4813. }
  4814. }
  4815. .modal-body {
  4816. padding: 24px;
  4817. max-height: 60vh;
  4818. overflow-y: auto;
  4819. }
  4820. .edit-section {
  4821. display: flex;
  4822. flex-direction: column;
  4823. gap: 16px;
  4824. label {
  4825. font-weight: 500;
  4826. color: #374151;
  4827. font-size: 14px;
  4828. }
  4829. .edit-input {
  4830. padding: 12px;
  4831. border: 1px solid #d1d5db;
  4832. border-radius: 6px;
  4833. font-size: 16px;
  4834. transition: border-color 0.2s ease;
  4835. &:focus {
  4836. outline: none;
  4837. border-color: #3e7bfa;
  4838. box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
  4839. }
  4840. }
  4841. .text-validation {
  4842. margin-top: 8px;
  4843. text-align: right;
  4844. .char-count {
  4845. font-size: 12px;
  4846. color: #6b7280;
  4847. &.warning {
  4848. color: #f59e0b;
  4849. }
  4850. }
  4851. }
  4852. .edit-textarea {
  4853. padding: 12px;
  4854. border: 1px solid #d1d5db;
  4855. border-radius: 6px;
  4856. font-size: 16px;
  4857. min-height: 100px;
  4858. resize: vertical;
  4859. font-family: inherit;
  4860. transition: border-color 0.2s ease;
  4861. &:focus {
  4862. outline: none;
  4863. border-color: #3e7bfa;
  4864. box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
  4865. }
  4866. }
  4867. .answer-section {
  4868. margin-top: 16px;
  4869. padding-top: 16px;
  4870. border-top: 1px solid #e5e7eb;
  4871. .answer-options {
  4872. margin-top: 8px;
  4873. .radio-label,
  4874. .checkbox-label {
  4875. display: flex;
  4876. align-items: center;
  4877. gap: 8px;
  4878. padding: 8px;
  4879. border-radius: 4px;
  4880. cursor: pointer;
  4881. transition: background-color 0.2s ease;
  4882. &:hover {
  4883. background: #f9fafb;
  4884. }
  4885. input[type="radio"],
  4886. input[type="checkbox"] {
  4887. margin: 0;
  4888. }
  4889. }
  4890. }
  4891. }
  4892. }
  4893. .modal-footer {
  4894. display: flex;
  4895. justify-content: flex-end;
  4896. gap: 12px;
  4897. padding: 16px 24px;
  4898. border-top: 1px solid #e5e7eb;
  4899. background: #f9fafb;
  4900. .btn {
  4901. padding: 8px 16px;
  4902. border-radius: 6px;
  4903. font-size: 14px;
  4904. font-weight: 500;
  4905. cursor: pointer;
  4906. transition: all 0.2s ease;
  4907. border: 1px solid transparent;
  4908. &.btn-cancel {
  4909. background: white;
  4910. color: #374151;
  4911. border-color: #d1d5db;
  4912. &:hover {
  4913. background: #f9fafb;
  4914. }
  4915. }
  4916. &.btn-confirm {
  4917. background: #3e7bfa;
  4918. color: white;
  4919. &:hover {
  4920. background: #2563eb;
  4921. }
  4922. }
  4923. }
  4924. }
  4925. /* 历史记录加载状态样式 */
  4926. .history-loading {
  4927. display: flex;
  4928. flex-direction: column;
  4929. align-items: center;
  4930. justify-content: center;
  4931. padding: 40px 20px;
  4932. min-height: 200px;
  4933. }
  4934. .history-loading .loading-spinner {
  4935. width: 32px;
  4936. height: 32px;
  4937. border: 3px solid #f3f3f3;
  4938. border-top: 3px solid #409eff;
  4939. border-radius: 50%;
  4940. animation: spin 1s linear infinite;
  4941. margin: 0 auto 16px auto;
  4942. }
  4943. .history-loading .loading-text {
  4944. color: #6B7280;
  4945. font-size: 14px;
  4946. margin: 0;
  4947. font-weight: 400;
  4948. }
  4949. @keyframes spin {
  4950. 0% { transform: rotate(0deg); }
  4951. 100% { transform: rotate(360deg); }
  4952. }
  4953. /* 工作区域加载遮罩样式 */
  4954. .work-content {
  4955. position: relative;
  4956. }
  4957. .loading-overlay {
  4958. position: absolute;
  4959. top: 0;
  4960. left: 0;
  4961. width: 100%;
  4962. height: 100%;
  4963. background: rgba(255, 255, 255, 0.9);
  4964. display: flex;
  4965. flex-direction: column;
  4966. align-items: center;
  4967. justify-content: center;
  4968. z-index: 1000;
  4969. }
  4970. .loading-overlay .loading-spinner {
  4971. width: 40px;
  4972. height: 40px;
  4973. border: 4px solid #f3f3f3;
  4974. border-top: 4px solid #409eff;
  4975. border-radius: 50%;
  4976. animation: spin 1s linear infinite;
  4977. margin-bottom: 16px;
  4978. }
  4979. .loading-overlay p {
  4980. color: #666;
  4981. font-size: 16px;
  4982. margin: 0;
  4983. font-weight: 500;
  4984. }
  4985. .return-ai-btn {
  4986. position: absolute;
  4987. top: 10px;
  4988. right: 20px;
  4989. z-index: 100;
  4990. background: white;
  4991. border: 1px solid rgba(0, 0, 0, 0.06);
  4992. border-radius: 12px;
  4993. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
  4994. padding: 6px 16px;
  4995. font-size: 13px;
  4996. font-weight: 500;
  4997. color: #2563eb; /* 改为蓝色文字 */
  4998. cursor: pointer;
  4999. display: flex;
  5000. align-items: center;
  5001. gap: 5px;
  5002. transition: all 0.3s ease;
  5003. height: 36px;
  5004. box-sizing: border-box;
  5005. }
  5006. .return-ai-btn:disabled {
  5007. opacity: 0.5;
  5008. cursor: not-allowed;
  5009. }
  5010. .return-ai-btn:hover:not(:disabled) {
  5011. box-shadow: 0 8px 24px rgba(13, 110, 253, 0.12);
  5012. color: #0d6efd;
  5013. border-color: rgba(13, 110, 253, 0.2);
  5014. }
  5015. .return-ai-btn.has-before::before {
  5016. content: '←';
  5017. font-size: 16px;
  5018. font-weight: bold;
  5019. }
  5020. </style>