| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802 |
- <template>
- <div class="chat-container">
- <!-- 最左侧边栏 -->
- <Sidebar v-if="!hideSidebar" />
- <!-- 中间历史记录区域 -->
- <div class="history-sidebar" v-if="!hideSidebar">
- <div class="history-header">
- <span class="section-title">历史记录</span>
- <img
- src="@/assets/Chat/2.png"
- alt="新建任务"
- class="new-chat-btn"
- @click="createNewChat"
- />
- </div>
-
- <div class="history-list">
- <!-- 历史记录加载状态 -->
- <div v-if="isLoadingHistory && historyTotal === 0" class="history-loading">
- <div class="loading-spinner"></div>
- <div class="loading-text">正在加载历史记录...</div>
- </div>
-
- <!-- 有历史记录时显示 -->
- <div
- v-else-if="historyTotal > 0"
- v-for="(item, index) in historyData"
- :key="index"
- :class="['history-item', { active: item.isActive }]"
- @click="item.isActive ? null : (isGenerating || isLoadingHistoryItem ? null : handleHistoryItem(item))"
- :style="{ cursor: item.isActive ? 'default' : (isGenerating || isLoadingHistoryItem ? 'not-allowed' : 'pointer'), opacity: item.isActive ? '0.8' : '1' }"
- >
- <div class="history-content">
- <div class="history-title">{{ item.title }}</div>
- <div class="history-time">{{ item.time }}</div>
- </div>
- <div
- class="delete-btn"
- @click.stop="deleteHistoryItem(item, index)"
- :class="{ 'always-visible': item.isActive }"
- >
- <img src="/src/assets/AIWriting/8.png" alt="删除" class="delete-icon" />
- </div>
- </div>
-
- <!-- 无历史记录时显示空状态 -->
- <div v-else class="empty-history">
- <img src="@/assets/Chat/22.png" alt="暂无数据" class="empty-icon">
- <div class="empty-text">暂无数据</div>
- </div>
- </div>
- </div>
- <!-- 右侧工作区域 -->
- <div class="main-work" :style="{ background: showExamDetail ? 'transparent' : '#ebf3ff', position: 'relative' }">
- <!-- 头部 -->
- <div class="work-header" v-if="showExamDetail">
- <h2>考试工坊</h2>
- </div>
- <!-- 工作内容区域 -->
- <div class="work-content" :class="{ 'exam-detail-mode': showExamDetail }">
- <!-- 加载状态遮罩 -->
- <div v-if="isLoadingHistoryItem" class="loading-overlay">
- <div class="loading-spinner"></div>
- <p>正在加载历史记录...</p>
- </div>
- <!-- 考试工坊主界面 -->
- <div v-if="!showExamDetail" class="exam-workshop-card app-container">
- <!-- 中间主操作区 -->
- <main class="main-content" style="padding-top: 36px; position: relative;">
- <!-- 返回AI问答按钮 -->
- <button v-if="!showExamDetail" class="return-ai-btn has-before" @click="handleReturnToAI">
- 返回AI问答
- </button>
-
- <div class="form-group" style="position: relative;">
- <label class="form-label">试卷名称</label>
- <input type="text" class="form-control" v-model="examName" maxlength="32" placeholder="请输入试卷名称..." :disabled="isGenerating">
- <div class="char-count">{{ examName?.length || 0 }}/32</div>
- </div>
- <div class="form-group">
- <label class="form-label">出题依据内容</label>
- <textarea class="form-control" v-model="questionBasis" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || uploadedFiles.length > 0"></textarea>
-
- <div class="ppt-upload-section" style="flex-direction: column; align-items: flex-start;" @click="!isGenerating ? triggerFileUpload() : null">
- <div style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
- <div class="ppt-upload-content">
- <div class="ppt-upload-icon-wrapper">
- <el-icon style="font-size: 28px; color: #4b5563;"><UploadFilled /></el-icon>
- </div>
- <div class="ppt-upload-text-wrapper">
- <div class="ppt-upload-title">从PPT生成考题</div>
- <div class="ppt-upload-hint">上传培训PPT,智能提取关键内容生成考题(支持多文件,单文件20M内)</div>
- </div>
- </div>
- <el-icon class="ppt-arrow"><ArrowRight /></el-icon>
- </div>
-
- <div v-if="uploadedFiles.length > 0" class="files-list" @click.stop style="width: 100%; display: flex; flex-wrap: wrap; gap: 8px;">
- <div v-for="(file, index) in uploadedFiles" :key="index" class="file-status-badge">
- <span class="file-name truncate">已上传: {{ file.name }}</span>
- <span @click.stop="removeSelectedFile(index)" class="remove-btn">×</span>
- </div>
- </div>
- </div>
- </div>
- <!-- =============== 题型配置区域 开始 =============== -->
- <div class="config-section">
- <div class="config-header">
- <h3>题型配置</h3>
- <div class="total-score">试卷总分 {{ calculatedTotalScore }}</div>
- </div>
- <!-- 动态渲染各题型 -->
- <div class="question-types-grid">
- <div class="question-type-card" v-for="(type, index) in questionTypes" :key="index">
- <div class="question-type-header">
- <div class="question-type-title">{{ type.name }}</div>
- <div class="question-type-score">
- 每题 <input type="number" class="score-input" v-model.number="type.scorePerQuestion" min="1" max="100" :disabled="isGenerating"> 分
- </div>
- </div>
- <div class="slider-container">
- <span class="slider-label">数量</span>
- <input type="range" class="question-slider" v-model.number="type.questionCount" min="0" :max="type.max || 50" :disabled="isGenerating">
- <div class="question-count-stepper">
- <span class="question-count">{{ type.questionCount }} 题</span>
- <div class="stepper-buttons">
- <button
- class="stepper-btn stepper-btn-up"
- type="button"
- @click="adjustQuestionCount(type, 1)"
- :disabled="isGenerating || type.questionCount >= 99"
- aria-label="增加题目数量"
- ></button>
- <button
- class="stepper-btn stepper-btn-down"
- type="button"
- @click="adjustQuestionCount(type, -1)"
- :disabled="isGenerating || type.questionCount <= 0"
- aria-label="减少题目数量"
- ></button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="action-buttons">
- <button class="clear-btn" @click="clearSettings" :disabled="isGenerating">
- <el-icon style="font-size: 18px; margin-right: 4px;"><Delete /></el-icon>
- 清空当前配置
- </button>
- <button class="generate-btn" @click="generateExam" :disabled="isGenerating">
- <el-icon v-if="!isGenerating" style="margin-right: 6px;"><MagicStick /></el-icon>
- <el-icon v-else class="is-loading" style="margin-right: 6px;"><Loading /></el-icon>
- {{ isGenerating ? '生成中...' : '开始智能生成试卷' }}
- </button>
- </div>
- </div>
- <!-- =============== 题型配置区域 结束 =============== -->
- </main>
- <!-- =============== 实时预览区域 开始 =============== -->
- <aside class="preview-panel">
- <div class="preview-header">
- <h3>实时预览</h3>
- </div>
- <div class="preview-name-card">
- <div class="preview-name-label">试卷名称</div>
- <div class="preview-title" :style="{ fontStyle: examName ? 'normal' : 'italic' }">{{ examName || '未命名试卷...' }}</div>
- </div>
- <div class="preview-section">
- <div class="preview-section-title">结构大纲</div>
-
- <div class="preview-item" v-for="(type, index) in questionTypes" :key="index">
- <div class="preview-item-top">
- <div class="preview-item-left">
- <div class="preview-dot" :style="{ backgroundColor: ['#2563eb', '#60a5fa', '#93c5fd', '#dbeafe'][index % 4] }"></div>
- <span class="preview-type-name">{{ type.name }}</span>
- </div>
- <span class="preview-type-count">{{ type.questionCount }}题</span>
- </div>
- <div class="preview-item-bottom">
- <span class="preview-type-score">{{ type.questionCount * type.scorePerQuestion }} 分</span>
- </div>
- </div>
- </div>
- <div class="preview-footer">
- <div class="preview-total">
- <span>配置总分</span>
- <span class="preview-total-score" style="color: #000000; font-size: 24px;">{{ totalScore }}</span>
- </div>
- <div class="preview-total" style="margin-top: 20px; font-size: 20px; color: #000000;">
- <span>试卷总分</span>
- <span style="color: var(--primary-color); font-size: 24px;">{{ calculatedTotalScore }}</span>
- </div>
- </div>
- </aside>
- <!-- =============== 实时预览区域 结束 =============== -->
- </div>
- <!-- 考试详情页 -->
- <div v-if="showExamDetail" class="exam-detail-card">
- <!-- 详情页头部 -->
- <div class="detail-header">
- <div class="header-left">
- </div>
- <div class="header-right" style="display: flex; align-items: center; gap: 12px;">
- <button class="return-ai-btn has-before" style="position: static;" @click="backToConfig" :disabled="isGenerating">
- 返回修改
- </button>
- <!-- <button class="save-btn" @click="saveExam" :disabled="isGenerating">
- <img :src="saveIcon" alt="保存试卷" class="save-icon" />
- </button> -->
- <div class="download-dropdown" :class="{ 'disabled': isGenerating, 'show': showDownloadMenu }" @click.stop>
- <button class="download-btn" :disabled="isGenerating" @click="toggleDownloadMenu">
- <img :src="downloadIcon" alt="下载Word" class="download-icon" />
- </button>
- <div class="dropdown-menu">
- <div class="dropdown-item" @click="exportToWordWithAnswers" :disabled="isGenerating">
- <span class="item-text">有答案</span>
- </div>
- <div class="dropdown-item" @click="exportToWordWithoutAnswers" :disabled="isGenerating">
- <span class="item-text">无答案</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 试卷信息 -->
- <div class="exam-info">
- <div>
- <h1 class="exam-title">{{ currentExam.title }}</h1>
- <div class="exam-stats">
- <span class="total-score">总分: {{ currentExam.totalScore }}分</span>
- <span class="question-count">题量: {{ currentExam.totalQuestions }}题</span>
- </div>
- </div>
- <!-- 生成时间 -->
- <div class="generation-time">生成时间: {{ currentTime}}</div>
- </div>
- <!-- 题型列表 -->
- <div class="question-sections">
- <!-- 单选题 -->
- <div class="question-section">
- <div class="section-header" @click="isGenerating ? null : toggleSection('single')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">一</span>
- <span class="section-name">单选题</span>
- <span class="section-score">(每题{{ currentExam.singleChoice.scorePerQuestion }}分, 共{{ currentExam.singleChoice.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.singleChoice.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.single }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.single" class="section-content">
- <div
- v-for="(question, index) in currentExam.singleChoice.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('single', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing[`single_${index}`] }"
- />
- </button>
- </div>
- <div class="options">
- <div
- v-for="option in question.options"
- :key="option.key"
- :class="['option', { selected: question.selectedAnswer === option.key }]"
- :style="{ cursor: 'default' }"
- >
- <div class="radio-wrapper">
- <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === option.key }">
- <div v-if="question.selectedAnswer === option.key" class="radio-dot"></div>
- </div>
- </div>
- <span class="option-key">{{ option.key }}.</span>
- <div class="option-content">
- <span class="option-text">{{ option.text }}</span>
- <!-- 编辑按钮已禁用 -->
- <!-- <button
- class="edit-option-btn"
- @click.stop="openEditModal('single', index, option.key)"
- :disabled="isGenerating"
- title="编辑选项"
- >
- <img src="@/assets/Chat/13.png" alt="编辑" class="edit-icon" />
- </button> -->
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 判断题 -->
- <div class="question-section">
- <div class="section-header" @click="isGenerating ? null : toggleSection('judge')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">二</span>
- <span class="section-name">判断题</span>
- <span class="section-score">(每题{{ currentExam.judge.scorePerQuestion }}分, 共{{ currentExam.judge.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.judge.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.judge }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.judge" class="section-content">
- <div
- v-for="(question, index) in currentExam.judge.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('judge', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing[`judge_${index}`] }"
- />
- </button>
- </div>
- <div class="options">
- <div
- class="option judge-option"
- :class="{ selected: question.selectedAnswer === '正确' }"
- :style="{ cursor: 'default' }"
- >
- <div class="radio-wrapper">
- <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === '正确' }">
- <div v-if="question.selectedAnswer === '正确'" class="radio-dot"></div>
- </div>
- </div>
- <span class="option-text">正确</span>
- </div>
- <div
- class="option judge-option"
- :class="{ selected: question.selectedAnswer === '错误' }"
- :style="{ cursor: 'default' }"
- >
- <div class="radio-wrapper">
- <div class="radio-circle" :class="{ 'selected': question.selectedAnswer === '错误' }">
- <div v-if="question.selectedAnswer === '错误'" class="radio-dot"></div>
- </div>
- </div>
- <span class="option-text">错误</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 多选题 -->
- <div class="question-section">
- <div class="section-header" @click="isGenerating ? null : toggleSection('multiple')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">三</span>
- <span class="section-name">多选题</span>
- <span class="section-score">(每题{{ currentExam.multiple.scorePerQuestion }}分, 共{{ currentExam.multiple.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.multiple.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.multiple }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.multiple" class="section-content">
- <div
- v-for="(question, index) in currentExam.multiple.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('multiple', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing[`multiple_${index}`] }"
- />
- </button>
- </div>
- <div class="options">
- <div
- v-for="option in question.options"
- :key="option.key"
- :class="['option', { selected: question.selectedAnswers.includes(option.key) }]"
- :style="{ cursor: 'default' }"
- >
- <div class="radio-wrapper">
- <div class="radio-circle" :class="{ 'selected': question.selectedAnswers.includes(option.key) }">
- <div v-if="question.selectedAnswers.includes(option.key)" class="radio-dot"></div>
- </div>
- </div>
- <span class="option-key">{{ option.key }}.</span>
- <div class="option-content">
- <span class="option-text">{{ option.text }}</span>
- <!-- 编辑按钮已禁用 -->
- <!-- <button
- class="edit-option-btn"
- @click.stop="openEditModal('multiple', index, option.key)"
- :disabled="isGenerating"
- title="编辑选项"
- >
- <img src="@/assets/Chat/13.png" alt="编辑" class="edit-icon" />
- </button> -->
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 简答题 -->
- <div class="question-section">
- <div class="section-header" @click="isGenerating ? null : toggleSection('short')" :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer' }">
- <div class="section-title">
- <span class="section-number">四</span>
- <span class="section-name">简答题</span>
- <span class="section-score">(每题{{ currentExam.short.scorePerQuestion }}分, 共{{ currentExam.short.totalScore }}分)</span>
- </div>
- <div class="section-controls">
- <span class="question-count-text">{{ currentExam.short.count }}题</span>
- <img
- :src="expandIcon"
- alt="收起/展开"
- class="toggle-icon"
- :class="{ 'expanded': !expandedSections.short }"
- />
- </div>
- </div>
-
- <div v-if="expandedSections.short" class="section-content">
- <div
- v-for="(question, index) in currentExam.short.questions"
- :key="index"
- class="question-item"
- >
- <div class="question-header">
- <span class="question-number">{{ index + 1 }}.</span>
- <span class="question-text">{{ question.text }}</span>
- <button class="refresh-btn" @click="refreshQuestion('short', index)" :disabled="isGenerating">
- <img
- :src="collapseIcon"
- alt="刷新"
- class="refresh-icon"
- :class="{ 'rotating': isRefreshing[`short_${index}`] }"
- />
- </button>
- </div>
- <div class="answer-box">
- <div class="answer-outline">
- <div class="outline-item">
- <span class="outline-text">{{ question.outline.keyFactors }}</span>
- <!-- 编辑按钮已禁用 -->
- <!-- <button
- class="edit-option-btn"
- @click="openEditModal('short', index, 'keyFactors')"
- :disabled="isGenerating"
- title="编辑答题要点"
- >
- <img src="@/assets/Chat/13.png" alt="编辑" class="edit-icon" />
- </button> -->
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 删除确认弹窗 -->
- <DeleteConfirmModal
- :visible="showDeleteModal"
- @close="showDeleteModal = false"
- @confirm="handleDeleteConfirm"
- @cancel="handleDeleteCancel"
- />
-
- <!-- 编辑选项弹窗已禁用 -->
- <!-- <div v-if="showEditModal" class="modal-overlay" @click="closeEditModal">
- <div class="edit-modal" @click.stop>
- <div class="modal-header">
- <h3>编辑{{ editModalData.type === 'short' ? '答题要点' : '选项' }}</h3>
- <button class="close-btn" @click="closeEditModal">×</button>
- </div>
- <div class="modal-body">
- <div v-if="editModalData.type === 'short'" class="edit-section">
- <label>答题要点:</label>
- <textarea
- v-model="editModalData.keyFactors"
- class="edit-textarea"
- placeholder="请输入答题要点..."
- ></textarea>
- </div>
- <div v-else class="edit-section">
- <label>选项文本:</label>
- <input
- v-model="editModalData.optionText"
- class="edit-input"
- placeholder="请输入选项内容..."
- maxlength="20"
- @input="validateOptionText"
- />
- <div class="text-validation" v-if="editModalData.optionText.length > 0">
- <span class="char-count" :class="{ 'warning': editModalData.optionText.length >= 18 }">
- {{ editModalData.optionText.length }}/20
- </span>
- </div>
- </div>
- </div>
- <div class="modal-footer">
- <button class="btn btn-cancel" @click="closeEditModal">取消</button>
- <button class="btn btn-confirm" @click="saveEdit">保存</button>
- </div>
- </div>
- </div> -->
-
- <!-- 隐藏的文件输入框 -->
- <input
- ref="fileInput"
- type="file"
- accept=".ppt,.pptx"
- multiple
- style="display: none"
- @change="handleFileSelect"
- />
-
- <!-- 删除确认弹窗 -->
- <DeleteConfirmModal
- :visible="showDeleteModal"
- title="删除历史记录"
- :message="deleteConfirmMessage"
- @confirm="confirmDeleteHistory"
- @cancel="cancelDeleteHistory"
- @close="cancelDeleteHistory"
- />
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted, onUnmounted, reactive, watch, defineProps, defineEmits } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import Sidebar from "@/components/Sidebar.vue";
- import DeleteConfirmModal from "@/components/DeleteConfirmModal.vue";
- import { UploadFilled, ArrowRight, Delete, MagicStick, Loading } from '@element-plus/icons-vue';
- const props = defineProps({
- hideSidebar: {
- type: Boolean,
- default: false
- }
- });
- const emit = defineEmits(['return-to-ai']);
- const route = useRoute();
- const router = useRouter();
- const handleReturnToAI = () => {
- emit('return-to-ai');
- };
- import { apis } from '@/request/apis.js'
- import { ElMessage } from 'element-plus'
- // ===== 已删除:getUserId - 不再需要,改用token =====
- // import { getUserId } from '@/utils/userManager.js'
- // 导入图片资源
- import bridgeIcon from '@/assets/Exam/4.png'
- import tunnelIcon from '@/assets/Exam/18.png'
- import equipmentIcon from '@/assets/Exam/5.png'
- import gasStationIcon from '@/assets/Exam/6.png'
- import highwayIcon from '@/assets/Exam/7.png'
- import comprehensiveIcon from '@/assets/Exam/8.png'
- import aiIcon from '@/assets/Exam/9.png'
- import pptIcon from '@/assets/Exam/10.png'
- import previewIcon from '@/assets/Exam/14.png'
- import clearIcon from '@/assets/Exam/11.png'
- import generateIcon from '@/assets/Exam/12.png'
- import saveIcon from '@/assets/Exam/15.png'
- import downloadIcon from '@/assets/Exam/13.png'
- import expandIcon from '@/assets/Exam/17.png'
- import collapseIcon from '@/assets/Exam/16.png'
- import attachmentIcon from '@/assets/Chat/9.png'
- // 响应式数据
- const selectedFunction = ref("ai");
- const selectedProjectType = ref("bridge");
- const examName = ref("桥梁工程施工技术考核");
- const totalScore = ref(100);
- const showExamDetail = ref(false);
- const currentTime = ref("");
- const isGenerating = ref(false); // 控制生成状态
- const isRefreshing = ref({}); // 控制单题刷新状态
- const showDownloadMenu = ref(false); // 控制下载菜单显示状态
- const isLoadingHistory = ref(false); // 是否正在加载历史记录
- const isLoadingHistoryItem = ref(false); // 是否正在加载历史记录详情
- const showDeleteModal = ref(false); // 控制删除确认弹窗显示
- const deleteTargetItem = ref(null); // 要删除的目标项
- // 编辑功能已禁用
- /*
- const showEditModal = ref(false); // 控制编辑选项弹窗显示
- const editModalData = ref({
- type: '', // 'single', 'multiple', 'short'
- questionIndex: -1,
- optionKey: '',
- optionText: '',
- keyFactors: '',
- correctAnswer: '',
- correctAnswers: []
- }); // 编辑模态框数据
- */
- // PPT文件上传相关
- const fileInput = ref(null);
- const uploadedFiles = ref([]);
- const isUploadingFile = ref(false);
- const fileContent = ref(''); // 存储文件内容
- const pptContentDescription = ref(''); // 存储用户输入的PPT内容描述
- const questionBasis = computed({
- get: () => pptContentDescription.value,
- set: (value) => {
- pptContentDescription.value = value;
- }
- });
- // 文件处理配置
- const fileConfig = reactive({
- maxSize: 20 * 1024 * 1024, // 20MB
- allowedTypes: ['.ppt', '.pptx'] // 只允许PPT文件
- });
- // 移除原有的Toast相关变量,使用ElMessage替代
- // const showToast = ref(false); // 控制轻提示显示
- // const toastMessage = ref(""); // 轻提示消息
- // const toastType = ref("success"); // 轻提示类型
- // 展开/收起状态
- const expandedSections = ref({
- single: true,
- judge: true,
- multiple: true,
- short: true
- });
- // 当前试卷数据
- const currentExam = ref(null);
- // 历史记录数据
- const historyData = ref([])
- const historyTotal = ref(0) // 历史记录总数
- // 获取历史记录列表
- const getHistoryRecordList = async () => {
- try {
- console.log('📋 开始获取考试工坊历史记录列表...')
- isLoadingHistory.value = true
- const startTime = performance.now()
-
- const response = await apis.getHistoryRecord({
- ai_conversation_id: 0,
- business_type: 3
- })
-
- const endTime = performance.now()
- console.log(`📋 考试工坊历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
- console.log('📋 考试工坊历史记录列表响应:', response)
-
- if (response.statusCode === 200) {
- historyTotal.value = response.total || 0
- historyData.value = response.data.map(conversation => ({
- id: conversation.id,
- title: generateConversationTitle(conversation.exam_name),
- time: formatTime(conversation.updated_at),
- businessType: conversation.business_type,
- isActive: false,
- rawData: conversation
- }))
- console.log(`✅ 考试工坊历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
- } else {
- console.error('❌ 获取考试工坊历史记录列表失败:', response.statusCode)
- }
- } finally {
- isLoadingHistory.value = false
- }
- }
- // 生成对话标题(从内容中提取)
- const generateConversationTitle = (content) => {
- if (!content) return '未知对话'
-
- // 清理HTML标签
- const cleanContent = content.replace(/<[^>]*>/g, '')
-
- // 提取第一句话作为标题
- const firstSentence = cleanContent.split(/[。!?\n]/)[0]
-
- // 限制标题长度
- if (firstSentence.length > 30) {
- return firstSentence.substring(0, 30) + '...'
- }
-
- return firstSentence || '新对话'
- }
- // 格式化时间
- const formatTime = (timestamp) => {
- if (!timestamp) return '未知时间'
-
- // 处理时间戳
- let date
- if (typeof timestamp === 'string') {
- // 如果是ISO字符串格式,直接创建Date对象
- date = new Date(timestamp)
- } else {
- // 如果是数字时间戳
- let timestampMs = timestamp
- if (timestamp.toString().length === 10) {
- timestampMs = timestamp * 1000
- } else if (timestamp.toString().length === 11) {
- timestampMs = timestamp * 1000
- } else if (timestamp.toString().length === 13) {
- // 13位时间戳,直接使用
- } else {
- timestampMs = timestamp * 1000
- }
- date = new Date(timestampMs)
- }
-
- const now = new Date()
-
- // 获取今天的开始时间(0点0分0秒)
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
- // 获取昨天的开始时间
- const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000)
-
- // 今天的对话(日期相同)
- if (date >= todayStart) {
- return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
-
- // 昨天的对话(日期是昨天)
- if (date >= yesterdayStart && date < todayStart) {
- return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
- }
-
- // 更早的对话:显示为 "8月30日 15:30" 格式
- const month = date.getMonth() + 1 // getMonth() 返回 0-11
- const day = date.getDate()
- const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
-
- return `${month}月${day}日 ${time}`
- }
- // 工程类型配置
- const projectTypes = {
- bridge: { name: "桥梁", icon: bridgeIcon },
- tunnel: { name: "隧道", icon: tunnelIcon },
- equipment: { name: "特种设备", icon: equipmentIcon },
- "gas-station": { name: "加油站", icon: gasStationIcon },
- highway: { name: "高速运营公路", icon: highwayIcon },
- comprehensive: { name: "综合", icon: comprehensiveIcon },
- };
- // 题型配置
- const questionTypes = ref([
- { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
- { name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
- { name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
- { name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
- ]);
- // 保存初始配置
- let initialConfig = null;
- const ai_conversation_id = ref(0)
- // 删除确认消息
- const deleteConfirmMessage = computed(() => {
- const title = deleteTargetItem.value?.item?.title || ''
- return `确定要删除历史记录"${title}"吗?删除后将无法恢复。`
- })
- // 计算总分(所有题目分数的总和)
- const calculatedTotalScore = computed(() => {
- return questionTypes.value.reduce((total, type) => {
- return total + (type.scorePerQuestion * type.questionCount);
- }, 0);
- })
- // 删除历史记录
- const deleteHistoryItem = (historyItem, index) => {
- console.log('准备删除考试工坊历史记录:', historyItem)
-
- // 设置要删除的项目并显示确认弹窗
- deleteTargetItem.value = { item: historyItem, index: index }
- showDeleteModal.value = true
- }
- // 确认删除历史记录
- const confirmDeleteHistory = async () => {
- if (!deleteTargetItem.value) return
-
- const { item: historyItem, index } = deleteTargetItem.value
-
- try {
- // 调用删除接口
- const response = await apis.deleteHistoryRecord({
- ai_conversation_id: historyItem.id
- })
-
- if (response.statusCode === 200) {
- // 删除成功,从列表中移除
- removeExamWorkshopHistory(historyItem.id)
-
- // 如果删除的是当前激活的历史记录,需要清空界面并调用新建任务
- if (historyItem.isActive) {
- await createNewChat()
- }
-
- console.log('考试工坊历史记录删除成功')
- ElMessage.success('删除成功')
- } else {
- console.error('删除考试工坊历史记录失败:', response.msg)
- ElMessage.error(response.msg || '删除失败')
- }
- } catch (error) {
- console.error('删除考试工坊历史记录失败:', error)
- ElMessage.error('删除失败,请稍后重试')
- } finally {
- // 关闭弹窗并清除目标项
- showDeleteModal.value = false
- deleteTargetItem.value = null
- }
- }
- // 取消删除
- const cancelDeleteHistory = () => {
- showDeleteModal.value = false
- deleteTargetItem.value = null
- }
- // 方法
- const createNewChat = async () => {
- if (isGenerating.value) return;
-
- console.log("创建新考试工坊任务");
-
- // 重置所有状态
- ai_conversation_id.value = 0
- showExamDetail.value = false
-
- // 重置配置到初始状态
- selectedFunction.value = "ai";
- selectedProjectType.value = "bridge";
- examName.value = "桥梁工程施工技术考核";
- totalScore.value = 100;
- currentTime.value = "";
-
- // 恢复题型配置到初始状态
- if (initialConfig) {
- questionTypes.value = initialConfig.questionTypes;
- totalScore.value = initialConfig.totalScore;
- selectedProjectType.value = initialConfig.selectedProjectType;
- examName.value = initialConfig.examName;
- } else {
- // 如果没有初始配置,使用默认配置
- questionTypes.value = [
- { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
- { name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
- { name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
- { name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
- ];
- }
-
- // 重置当前试卷数据到初始状态
- currentExam.value = null;
-
- // 重置展开/收起状态
- expandedSections.value = {
- single: true,
- judge: true,
- multiple: true,
- short: true
- };
-
- // 重置生成状态
- isGenerating.value = false;
- isRefreshing.value = {};
-
- // 清理文件
- uploadedFiles.value = [];
- pptContentDescription.value = '';
-
- // 清除所有历史记录的选中状态
- historyData.value.forEach((item) => {
- item.isActive = false
- })
-
- // 刷新历史记录列表
- await getHistoryRecordList()
- };
- const handleHistoryItem = async (historyItem) => {
- if (isGenerating.value || isLoadingHistoryItem.value) return;
-
- console.log("点击考试工坊历史记录:", historyItem);
- ai_conversation_id.value = historyItem.id;
- isLoadingHistoryItem.value = true;
-
- try {
- historyData.value.forEach((item) => {
- item.isActive = item.id === historyItem.id;
- });
-
- const response = await apis.getHistoryRecord({
- ai_conversation_id: historyItem.id,
- business_type: 3
- });
- console.log(response.data)
- if (response.statusCode === 200 && response.data && response.data.length > 0) {
- const latestRecord = response.data[response.data.length - 1];
- console.log('获取到的试卷数据:', latestRecord);
- console.log('试卷数据结构:', JSON.stringify(latestRecord, null, 2));
- currentTime.value = formatTime(latestRecord.created_at)
- if (latestRecord && latestRecord.content) {
- try {
- const examData = extractExamDataFromContent(latestRecord.content);
- restoreExamFromHistory(examData);
- showExamDetail.value = true;
- } catch (error) {
- console.error('解析试卷数据失败:', error);
- showExamDetail.value = true;
- currentTime.value = historyItem.time;
- }
- } else {
- showExamDetail.value = true;
- currentTime.value = historyItem.time;
- }
- } else {
- console.error('获取历史记录详情失败:', response);
- showExamDetail.value = true;
- currentTime.value = historyItem.time;
- }
- } catch (error) {
- console.error('获取历史记录详情失败:', error);
- showExamDetail.value = true;
- currentTime.value = historyItem.time;
- } finally {
- isLoadingHistoryItem.value = false;
- }
- };
- const selectFunction = (functionType) => {
- selectedFunction.value = functionType;
- console.log("选择功能:", functionType);
- };
- const selectProjectType = (typeKey) => {
- selectedProjectType.value = typeKey;
- console.log("选择工程类型:", projectTypes[typeKey].name);
-
- // 自动更新试卷名称
- const projectTypeName = projectTypes[typeKey].name;
- examName.value = `${projectTypeName}工程施工技术考核`;
-
- // 同时更新当前试卷的标题
- if (currentExam.value) {
- currentExam.value.title = examName.value;
- }
- };
- const clearSettings = () => {
- // 根据当前选择的工程类型设置试卷名称
- const projectTypeName = projectTypes[selectedProjectType.value].name;
- examName.value = `${projectTypeName}工程施工技术考核`;
- totalScore.value = 100; // 清空时配置总分恢复默认值 100
- // 保留原数组引用,更新每个对象的属性,避免破坏 Vue 3 响应式绑定
- questionTypes.value.forEach(type => {
- type.scorePerQuestion = 0;
- type.questionCount = 0;
- });
- console.log("清除设置");
- };
- const validateExamName = () => {
- if (examName.value.length > 32) {
- examName.value = examName.value.slice(0, 20);
- }
- };
- // 验证总分
- const validateTotalScore = () => {
- if (totalScore.value > 1000) {
- totalScore.value = 1000;
- ElMessage.warning("试卷总分不能超过1000分");
- }
- if (totalScore.value < 1) {
- totalScore.value = 1;
- }
- };
- // 验证每题分数
- const validateScorePerQuestion = (type) => {
- if (type.scorePerQuestion > 99) {
- type.scorePerQuestion = 99;
- ElMessage.warning(`${type.name}每题分数不能超过99分`);
- }
- if (type.scorePerQuestion < 1) {
- type.scorePerQuestion = 1;
- }
- };
- // 验证题目数量
- const validateQuestionCount = (type) => {
- if (type.questionCount > 99) {
- type.questionCount = 99;
- ElMessage.warning(`${type.name}题目数量不能超过99题`);
- }
- if (type.questionCount < 0) {
- type.questionCount = 0;
- }
- };
- const adjustQuestionCount = (type, delta) => {
- type.questionCount = Number(type.questionCount || 0) + delta;
- validateQuestionCount(type);
- };
- const generateExam = async () => {
- if (!examName.value.trim()) {
- ElMessage.warning("请输入试卷名称");
- return;
- }
- if (examName.value.trim().length === 0) {
- ElMessage.warning("试卷名称不能为空");
- return;
- }
- // 检查总分是否超过限制
- if (totalScore.value > 1000) {
- ElMessage.warning("试卷总分不能超过1000分");
- return;
- }
- // 检查每题分数和题目数量是否超过限制
- for (const type of questionTypes.value) {
- if (type.scorePerQuestion > 99) {
- ElMessage.warning(`${type.name}每题分数不能超过99分`);
- return;
- }
- if (type.questionCount > 99) {
- ElMessage.warning(`${type.name}题目数量不能超过99题`);
- return;
- }
- }
- // 检查总分是否合理
- const calculatedScore = questionTypes.value.reduce((total, type) => {
- return total + (type.scorePerQuestion * type.questionCount);
- }, 0);
-
- if (calculatedScore !== totalScore.value) {
- ElMessage.warning(`总分不匹配!当前配置总分为${calculatedScore}分,请检查配置`);
- return;
- }
- console.log("生成试卷:", {
- function: selectedFunction.value,
- projectType: projectTypes[selectedProjectType.value].name,
- examName: examName.value,
- totalScore: totalScore.value,
- questionTypes: questionTypes.value,
- });
- // 设置当前时间
- const now = new Date();
- 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')}`;
-
- if (selectedFunction.value === 'ai') {
- // AI智能生成试卷
- await generateAIExam();
- } else {
- // PPT生成方式
- if (uploadedFiles.value.length === 0) {
- ElMessage.warning("请先上传PPT文件");
- return;
- }
- await generatePPTExam();
- }
- };
- // PPT生成试卷
- const generatePPTExam = async () => {
- try {
- isGenerating.value = true;
-
- // 构建PPT生成提示词(服务端构建)
- const prompt = await fetchExamPrompt('ppt');
-
- console.log('发送给AI的PPT生成提示词:', prompt);
-
- // 调用AI接口
- const response = await apis.sendDeepseekMessage({
- // ===== 已删除:user_id - 后端从token解析 =====
- business_type: 3,
- message: prompt,
- exam_name: examName.value,
- ai_conversation_id: ai_conversation_id.value
- });
-
- if (response.statusCode === 200) {
- const aiReply = response.data.reply;
- const aiConversationId = response.data.ai_conversation_id; // 保存对话ID
- console.log('AI生成的PPT试卷:', aiReply);
- console.log('AI对话ID:', aiConversationId);
-
- // 保存对话ID
- ai_conversation_id.value = aiConversationId;
-
- // 解析AI回复并生成试卷
- const generatedExam = parseAIExamResponse(aiReply);
-
- // 更新当前试卷数据
- updateCurrentExam(generatedExam);
-
- // 显示详情页
- showExamDetail.value = true;
- ElMessage.success("PPT试卷生成完成!");
-
- await getHistoryRecordList();
-
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
- } else {
- selectLatestHistoryRecord();
- }
- } else {
- throw new Error('AI接口调用失败');
- }
-
- } catch (error) {
- console.error('PPT生成试卷失败:', error);
- ElMessage.error('PPT生成试卷失败,请稍后重试或检查网络连接');
-
- // 失败时显示默认试卷
- showExamDetail.value = true;
- } finally {
- // 重置发送状态
- isGenerating.value = false;
- }
- };
- // AI生成试卷
- const generateAIExam = async () => {
- try {
- isGenerating.value = true;
-
- // 构建AI提示词(服务端构建)
- const prompt = await fetchExamPrompt('ai');
-
- console.log('发送给AI的提示词:', prompt);
-
- // 调用AI接口
- const response = await apis.sendDeepseekMessage({
- // ===== 已删除:user_id - 后端从token解析 =====
- business_type: 3,
- message: prompt,
- exam_name: examName.value,
- ai_conversation_id: ai_conversation_id.value
- });
-
- if (response.statusCode === 200) {
- const aiReply = response.data.reply;
- const aiConversationId = response.data.ai_conversation_id; // 保存对话ID
- console.log('AI生成的试卷:', aiReply);
- console.log('AI对话ID:', aiConversationId);
-
- // 保存对话ID
- ai_conversation_id.value = aiConversationId;
-
- // 解析AI回复并生成试卷
- const generatedExam = parseAIExamResponse(aiReply);
-
- // 更新当前试卷数据
- updateCurrentExam(generatedExam);
-
- // 显示详情页
- showExamDetail.value = true;
- ElMessage.success("AI试卷生成完成!");
-
- await getHistoryRecordList();
-
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
- } else {
- selectLatestHistoryRecord();
- }
- } else {
- throw new Error('AI接口调用失败');
- }
-
- } catch (error) {
- console.error('AI生成试卷失败:', error);
- ElMessage.error('AI生成试卷失败,请稍后重试或检查网络连接');
-
- // 失败时显示默认试卷
- showExamDetail.value = true;
- } finally {
- // 重置发送状态
- isGenerating.value = false;
- }
- };
- // 从服务端构建提示词
- const fetchExamPrompt = async (mode = 'ai') => {
- const normalizedQuestionTypes = questionTypes.value.map(type => ({
- name: type.name,
- romanNumeral: type.romanNumeral,
- questionCount: Number(type.questionCount) || 0,
- scorePerQuestion: Number(type.scorePerQuestion) || 0,
- }));
- const pptContents = uploadedFiles.value.map(file => file.content).join('\n\n');
- const finalContentBasis = pptContents || questionBasis.value || '';
- const payload = {
- mode,
- client: 'pc',
- projectType: projectTypes[selectedProjectType.value]?.name || '',
- examTitle: examName.value,
- totalScore: totalScore.value,
- questionTypes: normalizedQuestionTypes,
- pptContent: finalContentBasis
- };
- try {
- const response = await apis.buildExamPrompt(payload);
- if (!response?.data?.prompt) {
- throw new Error(response?.msg || '提示词构建失败');
- }
- return response.data.prompt;
- } catch (error) {
- console.error('获取提示词失败:', error);
- throw error;
- }
- };
- // 解析AI回复
- const extractExamDataFromContent = (content) => {
- if (!content || typeof content !== 'string') {
- throw new Error('试卷内容为空');
- }
- const jsonMatch = content.match(/\{[\s\S]*\}/);
- if (!jsonMatch) {
- throw new Error('未找到有效的JSON数据');
- }
- return JSON.parse(jsonMatch[0]);
- };
- const parseAIExamResponse = (aiReply) => {
- try {
- const examData = extractExamDataFromContent(aiReply);
- const normalizedExam = normalizeGeneratedExam(examData);
- // 确保所有题目都有正确的初始值
- ensureQuestionInitialValues(normalizedExam);
- return normalizedExam;
- } catch (error) {
- console.error('解析AI回复失败:', error);
- // 返回默认试卷结构
- return generateDefaultExam();
- }
- };
- const getQuestionTypeConfig = (name, fallbackScore = 0) => {
- const config = questionTypes.value.find(type => type.name === name);
- return {
- scorePerQuestion: Number(config?.scorePerQuestion) || fallbackScore,
- questionCount: Number(config?.questionCount) || 0,
- };
- };
- const normalizeOptions = (options = []) => {
- if (!Array.isArray(options)) {
- return [];
- }
- return options.map((option, index) => {
- if (typeof option === 'string') {
- return {
- key: String.fromCharCode(65 + index),
- text: option,
- };
- }
- return {
- key: option?.key || String.fromCharCode(65 + index),
- text: option?.text || option?.content || option?.label || "",
- };
- });
- };
- const normalizeQuestions = (questions = [], sectionKey) => {
- if (!Array.isArray(questions)) {
- return [];
- }
- return questions.map((question = {}) => {
- if (sectionKey === 'singleChoice') {
- return {
- text: question.text || question.question_text || "",
- options: normalizeOptions(question.options),
- selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
- analysis: question.analysis || question.explanation || "",
- };
- }
- if (sectionKey === 'judge') {
- return {
- text: question.text || question.question_text || "",
- selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
- analysis: question.analysis || question.explanation || "",
- };
- }
- if (sectionKey === 'multiple') {
- const selectedAnswers = question.selectedAnswers || question.correct_answers || question.answers || [];
- return {
- text: question.text || question.question_text || "",
- options: normalizeOptions(question.options),
- selectedAnswers: Array.isArray(selectedAnswers) ? selectedAnswers : [selectedAnswers].filter(Boolean),
- analysis: question.analysis || question.explanation || "",
- };
- }
- return {
- text: question.text || question.question_text || "",
- outline: question.outline || question.answer_outline || { keyFactors: question.answer || "答题要点、关键因素、示例答案" },
- analysis: question.analysis || question.explanation || "",
- };
- });
- };
- const normalizeSection = (rawSection, sectionKey, fallbackName, fallbackScore = 0) => {
- const section = rawSection || {};
- const config = getQuestionTypeConfig(fallbackName, fallbackScore);
- const sourceQuestions = Array.isArray(section)
- ? section
- : (section.questions || section.items || section.question_list || []);
- const normalizedQuestions = normalizeQuestions(sourceQuestions, sectionKey);
- const count = Number(section.count ?? section.question_count ?? normalizedQuestions.length ?? config.questionCount) || 0;
- const scorePerQuestion = Number(section.scorePerQuestion ?? section.score_per_question ?? config.scorePerQuestion) || 0;
- const totalScore = Number(section.totalScore ?? section.total_score ?? (scorePerQuestion * count)) || 0;
- return {
- scorePerQuestion,
- totalScore,
- count,
- questions: normalizedQuestions,
- };
- };
- const normalizeGeneratedExam = (examData = {}) => {
- const singleSource = examData.singleChoice || examData.questions?.single_choice || examData.single_choice;
- const judgeSource = examData.judge || examData.questions?.judge;
- const multipleSource = examData.multiple || examData.questions?.multiple;
- const shortSource = examData.short || examData.questions?.short;
- const normalizedExam = {
- title: examData.title || examData.exam_name || examName.value,
- totalScore: Number(examData.totalScore ?? examData.total_score ?? totalScore.value) || 0,
- totalQuestions: Number(examData.totalQuestions ?? examData.total_questions) || 0,
- singleChoice: normalizeSection(singleSource, 'singleChoice', '单选题', 2),
- judge: normalizeSection(judgeSource, 'judge', '判断题', 2),
- multiple: normalizeSection(multipleSource, 'multiple', '多选题', 3),
- short: normalizeSection(shortSource, 'short', '简答题', 10),
- };
- if (!normalizedExam.totalQuestions) {
- normalizedExam.totalQuestions =
- normalizedExam.singleChoice.count +
- normalizedExam.judge.count +
- normalizedExam.multiple.count +
- normalizedExam.short.count;
- }
- return normalizedExam;
- };
- // 确保题目初始值正确
- const ensureQuestionInitialValues = (examData) => {
- // 单选题
- if (examData.singleChoice && examData.singleChoice.questions) {
- examData.singleChoice.questions.forEach(question => {
- if (!question.selectedAnswer) {
- question.selectedAnswer = "";
- }
- if (!question.options || question.options.length === 0) {
- question.options = [
- { key: "A", text: "选项A" },
- { key: "B", text: "选项B" },
- { key: "C", text: "选项C" },
- { key: "D", text: "选项D" }
- ];
- }
- });
- }
-
- // 判断题
- if (examData.judge && examData.judge.questions) {
- examData.judge.questions.forEach(question => {
- if (!question.selectedAnswer) {
- question.selectedAnswer = "";
- }
- });
- }
-
- // 多选题
- if (examData.multiple && examData.multiple.questions) {
- examData.multiple.questions.forEach(question => {
- if (!question.selectedAnswers) {
- question.selectedAnswers = [];
- }
- if (!question.options || question.options.length === 0) {
- question.options = [
- { key: "A", text: "选项A" },
- { key: "B", text: "选项B" },
- { key: "C", text: "选项C" },
- { key: "D", text: "选项D" }
- ];
- }
- });
- }
-
- // 简答题
- if (examData.short && examData.short.questions) {
- examData.short.questions.forEach(question => {
- if (!question.outline) {
- question.outline = { keyFactors: "答题要点、关键因素、示例答案" };
- }
- });
- }
-
- // 不再自动设置默认答案,让AI自己生成正确答案
- // setDefaultAnswers(examData);
- };
- // 设置默认答案
- const setDefaultAnswers = (examData) => {
- // 单选题设置默认答案
- if (examData.singleChoice && examData.singleChoice.questions) {
- examData.singleChoice.questions.forEach((question, index) => {
- // 根据题目内容智能设置答案
- if (question.text.includes('桩身倾斜') || question.text.includes('桩位纠偏')) {
- question.selectedAnswer = "B";
- } else if (question.text.includes('深水河流') || question.text.includes('悬臂浇筑')) {
- question.selectedAnswer = "B";
- } else if (question.text.includes('预应力张拉') || question.text.includes('对称张拉')) {
- question.selectedAnswer = "C";
- } else if (question.text.includes('混凝土裂缝') || question.text.includes('伸缩缝')) {
- question.selectedAnswer = "C";
- } else if (question.text.includes('沉井下沉') || question.text.includes('土体破坏')) {
- question.selectedAnswer = "C";
- } else {
- // 随机设置一个答案
- const answers = ["A", "B", "C", "D"];
- question.selectedAnswer = answers[index % answers.length];
- }
- });
- }
-
- // 判断题设置默认答案
- if (examData.judge && examData.judge.questions) {
- examData.judge.questions.forEach((question, index) => {
- // 根据题目内容智能设置答案
- if (question.text.includes('水灰比无关')) {
- question.selectedAnswer = "错误";
- } else if (question.text.includes('张拉顺序') || question.text.includes('预应力损失')) {
- question.selectedAnswer = "正确";
- } else if (question.text.includes('养护时间越长越好')) {
- question.selectedAnswer = "错误";
- } else if (question.text.includes('下沉速度应控制')) {
- question.selectedAnswer = "正确";
- } else if (question.text.includes('张拉应力值可以超过设计值')) {
- question.selectedAnswer = "错误";
- } else {
- // 随机设置一个答案
- question.selectedAnswer = index % 2 === 0 ? "正确" : "错误";
- }
- });
- }
-
- // 多选题设置默认答案
- if (examData.multiple && examData.multiple.questions) {
- examData.multiple.questions.forEach((question, index) => {
- if (!question.selectedAnswers) {
- question.selectedAnswers = [];
- }
- // 根据题目内容智能设置答案
- if (question.text.includes('桥梁基础施工')) {
- question.selectedAnswers = ["A", "C", "D"];
- } else if (question.text.includes('预应力技术')) {
- question.selectedAnswers = ["A", "B"];
- } else if (question.text.includes('质量控制')) {
- question.selectedAnswers = ["A", "B", "C"];
- } else if (question.text.includes('安全措施')) {
- question.selectedAnswers = ["A", "B", "C", "D"];
- } else {
- // 随机设置2-3个答案
- const answers = ["A", "B", "C", "D"];
- const count = Math.floor(Math.random() * 2) + 2; // 2-3个答案
- question.selectedAnswers = answers.slice(0, count);
- }
- });
- }
- };
- // 生成默认试卷(当AI解析失败时)
- const generateDefaultExam = () => {
- return {
- title: examName.value,
- totalScore: totalScore.value,
- totalQuestions: questionTypes.value.reduce((sum, type) => sum + type.questionCount, 0),
- singleChoice: {
- scorePerQuestion: questionTypes.value.find(t => t.name === '单选题')?.scorePerQuestion || 2,
- totalScore: (questionTypes.value.find(t => t.name === '单选题')?.scorePerQuestion || 2) * (questionTypes.value.find(t => t.name === '单选题')?.questionCount || 15),
- count: questionTypes.value.find(t => t.name === '单选题')?.questionCount || 15,
- questions: generateDefaultQuestions('single', questionTypes.value.find(t => t.name === '单选题')?.questionCount || 15)
- },
- judge: {
- scorePerQuestion: questionTypes.value.find(t => t.name === '判断题')?.scorePerQuestion || 2,
- totalScore: (questionTypes.value.find(t => t.name === '判断题')?.scorePerQuestion || 2) * (questionTypes.value.find(t => t.name === '判断题')?.questionCount || 10),
- count: questionTypes.value.find(t => t.name === '判断题')?.questionCount || 10,
- questions: generateDefaultQuestions('judge', questionTypes.value.find(t => t.name === '判断题')?.questionCount || 10)
- },
- multiple: {
- scorePerQuestion: questionTypes.value.find(t => t.name === '多选题')?.scorePerQuestion || 3,
- totalScore: (questionTypes.value.find(t => t.name === '多选题')?.scorePerQuestion || 3) * (questionTypes.value.find(t => t.name === '多选题')?.questionCount || 10),
- count: questionTypes.value.find(t => t.name === '多选题')?.questionCount || 10,
- questions: generateDefaultQuestions('multiple', questionTypes.value.find(t => t.name === '多选题')?.questionCount || 10)
- },
- short: {
- scorePerQuestion: questionTypes.value.find(t => t.name === '简答题')?.scorePerQuestion || 10,
- totalScore: (questionTypes.value.find(t => t.name === '简答题')?.scorePerQuestion || 10) * (questionTypes.value.find(t => t.name === '简答题')?.questionCount || 2),
- count: questionTypes.value.find(t => t.name === '简答题')?.questionCount || 2,
- questions: generateDefaultQuestions('short', questionTypes.value.find(t => t.name === '简答题')?.questionCount || 2)
- }
- };
- };
- // 生成默认题目
- const generateDefaultQuestions = (type, count) => {
- const questions = [];
- const projectType = projectTypes[selectedProjectType.value].name;
-
- for (let i = 0; i < count; i++) {
- if (type === 'single') {
- questions.push({
- text: `${projectType}工程相关单选题${i + 1}`,
- options: [
- { key: "A", text: "选项A" },
- { key: "B", text: "选项B" },
- { key: "C", text: "选项C" },
- { key: "D", text: "选项D" }
- ],
- selectedAnswer: ""
- });
- } else if (type === 'judge') {
- questions.push({
- text: `${projectType}工程相关判断题${i + 1}`,
- selectedAnswer: ""
- });
- } else if (type === 'multiple') {
- questions.push({
- text: `${projectType}工程相关多选题${i + 1}`,
- options: [
- { key: "A", text: "选项A" },
- { key: "B", text: "选项B" },
- { key: "C", text: "选项C" },
- { key: "D", text: "选项D" }
- ],
- selectedAnswers: []
- });
- } else if (type === 'short') {
- questions.push({
- text: `${projectType}工程相关简答题${i + 1}`,
- outline: {
- keyFactors: "答题要点、关键因素、示例答案"
- }
- });
- }
- }
-
- return questions;
- };
- // 更新当前试卷数据
- const updateCurrentExam = (generatedExam) => {
- currentExam.value = generatedExam;
- };
- // 返回配置页面
- const backToConfig = () => {
- showExamDetail.value = false;
- };
- // 切换题型展开/收起
- const toggleSection = (sectionType) => {
- expandedSections.value[sectionType] = !expandedSections.value[sectionType];
- };
- // 选中最新历史记录
- const selectLatestHistoryRecord = () => {
- if (historyData.value.length > 0) {
- // 清除所有选中状态
- historyData.value.forEach((item) => {
- item.isActive = false;
- });
- // 选中第一条(最新的)记录
- historyData.value[0].isActive = true;
- }
- };
- // 选择答案功能已禁用
- /*
- // 选择答案(单选题和判断题)
- const selectAnswer = async (sectionType, questionIndex, answer) => {
- if (sectionType === 'single') {
- currentExam.value.singleChoice.questions[questionIndex].selectedAnswer = answer;
- } else if (sectionType === 'judge') {
- currentExam.value.judge.questions[questionIndex].selectedAnswer = answer;
- }
-
- // 保存答案修改到后端
- await saveToReModifyQuestion(sectionType, questionIndex, null);
-
- // 给用户提示
- ElMessage.success("答案已保存");
-
- // AI回复完成后,获取最新的历史记录
- await getHistoryRecordList();
-
- // 如果是新对话,将最新的历史记录设为激活状态
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
- } else {
- // 如果没有对话ID,选中第一条记录
- selectLatestHistoryRecord();
- }
- };
- */
- // 切换多选题答案功能已禁用
- /*
- // 切换多选题答案
- const toggleMultipleAnswer = async (sectionType, questionIndex, answer) => {
- if (sectionType === 'multiple') {
- const question = currentExam.value.multiple.questions[questionIndex];
- const index = question.selectedAnswers.indexOf(answer);
- if (index > -1) {
- question.selectedAnswers.splice(index, 1);
- } else {
- question.selectedAnswers.push(answer);
- }
- }
-
- // 保存答案修改到后端
- await saveToReModifyQuestion(sectionType, questionIndex, null);
-
- // 给用户提示
- ElMessage.success("答案已保存");
-
- // AI回复完成后,获取最新的历史记录
- await getHistoryRecordList();
-
- // 如果是新对话,将最新的历史记录设为激活状态
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
- } else {
- // 如果没有对话ID,选中第一条记录
- selectLatestHistoryRecord();
- }
- };
- */
- // 刷新题目
- const refreshQuestion = async (sectionType, questionIndex) => {
- try {
- console.log(`刷新${sectionType}类型第${questionIndex + 1}题`);
-
- // 设置刷新状态
- const key = `${sectionType}_${questionIndex}`;
- isRefreshing.value[key] = true;
-
- // 构建单题重新生成的提示词
- const prompt = buildSingleQuestionPrompt(sectionType, questionIndex);
-
- // 第一步:调用 /re_produce_single_question 接口,AI只生成题目
- const response = await apis.reProduceSingleQuestion({
- message: prompt
- });
-
- if (response.statusCode === 200) {
- const aiReply = response.data.reply;
- console.log('AI重新生成的题目:', aiReply);
-
- // 不更新对话ID,使用当前的ai_conversation_id
- console.log('使用当前对话ID:', ai_conversation_id.value);
-
- // 解析AI回复并更新题目
- const newQuestion = parseSingleQuestionResponse(aiReply, sectionType);
- console.log('解析后的新题目:', newQuestion);
-
- if (newQuestion) {
- updateQuestion(sectionType, questionIndex, newQuestion);
-
- console.log('准备保存到后端,对话ID:', ai_conversation_id.value);
-
- // 第二步:使用 /re_modify_question 接口保存修改
- await saveToReModifyQuestion(sectionType, questionIndex, newQuestion);
-
- ElMessage.success("题目重新生成成功!");
-
- // AI回复完成后,获取最新的历史记录
- await getHistoryRecordList();
-
- // 如果是新对话,将最新的历史记录设为激活状态
- if (ai_conversation_id.value > 0) {
- historyData.value.forEach((item) => {
- item.isActive = item.id === ai_conversation_id.value;
- });
- console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
- } else {
- // 如果没有对话ID,选中第一条记录
- selectLatestHistoryRecord();
- }
- } else {
- ElMessage.error("题目重新生成失败,请稍后重试");
- }
- } else {
- throw new Error('AI接口调用失败');
- }
-
- } catch (error) {
- console.error('重新生成题目失败:', error);
- ElMessage.error('重新生成题目失败,请稍后重试');
- } finally {
- // 重置刷新状态
- const key = `${sectionType}_${questionIndex}`;
- isRefreshing.value[key] = false;
- }
- };
- // 获取当前题目
- const getCurrentQuestion = (sectionType, questionIndex) => {
- if (sectionType === 'single') {
- return currentExam.value.singleChoice.questions[questionIndex];
- } else if (sectionType === 'judge') {
- return currentExam.value.judge.questions[questionIndex];
- } else if (sectionType === 'multiple') {
- return currentExam.value.multiple.questions[questionIndex];
- } else if (sectionType === 'short') {
- return currentExam.value.short.questions[questionIndex];
- }
- return null;
- };
- // 构建单题重新生成的提示词
- const buildSingleQuestionPrompt = (sectionType, questionIndex) => {
- const projectType = projectTypes[selectedProjectType.value].name;
- const questionType = getQuestionTypeName(sectionType);
- const scorePerQuestion = getQuestionScore(sectionType);
-
- // 获取当前题目作为参考
- const currentQuestion = getCurrentQuestion(sectionType, questionIndex);
-
- let prompt = `请基于以下${projectType}工程的${questionType}题目,重新生成一道相似主题的题目,要求如下:
- 当前题目参考:
- ${JSON.stringify(currentQuestion, null, 2)}
- 题目类型:${questionType}
- 每题分值:${scorePerQuestion}分
- 题目序号:第${questionIndex + 1}题
- 请严格按照以下JSON格式返回,不要包含任何其他文字:
- `;
- if (sectionType === 'single') {
- prompt += `{
- "text": "题目内容",
- "options": [
- {"key": "A", "text": "选项A内容"},
- {"key": "B", "text": "选项B内容"},
- {"key": "C", "text": "选项C内容"},
- {"key": "D", "text": "选项D内容"}
- ],
- "selectedAnswer": "正确答案选项(A/B/C/D)"
- }`;
- } else if (sectionType === 'judge') {
- prompt += `{
- "text": "题目内容",
- "selectedAnswer": "正确答案(正确/错误)"
- }`;
- } else if (sectionType === 'multiple') {
- prompt += `{
- "text": "题目内容",
- "options": [
- {"key": "A", "text": "选项A内容"},
- {"key": "B", "text": "选项B内容"},
- {"key": "C", "text": "选项C内容"},
- {"key": "D", "text": "选项D内容"}
- ],
- "selectedAnswers": ["正确答案选项1", "正确答案选项2"]
- }`;
- } else if (sectionType === 'short') {
- prompt += `{
- "text": "题目内容",
- "outline": {
- "keyFactors": "答题要点、关键因素、示例答案"
- }
- }`;
- }
- prompt += `
- 注意:
- 1. 新题目必须与当前题目保持相似的主题和难度
- 2. 题目内容必须与${projectType}工程相关
- 3. 题目难度适中,符合考试要求
- 4. 严格按照JSON格式返回,不要有多余字符
- 5. 单选题和判断题的选项要合理
- 6. 多选题至少要有2个正确答案
- 7. 简答题要提供清晰的答题要点
- 8. 必须为每道题设置正确答案:
- - 单选题:selectedAnswer字段填写正确的选项(A/B/C/D)
- - 判断题:selectedAnswer字段填写"正确"或"错误"
- - 多选题:selectedAnswers数组包含所有正确答案选项
- 9. 简答题答案字数不超过500字
- 10. 新题目应该是当前题目的变体,保持主题一致性但内容要有所变化`;
- return prompt;
- };
- // 获取题型名称
- const getQuestionTypeName = (sectionType) => {
- const typeMap = {
- 'single': '单选题',
- 'judge': '判断题',
- 'multiple': '多选题',
- 'short': '简答题'
- };
- return typeMap[sectionType] || '题目';
- };
- // 获取题型分值
- const getQuestionScore = (sectionType) => {
- const scoreMap = {
- 'single': currentExam.value.singleChoice.scorePerQuestion,
- 'judge': currentExam.value.judge.scorePerQuestion,
- 'multiple': currentExam.value.multiple.scorePerQuestion,
- 'short': currentExam.value.short.scorePerQuestion
- };
- return scoreMap[sectionType] || 2;
- };
- // 解析单题AI回复
- const parseSingleQuestionResponse = (aiReply, sectionType) => {
- try {
- console.log('AI回复内容:', aiReply);
- console.log('题目类型:', sectionType);
-
- // 尝试提取JSON内容
- const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const questionData = JSON.parse(jsonMatch[0]);
- console.log('解析后的题目数据:', questionData);
-
- // 如果是简答题,检查keyFactors字段
- if (sectionType === 'short' && questionData.outline && questionData.outline.keyFactors) {
- console.log('简答题keyFactors原始值:', questionData.outline.keyFactors);
-
- // 如果keyFactors是数组,转换为字符串
- if (Array.isArray(questionData.outline.keyFactors)) {
- questionData.outline.keyFactors = questionData.outline.keyFactors.join(' ');
- console.log('转换后的keyFactors:', questionData.outline.keyFactors);
- }
- }
-
- return questionData;
- } else {
- throw new Error('未找到有效的JSON数据');
- }
- } catch (error) {
- console.error('解析单题AI回复失败:', error);
- return null;
- }
- };
- // 更新题目
- const updateQuestion = (sectionType, questionIndex, newQuestion) => {
- let updatedQuestion;
-
- if (sectionType === 'single') {
- // 直接使用AI生成的题目和答案,不重置
- updatedQuestion = {
- ...newQuestion
- };
- // 如果AI没有生成答案,才设置默认答案
- if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
- setDefaultAnswerForQuestion(sectionType, questionIndex, updatedQuestion);
- }
- currentExam.value.singleChoice.questions[questionIndex] = updatedQuestion;
- } else if (sectionType === 'judge') {
- // 直接使用AI生成的题目和答案,不重置
- updatedQuestion = {
- ...newQuestion
- };
- // 如果AI没有生成答案,才设置默认答案
- if (!updatedQuestion.selectedAnswer || updatedQuestion.selectedAnswer === "") {
- setDefaultAnswerForQuestion(sectionType, questionIndex, updatedQuestion);
- }
- currentExam.value.judge.questions[questionIndex] = updatedQuestion;
- } else if (sectionType === 'multiple') {
- // 直接使用AI生成的题目和答案,不重置
- updatedQuestion = {
- ...newQuestion
- };
- // 如果AI没有生成答案,才设置默认答案
- if (!updatedQuestion.selectedAnswers || updatedQuestion.selectedAnswers.length === 0) {
- setDefaultAnswerForQuestion(sectionType, questionIndex, updatedQuestion);
- }
- currentExam.value.multiple.questions[questionIndex] = updatedQuestion;
- } else if (sectionType === 'short') {
- currentExam.value.short.questions[questionIndex] = newQuestion;
- }
-
- // 强制触发Vue响应式更新
- currentExam.value = { ...currentExam.value };
-
- // 调试输出
- console.log(`更新后的题目${sectionType}_${questionIndex}:`, updatedQuestion);
- console.log('当前试卷数据:', currentExam.value);
- };
- // 为新生成的题目设置默认答案
- const setDefaultAnswerForQuestion = (sectionType, questionIndex, question) => {
- if (sectionType === 'single') {
- // 单选题:根据题目内容智能设置答案
- if (question.text.includes('桩身倾斜') || question.text.includes('桩位纠偏')) {
- question.selectedAnswer = "B";
- } else if (question.text.includes('深水河流') || question.text.includes('悬臂浇筑')) {
- question.selectedAnswer = "B";
- } else if (question.text.includes('预应力张拉') || question.text.includes('对称张拉')) {
- question.selectedAnswer = "C";
- } else if (question.text.includes('混凝土裂缝') || question.text.includes('伸缩缝')) {
- question.selectedAnswer = "C";
- } else if (question.text.includes('沉井下沉') || question.text.includes('土体破坏')) {
- question.selectedAnswer = "C";
- } else {
- // 随机设置一个答案
- const answers = ["A", "B", "C", "D"];
- question.selectedAnswer = answers[questionIndex % answers.length];
- }
- } else if (sectionType === 'judge') {
- // 判断题:根据题目内容智能设置答案
- if (question.text.includes('水灰比无关')) {
- question.selectedAnswer = "错误";
- } else if (question.text.includes('张拉顺序') || question.text.includes('预应力损失')) {
- question.selectedAnswer = "正确";
- } else if (question.text.includes('养护时间越长越好')) {
- question.selectedAnswer = "错误";
- } else if (question.text.includes('下沉速度应控制')) {
- question.selectedAnswer = "正确";
- } else if (question.text.includes('张拉应力值可以超过设计值')) {
- question.selectedAnswer = "错误";
- } else {
- // 随机设置一个答案
- question.selectedAnswer = questionIndex % 2 === 0 ? "正确" : "错误";
- }
- } else if (sectionType === 'multiple') {
- // 多选题:根据题目内容智能设置答案
- if (!question.selectedAnswers) {
- question.selectedAnswers = [];
- }
-
- if (question.text.includes('桥梁基础施工')) {
- question.selectedAnswers = ["A", "C", "D"];
- } else if (question.text.includes('预应力技术')) {
- question.selectedAnswers = ["A", "B"];
- } else if (question.text.includes('质量控制')) {
- question.selectedAnswers = ["A", "B", "C"];
- } else if (question.text.includes('安全措施')) {
- question.selectedAnswers = ["A", "B", "C", "D"];
- } else {
- // 随机设置2-3个答案
- const answers = ["A", "B", "C", "D"];
- const count = Math.floor(Math.random() * 2) + 2; // 2-3个答案
- question.selectedAnswers = answers.slice(0, count);
- }
- }
- // 简答题不需要设置答案,保持原有逻辑
- };
- // 使用 /re_modify_question 接口保存修改
- const saveToReModifyQuestion = async (sectionType, questionIndex, newQuestion) => {
- console.log('对话id',ai_conversation_id.value);
- try {
- // 使用当前保存的对话ID
- if (!ai_conversation_id.value) {
- console.warn('没有找到对话ID,跳过保存');
- return;
- }
-
- // 构建要保存的内容 - 保存整个试卷的JSON字符串
- const content = JSON.stringify(currentExam.value);
-
- console.log('保存到 /re_modify_question 的内容:', content);
-
- // 调用后端接口保存修改
- const response = await apis.reModifyQuestion({
- ai_conversation_id: ai_conversation_id.value,
- content: content
- });
-
- if (response.statusCode === 200) {
- console.log('修改已保存到后端');
- // 如果是重新生成题目,不显示额外提示,因为已经有成功提示了
- if (newQuestion) {
- console.log('题目重新生成并保存成功');
- }
- } else {
- console.error('保存修改失败:', response);
- }
-
- } catch (error) {
- console.error('保存修改失败:', error);
- // 不抛出错误,避免影响用户体验
- }
- };
- // 控制下载菜单显示/隐藏
- const toggleDownloadMenu = () => {
- if (!isGenerating.value) {
- showDownloadMenu.value = !showDownloadMenu.value;
- }
- };
- // 关闭下载菜单
- const closeDownloadMenu = () => {
- showDownloadMenu.value = false;
- };
- // 点击外部区域关闭下载菜单
- const handleClickOutside = (event) => {
- const dropdown = event.target.closest('.download-dropdown');
- if (!dropdown) {
- showDownloadMenu.value = false;
- }
- };
- // 导出Word(有答案)
- const exportToWordWithAnswers = async () => {
- try {
- closeDownloadMenu(); // 关闭下拉菜单
- // 准备导出数据
- const exportData = prepareExamDataForExport();
-
- console.log('准备导出的数据(有答案):', exportData);
-
- // 直接使用模拟导出功能
- console.log('使用模拟Word导出功能(有答案)');
- await simulateWordExport(true);
-
- } catch (error) {
- console.error('Word导出失败:', error);
- ElMessage.error('Word导出失败,请稍后重试');
- }
- };
- // 导出Word(无答案)
- const exportToWordWithoutAnswers = async () => {
- try {
- closeDownloadMenu(); // 关闭下拉菜单
- // 准备导出数据
- const exportData = prepareExamDataForExport();
-
- console.log('准备导出的数据(无答案):', exportData);
-
- // 直接使用模拟导出功能
- console.log('使用模拟Word导出功能(无答案)');
- await simulateWordExport(false);
-
- } catch (error) {
- console.error('Word导出失败:', error);
- ElMessage.error('Word导出失败,请稍后重试');
- }
- };
- // 导出Word(保留原函数以兼容其他可能的调用)
- const exportToWord = async () => {
- // 默认导出有答案版本
- await exportToWordWithAnswers();
- };
- // 模拟Word导出功能(临时使用)
- const simulateWordExport = async (includeAnswers = true) => {
- try {
- // 准备导出数据
- const exportData = prepareExamDataForExport();
-
- // 创建Word文档内容(使用HTML格式,兼容WPS和Word)
- const wordContent = createHTMLContent(exportData, includeAnswers);
-
- // 创建Blob对象 - 使用HTML格式
- const blob = new Blob([wordContent], {
- type: 'application/msword'
- });
-
- // 下载文件
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- const fileName = includeAnswers
- ? `${currentExam.value.title}_有答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`
- : `${currentExam.value.title}_无答案_${currentTime.value.replace(/[:\s]/g, '_')}.doc`;
- link.setAttribute('href', url);
- link.setAttribute('download', fileName);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
-
- ElMessage.success(`导出成功${includeAnswers ? '(含答案)' : '(不含答案)'}`);
-
- } catch (error) {
- console.error('模拟Word导出失败:', error);
- ElMessage.error('Word导出失败,请稍后重试');
- }
- };
- // 创建HTML格式的Word文档内容(兼容WPS和Word)
- const createHTMLContent = (exportData, includeAnswers = true) => {
- const exam = currentExam.value;
-
- // HTML文档内容,使用Word兼容的格式
- let htmlContent = `<!DOCTYPE html>
- <html xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:w="urn:schemas-microsoft-com:office:word"
- xmlns="http://www.w3.org/TR/REC-html40">
- <head>
- <meta charset="utf-8">
- <meta name="ProgId" content="Word.Document">
- <meta name="Generator" content="Microsoft Word 15">
- <meta name="Originator" content="Microsoft Word 15">
- <title>${exam.title || '试卷'}</title>
- <!--[if gte mso 9]>
- <xml>
- <w:WordDocument>
- <w:View>Print</w:View>
- <w:Zoom>100</w:Zoom>
- <w:DoNotPromptForConvert/>
- <w:DoNotShowRevisions/>
- <w:DoNotPrintRevisions/>
- <w:DoNotShowComments/>
- <w:DoNotShowInsertionsAndDeletions/>
- <w:DoNotShowPropertyChanges/>
- <w:Compatibility>
- <w:BreakWrappedTables/>
- <w:SnapToGridInCell/>
- <w:WrapTextWithPunct/>
- <w:UseAsianBreakRules/>
- <w:DontGrowAutofit/>
- </w:Compatibility>
- </w:WordDocument>
- </xml>
- <![endif]-->
- <style>
- body {
- font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
- font-size: 14px;
- line-height: 1.6;
- margin: 24px;
- color: #000;
- }
- .header {
- text-align: center;
- margin-bottom: 14px;
- }
- .exam-title {
- font-size: 24px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- }
- .exam-info {
- font-size: 14px;
- color: #666;
- margin-bottom: 14px;
- }
- .section {
- margin-bottom: 14px;
- }
- .section-title {
- font-size: 18px;
- font-weight: bold;
- margin-bottom: 14px;
- color: #000;
- border-bottom: 2px solid #3e7bfa;
- padding-bottom: 5px;
- }
- .question {
- margin-bottom: 14px;
- padding: 10px;
- background-color: #f9f9f9;
- border-left: 4px solid #3e7bfa;
- }
- .question-header {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- margin-bottom: 14px;
- }
- .question-number {
- font-weight: bold;
- color: #3e7bfa;
- flex-shrink: 0;
- }
- .question-text {
- flex: 1;
- }
- .options {
- margin-left: 12px;
- }
- .option {
- margin-bottom: 5px;
- }
- .answer {
- margin-top: 10px;
- padding: 8px;
- background: #e8f4fd;
- border-radius: 4px;
- font-weight: bold;
- color: #2c5aa0;
- }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="exam-title">${exam.title || '试卷'}</div>
- <div class="exam-info">
- 总分:${exam.totalScore || 0}分 | 总题数:${exam.totalQuestions || 0}题 | 生成时间:${currentTime.value}
- </div>
- </div>`;
- // 单选题
- if (exam.singleChoice && exam.singleChoice.questions && exam.singleChoice.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">一、单选题(每题${exam.singleChoice.scorePerQuestion || 0}分,共${exam.singleChoice.totalScore || 0}分)</div>`;
-
- exam.singleChoice.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text || '题目内容'}</span>
- </div>`;
-
- if (question.options && question.options.length > 0) {
- htmlContent += `<div class="options">`;
- question.options.forEach((option, optIndex) => {
- const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
- htmlContent += `<div class="option">${optionLabel}. ${option.text || '选项内容'}</div>`;
- });
- htmlContent += `</div>`;
- }
-
- htmlContent += `${includeAnswers ? `<div class="answer">答案:${question.selectedAnswer || '未设置'}</div>` : ''}
- </div>`;
- });
-
- htmlContent += `</div>`;
- }
- // 判断题
- if (exam.judge && exam.judge.questions && exam.judge.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">二、判断题(每题${exam.judge.scorePerQuestion || 0}分,共${exam.judge.totalScore || 0}分)</div>`;
-
- exam.judge.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text || '题目内容'}</span>
- </div>
- ${includeAnswers ? `<div class="answer">答案:${question.selectedAnswer || '未设置'}</div>` : ''}
- </div>`;
- });
-
- htmlContent += `</div>`;
- }
- // 多选题
- if (exam.multipleChoice && exam.multipleChoice.questions && exam.multipleChoice.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">三、多选题(每题${exam.multipleChoice.scorePerQuestion || 0}分,共${exam.multipleChoice.totalScore || 0}分)</div>`;
-
- exam.multipleChoice.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text || '题目内容'}</span>
- </div>`;
-
- if (question.options && question.options.length > 0) {
- htmlContent += `<div class="options">`;
- question.options.forEach((option, optIndex) => {
- const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
- htmlContent += `<div class="option">${optionLabel}. ${option.text || '选项内容'}</div>`;
- });
- htmlContent += `</div>`;
- }
-
- htmlContent += `${includeAnswers ? `<div class="answer">答案:${question.selectedAnswer || question.selectedAnswers?.join(', ') || '未设置'}</div>` : ''}
- </div>`;
- });
-
- htmlContent += `</div>`;
- }
- // 简答题
- if (exam.short && exam.short.questions && exam.short.questions.length > 0) {
- htmlContent += `
- <div class="section">
- <div class="section-title">四、简答题(每题${exam.short.scorePerQuestion || 0}分,共${exam.short.totalScore || 0}分)</div>`;
-
- exam.short.questions.forEach((question, index) => {
- htmlContent += `
- <div class="question">
- <div class="question-header">
- <span class="question-number">${index + 1}.</span>
- <span class="question-text">${question.text || '题目内容'}</span>
- </div>
- ${includeAnswers ? `<div class="answer">答题要点:${question.outline?.keyFactors || '未设置'}</div>` : ''}
- </div>`;
- });
-
- htmlContent += `</div>`;
- }
- htmlContent += `
- </body>
- </html>`;
-
- return htmlContent;
- };
- // 创建纯文本格式的文档内容(避免编码问题)
- const createTextContent = (exportData, includeAnswers = true) => {
- const exam = currentExam.value;
-
- let textContent = '';
-
- // 试卷标题
- textContent += `${exam.title || '试卷'}\n`;
- textContent += '='.repeat(50) + '\n\n';
-
- // 试卷信息
- textContent += `总分:${exam.totalScore || 0}分\n`;
- textContent += `总题数:${exam.totalQuestions || 0}题\n`;
- textContent += `生成时间:${currentTime.value}\n\n`;
-
- // 单选题
- if (exam.singleChoice && exam.singleChoice.questions && exam.singleChoice.questions.length > 0) {
- textContent += `一、单选题(每题${exam.singleChoice.scorePerQuestion || 0}分,共${exam.singleChoice.totalScore || 0}分)\n`;
- textContent += '-'.repeat(30) + '\n';
-
- exam.singleChoice.questions.forEach((question, index) => {
- textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
-
- if (question.options && question.options.length > 0) {
- question.options.forEach((option, optIndex) => {
- const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
- textContent += ` ${optionLabel}. ${option.text || '选项内容'}\n`;
- });
- }
-
- if (includeAnswers) {
- textContent += ` 答案:${question.selectedAnswer || '未设置'}\n\n`;
- } else {
- textContent += `\n`;
- }
- });
- }
- // 判断题
- if (exam.judge && exam.judge.questions && exam.judge.questions.length > 0) {
- textContent += `二、判断题(每题${exam.judge.scorePerQuestion || 0}分,共${exam.judge.totalScore || 0}分)\n`;
- textContent += '-'.repeat(30) + '\n';
-
- exam.judge.questions.forEach((question, index) => {
- textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
- if (includeAnswers) {
- textContent += ` 答案:${question.selectedAnswer || '未设置'}\n\n`;
- } else {
- textContent += `\n`;
- }
- });
- }
- // 多选题
- if (exam.multipleChoice && exam.multipleChoice.questions && exam.multipleChoice.questions.length > 0) {
- textContent += `三、多选题(每题${exam.multipleChoice.scorePerQuestion || 0}分,共${exam.multipleChoice.totalScore || 0}分)\n`;
- textContent += '-'.repeat(30) + '\n';
-
- exam.multipleChoice.questions.forEach((question, index) => {
- textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
-
- if (question.options && question.options.length > 0) {
- question.options.forEach((option, optIndex) => {
- const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
- textContent += ` ${optionLabel}. ${option.text || '选项内容'}\n`;
- });
- }
-
- if (includeAnswers) {
- textContent += ` 答案:${question.selectedAnswer || question.selectedAnswers?.join(', ') || '未设置'}\n\n`;
- } else {
- textContent += `\n`;
- }
- });
- }
- // 简答题
- if (exam.short && exam.short.questions && exam.short.questions.length > 0) {
- textContent += `四、简答题(每题${exam.short.scorePerQuestion || 0}分,共${exam.short.totalScore || 0}分)\n`;
- textContent += '-'.repeat(30) + '\n';
-
- exam.short.questions.forEach((question, index) => {
- textContent += `${index + 1}. ${question.text || '题目内容'}\n`;
- if (includeAnswers) {
- textContent += ` 答题要点:${question.outline?.keyFactors || '未设置'}\n\n`;
- } else {
- textContent += `\n`;
- }
- });
- }
-
- return textContent;
- };
- // 创建RTF格式的Word文档内容(兼容性更好)
- const createRTFContent = (exportData, includeAnswers = true) => {
- const exam = currentExam.value;
-
- // RTF文档头部
- let rtfContent = `{\\rtf1\\ansi\\deff0 {\\fonttbl {\\f0 Times New Roman;}{\\f1 Microsoft YaHei;}}
- {\\colortbl;\\red0\\green0\\blue0;\\red62\\green123\\blue250;}
- {\\*\\generator Microsoft Word;}\\f1\\fs24\\par`;
- // 试卷标题
- rtfContent += `{\\qc\\b\\fs32 ${exam.title || '试卷'}\\par}\\par`;
- // 试卷信息
- rtfContent += `{\\qc 总分:${exam.totalScore || 0}分 | 总题数:${exam.totalQuestions || 0}题 | 生成时间:${currentTime.value}\\par}\\par`;
- // 单选题
- if (exam.singleChoice && exam.singleChoice.questions && exam.singleChoice.questions.length > 0) {
- rtfContent += `{\\b\\fs28 一、单选题(每题${exam.singleChoice.scorePerQuestion || 0}分,共${exam.singleChoice.totalScore || 0}分)\\par}\\par`;
-
- exam.singleChoice.questions.forEach((question, index) => {
- rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
-
- if (question.options && question.options.length > 0) {
- question.options.forEach((option, optIndex) => {
- const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
- rtfContent += ` ${optionLabel}. ${option.text || '选项内容'}\\par`;
- });
- }
-
- if (includeAnswers) {
- rtfContent += `{\\b 答案:}${question.selectedAnswer || '未设置'}\\par\\par`;
- } else {
- rtfContent += `\\par\\par`;
- }
- });
- }
- // 判断题
- if (exam.judge && exam.judge.questions && exam.judge.questions.length > 0) {
- rtfContent += `{\\b\\fs28 二、判断题(每题${exam.judge.scorePerQuestion || 0}分,共${exam.judge.totalScore || 0}分)\\par}\\par`;
-
- exam.judge.questions.forEach((question, index) => {
- rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
- if (includeAnswers) {
- rtfContent += `{\\b 答案:}${question.selectedAnswer || '未设置'}\\par\\par`;
- } else {
- rtfContent += `\\par\\par`;
- }
- });
- }
- // 多选题
- if (exam.multipleChoice && exam.multipleChoice.questions && exam.multipleChoice.questions.length > 0) {
- rtfContent += `{\\b\\fs28 三、多选题(每题${exam.multipleChoice.scorePerQuestion || 0}分,共${exam.multipleChoice.totalScore || 0}分)\\par}\\par`;
-
- exam.multipleChoice.questions.forEach((question, index) => {
- rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
-
- if (question.options && question.options.length > 0) {
- question.options.forEach((option, optIndex) => {
- const optionLabel = String.fromCharCode(65 + optIndex); // A, B, C, D
- rtfContent += ` ${optionLabel}. ${option.text || '选项内容'}\\par`;
- });
- }
-
- if (includeAnswers) {
- rtfContent += `{\\b 答案:}${question.selectedAnswer || question.selectedAnswers?.join(', ') || '未设置'}\\par\\par`;
- } else {
- rtfContent += `\\par\\par`;
- }
- });
- }
- // 简答题
- if (exam.short && exam.short.questions && exam.short.questions.length > 0) {
- rtfContent += `{\\b\\fs28 四、简答题(每题${exam.short.scorePerQuestion || 0}分,共${exam.short.totalScore || 0}分)\\par}\\par`;
-
- exam.short.questions.forEach((question, index) => {
- rtfContent += `{\\b ${index + 1}.} ${question.text || '题目内容'}\\par`;
- if (includeAnswers) {
- rtfContent += `{\\b 答题要点:}${question.outline?.keyFactors || '未设置'}\\par\\par`;
- } else {
- rtfContent += `\\par\\par`;
- }
- });
- }
- // RTF文档结尾
- rtfContent += `}`;
-
- return rtfContent;
- };
- // 创建Word文档内容(保留原函数以防需要)
- const createWordContent = (exportData) => {
- const exam = currentExam.value;
-
- // 创建HTML格式的文档内容
- let htmlContent = `
- <html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
- <head>
- <meta charset='utf-8'>
- <title>${exam.title}</title>
- <style>
- body { font-family: 'Microsoft YaHei', Arial, sans-serif; font-size: 14px; line-height: 1.6; }
- .header { text-align: center; margin-bottom: 30px; }
- .exam-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
- .exam-info { margin-bottom: 12px; }
- .section { margin-bottom: 30px; }
- .section-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; color: #333; }
- .question { margin-bottom: 12px; }
- .question-header { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 12px; }
- .question-number { font-weight: bold; color: #3e7bfa; flex-shrink: 0; }
- .question-text { flex: 1; }
- .options { margin-left: 20px; }
- .option { margin-bottom: 5px; }
- .answer { margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 5px; }
- </style>
- </head>
- <body>
- <div class='header'>
- <div class='exam-title'>${exam.title}</div>
- <div class='exam-info'>
- <p>总分:${exam.totalScore}分 | 总题数:${exam.totalQuestions}题 | 生成时间:${currentTime.value}</p>
- </div>
- </div>
- `;
-
- // 单选题
- htmlContent += `
- <div class='section'>
- <div class='section-title'>一、单选题(每题${exam.singleChoice.scorePerQuestion}分,共${exam.singleChoice.totalScore}分)</div>
- `;
-
- exam.singleChoice.questions.forEach((question, index) => {
- htmlContent += `
- <div class='question'>
- <div class='question-header'>
- <span class='question-number'>${index + 1}.</span>
- <span class='question-text'>${question.text}</span>
- </div>
- <div class='options'>
- ${question.options.map(option => `<div class='option'>${option.key}. ${option.text}</div>`).join('')}
- </div>
- <div class='answer'>正确答案:${question.selectedAnswer || '未设置'}</div>
- </div>
- `;
- });
-
- htmlContent += '</div>';
-
- // 判断题
- htmlContent += `
- <div class='section'>
- <div class='section-title'>二、判断题(每题${exam.judge.scorePerQuestion}分,共${exam.judge.totalScore}分)</div>
- `;
-
- exam.judge.questions.forEach((question, index) => {
- htmlContent += `
- <div class='question'>
- <div class='question-header'>
- <span class='question-number'>${index + 1}.</span>
- <span class='question-text'>${question.text}</span>
- </div>
- <div class='answer'>正确答案:${question.selectedAnswer || '未设置'}</div>
- </div>
- `;
- });
-
- htmlContent += '</div>';
-
- // 多选题
- htmlContent += `
- <div class='section'>
- <div class='section-title'>三、多选题(每题${exam.multiple.scorePerQuestion}分,共${exam.multiple.totalScore}分)</div>
- `;
-
- exam.multiple.questions.forEach((question, index) => {
- htmlContent += `
- <div class='question'>
- <div class='question-header'>
- <span class='question-number'>${index + 1}.</span>
- <span class='question-text'>${question.text}</span>
- </div>
- <div class='options'>
- ${question.options.map(option => `<div class='option'>${option.key}. ${option.text}</div>`).join('')}
- </div>
- <div class='answer'>正确答案:${question.selectedAnswers.join(', ') || '未设置'}</div>
- </div>
- `;
- });
-
- htmlContent += '</div>';
-
- // 简答题
- htmlContent += `
- <div class='section'>
- <div class='section-title'>四、简答题(每题${exam.short.scorePerQuestion}分,共${exam.short.totalScore}分)</div>
- `;
-
- exam.short.questions.forEach((question, index) => {
- htmlContent += `
- <div class='question'>
- <div class='question-header'>
- <span class='question-number'>${index + 1}.</span>
- <span class='question-text'>${question.text}</span>
- </div>
- <div class='answer'>答题要点:${question.outline?.keyFactors || '未设置'}</div>
- </div>
- `;
- });
-
- htmlContent += '</div>';
-
- htmlContent += '</body></html>';
-
- return htmlContent;
- };
- // 准备Word数据
- const prepareExamDataForExport = () => {
- const exam = currentExam.value;
-
- // 试卷基本信息
- const basicInfo = [
- ['试卷名称', exam.title],
- ['总分', `${exam.totalScore}分`],
- ['总题数', `${exam.totalQuestions}题`],
- ['生成时间', currentTime.value],
- ['', ''], // 空行
- ];
-
- // 单选题数据
- const singleChoiceData = [
- ['一、单选题', `(每题${exam.singleChoice.scorePerQuestion}分,共${exam.singleChoice.totalScore}分)`],
- ['题号', '题目内容', '选项A', '选项B', '选项C', '选项D', '正确答案'],
- ];
-
- exam.singleChoice.questions.forEach((question, index) => {
- singleChoiceData.push([
- `${index + 1}`,
- question.text,
- question.options[0]?.text || '',
- question.options[1]?.text || '',
- question.options[2]?.text || '',
- question.options[3]?.text || '',
- question.selectedAnswer || ''
- ]);
- });
-
- // 判断题数据
- const judgeData = [
- ['', ''], // 空行
- ['二、判断题', `(每题${exam.judge.scorePerQuestion}分,共${exam.judge.totalScore}分)`],
- ['题号', '题目内容', '正确答案'],
- ];
-
- exam.judge.questions.forEach((question, index) => {
- judgeData.push([
- `${index + 1}`,
- question.text,
- question.selectedAnswer || ''
- ]);
- });
-
- // 多选题数据
- const multipleData = [
- ['', ''], // 空行
- ['三、多选题', `(每题${exam.multiple.scorePerQuestion}分,共${exam.multiple.totalScore}分)`],
- ['题号', '题目内容', '选项A', '选项B', '选项C', '选项D', '正确答案'],
- ];
-
- exam.multiple.questions.forEach((question, index) => {
- multipleData.push([
- `${index + 1}`,
- question.text,
- question.options[0]?.text || '',
- question.options[1]?.text || '',
- question.options[2]?.text || '',
- question.options[3]?.text || '',
- question.selectedAnswers.join(', ') || ''
- ]);
- });
-
- // 简答题数据
- const shortData = [
- ['', ''], // 空行
- ['四、简答题', `(每题${exam.short.scorePerQuestion}分,共${exam.short.totalScore}分)`],
- ['题号', '题目内容', '答题要点'],
- ];
-
- exam.short.questions.forEach((question, index) => {
- shortData.push([
- `${index + 1}`,
- question.text,
- question.outline?.keyFactors || ''
- ]);
- });
-
- // 合并所有数据
- return [
- ...basicInfo,
- ...singleChoiceData,
- ...judgeData,
- ...multipleData,
- ...shortData
- ];
- };
- const saveExam = async () => {
- try {
- // 准备保存的试卷数据
- const examData = prepareExamDataForSave();
-
- console.log('准备保存的试卷数据:', examData);
-
- const response = await apis.saveExam(examData);
-
- if (response.statusCode === 200) {
- ElMessage.success("试卷保存成功!");
- updateHistoryData(examData);
- console.log('试卷已保存到历史记录');
- } else {
- throw new Error('保存失败');
- }
-
- } catch (error) {
- console.error('保存试卷失败:', error);
- ElMessage.error('保存试卷失败,请稍后重试');
- }
- };
- // 准备保存的试卷数据
- const prepareExamDataForSave = () => {
- const exam = currentExam.value;
-
- return {
- // 基本信息
- exam_name: exam.title,
- exam_type: selectedProjectType.value,
- total_score: exam.totalScore,
- total_questions: exam.totalQuestions,
- generation_method: selectedFunction.value,
- generation_time: currentTime.value,
- created_at: new Date().toISOString(),
-
- // 题型配置
- question_config: {
- single_choice: {
- score_per_question: exam.singleChoice.scorePerQuestion,
- total_score: exam.singleChoice.totalScore,
- count: exam.singleChoice.count
- },
- judge: {
- score_per_question: exam.judge.scorePerQuestion,
- total_score: exam.judge.totalScore,
- count: exam.judge.count
- },
- multiple: {
- score_per_question: exam.multiple.scorePerQuestion,
- total_score: exam.multiple.totalScore,
- count: exam.multiple.count
- },
- short: {
- score_per_question: exam.short.scorePerQuestion,
- total_score: exam.short.totalScore,
- count: exam.short.count
- }
- },
-
- // 题目内容
- questions: {
- single_choice: exam.singleChoice.questions.map(q => ({
- question_text: q.text,
- options: q.options,
- correct_answer: q.selectedAnswer
- })),
- judge: exam.judge.questions.map(q => ({
- question_text: q.text,
- correct_answer: q.selectedAnswer
- })),
- multiple: exam.multiple.questions.map(q => ({
- question_text: q.text,
- options: q.options,
- correct_answers: q.selectedAnswers
- })),
- short: exam.short.questions.map(q => ({
- question_text: q.text,
- answer_outline: q.outline
- }))
- },
-
- // 用户答案(可选,用于保存用户的答题状态)
- user_answers: {
- single_choice: exam.singleChoice.questions.map(q => q.selectedAnswer),
- judge: exam.judge.questions.map(q => q.selectedAnswer),
- multiple: exam.multiple.questions.map(q => q.selectedAnswers),
- short: exam.short.questions.map(q => null) // 简答题不保存用户答案
- }
- };
- };
- // 更新历史记录数据
- const updateHistoryData = (examData) => {
- const newHistoryItem = {
- id: Date.now(),
- title: examData.exam_name,
- time: examData.generation_time,
- isActive: false,
- examData: examData
- };
-
- historyData.value.unshift(newHistoryItem);
-
- if (historyData.value.length > 20) {
- historyData.value = historyData.value.slice(0, 20);
- }
- };
- // 从历史记录恢复试卷
- const restoreExamFromHistory = (examData) => {
- try {
- console.log('开始恢复试卷数据:', examData);
- console.log('试卷数据详细结构:', JSON.stringify(examData, null, 2));
-
- // 检查数据格式,支持多种格式
- let exam = examData;
-
- // 如果数据是嵌套在exam字段中
- if (examData.exam) {
- exam = examData.exam;
- }
-
- // 恢复基本信息
- if (exam.title) {
- examName.value = exam.title;
- } else if (exam.exam_name) {
- examName.value = exam.exam_name;
- }
-
- if (exam.exam_type) {
- selectedProjectType.value = exam.exam_type;
- }
-
- if (exam.totalScore) {
- totalScore.value = exam.totalScore;
- } else if (exam.total_score) {
- totalScore.value = exam.total_score;
- }
-
- if (exam.generation_method) {
- selectedFunction.value = exam.generation_method;
- }
-
- if (exam.generationTime) {
- currentTime.value = exam.generationTime;
- } else if (exam.generation_time) {
- currentTime.value = exam.generation_time;
- }
-
- // 恢复题型配置
- if (exam.question_config) {
- questionTypes.value = [
- {
- name: "单选题",
- scorePerQuestion: exam.question_config.single_choice.score_per_question,
- questionCount: exam.question_config.single_choice.count,
- romanNumeral: "一"
- },
- {
- name: "判断题",
- scorePerQuestion: exam.question_config.judge.score_per_question,
- questionCount: exam.question_config.judge.count,
- romanNumeral: "二"
- },
- {
- name: "多选题",
- scorePerQuestion: exam.question_config.multiple.score_per_question,
- questionCount: exam.question_config.multiple.count,
- romanNumeral: "三"
- },
- {
- name: "简答题",
- scorePerQuestion: exam.question_config.short.score_per_question,
- questionCount: exam.question_config.short.count,
- romanNumeral: "四"
- }
- ];
- }
-
- // 恢复题目内容
- if (exam.singleChoice || exam.questions?.single_choice) {
- const singleChoice = exam.singleChoice || exam.questions.single_choice;
- const judge = exam.judge || exam.questions.judge;
- const multiple = exam.multiple || exam.questions.multiple;
- const short = exam.short || exam.questions.short;
-
- console.log('单选题数据:', singleChoice);
- console.log('判断题数据:', judge);
- console.log('多选题数据:', multiple);
- console.log('简答题数据:', short);
-
- currentExam.value = {
- title: examName.value,
- totalScore: totalScore.value,
- totalQuestions: exam.totalQuestions || exam.total_questions,
- singleChoice: {
- scorePerQuestion: singleChoice.scorePerQuestion || singleChoice.score_per_question,
- totalScore: singleChoice.totalScore || singleChoice.total_score,
- count: singleChoice.count,
- questions: singleChoice.questions.map(q => ({
- text: q.text || q.question_text,
- options: q.options || [],
- selectedAnswer: q.selectedAnswer || q.correct_answer || q.answer || ""
- }))
- },
- judge: {
- scorePerQuestion: judge.scorePerQuestion || judge.score_per_question,
- totalScore: judge.totalScore || judge.total_score,
- count: judge.count,
- questions: judge.questions.map(q => ({
- text: q.text || q.question_text,
- selectedAnswer: q.selectedAnswer || q.correct_answer || q.answer || ""
- }))
- },
- multiple: {
- scorePerQuestion: multiple.scorePerQuestion || multiple.score_per_question,
- totalScore: multiple.totalScore || multiple.total_score,
- count: multiple.count,
- questions: multiple.questions.map(q => ({
- text: q.text || q.question_text,
- options: q.options || [],
- selectedAnswers: q.selectedAnswers || q.correct_answers || q.answers || []
- }))
- },
- short: {
- scorePerQuestion: short.scorePerQuestion || short.score_per_question,
- totalScore: short.totalScore || short.total_score,
- count: short.count,
- questions: short.questions.map(q => ({
- text: q.text || q.question_text,
- outline: q.outline || q.answer_outline || { keyFactors: "答题要点、关键因素、示例答案" }
- }))
- }
- };
- }
-
- // 恢复用户答案(如果有)
- if (exam.user_answers) {
- if (exam.user_answers.single_choice) {
- exam.user_answers.single_choice.forEach((answer, index) => {
- if (currentExam.value.singleChoice.questions[index]) {
- currentExam.value.singleChoice.questions[index].selectedAnswer = answer || "";
- }
- });
- }
-
- if (exam.user_answers.judge) {
- exam.user_answers.judge.forEach((answer, index) => {
- if (currentExam.value.judge.questions[index]) {
- currentExam.value.judge.questions[index].selectedAnswer = answer || "";
- }
- });
- }
-
- if (exam.user_answers.multiple) {
- exam.user_answers.multiple.forEach((answers, index) => {
- if (currentExam.value.multiple.questions[index]) {
- currentExam.value.multiple.questions[index].selectedAnswers = answers || [];
- }
- });
- }
- }
-
- console.log('恢复完成后的试卷数据:', currentExam.value);
- console.log('单选题答案:', currentExam.value.singleChoice?.questions?.map(q => q.selectedAnswer));
- console.log('判断题答案:', currentExam.value.judge?.questions?.map(q => q.selectedAnswer));
- console.log('多选题答案:', currentExam.value.multiple?.questions?.map(q => q.selectedAnswers));
-
- // 显示详情页
- showExamDetail.value = true;
- // ElMessage.success("试卷已从历史记录恢复");
-
- } catch (error) {
- console.error('恢复试卷失败:', error);
- ElMessage.error('恢复试卷失败,请稍后重试');
- }
- };
- // 文件处理工具函数
- const validateFile = (file) => {
- // 检查文件大小
- if (file.size > fileConfig.maxSize) {
- throw new Error('文件大小不能超过20MB')
- }
-
- // 检查文件类型
- const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
- if (!fileConfig.allowedTypes.includes(fileExtension)) {
- throw new Error('只支持PPT格式文件(.ppt/.pptx)')
- }
-
- return fileExtension
- }
- // 获取文件图标
- const getFileIcon = (fileType) => {
- switch (fileType) {
- case '.ppt':
- case '.pptx':
- return '📊'
- default:
- return '📎'
- }
- }
- // 格式化文件大小
- const formatFileSize = (bytes) => {
- if (bytes === 0) return '0 B'
- const k = 1024
- const sizes = ['B', 'KB', 'MB', 'GB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
- }
- // 从PPT文件中提取文本内容
- const extractTextFromPPT = async (uint8Array) => {
- try {
- // 尝试使用JSZip解析PPTX文件
- const JSZip = await import('jszip')
- const zip = new JSZip.default()
-
- // 加载PPTX文件
- const zipContent = await zip.loadAsync(uint8Array)
-
- // 查找幻灯片文件
- const slideFiles = Object.keys(zipContent.files).filter(name =>
- name.startsWith('ppt/slides/slide') && name.endsWith('.xml')
- )
-
- let extractedText = ''
-
- // 读取第一张幻灯片的内容
- if (slideFiles.length > 0) {
- const firstSlide = slideFiles[0]
- const slideContent = await zipContent.file(firstSlide).async('text')
-
- // 提取XML中的文本内容
- const textMatches = slideContent.match(/<a:t[^>]*>([^<]+)<\/a:t>/g)
- if (textMatches) {
- textMatches.forEach(match => {
- const text = match.replace(/<[^>]*>/g, '').trim()
- if (text.length > 0) {
- extractedText += text + ' '
- }
- })
- }
-
- // 如果没有找到a:t标签,尝试其他方式
- if (extractedText.length === 0) {
- const generalTextMatches = slideContent.match(/>([^<]{2,})</g)
- if (generalTextMatches) {
- generalTextMatches.forEach(match => {
- const text = match.replace(/[<>]/g, '').trim()
- if (text.length > 2 && /[\u4e00-\u9fa5a-zA-Z]/.test(text)) {
- extractedText += text + ' '
- }
- })
- }
- }
- }
-
- // 如果还是没有内容,尝试读取其他XML文件
- if (extractedText.length === 0) {
- const xmlFiles = Object.keys(zipContent.files).filter(name =>
- name.endsWith('.xml') && !name.includes('_rels')
- )
-
- for (const xmlFile of xmlFiles.slice(0, 3)) { // 只检查前3个文件
- try {
- const xmlContent = await zipContent.file(xmlFile).async('text')
- const textMatches = xmlContent.match(/>([^<]{2,})</g)
- if (textMatches) {
- textMatches.forEach(match => {
- const text = match.replace(/[<>]/g, '').trim()
- if (text.length > 2 && /[\u4e00-\u9fa5a-zA-Z]/.test(text)) {
- extractedText += text + ' '
- }
- })
- }
- } catch (error) {
- console.log('读取XML文件失败:', xmlFile, error)
- }
- }
- }
-
- // 清理文本
- if (extractedText.length > 0) {
- extractedText = extractedText
- .replace(/\s+/g, ' ')
- .replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '')
- .trim()
-
- return extractedText.substring(0, 800)
- }
-
- return ''
- } catch (error) {
- console.error('PPT文本提取失败:', error)
-
- // 如果JSZip方法失败,回退到简单文本提取
- try {
- const decoder = new TextDecoder('utf-8')
- const content = decoder.decode(uint8Array)
-
- const chineseMatches = content.match(/[\u4e00-\u9fa5]{2,}/g)
- const englishMatches = content.match(/[a-zA-Z]{3,}/g)
-
- let extractedText = ''
-
- if (chineseMatches && chineseMatches.length > 0) {
- extractedText += chineseMatches.join(' ')
- }
-
- if (englishMatches && englishMatches.length > 0) {
- extractedText += ' ' + englishMatches.join(' ')
- }
-
- if (extractedText.length > 0) {
- extractedText = extractedText
- .replace(/\s+/g, ' ')
- .replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '')
- .trim()
-
- return extractedText.substring(0, 800)
- }
- } catch (fallbackError) {
- console.error('回退文本提取也失败:', fallbackError)
- }
-
- return ''
- }
- }
- // 读取PPT文件内容
- const readPPTFile = async (file) => {
- try {
- console.log('开始读取PPT文件:', file.name, '文件大小:', file.size)
-
- // 检查文件是否为空
- if (file.size === 0) {
- throw new Error('PPT文件为空')
- }
-
- // 使用FileReader读取文件内容
- console.log('开始读取PPT文件内容...')
-
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
-
- reader.onload = async (event) => {
- try {
- const arrayBuffer = event.target.result
- console.log('文件读取成功,大小:', arrayBuffer.byteLength)
-
- // 使用简单的方法读取PPT文件第一页内容
- console.log('开始读取PPT文件第一页内容...')
-
- try {
- // 将ArrayBuffer转换为Uint8Array
- const uint8Array = new Uint8Array(arrayBuffer)
-
- // 查找PPT文件中的文本内容
- // PPT文件是ZIP格式,包含XML文件
- let extractedText = `PPT文件信息:
- 文件名:${file.name}
- 文件大小:${formatFileSize(file.size)}
- 文件类型:${file.type}
- 修改时间:${new Date(file.lastModified).toLocaleString('zh-CN')}
- PPT第一页内容提取结果:
- `
-
- // 尝试从PPT文件中提取文本内容
- const textContent = await extractTextFromPPT(uint8Array)
-
- if (textContent && textContent.length > 0) {
- extractedText += `\n提取的文本内容:\n${textContent}`
- console.log('PPT文本提取成功,长度:', textContent.length)
- } else {
- extractedText += `\n无法自动提取PPT文本内容。\n`
- }
-
- extractedText += `\n\n请在下方文本框中补充或修正PPT内容描述,AI将基于这些信息生成相关考题。`
-
- console.log('PPT内容提取完成,长度:', extractedText.length)
- console.log('提取的内容预览:', extractedText.substring(0, 500))
- resolve(extractedText)
-
- } catch (parseError) {
- console.error('PPT解析失败:', parseError)
-
- // 如果解析失败,返回文件信息
- const fallbackText = `PPT文件:${file.name}
- 文件大小:${formatFileSize(file.size)}
- 文件类型:${file.type}
- 修改时间:${new Date(file.lastModified).toLocaleString('zh-CN')}
- PPT内容提取失败,请手动描述PPT的主要内容、关键知识点、培训目标等信息,AI将基于您的描述生成相关考题。
- 您可以描述:
- 1. PPT的主要主题和内容
- 2. 关键知识点和重点
- 3. 培训目标和学习要求
- 4. 相关的技术要点和注意事项`
-
- resolve(fallbackText)
- }
-
- } catch (error) {
- console.error('PPT处理失败:', error)
-
- // 如果处理失败,返回文件信息
- const fallbackText = `PPT文件:${file.name}
- 文件大小:${formatFileSize(file.size)}
- 文件类型:${file.type}
- 修改时间:${new Date(file.lastModified).toLocaleString('zh-CN')}
- PPT文件处理失败,请手动描述PPT的主要内容、关键知识点、培训目标等信息,AI将基于您的描述生成相关考题。
- 您可以描述:
- 1. PPT的主要主题和内容
- 2. 关键知识点和重点
- 3. 培训目标和学习要求
- 4. 相关的技术要点和注意事项`
-
- resolve(fallbackText)
- }
- }
-
- reader.onerror = () => {
- reject(new Error('文件读取失败'))
- }
-
- // 读取文件为ArrayBuffer
- reader.readAsArrayBuffer(file)
- })
-
- } catch (error) {
- console.error('PPT文件读取失败,详细错误:', error)
- console.error('错误堆栈:', error.stack)
-
- // 提供更具体的错误信息
- if (error.message.includes('Invalid file format')) {
- throw new Error('PPT文件格式无效或已损坏')
- } else if (error.message.includes('File is empty')) {
- throw new Error('PPT文件为空')
- } else {
- throw new Error(`PPT文件读取失败: ${error.message}`)
- }
- }
- }
- // 处理文件选择
- const handleFileSelect = async (event) => {
- const files = Array.from(event.target.files)
- if (!files || files.length === 0) return
-
- isUploadingFile.value = true
- let successCount = 0;
-
- for (const file of files) {
- try {
- // 验证文件
- const fileExtension = validateFile(file)
- console.log('开始读取文件内容:', file.name)
-
- // 处理PPT文档
- const extractedContent = await readPPTFile(file)
-
- // 创建文件信息对象
- uploadedFiles.value.push({
- file,
- name: file.name,
- size: file.size,
- type: fileExtension,
- icon: getFileIcon(fileExtension),
- content: extractedContent // 存储提取的内容
- })
- successCount++;
-
- // 如果是第一个上传的文件,且当前试卷名称还是默认状态或为空,使用该文件名作为试卷名称
- if (uploadedFiles.value.length === 1 && (!examName.value || examName.value.includes('工程施工技术考核'))) {
- const fileNameWithoutExt = file.name.replace(/\.(ppt|pptx)$/i, '')
- examName.value = `${fileNameWithoutExt}考试试卷`
- }
-
- } catch (error) {
- console.error(`文件 ${file.name} 读取失败:`, error)
- ElMessage.error(`${file.name}读取失败: ${error.message || '请重试'}`)
- }
- }
-
- if (successCount > 0) {
- ElMessage.success(`成功读取了 ${successCount} 个文件`)
- }
-
- isUploadingFile.value = false
- event.target.value = ''
- }
- // 删除选中的文件
- const removeSelectedFile = (index) => {
- if (index >= 0 && index < uploadedFiles.value.length) {
- uploadedFiles.value.splice(index, 1)
-
- // 如果全部删除了,重置相关状态
- if (uploadedFiles.value.length === 0) {
- pptContentDescription.value = ''
- const projectTypeName = projectTypes[selectedProjectType.value]?.name || '桥梁'
- examName.value = `${projectTypeName}工程施工技术考核`
- }
- }
- }
- // 触发文件上传
- const triggerFileUpload = () => {
- fileInput.value?.click()
- }
- // 删除确认弹窗处理函数
- const handleDeleteConfirm = () => {
- // 处理删除确认逻辑
- showDeleteModal.value = false;
- ElMessage.success('删除成功');
- };
- const handleDeleteCancel = () => {
- showDeleteModal.value = false;
- };
- // 生命周期钩子
- onMounted(async () => {
- // 保存初始配置
- initialConfig = {
- questionTypes: JSON.parse(JSON.stringify(questionTypes.value)),
- totalScore: totalScore.value,
- selectedProjectType: selectedProjectType.value,
- examName: examName.value
- };
- console.log('初始配置已保存:', initialConfig);
-
- // 获取历史记录列表
- await getHistoryRecordList()
-
- // 检查URL参数是否有historyId需要加载
- const historyId = route.query.historyId
- if (historyId) {
- const targetItem = historyData.value.find(item => String(item.id) === String(historyId))
- if (targetItem) {
- await handleHistoryItem(targetItem)
- }
- }
-
- // 添加全局点击事件监听器
- document.addEventListener('click', handleClickOutside);
-
- })
- onUnmounted(() => {
- // 清理事件监听器
- document.removeEventListener('click', handleClickOutside);
- })
- </script>
- <style lang="less" scoped>
- // 删除图标样式
- .delete-icon {
- width: 16px;
- height: 16px;
- }
- .chat-container {
- display: flex;
- height: 100vh;
- font-family: "Alibaba PuHuiTi 3.0", sans-serif;
- }
- /* 中间历史记录栏 */
- .history-sidebar {
- width: 280px;
- background: #e8f0ff;
- display: flex;
- flex-direction: column;
- }
- /* 中间历史记录栏样式 */
- .history-sidebar {
- padding: 24px 16px 0 16px;
- .history-header {
- background: transparent;
-
- .section-title {
- font-size: 16px;
- font-weight: 600;
- color: #2c3e50;
- }
-
- .new-chat-btn {
- width: 248px;
- height: 40px;
- cursor: pointer;
- transition: opacity 0.3s ease;
- object-fit: contain;
- display: block;
- margin-top: 16px;
- margin-bottom: 14px;
-
- &:hover {
- opacity: 0.8;
- }
- }
- }
-
- .history-list {
- flex: 1;
- overflow-y: auto;
- width: 248px;
- height: 64px;
-
- /* 隐藏滚动条 */
- &::-webkit-scrollbar {
- display: none;
- }
-
- -ms-overflow-style: none; /* IE and Edge */
- scrollbar-width: none; /* Firefox */
-
- .history-item {
- background: white;
- border-radius: 8px;
- padding: 15px;
- margin-bottom: 14px;
- cursor: pointer;
- transition: all 0.3s ease;
- border-left: 3px solid transparent;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- display: flex;
- align-items: center;
- justify-content: space-between;
- position: relative;
-
- &:hover {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
-
- .delete-btn {
- opacity: 1;
- visibility: visible;
- }
- }
-
- &.active {
- border-left-color: #3e7bfa;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
- cursor: default;
- opacity: 0.8;
-
- &:hover {
- transform: none;
- box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
- }
- }
-
- .history-content {
- flex: 1;
- min-width: 0; // 允许内容收缩
- }
-
- .history-title {
- font-size: 14px;
- line-height: 1.4;
- margin-bottom: 14px;
- color: #2c3e50;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .history-time {
- font-size: 12px;
- color: #7c8db5;
- }
-
- .delete-btn {
- opacity: 0;
- visibility: hidden;
- transition: all 0.2s ease;
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
- color: #7c8db5;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-left: 8px;
- flex-shrink: 0;
-
- &:hover {
- color: #ff4757;
- background-color: rgba(255, 71, 87, 0.1);
- }
-
- &.always-visible {
- opacity: 1;
- visibility: visible;
- }
- }
- }
-
- .empty-history {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- margin-top: 236px;
-
- .empty-icon {
- width: 147px;
- height: 148px;
- object-fit: contain;
- margin-bottom: 14px;
- }
-
- .empty-text {
- font-size: 16px;
- color: #9C9FA7;
- text-align: center;
- }
- }
- }
- }
- /* 主工作区域 */
- .main-work {
- flex: 1;
- background: #ebf3ff;
- display: flex;
- flex-direction: column;
-
- &.exam-detail-mode {
- background: transparent;
- }
- }
- /* 工作头部 */
- .work-header {
- background: transparent;
- padding: 40px 0px 0px 18px;
-
- h2 {
- margin: 0;
- font-size: 25px;
- font-weight: 600;
- color: #2c3e50;
- }
- }
- /* 工作内容区域 */
- .work-content {
- flex: 1;
- padding: 0;
- // overflow-y: auto;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- height: 100%;
-
- /* 隐藏滚动条样式 */
- &::-webkit-scrollbar {
- width: 0;
- background: transparent;
- }
-
- &::-webkit-scrollbar-track {
- background: transparent;
- }
-
- &::-webkit-scrollbar-thumb {
- background: transparent;
- }
- }
- .app-container {
- --primary-color: #0d6efd;
- --danger-color: #dc3545;
- --warning-color: #ffc107;
- --border-color: #dee2e6;
- --bg-light: #f8f9fa;
- --text-dark: #212529;
- --text-muted: #6c757d;
- display: flex;
- height: 100%;
- width: 100%;
- padding: 0 !important;
- background-color: #f5f5f5;
- overflow: hidden;
- margin: 0 !important;
- max-width: 100% !important;
- border-radius: 0 !important;
- box-shadow: none !important;
- /* 中间主操作区 */
- .main-content {
- flex: 1;
- padding: 20px;
- overflow-y: hidden;
- background: white;
- scrollbar-width: none; /* Firefox */
- -ms-overflow-style: none; /* IE and Edge */
- }
- .main-content::-webkit-scrollbar {
- display: none; /* Chrome, Safari and Opera */
- }
- .form-group {
- margin-bottom: 12px;
- max-width: 1150px; /* 限制输入框模块的最大宽度 */
- margin-left: auto;
- margin-right: auto; /* 使其在工作区居中 */
- }
- .form-label {
- font-weight: 600;
- margin-bottom: 6px;
- display: block;
- font-size: 14px;
- }
- .form-control {
- width: 100%;
- border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
- border-radius: 12px; /* 统一圆角 */
- padding: 12px 16px; /* 稍微增加内边距让它看起来更像卡片 */
- font-size: 14px;
- transition: all 0.3s;
- box-sizing: border-box;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
- }
- .form-control:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
- }
- textarea.form-control {
- resize: none;
- height: 250px;
- }
- .char-count {
- text-align: right;
- font-size: 12px;
- color: var(--text-muted);
- margin-top: 4px;
- }
- /* PPT上传区域 */
- .ppt-upload-section {
- background: white;
- border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
- border-radius: 12px; /* 统一圆角 */
- padding: 16px 20px;
- margin-top: 40px;
- cursor: pointer;
- transition: all 0.3s;
- display: flex;
- align-items: center;
- justify-content: space-between;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
- position: relative;
- }
- .ppt-upload-section:hover {
- border-color: var(--primary-color);
- box-shadow: 0 8px 24px rgba(13, 110, 253, 0.12); /* 悬浮时加深发光阴影 */
- }
- .ppt-upload-content {
- display: flex;
- align-items: center;
- gap: 16px;
- }
- .ppt-upload-icon-wrapper {
- width: 48px;
- height: 48px;
- background: #f3f4f6;
- border-radius: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
- .ppt-upload-text-wrapper {
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
- .ppt-upload-title {
- font-size: 15px;
- font-weight: 600;
- color: #1f2937;
- margin-bottom: 4px;
- }
- .ppt-upload-hint {
- font-size: 13px;
- color: #6b7280;
- }
- .ppt-arrow {
- color: #9ca3af;
- font-size: 24px;
- transition: transform 0.3s;
- }
-
- .ppt-upload-section:hover .ppt-arrow {
- color: var(--primary-color);
- transform: translateX(2px);
- }
- .file-status-badge {
- background: #ebf3ff;
- color: var(--primary-color);
- padding: 5px 12px;
- border-radius: 8px;
- font-size: 10px;
- display: flex;
- align-items: center;
- gap: 8px;
- border: 1px solid rgba(13, 110, 253, 0.1);
- max-width: 100%;
- margin-top: 12px;
- }
- .file-name {
- font-weight: 500;
- }
- .remove-btn {
- color: #ef4444;
- font-size: 16px;
- font-weight: bold;
- cursor: pointer;
- padding: 0 4px;
- }
- .remove-btn:hover {
- color: #b91c1c;
- }
- /* =============== 题型配置区域 样式开始 =============== */
- .config-section {
- margin-top: 16px;
- }
- .config-header {
- display: flex;
- justify-content: center; /* 改为靠左对齐,而不是两端对齐 */
- align-items: center;
- gap: 960px; /* 控制“题型配置”和“试卷总分”之间的固定间距 */
- margin-bottom: 6px;
- }
- .config-header h3 {
- font-size: 18px;
- font-weight: 600;
- }
- .total-score {
- background: var(--bg-light);
- padding: 8px 16px;
- border-radius: 20px;
- font-size: 14px;
- font-weight: 600;
- color: var(--primary-color);
- }
- .question-types-grid {
- display: grid;
- /* 为了减小卡片宽度,我们不再让它们自动拉伸占满,而是指定最大宽度并居中,或者留出更大的列间距 */
- grid-template-columns: repeat(2, minmax(0, 500px));
- justify-content: center; /* 让网格居中,而不是两端拉伸 */
- gap: 20px 150px; /* 行间距(高度)20px,列间距(宽度)40px */
- margin-bottom: 12px;
- }
- .question-type-card {
- background: white; /* 改为白色背景 */
- border-radius: 12px;
- padding: 16px 20px; /* 根据截图稍微调大内边距以容纳阴影内容 */
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 加深阴影 */
- border: 1px solid rgba(0, 0, 0, 0.06); /* 稍微加深边框线 */
- }
- .question-type-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px; /* 稍微增加与滑动条的间距 */
- }
- .question-type-title {
- font-weight: 600;
- font-size: 16px; /* 字体稍微调大 */
- color: #1f2937;
- }
- .question-type-score {
- font-size: 13px;
- color: #3b82f6; /* 改变字体颜色为蓝色系 */
- background: #eff6ff; /* 添加淡蓝色背景 */
- padding: 4px 12px;
- border-radius: 20px; /* 胶囊形状 */
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .score-input {
- width: 32px;
- text-align: center;
- border: none; /* 移除输入框边框 */
- border-radius: 4px;
- padding: 0;
- font-size: 14px;
- font-weight: 600;
- color: #2563eb; /* 加深数字颜色 */
- background: transparent; /* 背景透明,融入胶囊 */
- transition: all 0.3s;
- -webkit-appearance: textfield;
- -moz-appearance: textfield;
- appearance: textfield;
- }
- .score-input::-webkit-outer-spin-button,
- .score-input::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
- }
- .score-input:focus {
- outline: none;
- background: white; /* 聚焦时背景变白 */
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
- }
- .slider-container {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .slider-label {
- font-size: 14px;
- color: #4b5563;
- font-weight: 500;
- min-width: 40px;
- }
- .question-slider {
- flex: 1;
- height: 6px;
- -webkit-appearance: none;
- appearance: none;
- background: #e5e7eb; /* 滑动条底色调浅 */
- border-radius: 3px;
- outline: none;
- }
- .question-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 18px; /* 滑块调大一点 */
- height: 18px;
- background: #2563eb; /* 蓝色滑块 */
- border: 2px solid white; /* 添加白色边框 */
- box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 滑块阴影 */
- border-radius: 50%;
- cursor: pointer;
- transition: all 0.3s;
- }
- .question-slider::-webkit-slider-thumb:hover {
- background: #1d4ed8;
- transform: scale(1.1);
- }
- .question-count {
- font-weight: bold; /* 数字加粗 */
- font-size: 15px;
- color: #1f2937;
- min-width: 40px;
- text-align: right;
- }
- .question-count-stepper {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 8px;
- min-width: 76px;
- }
- .stepper-buttons {
- display: flex;
- flex-direction: column;
- gap: 3px;
- }
- .stepper-btn {
- width: 10px;
- height: 7px;
- padding: 0;
- border: none;
- outline: none;
- appearance: none;
- -webkit-appearance: none;
- background: #2563eb;
- display: block;
- cursor: pointer;
- transition: opacity 0.2s ease, transform 0.2s ease;
- }
- .stepper-btn-up {
- clip-path: polygon(50% 0, 0 100%, 100% 100%);
- }
- .stepper-btn-down {
- clip-path: polygon(0 0, 100% 0, 50% 100%);
- }
- .stepper-btn:hover:not(:disabled) {
- transform: scale(1.08);
- }
- .stepper-btn:disabled {
- opacity: 0.35;
- cursor: not-allowed;
- }
- .action-buttons {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 16px;
- padding-top: 16px;
- border-top: 1px solid var(--border-color);
- }
- .clear-btn {
- background: white;
- border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
- border-radius: 8px; /* 添加圆角 */
- color: var(--text-muted);
- font-size: 14px;
- cursor: pointer;
- padding: 8px 16px;
- transition: all 0.3s;
- display: flex;
- align-items: center;
- gap: 6px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
- }
- .clear-btn:hover {
- color: var(--danger-color);
- border-color: rgba(220, 53, 69, 0.2); /* 悬浮时边框微红 */
- box-shadow: 0 6px 20px rgba(220, 53, 69, 0.1); /* 悬浮时带红色的发光阴影 */
- }
- .generate-btn {
- background: var(--primary-color);
- color: white;
- border: none;
- padding: 10px 24px;
- border-radius: 8px;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.3s;
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .generate-btn:hover:not(:disabled) {
- background: #0b5ed7;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(13, 110, 253, 0.3);
- }
-
- .generate-btn:disabled {
- background: #93c5fd; /* 浅蓝色背景 */
- cursor: not-allowed;
- box-shadow: 0 4px 12px rgba(147, 197, 253, 0.4); /* 浅蓝色阴影 */
- opacity: 0.9;
- }
- /* =============== 题型配置区域 样式结束 =============== */
- /* =============== 实时预览区域 样式开始 =============== */
- .preview-panel {
- width: 320px;
- background: #f7f9fb; /* 匹配截图背景 */
- border-left: 1px solid var(--border-color);
- padding: 24px;
- overflow-y: hidden;
- flex-shrink: 0;
- scrollbar-width: none;
- -ms-overflow-style: none;
- }
- .preview-panel::-webkit-scrollbar {
- display: none;
- }
- .preview-header {
- margin-bottom: 24px;
- }
- .preview-header h3 {
- font-size: 18px;
- font-weight: bold;
- color: #1f2937;
- margin: 0;
- }
- .preview-name-card {
- background: white;
- border-radius: 16px;
- padding: 16px 20px;
- margin-bottom: 24px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 加深阴影 */
- border: 1px solid rgba(0, 0, 0, 0.06); /* 稍微加深边框线 */
- }
- .preview-name-label {
- font-size: 13px;
- font-weight: 600;
- color: #4b5563;
- margin-bottom: 12px;
- }
- .preview-title {
- font-size: 15px;
- color: #9ca3af;
- line-height: 1.4;
- }
- .preview-section {
- margin-bottom: 24px;
- }
- .preview-section-title {
- font-size: 14px;
- font-weight: bold;
- color: #4b5563;
- margin-bottom: 16px;
- }
- .preview-item {
- margin-bottom: 16px;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .preview-item-top {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .preview-item-left {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .preview-dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- }
- .preview-type-name {
- font-size: 14px;
- color: #1f2937;
- }
- .preview-type-count {
- font-size: 14px;
- font-weight: 600;
- color: #1f2937;
- }
- .preview-item-bottom {
- padding-left: 14px; /* Align with text (6px dot + 8px gap) */
- }
- .preview-type-score {
- font-size: 12px;
- color: #9ca3af;
- }
- .preview-footer {
- margin-top: 24px;
- padding-top: 20px;
- border-top: 1px solid #e5e7eb;
- }
- .preview-total {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 15px;
- font-weight: 600;
- color: #000000;
- padding: 0 10px; /* 左右各加16px的内边距,使文字向中间靠拢 */
- }
- .preview-total-score {
- font-weight: bold;
- }
- /* =============== 实时预览区域 样式结束 =============== */
- }
- .exam-workshop-card {
- background: white;
- width: 100%;
- height: 100%;
- padding: 0;
- border-radius: 0;
- box-shadow: none;
- max-width: 100%;
- .config-section {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 40px;
- .config-item {
- .config-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 14px;
- .step-number {
- width: 24px;
- height: 24px;
- background: #3e7bfa;
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- font-weight: 600;
- }
- h3 {
- font-size: 18px;
- font-weight: 600;
- color: #1f2937;
- margin: 0;
- }
- }
- .type-cards {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- justify-content: space-between;
- .type-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 14px;
- padding: 14px;
- border: 2px solid #e5e7eb;
- border-radius: 12px;
- background: white;
- cursor: pointer;
- transition: all 0.3s ease;
- flex: 1;
- min-width: 180px;
- &:hover {
- border-color: #3e7bfa;
- transform: translateY(-2px);
- }
- &.active {
- border-color: #3e7bfa;
- background: rgba(62, 123, 250, 0.1);
- .type-icon {
- filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
- saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
- }
- span {
- color: #3e7bfa;
- }
- }
- .type-icon {
- width: 40px;
- height: 40px;
- transition: filter 0.3s ease;
- }
- span {
- font-size: 14px;
- color: #374151;
- font-weight: 500;
- transition: color 0.3s ease;
- }
- }
- }
- .generation-methods {
- display: flex;
- gap: 16px;
- .method-card {
- flex: 1;
- display: flex;
- align-items: center;
- gap: 14px;
- padding: 18px;
- border: 2px solid #e5e7eb;
- border-radius: 12px;
- background: white;
- cursor: pointer;
- transition: all 0.3s ease;
- position: relative;
- &:hover {
- border-color: #3e7bfa;
- }
- &.active {
- border-color: #3e7bfa;
- background: rgba(62, 123, 250, 0.1);
- .method-icon {
- filter: brightness(0) saturate(100%) invert(27%) sepia(51%)
- saturate(2878%) hue-rotate(199deg) brightness(104%) contrast(97%);
- }
- .method-content {
- h4 {
- color: #3e7bfa;
- }
- }
- }
- .method-icon {
- width: 32px;
- height: 32px;
- flex-shrink: 0;
- transition: filter 0.3s ease;
- }
- .method-content {
- h4 {
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 6px 0;
- transition: color 0.3s ease;
- }
-
- p {
- font-size: 14px;
- color: #6b7280;
- margin: 0;
- line-height: 1.5;
- }
-
- // PPT文件预览样式(绝对定位)
- .ppt-file-preview {
- position: absolute;
- top: 18px;
- right: 10px;
- width: 200px;
- z-index: 10;
-
- .file-preview {
- display: flex;
- align-items: center;
- background: white;
- border: 1px solid #E5E7EB;
- border-radius: 8px;
- padding: 12px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-
- .file-icon {
- font-size: 24px;
- margin-right: 12px;
- width: 32px;
- text-align: center;
- }
-
- .file-info {
- flex: 1;
-
- .file-name {
- font-size: 12px;
- font-weight: 500;
- color: #374151;
- margin-bottom: 2px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 150px;
- }
-
- .file-size {
- font-size: 10px;
- color: #6B7280;
- }
- }
-
- .remove-file-btn {
- width: 20px;
- height: 20px;
- border: none;
- background: rgba(239, 68, 68, 0.1);
- color: #DC2626;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
-
- &:hover {
- background: rgba(239, 68, 68, 0.2);
- transform: scale(1.1);
- }
-
- .remove-icon {
- font-size: 12px;
- font-weight: bold;
- }
- }
- }
- }
- }
- }
- }
- .exam-config-container {
- display: flex;
- gap: 20px;
- align-items: flex-start;
- .config-left {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 14px;
- position: relative;
- .section-title {
- font-size: 14px;
- color: #374151;
- font-weight: 500;
- }
- .config-row {
- display: flex;
- align-items: flex-start;
- gap: 40px;
- margin-bottom: 14px;
- .config-group {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 8px;
- label {
- font-size: 14px;
- font-weight: 500;
- color: #374151;
- }
- .input-wrapper {
- position: relative;
- width: 100%;
- }
- .config-input {
- width: 100%;
- padding: 12px 16px;
- padding-right: 60px;
- border: 2px solid #e5e7eb;
- border-radius: 8px;
- font-size: 14px;
- color: #374151;
- height: 42px;
- &:focus {
- outline: none;
- border-color: #3e7bfa;
- }
- }
- .char-count-inline {
- position: absolute;
- right: 16px;
- top: 50%;
- transform: translateY(-50%);
- font-size: 12px;
- color: #6b7280;
- pointer-events: none;
-
- &.warning {
- color: #f59e0b;
- }
- }
- .score-input {
- display: flex;
- align-items: center;
- gap: 8px;
- width: 180px;
- height: 42px;
- .config-input {
- width: 110px;
- padding: 8px 12px;
- height: 42px;
- }
- .unit {
- font-size: 14px;
- color: #374151;
- }
- }
- }
- }
- .question-types {
- .question-type {
- margin-bottom: 14px;
- .type-row {
- display: flex;
- align-items: center;
- gap: 16px;
- .type-name {
- font-size: 14px;
- font-weight: 500;
- color: #374151;
- min-width: 60px;
- }
- .progress-bar {
- flex: 1;
- height: 8px;
- background: #e5e7eb;
- border-radius: 4px;
- overflow: hidden;
- .progress-fill {
- height: 100%;
- background: #3e7bfa;
- transition: width 0.3s ease;
- border-radius: 4px;
- }
- }
- .score-config {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 14px;
- color: #6b7280;
- white-space: nowrap;
- .score-input-field,
- .count-input-field {
- width: 50px;
- padding: 4px 8px;
- border: 1px solid #d1d5db;
- border-radius: 4px;
- text-align: center;
- font-size: 14px;
- &:focus {
- outline: none;
- border-color: #3e7bfa;
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
- .preview-panel {
- width: 320px;
- background: #f8faff;
- border-radius: 12px;
- padding: 24px;
- flex-shrink: 0;
- .preview-header {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 24px;
- .preview-icon {
- width: 20px;
- height: 16px;
- }
- h3 {
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- margin: 0;
- }
- }
- .preview-content {
- background-color: #ffffff;
- border-radius: 8px;
- border: 1px solid #e5e7eb;
- padding: 10px 10px 16px 10px;
- .preview-title {
- font-size: 14px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 10px 0;
- text-align: center;
- }
- .question-breakdown {
- .breakdown-item {
- margin-bottom: 14px;
- .breakdown-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 12px;
- color: #374151;
- line-height: 1.5;
- .breakdown-left {
- flex: 1;
- }
- .breakdown-right {
- font-weight: 500;
- }
- }
- }
- }
- .divider {
- height: 1px;
- background: #e5e7eb;
- margin: 12px 0 8px 0;
- }
- .calculated-score-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 12px;
- font-weight: 500;
- color: #6b7280;
- margin-bottom: 14px;
- .calculated-label {
- color: #6b7280;
- }
- .calculated-value {
- color: #6b7280;
- }
- }
- .total-score-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 12px;
- font-weight: 600;
- color: #4b5563;
- .total-label {
- color: #374151;
- }
- .total-value {
- color: #4b5563;
- }
- }
- }
- }
- }
- .bottom-actions {
- display: flex;
- justify-content: center;
- gap: 24px;
- margin-top: 20px;
- width: 100%;
- .clear-btn {
- padding: 0;
- border: none;
- background: none;
- cursor: pointer;
- transition: opacity 0.3s ease;
- &:hover {
- opacity: 0.8;
- }
- .clear-icon {
- width: 128px;
- height: 50px;
- }
- }
- .generate-btn {
- padding: 0;
- border: none;
- background: none;
- cursor: pointer;
- transition: opacity 0.3s ease;
- &:hover:not(:disabled) {
- opacity: 0.8;
- }
-
- &:disabled {
- cursor: not-allowed;
- opacity: 0.6;
- }
- .generate-icon {
- width: 154px;
- height: 50px;
- }
-
- .generating-text {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 154px;
- height: 50px;
- text-align: center;
- background: #f3f4f6;
- color: #6b7280;
- border-radius: 8px;
- // border: 1px solid #e5e7eb;
- font-size: 16px;
- font-weight: 500;
-
- .loading-dots {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- margin-left: 2px;
-
- .dot {
- display: inline-block;
- width: 4px;
- height: 4px;
- border-radius: 50%;
- background: #6b7280;
- animation: loading-dot 1.4s infinite ease-in-out;
-
- &:nth-child(1) {
- animation-delay: 0s;
- }
-
- &:nth-child(2) {
- animation-delay: 0.2s;
- }
-
- &:nth-child(3) {
- animation-delay: 0.4s;
- }
- }
- }
- }
-
- @keyframes loading-dot {
- 0%, 40%, 100% {
- opacity: 0.3;
- transform: scale(0.8);
- }
- 20% {
- opacity: 1;
- transform: scale(1);
- }
- }
- }
- }
- /* 固定布局,支持横向滚动 */
- .chat-container {
- min-width: 1428px;
- }
- .work-content {
- min-width: 1428px;
- overflow-x: auto;
-
- &.exam-detail-mode {
- background: transparent;
- }
- }
- .exam-workshop-card {
- min-width: 1428px; /* 与AI写作保持一致 */
- }
- /* 考试详情页样式 */
- .exam-detail-card {
- // background: white;
- width: 100%;
- min-height: 800px;
- // padding: 32px;
- border-radius: 16px;
- // box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
- max-width: 1528px;
- min-width: 1428px;
- .detail-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24px;
- .header-left {
- .back-btn {
- border: none;
- background: transparent;
- color: #3E7BFA;
- font-size: 16px;
- cursor: pointer;
- transition: all 0.3s ease;
- &:hover {
- background: transparent;
- color: #3e7bfa;
- }
- .back-arrow {
- font-size: 16px;
- font-weight: bold;
- }
- }
- }
- .header-right {
- display: flex;
- align-items: center;
- gap: 24px;
- .save-btn {
- // padding: 8px;
- border: none;
- background: transparent;
- cursor: pointer;
- transition: opacity 0.3s ease;
- &:hover {
- opacity: 0.8;
- }
- .save-icon {
- width: 134px;
- height: 42px;
- }
- }
- .download-dropdown {
- position: relative;
- display: inline-block;
- &.disabled {
- opacity: 0.5;
- cursor: not-allowed;
- pointer-events: none;
- }
- .download-btn {
- // padding: 8px;
- border: none;
- background: transparent;
- cursor: pointer;
- transition: opacity 0.3s ease;
- &:hover {
- opacity: 0.8;
- }
- .download-icon {
- width: 107px;
- height: 34px;
- }
- }
- .dropdown-menu {
- position: absolute;
- top: 100%;
- right: 0;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- min-width: 180px;
- opacity: 0;
- visibility: hidden;
- transform: translateY(-10px);
- transition: all 0.3s ease;
- .dropdown-item {
- padding: 12px 16px;
- cursor: pointer;
- transition: background-color 0.2s ease;
- border-bottom: 1px solid #f3f4f6;
- &:last-child {
- border-bottom: none;
- }
- &:hover {
- background: #f8fafc;
- }
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- pointer-events: none;
- }
- .item-text {
- font-size: 14px;
- color: #374151;
- }
- }
- }
- &.show .dropdown-menu {
- opacity: 1;
- visibility: visible;
- transform: translateY(0);
- }
- }
- }
- }
- .generation-time {
- font-size: 14px;
- color: #6B7280;
- }
- .exam-info {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: white;
- padding: 24px;
- border-radius: 12px;
- margin-bottom: 24px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- .exam-title {
- font-size: 24px;
- font-weight: 600;
- color: #1f2937;
- margin: 0 0 16px 0;
- text-align: left;
- }
- .exam-stats {
- display: flex;
- // justify-content: space-between;
- gap: 24px;
- align-items: center;
- font-size: 16px;
- color: #6b7280;
- .left-stats {
- display: flex;
- gap: 24px;
- align-items: center;
- }
-
- .total-score,
- .question-count {
- font-weight: 500;
- font-size: 14px;
- }
-
- }
- }
- .question-sections {
- display: flex;
- flex-direction: column;
- gap: 24px;
- .question-section {
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- overflow: hidden;
- .section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 14px 18px;
- background: #F9FAFB;
- cursor: pointer;
- transition: background-color 0.3s ease;
- &:hover {
- background: #e8f0ff;
- }
- .section-title {
- display: flex;
- align-items: center;
- gap: 12px;
- font-size: 16px;
- font-weight: 600;
- color: #1f2937;
- .section-number {
- width: 24px;
- height: 24px;
- background: #3e7bfa;
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- font-weight: bold;
- }
- .section-name {
- color: #1f2937;
- }
- .section-score {
- font-size: 14px;
- font-weight: 400;
- color: #6b7280;
- }
- }
- .section-controls {
- display: flex;
- align-items: center;
- gap: 16px;
- .question-count-text {
- font-size: 14px;
- color: #6b7280;
- }
- .toggle-icon {
- width: 24px;
- height: 24px;
- transition: transform 0.3s ease;
- &.expanded {
- transform: rotate(180deg);
- }
- }
- }
- }
- .section-content {
- padding: 14px;
- background: white;
- .question-item {
- margin-bottom: 14px;
- padding: 14px;
- border: 1px solid #f3f4f6;
- border-radius: 8px;
- background: #fafbfc;
- &:last-child {
- margin-bottom: 0;
- }
- .question-header {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- margin-bottom: 14px;
- .question-number {
- font-size: 16px;
- font-weight: 600;
- color: #3e7bfa;
- min-width: 24px;
- }
- .question-text {
- flex: 1;
- font-size: 16px;
- line-height: 1.6;
- color: #1f2937;
- }
- .refresh-btn {
- border: none;
- background: transparent;
- cursor: pointer;
- transition: all 0.3s ease;
- &:hover {
- background: transparent;
- opacity: 0.8;
- }
- flex-shrink: 0;
- &:hover {
- border-color: #3e7bfa;
- color: #3e7bfa;
- }
- .refresh-icon {
- width: 24px;
- height: 24px;
- }
- }
- }
- .options {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px 24px;
- .option {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 8px 0;
- border: none;
- background: transparent;
- cursor: pointer;
- transition: all 0.3s ease;
- &:hover {
- opacity: 0.8;
- }
- &.selected {
- color: #3e7bfa;
- .option-key {
- color: #3e7bfa;
- }
- }
- .radio-wrapper,
- .checkbox-wrapper {
- flex-shrink: 0;
-
- /* 重置浏览器默认样式 */
- input[type="radio"],
- input[type="checkbox"] {
- display: none !important;
- }
- }
- .radio-circle {
- width: 20px !important;
- height: 20px !important;
- border: 1px solid #d1d5db !important;
- border-radius: 50% !important;
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- transition: all 0.3s ease;
- background: white !important;
- flex-shrink: 0;
- position: relative !important;
- z-index: 1 !important;
- &.selected {
- border-color: #d1d5db !important;
- background: white !important;
- }
- .radio-dot {
- width: 12px !important;
- height: 12px !important;
- background: #3e7bfa !important;
- border-radius: 50% !important;
- }
- }
- .checkbox-square {
- width: 20px !important;
- height: 20px !important;
- border: 1px solid #d1d5db !important;
- border-radius: 4px !important;
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- transition: all 0.3s ease;
- background: white !important;
- flex-shrink: 0;
- &.selected {
- border-color: #3e7bfa !important;
- background: #3e7bfa !important;
- }
- .checkbox-check {
- color: white !important;
- font-size: 12px;
- font-weight: bold;
- }
- }
- .option-key {
- font-weight: 600;
- color: #6b7280;
- font-size: 16px;
- min-width: 24px;
- margin-right: 8px;
- font-family: "Alibaba PuHuiTi 3.0", sans-serif;
- transition: color 0.3s ease;
- }
- .option.selected .option-key {
- color: #3e7bfa;
- }
- .option-content {
- flex: 1;
- display: flex;
- align-items: center;
- gap: 4px;
- }
- .option-text {
- font-size: 16px;
- line-height: 1.5;
- color: #6b7280;
- font-family: "Alibaba PuHuiTi 3.0", sans-serif;
- transition: color 0.3s ease;
- }
- .option.selected .option-text {
- color: #374151;
- }
- .edit-option-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- border: none;
- background: transparent;
- cursor: pointer;
- border-radius: 4px;
- transition: all 0.2s ease;
- opacity: 0.8;
- flex-shrink: 0;
- &:hover {
- opacity: 1;
- background: #f3f4f6;
- }
- &:disabled {
- opacity: 0.3;
- cursor: not-allowed;
- }
- .edit-icon {
- width: 14px;
- height: 14px;
- }
- }
- }
- }
- .answer-box {
- .answer-outline {
- display: flex;
- flex-direction: column;
- gap: 16px;
- .outline-item {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- .outline-text {
- flex: 1;
- font-size: 16px;
- line-height: 1.6;
- color: #374151;
- padding: 12px;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 6px;
- margin-right: 0;
- }
- .edit-option-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- border: none;
- background: transparent;
- cursor: pointer;
- border-radius: 4px;
- transition: all 0.2s ease;
- opacity: 0.8;
- flex-shrink: 0;
- align-self: flex-start;
- margin-top: 12px;
- margin-left: 4px;
- &:hover {
- opacity: 1;
- background: #f3f4f6;
- }
- &:disabled {
- opacity: 0.3;
- cursor: not-allowed;
- }
- .edit-icon {
- width: 14px;
- height: 14px;
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
- /* 旋转动效样式 */
- .refresh-icon.rotating {
- animation: rotate 1s linear infinite;
- }
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- /* 禁用状态样式 */
- .type-card:disabled,
- .method-card:disabled,
- .config-input:disabled,
- .score-input-field:disabled,
- .count-input-field:disabled,
- .generate-btn:disabled,
- .back-btn:disabled,
- .save-btn:disabled,
- .download-btn:disabled,
- .refresh-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed !important;
- pointer-events: none;
- }
- /* 一键清除按钮禁用状态 - 保持原色,只改变鼠标状态 */
- .clear-btn:disabled {
- cursor: not-allowed !important;
- pointer-events: none;
- /* 不改变opacity,保持原色 */
- }
- /* 生成中状态的按钮样式 */
- .generate-btn:disabled {
- background: #f3f4f6;
- color: #9ca3af;
- }
- .generate-btn:disabled .generate-icon {
- opacity: 0.5;
- }
- /* 禁用状态的输入框样式 */
- .config-input:disabled,
- .score-input-field:disabled,
- .count-input-field:disabled {
- background: #f9fafb;
- color: #9ca3af;
- border-color: #d1d5db;
- }
- /* 禁用状态的选项样式 */
- .option[style*="cursor: not-allowed"] {
- opacity: 0.5;
- pointer-events: none;
- }
- /* 禁用状态的section header样式 */
- .section-header[style*="cursor: not-allowed"] {
- opacity: 0.7;
- }
- /* 生成中状态的视觉反馈 */
- .generating-text {
- color: #6b7280;
- font-weight: 500;
- }
- /* 编辑模态框样式 */
- .modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- .edit-modal {
- background: white;
- border-radius: 8px;
- width: 90%;
- max-width: 500px;
- max-height: 80vh;
- overflow: hidden;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
- }
- .modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 24px;
- border-bottom: 1px solid #e5e7eb;
- h3 {
- margin: 0;
- font-size: 18px;
- font-weight: 600;
- color: #111827;
- }
- .close-btn {
- background: none;
- border: none;
- font-size: 24px;
- cursor: pointer;
- color: #6b7280;
- padding: 0;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- &:hover {
- background: #f3f4f6;
- color: #374151;
- }
- }
- }
- .modal-body {
- padding: 24px;
- max-height: 60vh;
- overflow-y: auto;
- }
- .edit-section {
- display: flex;
- flex-direction: column;
- gap: 16px;
- label {
- font-weight: 500;
- color: #374151;
- font-size: 14px;
- }
- .edit-input {
- padding: 12px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- font-size: 16px;
- transition: border-color 0.2s ease;
- &:focus {
- outline: none;
- border-color: #3e7bfa;
- box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
- }
- }
- .text-validation {
- margin-top: 8px;
- text-align: right;
- .char-count {
- font-size: 12px;
- color: #6b7280;
- &.warning {
- color: #f59e0b;
- }
- }
- }
- .edit-textarea {
- padding: 12px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- font-size: 16px;
- min-height: 100px;
- resize: vertical;
- font-family: inherit;
- transition: border-color 0.2s ease;
- &:focus {
- outline: none;
- border-color: #3e7bfa;
- box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
- }
- }
- .answer-section {
- margin-top: 16px;
- padding-top: 16px;
- border-top: 1px solid #e5e7eb;
- .answer-options {
- margin-top: 8px;
- .radio-label,
- .checkbox-label {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color 0.2s ease;
- &:hover {
- background: #f9fafb;
- }
- input[type="radio"],
- input[type="checkbox"] {
- margin: 0;
- }
- }
- }
- }
- }
- .modal-footer {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- padding: 16px 24px;
- border-top: 1px solid #e5e7eb;
- background: #f9fafb;
- .btn {
- padding: 8px 16px;
- border-radius: 6px;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s ease;
- border: 1px solid transparent;
- &.btn-cancel {
- background: white;
- color: #374151;
- border-color: #d1d5db;
- &:hover {
- background: #f9fafb;
- }
- }
- &.btn-confirm {
- background: #3e7bfa;
- color: white;
- &:hover {
- background: #2563eb;
- }
- }
- }
- }
- /* 历史记录加载状态样式 */
- .history-loading {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- min-height: 200px;
- }
- .history-loading .loading-spinner {
- width: 32px;
- height: 32px;
- border: 3px solid #f3f3f3;
- border-top: 3px solid #409eff;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 16px auto;
- }
- .history-loading .loading-text {
- color: #6B7280;
- font-size: 14px;
- margin: 0;
- font-weight: 400;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- /* 工作区域加载遮罩样式 */
- .work-content {
- position: relative;
- }
- .loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(255, 255, 255, 0.9);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- .loading-overlay .loading-spinner {
- width: 40px;
- height: 40px;
- border: 4px solid #f3f3f3;
- border-top: 4px solid #409eff;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-bottom: 16px;
- }
- .loading-overlay p {
- color: #666;
- font-size: 16px;
- margin: 0;
- font-weight: 500;
- }
- .return-ai-btn {
- position: absolute;
- top: 10px;
- right: 20px;
- z-index: 100;
- background: white;
- border: 1px solid rgba(0, 0, 0, 0.06);
- border-radius: 12px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
- padding: 6px 16px;
- font-size: 13px;
- font-weight: 500;
- color: #2563eb; /* 改为蓝色文字 */
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 5px;
- transition: all 0.3s ease;
- height: 36px;
- box-sizing: border-box;
- }
- .return-ai-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .return-ai-btn:hover:not(:disabled) {
- box-shadow: 0 8px 24px rgba(13, 110, 253, 0.12);
- color: #0d6efd;
- border-color: rgba(13, 110, 253, 0.2);
- }
- .return-ai-btn.has-before::before {
- content: '←';
- font-size: 16px;
- font-weight: bold;
- }
- </style>
|