SafetyHazard.vue 558 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112121131211412115121161211712118121191212012121121221212312124121251212612127121281212912130121311213212133121341213512136121371213812139121401214112142121431214412145121461214712148121491215012151121521215312154121551215612157121581215912160121611216212163121641216512166121671216812169121701217112172121731217412175121761217712178121791218012181121821218312184121851218612187121881218912190121911219212193121941219512196121971219812199122001220112202122031220412205122061220712208122091221012211122121221312214122151221612217122181221912220122211222212223122241222512226122271222812229122301223112232122331223412235122361223712238122391224012241122421224312244122451224612247122481224912250122511225212253122541225512256122571225812259122601226112262122631226412265122661226712268122691227012271122721227312274122751227612277122781227912280122811228212283122841228512286122871228812289122901229112292122931229412295122961229712298122991230012301123021230312304123051230612307123081230912310123111231212313123141231512316123171231812319123201232112322123231232412325123261232712328123291233012331123321233312334123351233612337123381233912340123411234212343123441234512346123471234812349123501235112352123531235412355123561235712358123591236012361123621236312364123651236612367123681236912370123711237212373123741237512376123771237812379123801238112382123831238412385123861238712388123891239012391123921239312394123951239612397123981239912400124011240212403124041240512406124071240812409124101241112412124131241412415124161241712418124191242012421124221242312424124251242612427124281242912430124311243212433124341243512436124371243812439124401244112442124431244412445124461244712448124491245012451124521245312454124551245612457124581245912460124611246212463124641246512466124671246812469124701247112472124731247412475124761247712478124791248012481124821248312484124851248612487124881248912490124911249212493124941249512496124971249812499125001250112502125031250412505125061250712508125091251012511125121251312514125151251612517125181251912520125211252212523125241252512526125271252812529125301253112532125331253412535125361253712538125391254012541125421254312544125451254612547125481254912550125511255212553125541255512556125571255812559125601256112562125631256412565125661256712568125691257012571125721257312574125751257612577125781257912580125811258212583125841258512586125871258812589125901259112592125931259412595125961259712598125991260012601126021260312604126051260612607126081260912610126111261212613126141261512616126171261812619126201262112622126231262412625126261262712628126291263012631126321263312634126351263612637126381263912640126411264212643126441264512646126471264812649126501265112652126531265412655126561265712658126591266012661126621266312664126651266612667126681266912670126711267212673126741267512676126771267812679126801268112682126831268412685126861268712688126891269012691126921269312694126951269612697126981269912700127011270212703127041270512706127071270812709127101271112712127131271412715127161271712718127191272012721127221272312724127251272612727127281272912730127311273212733127341273512736127371273812739127401274112742127431274412745127461274712748127491275012751127521275312754127551275612757127581275912760127611276212763127641276512766127671276812769127701277112772127731277412775127761277712778127791278012781127821278312784127851278612787127881278912790127911279212793127941279512796127971279812799128001280112802128031280412805128061280712808128091281012811128121281312814128151281612817128181281912820128211282212823128241282512826128271282812829128301283112832128331283412835128361283712838128391284012841128421284312844128451284612847128481284912850128511285212853128541285512856128571285812859128601286112862128631286412865128661286712868128691287012871128721287312874128751287612877128781287912880128811288212883128841288512886128871288812889128901289112892128931289412895128961289712898128991290012901129021290312904129051290612907129081290912910129111291212913129141291512916129171291812919129201292112922129231292412925129261292712928129291293012931129321293312934129351293612937129381293912940129411294212943129441294512946129471294812949129501295112952129531295412955129561295712958129591296012961129621296312964129651296612967129681296912970129711297212973129741297512976129771297812979129801298112982129831298412985129861298712988129891299012991129921299312994129951299612997129981299913000130011300213003130041300513006130071300813009130101301113012130131301413015130161301713018130191302013021130221302313024130251302613027130281302913030130311303213033130341303513036130371303813039130401304113042130431304413045130461304713048130491305013051130521305313054130551305613057130581305913060130611306213063130641306513066130671306813069130701307113072130731307413075130761307713078130791308013081130821308313084130851308613087130881308913090130911309213093130941309513096130971309813099131001310113102131031310413105131061310713108131091311013111131121311313114131151311613117131181311913120131211312213123131241312513126131271312813129131301313113132131331313413135131361313713138131391314013141131421314313144131451314613147131481314913150131511315213153131541315513156131571315813159131601316113162131631316413165131661316713168131691317013171131721317313174131751317613177131781317913180131811318213183131841318513186131871318813189131901319113192131931319413195131961319713198131991320013201132021320313204132051320613207132081320913210132111321213213132141321513216132171321813219132201322113222132231322413225132261322713228132291323013231132321323313234132351323613237132381323913240132411324213243132441324513246132471324813249132501325113252132531325413255132561325713258132591326013261132621326313264132651326613267132681326913270132711327213273132741327513276132771327813279132801328113282132831328413285132861328713288132891329013291132921329313294132951329613297132981329913300133011330213303133041330513306133071330813309133101331113312133131331413315133161331713318133191332013321133221332313324133251332613327133281332913330133311333213333133341333513336133371333813339133401334113342133431334413345133461334713348133491335013351133521335313354133551335613357133581335913360133611336213363133641336513366133671336813369133701337113372133731337413375133761337713378133791338013381133821338313384133851338613387133881338913390133911339213393133941339513396133971339813399134001340113402134031340413405134061340713408134091341013411134121341313414134151341613417134181341913420134211342213423134241342513426134271342813429134301343113432134331343413435134361343713438134391344013441134421344313444134451344613447134481344913450134511345213453134541345513456134571345813459134601346113462134631346413465134661346713468134691347013471134721347313474134751347613477134781347913480134811348213483134841348513486134871348813489134901349113492134931349413495134961349713498134991350013501135021350313504135051350613507135081350913510135111351213513135141351513516135171351813519135201352113522135231352413525135261352713528135291353013531135321353313534135351353613537135381353913540135411354213543135441354513546135471354813549135501355113552135531355413555135561355713558135591356013561135621356313564135651356613567135681356913570135711357213573135741357513576135771357813579135801358113582135831358413585135861358713588135891359013591135921359313594135951359613597135981359913600136011360213603136041360513606136071360813609136101361113612136131361413615136161361713618136191362013621136221362313624136251362613627136281362913630136311363213633136341363513636136371363813639136401364113642136431364413645136461364713648136491365013651136521365313654136551365613657136581365913660136611366213663136641366513666136671366813669136701367113672136731367413675136761367713678136791368013681136821368313684136851368613687136881368913690136911369213693136941369513696136971369813699137001370113702137031370413705137061370713708137091371013711137121371313714137151371613717137181371913720137211372213723137241372513726137271372813729137301373113732137331373413735137361373713738137391374013741137421374313744137451374613747137481374913750137511375213753137541375513756137571375813759137601376113762137631376413765137661376713768137691377013771137721377313774137751377613777137781377913780137811378213783137841378513786137871378813789137901379113792137931379413795137961379713798137991380013801138021380313804138051380613807138081380913810138111381213813138141381513816138171381813819138201382113822138231382413825138261382713828138291383013831138321383313834138351383613837138381383913840138411384213843138441384513846138471384813849138501385113852138531385413855138561385713858138591386013861138621386313864138651386613867138681386913870138711387213873138741387513876138771387813879138801388113882138831388413885138861388713888138891389013891138921389313894138951389613897138981389913900139011390213903139041390513906139071390813909139101391113912139131391413915139161391713918139191392013921139221392313924139251392613927139281392913930139311393213933139341393513936139371393813939139401394113942139431394413945139461394713948139491395013951139521395313954139551395613957139581395913960139611396213963139641396513966139671396813969139701397113972139731397413975139761397713978139791398013981139821398313984139851398613987139881398913990139911399213993139941399513996139971399813999140001400114002140031400414005140061400714008140091401014011140121401314014140151401614017140181401914020140211402214023140241402514026140271402814029140301403114032140331403414035140361403714038140391404014041140421404314044140451404614047140481404914050140511405214053140541405514056140571405814059140601406114062140631406414065140661406714068140691407014071140721407314074140751407614077140781407914080140811408214083140841408514086140871408814089140901409114092140931409414095140961409714098140991410014101141021410314104141051410614107141081410914110141111411214113141141411514116141171411814119141201412114122141231412414125141261412714128141291413014131141321413314134141351413614137141381413914140141411414214143141441414514146141471414814149141501415114152141531415414155141561415714158141591416014161141621416314164141651416614167141681416914170141711417214173141741417514176141771417814179141801418114182141831418414185141861418714188141891419014191141921419314194141951419614197141981419914200142011420214203142041420514206142071420814209142101421114212142131421414215142161421714218142191422014221142221422314224142251422614227142281422914230142311423214233142341423514236142371423814239142401424114242142431424414245142461424714248142491425014251142521425314254142551425614257142581425914260142611426214263142641426514266142671426814269142701427114272142731427414275142761427714278142791428014281142821428314284142851428614287142881428914290142911429214293142941429514296142971429814299143001430114302143031430414305143061430714308143091431014311143121431314314143151431614317143181431914320143211432214323143241432514326143271432814329143301433114332143331433414335143361433714338143391434014341143421434314344143451434614347143481434914350143511435214353143541435514356143571435814359143601436114362143631436414365143661436714368143691437014371143721437314374143751437614377143781437914380143811438214383143841438514386143871438814389143901439114392143931439414395143961439714398143991440014401144021440314404144051440614407144081440914410144111441214413144141441514416144171441814419144201442114422144231442414425144261442714428144291443014431144321443314434144351443614437144381443914440144411444214443144441444514446144471444814449144501445114452144531445414455144561445714458144591446014461144621446314464144651446614467144681446914470144711447214473144741447514476144771447814479144801448114482144831448414485144861448714488144891449014491144921449314494144951449614497144981449914500145011450214503145041450514506145071450814509145101451114512145131451414515145161451714518145191452014521145221452314524145251452614527145281452914530145311453214533145341453514536145371453814539145401454114542145431454414545145461454714548145491455014551145521455314554145551455614557145581455914560145611456214563145641456514566145671456814569145701457114572145731457414575145761457714578145791458014581145821458314584145851458614587145881458914590145911459214593145941459514596145971459814599146001460114602146031460414605146061460714608146091461014611146121461314614146151461614617146181461914620146211462214623146241462514626146271462814629146301463114632146331463414635146361463714638146391464014641146421464314644146451464614647146481464914650146511465214653146541465514656146571465814659146601466114662146631466414665146661466714668146691467014671146721467314674146751467614677146781467914680146811468214683146841468514686146871468814689146901469114692146931469414695146961469714698146991470014701147021470314704147051470614707147081470914710147111471214713147141471514716147171471814719147201472114722147231472414725147261472714728147291473014731147321473314734147351473614737147381473914740147411474214743147441474514746147471474814749147501475114752147531475414755147561475714758147591476014761147621476314764147651476614767147681476914770147711477214773147741477514776147771477814779147801478114782147831478414785147861478714788147891479014791147921479314794147951479614797147981479914800148011480214803148041480514806148071480814809148101481114812148131481414815148161481714818148191482014821148221482314824148251482614827148281482914830148311483214833148341483514836148371483814839148401484114842148431484414845148461484714848148491485014851148521485314854148551485614857148581485914860148611486214863148641486514866148671486814869148701487114872148731487414875148761487714878148791488014881148821488314884148851488614887148881488914890148911489214893148941489514896148971489814899149001490114902149031490414905149061490714908149091491014911149121491314914149151491614917149181491914920149211492214923149241492514926149271492814929149301493114932149331493414935149361493714938149391494014941149421494314944149451494614947149481494914950149511495214953149541495514956149571495814959149601496114962149631496414965149661496714968149691497014971149721497314974149751497614977149781497914980149811498214983149841498514986149871498814989149901499114992149931499414995149961499714998149991500015001150021500315004150051500615007150081500915010150111501215013150141501515016150171501815019150201502115022150231502415025150261502715028150291503015031150321503315034150351503615037150381503915040150411504215043150441504515046150471504815049150501505115052150531505415055150561505715058150591506015061150621506315064150651506615067150681506915070150711507215073150741507515076150771507815079150801508115082150831508415085150861508715088150891509015091150921509315094150951509615097150981509915100151011510215103151041510515106151071510815109151101511115112151131511415115151161511715118151191512015121151221512315124151251512615127151281512915130151311513215133151341513515136151371513815139151401514115142151431514415145151461514715148151491515015151151521515315154151551515615157151581515915160151611516215163151641516515166151671516815169151701517115172151731517415175151761517715178151791518015181151821518315184151851518615187151881518915190151911519215193151941519515196151971519815199152001520115202152031520415205152061520715208152091521015211152121521315214152151521615217152181521915220152211522215223152241522515226152271522815229152301523115232152331523415235152361523715238152391524015241152421524315244152451524615247152481524915250152511525215253152541525515256152571525815259152601526115262152631526415265152661526715268152691527015271152721527315274152751527615277152781527915280152811528215283152841528515286152871528815289152901529115292152931529415295152961529715298152991530015301153021530315304153051530615307153081530915310153111531215313153141531515316153171531815319153201532115322153231532415325153261532715328153291533015331153321533315334153351533615337153381533915340153411534215343153441534515346153471534815349153501535115352153531535415355153561535715358153591536015361153621536315364153651536615367153681536915370153711537215373153741537515376153771537815379153801538115382153831538415385153861538715388153891539015391153921539315394153951539615397153981539915400154011540215403154041540515406154071540815409154101541115412154131541415415154161541715418154191542015421154221542315424154251542615427154281542915430154311543215433154341543515436154371543815439154401544115442154431544415445154461544715448154491545015451154521545315454154551545615457154581545915460154611546215463154641546515466154671546815469154701547115472154731547415475154761547715478154791548015481154821548315484154851548615487154881548915490154911549215493154941549515496154971549815499155001550115502155031550415505155061550715508155091551015511155121551315514155151551615517155181551915520155211552215523155241552515526155271552815529155301553115532155331553415535155361553715538155391554015541155421554315544155451554615547155481554915550155511555215553155541555515556155571555815559155601556115562155631556415565155661556715568155691557015571155721557315574155751557615577155781557915580155811558215583155841558515586155871558815589155901559115592155931559415595155961559715598155991560015601156021560315604156051560615607156081560915610156111561215613156141561515616156171561815619156201562115622156231562415625156261562715628156291563015631156321563315634156351563615637156381563915640156411564215643156441564515646156471564815649156501565115652156531565415655156561565715658156591566015661156621566315664156651566615667156681566915670156711567215673156741567515676156771567815679156801568115682156831568415685156861568715688156891569015691156921569315694156951569615697156981569915700157011570215703157041570515706157071570815709157101571115712157131571415715157161571715718157191572015721157221572315724157251572615727157281572915730157311573215733157341573515736157371573815739157401574115742157431574415745157461574715748157491575015751157521575315754157551575615757157581575915760157611576215763157641576515766157671576815769157701577115772157731577415775157761577715778157791578015781157821578315784157851578615787157881578915790157911579215793157941579515796157971579815799158001580115802158031580415805158061580715808158091581015811158121581315814158151581615817158181581915820158211582215823158241582515826158271582815829158301583115832158331583415835158361583715838158391584015841158421584315844158451584615847158481584915850158511585215853158541585515856158571585815859158601586115862158631586415865158661586715868158691587015871158721587315874158751587615877158781587915880158811588215883158841588515886158871588815889158901589115892158931589415895158961589715898158991590015901159021590315904159051590615907159081590915910159111591215913159141591515916159171591815919159201592115922159231592415925159261592715928159291593015931159321593315934159351593615937159381593915940159411594215943159441594515946159471594815949159501595115952159531595415955159561595715958159591596015961159621596315964159651596615967159681596915970159711597215973159741597515976159771597815979159801598115982159831598415985159861598715988159891599015991159921599315994159951599615997159981599916000160011600216003160041600516006160071600816009160101601116012160131601416015160161601716018160191602016021160221602316024160251602616027160281602916030160311603216033160341603516036160371603816039160401604116042160431604416045160461604716048160491605016051160521605316054160551605616057160581605916060160611606216063160641606516066160671606816069160701607116072160731607416075160761607716078160791608016081160821608316084160851608616087160881608916090160911609216093160941609516096160971609816099161001610116102161031610416105161061610716108161091611016111161121611316114161151611616117161181611916120161211612216123161241612516126161271612816129161301613116132161331613416135161361613716138161391614016141161421614316144161451614616147161481614916150161511615216153161541615516156161571615816159161601616116162161631616416165161661616716168161691617016171161721617316174161751617616177161781617916180161811618216183161841618516186161871618816189161901619116192161931619416195161961619716198161991620016201162021620316204162051620616207162081620916210162111621216213162141621516216162171621816219162201622116222162231622416225162261622716228162291623016231162321623316234162351623616237162381623916240162411624216243162441624516246162471624816249162501625116252162531625416255162561625716258162591626016261162621626316264162651626616267162681626916270162711627216273162741627516276162771627816279162801628116282162831628416285162861628716288162891629016291162921629316294162951629616297162981629916300163011630216303163041630516306163071630816309163101631116312163131631416315163161631716318163191632016321163221632316324163251632616327163281632916330163311633216333163341633516336163371633816339163401634116342163431634416345163461634716348163491635016351163521635316354163551635616357163581635916360163611636216363163641636516366163671636816369163701637116372163731637416375163761637716378163791638016381163821638316384163851638616387163881638916390163911639216393163941639516396163971639816399164001640116402164031640416405164061640716408164091641016411164121641316414164151641616417164181641916420164211642216423164241642516426164271642816429164301643116432164331643416435164361643716438164391644016441164421644316444164451644616447164481644916450164511645216453164541645516456164571645816459164601646116462164631646416465164661646716468164691647016471164721647316474164751647616477164781647916480164811648216483164841648516486164871648816489164901649116492164931649416495164961649716498164991650016501165021650316504165051650616507165081650916510165111651216513165141651516516165171651816519165201652116522165231652416525165261652716528165291653016531165321653316534165351653616537165381653916540165411654216543165441654516546165471654816549165501655116552165531655416555165561655716558165591656016561165621656316564165651656616567165681656916570165711657216573165741657516576165771657816579165801658116582165831658416585165861658716588165891659016591165921659316594165951659616597165981659916600166011660216603166041660516606166071660816609166101661116612166131661416615166161661716618166191662016621166221662316624166251662616627166281662916630166311663216633166341663516636166371663816639166401664116642166431664416645166461664716648166491665016651166521665316654166551665616657166581665916660166611666216663166641666516666166671666816669166701667116672166731667416675166761667716678166791668016681166821668316684166851668616687166881668916690166911669216693166941669516696166971669816699167001670116702167031670416705167061670716708167091671016711167121671316714167151671616717167181671916720167211672216723167241672516726167271672816729167301673116732167331673416735167361673716738167391674016741167421674316744167451674616747167481674916750167511675216753167541675516756167571675816759167601676116762167631676416765167661676716768167691677016771167721677316774167751677616777167781677916780167811678216783167841678516786167871678816789167901679116792167931679416795167961679716798167991680016801168021680316804168051680616807168081680916810168111681216813168141681516816168171681816819168201682116822168231682416825168261682716828168291683016831168321683316834168351683616837168381683916840168411684216843168441684516846168471684816849168501685116852168531685416855168561685716858168591686016861168621686316864168651686616867168681686916870168711687216873168741687516876168771687816879168801688116882168831688416885168861688716888168891689016891168921689316894168951689616897168981689916900169011690216903169041690516906169071690816909169101691116912169131691416915169161691716918169191692016921169221692316924169251692616927169281692916930169311693216933169341693516936169371693816939169401694116942169431694416945169461694716948169491695016951169521695316954169551695616957169581695916960169611696216963169641696516966169671696816969169701697116972169731697416975169761697716978169791698016981169821698316984169851698616987169881698916990169911699216993169941699516996169971699816999170001700117002170031700417005170061700717008170091701017011170121701317014170151701617017170181701917020170211702217023170241702517026170271702817029170301703117032170331703417035170361703717038170391704017041170421704317044170451704617047170481704917050170511705217053170541705517056170571705817059170601706117062170631706417065170661706717068170691707017071170721707317074170751707617077170781707917080170811708217083170841708517086170871708817089170901709117092170931709417095170961709717098170991710017101171021710317104171051710617107171081710917110171111711217113171141711517116171171711817119171201712117122171231712417125171261712717128171291713017131171321713317134171351713617137171381713917140171411714217143171441714517146171471714817149171501715117152171531715417155171561715717158171591716017161171621716317164171651716617167171681716917170171711717217173171741717517176171771717817179171801718117182171831718417185171861718717188171891719017191171921719317194171951719617197171981719917200172011720217203172041720517206172071720817209172101721117212172131721417215172161721717218172191722017221172221722317224172251722617227172281722917230172311723217233172341723517236172371723817239172401724117242172431724417245172461724717248172491725017251172521725317254172551725617257172581725917260172611726217263172641726517266172671726817269172701727117272172731727417275172761727717278172791728017281172821728317284172851728617287172881728917290172911729217293172941729517296172971729817299173001730117302173031730417305173061730717308173091731017311173121731317314173151731617317173181731917320173211732217323173241732517326173271732817329173301733117332173331733417335173361733717338173391734017341173421734317344173451734617347173481734917350173511735217353173541735517356173571735817359173601736117362173631736417365173661736717368173691737017371173721737317374173751737617377173781737917380173811738217383173841738517386173871738817389173901739117392173931739417395173961739717398173991740017401174021740317404174051740617407174081740917410174111741217413174141741517416174171741817419174201742117422174231742417425174261742717428174291743017431174321743317434174351743617437174381743917440174411744217443174441744517446174471744817449174501745117452174531745417455174561745717458174591746017461174621746317464174651746617467174681746917470174711747217473174741747517476174771747817479174801748117482174831748417485174861748717488174891749017491174921749317494174951749617497174981749917500175011750217503175041750517506175071750817509175101751117512175131751417515175161751717518175191752017521175221752317524175251752617527175281752917530175311753217533175341753517536175371753817539175401754117542175431754417545175461754717548175491755017551175521755317554175551755617557175581755917560175611756217563175641756517566175671756817569175701757117572175731757417575175761757717578175791758017581175821758317584175851758617587175881758917590175911759217593175941759517596175971759817599176001760117602176031760417605176061760717608176091761017611176121761317614176151761617617176181761917620176211762217623176241762517626176271762817629176301763117632176331763417635176361763717638176391764017641176421764317644176451764617647176481764917650176511765217653176541765517656176571765817659176601766117662176631766417665176661766717668176691767017671176721767317674176751767617677176781767917680176811768217683176841768517686176871768817689176901769117692176931769417695176961769717698176991770017701177021770317704177051770617707177081770917710177111771217713177141771517716177171771817719177201772117722177231772417725177261772717728177291773017731177321773317734177351773617737177381773917740177411774217743177441774517746177471774817749177501775117752177531775417755177561775717758177591776017761177621776317764177651776617767177681776917770177711777217773177741777517776177771777817779177801778117782177831778417785177861778717788177891779017791177921779317794177951779617797177981779917800178011780217803178041780517806178071780817809178101781117812178131781417815178161781717818178191782017821178221782317824178251782617827178281782917830178311783217833178341783517836178371783817839178401784117842178431784417845178461784717848178491785017851178521785317854178551785617857178581785917860178611786217863178641786517866178671786817869178701787117872178731787417875178761787717878178791788017881178821788317884178851788617887178881788917890178911789217893178941789517896178971789817899179001790117902179031790417905179061790717908179091791017911179121791317914179151791617917179181791917920179211792217923179241792517926179271792817929179301793117932179331793417935179361793717938179391794017941179421794317944179451794617947179481794917950179511795217953179541795517956179571795817959179601796117962179631796417965179661796717968179691797017971179721797317974179751797617977179781797917980179811798217983179841798517986179871798817989179901799117992179931799417995179961799717998179991800018001180021800318004180051800618007180081800918010180111801218013180141801518016180171801818019180201802118022180231802418025180261802718028180291803018031180321803318034180351803618037180381803918040180411804218043180441804518046180471804818049180501805118052180531805418055180561805718058180591806018061180621806318064180651806618067180681806918070180711807218073180741807518076180771807818079180801808118082180831808418085180861808718088180891809018091180921809318094180951809618097180981809918100181011810218103181041810518106181071810818109181101811118112181131811418115181161811718118181191812018121181221812318124181251812618127181281812918130181311813218133181341813518136181371813818139181401814118142181431814418145181461814718148181491815018151181521815318154181551815618157181581815918160181611816218163181641816518166181671816818169181701817118172181731817418175181761817718178181791818018181181821818318184181851818618187181881818918190181911819218193181941819518196181971819818199182001820118202182031820418205182061820718208182091821018211182121821318214182151821618217182181821918220182211822218223182241822518226182271822818229182301823118232182331823418235182361823718238182391824018241182421824318244182451824618247182481824918250182511825218253182541825518256182571825818259182601826118262182631826418265182661826718268182691827018271182721827318274182751827618277182781827918280182811828218283182841828518286182871828818289182901829118292182931829418295182961829718298182991830018301183021830318304183051830618307183081830918310183111831218313183141831518316183171831818319183201832118322183231832418325183261832718328183291833018331183321833318334183351833618337183381833918340183411834218343183441834518346183471834818349183501835118352183531835418355183561835718358183591836018361183621836318364183651836618367183681836918370183711837218373183741837518376183771837818379183801838118382183831838418385183861838718388183891839018391183921839318394183951839618397183981839918400184011840218403184041840518406184071840818409184101841118412184131841418415184161841718418184191842018421184221842318424184251842618427184281842918430184311843218433184341843518436184371843818439184401844118442184431844418445184461844718448184491845018451184521845318454184551845618457184581845918460184611846218463184641846518466184671846818469184701847118472184731847418475184761847718478184791848018481184821848318484184851848618487184881848918490184911849218493184941849518496184971849818499185001850118502185031850418505185061850718508185091851018511185121851318514185151851618517185181851918520185211852218523185241852518526185271852818529185301853118532185331853418535185361853718538185391854018541185421854318544185451854618547185481854918550185511855218553185541855518556185571855818559185601856118562185631856418565185661856718568185691857018571185721857318574185751857618577185781857918580185811858218583185841858518586185871858818589185901859118592185931859418595185961859718598185991860018601186021860318604186051860618607186081860918610186111861218613186141861518616186171861818619186201862118622186231862418625186261862718628186291863018631186321863318634186351863618637186381863918640186411864218643186441864518646186471864818649186501865118652186531865418655186561865718658186591866018661186621866318664186651866618667186681866918670186711867218673186741867518676186771867818679186801868118682186831868418685186861868718688186891869018691186921869318694186951869618697186981869918700187011870218703187041870518706187071870818709187101871118712187131871418715187161871718718187191872018721187221872318724187251872618727187281872918730187311873218733187341873518736187371873818739187401874118742187431874418745187461874718748187491875018751187521875318754187551875618757187581875918760187611876218763187641876518766187671876818769187701877118772187731877418775187761877718778187791878018781187821878318784187851878618787187881878918790187911879218793187941879518796187971879818799188001880118802188031880418805188061880718808188091881018811188121881318814188151881618817188181881918820188211882218823188241882518826188271882818829188301883118832188331883418835188361883718838188391884018841188421884318844188451884618847188481884918850188511885218853188541885518856188571885818859188601886118862188631886418865188661886718868188691887018871188721887318874188751887618877188781887918880188811888218883188841888518886188871888818889188901889118892188931889418895188961889718898188991890018901189021890318904189051890618907189081890918910189111891218913189141891518916189171891818919189201892118922189231892418925189261892718928189291893018931189321893318934189351893618937189381893918940189411894218943189441894518946189471894818949189501895118952189531895418955189561895718958189591896018961189621896318964189651896618967189681896918970189711897218973189741897518976189771897818979189801898118982189831898418985189861898718988189891899018991189921899318994189951899618997189981899919000190011900219003190041900519006190071900819009190101901119012190131901419015190161901719018190191902019021190221902319024190251902619027190281902919030190311903219033190341903519036190371903819039190401904119042190431904419045190461904719048190491905019051190521905319054190551905619057190581905919060190611906219063190641906519066190671906819069190701907119072190731907419075190761907719078190791908019081190821908319084190851908619087190881908919090190911909219093190941909519096190971909819099191001910119102191031910419105191061910719108191091911019111191121911319114191151911619117191181911919120191211912219123191241912519126191271912819129191301913119132191331913419135191361913719138191391914019141191421914319144191451914619147191481914919150191511915219153191541915519156191571915819159191601916119162191631916419165191661916719168191691917019171191721917319174191751917619177191781917919180191811918219183191841918519186191871918819189191901919119192191931919419195191961919719198191991920019201192021920319204192051920619207192081920919210192111921219213192141921519216192171921819219192201922119222192231922419225192261922719228192291923019231192321923319234192351923619237192381923919240192411924219243192441924519246192471924819249192501925119252192531925419255192561925719258192591926019261192621926319264192651926619267192681926919270192711927219273192741927519276192771927819279192801928119282192831928419285192861928719288192891929019291192921929319294192951929619297192981929919300193011930219303193041930519306193071930819309193101931119312193131931419315193161931719318193191932019321193221932319324193251932619327193281932919330193311933219333193341933519336193371933819339193401934119342193431934419345193461934719348193491935019351193521935319354193551935619357193581935919360193611936219363193641936519366193671936819369193701937119372193731937419375193761937719378193791938019381193821938319384193851938619387193881938919390193911939219393193941939519396193971939819399194001940119402194031940419405194061940719408194091941019411194121941319414194151941619417194181941919420194211942219423194241942519426194271942819429194301943119432194331943419435194361943719438194391944019441194421944319444194451944619447194481944919450194511945219453194541945519456194571945819459194601946119462194631946419465194661946719468194691947019471194721947319474194751947619477194781947919480194811948219483194841948519486194871948819489194901949119492194931949419495194961949719498194991950019501195021950319504195051950619507195081950919510195111951219513195141951519516195171951819519195201952119522195231952419525195261952719528195291953019531195321953319534195351953619537195381953919540195411954219543195441954519546195471954819549195501955119552195531955419555195561955719558195591956019561195621956319564195651956619567195681956919570195711957219573195741957519576195771957819579195801958119582195831958419585195861958719588195891959019591195921959319594195951959619597195981959919600196011960219603196041960519606196071960819609196101961119612196131961419615196161961719618196191962019621196221962319624196251962619627196281962919630196311963219633196341963519636196371963819639196401964119642196431964419645196461964719648196491965019651196521965319654196551965619657196581965919660196611966219663196641966519666196671966819669196701967119672196731967419675196761967719678196791968019681196821968319684196851968619687196881968919690196911969219693196941969519696196971969819699197001970119702197031970419705197061970719708197091971019711197121971319714197151971619717197181971919720197211972219723197241972519726197271972819729197301973119732197331973419735197361973719738197391974019741197421974319744197451974619747197481974919750197511975219753197541975519756197571975819759197601976119762197631976419765197661976719768197691977019771197721977319774197751977619777197781977919780197811978219783197841978519786197871978819789197901979119792197931979419795197961979719798197991980019801198021980319804198051980619807198081980919810198111981219813198141981519816198171981819819198201982119822198231982419825198261982719828198291983019831198321983319834198351983619837198381983919840198411984219843198441984519846198471984819849198501985119852198531985419855198561985719858198591986019861198621986319864198651986619867198681986919870198711987219873198741987519876198771987819879198801988119882198831988419885198861988719888198891989019891198921989319894198951989619897198981989919900199011990219903199041990519906199071990819909199101991119912199131991419915199161991719918199191992019921199221992319924199251992619927199281992919930199311993219933199341993519936199371993819939199401994119942199431994419945199461994719948199491995019951199521995319954199551995619957199581995919960199611996219963199641996519966199671996819969199701997119972199731997419975199761997719978199791998019981199821998319984199851998619987199881998919990199911999219993199941999519996199971999819999200002000120002200032000420005200062000720008200092001020011200122001320014200152001620017200182001920020200212002220023200242002520026200272002820029200302003120032200332003420035200362003720038200392004020041200422004320044200452004620047200482004920050200512005220053200542005520056200572005820059200602006120062200632006420065200662006720068200692007020071200722007320074200752007620077200782007920080200812008220083200842008520086200872008820089200902009120092200932009420095200962009720098200992010020101201022010320104201052010620107201082010920110201112011220113201142011520116201172011820119201202012120122201232012420125201262012720128201292013020131201322013320134201352013620137201382013920140201412014220143201442014520146201472014820149201502015120152201532015420155201562015720158201592016020161201622016320164201652016620167201682016920170201712017220173201742017520176201772017820179201802018120182201832018420185201862018720188201892019020191201922019320194201952019620197201982019920200202012020220203202042020520206202072020820209202102021120212202132021420215202162021720218202192022020221202222022320224202252022620227202282022920230202312023220233202342023520236202372023820239202402024120242202432024420245202462024720248202492025020251202522025320254202552025620257202582025920260202612026220263202642026520266202672026820269202702027120272202732027420275202762027720278202792028020281202822028320284202852028620287202882028920290202912029220293202942029520296202972029820299203002030120302203032030420305203062030720308203092031020311203122031320314203152031620317203182031920320203212032220323203242032520326203272032820329203302033120332203332033420335203362033720338203392034020341203422034320344203452034620347203482034920350203512035220353203542035520356203572035820359203602036120362203632036420365203662036720368203692037020371203722037320374203752037620377203782037920380203812038220383203842038520386203872038820389203902039120392203932039420395203962039720398203992040020401204022040320404204052040620407204082040920410204112041220413204142041520416204172041820419204202042120422204232042420425204262042720428204292043020431204322043320434204352043620437204382043920440204412044220443204442044520446204472044820449204502045120452204532045420455204562045720458204592046020461204622046320464204652046620467204682046920470204712047220473204742047520476204772047820479204802048120482204832048420485204862048720488204892049020491204922049320494204952049620497204982049920500205012050220503205042050520506205072050820509205102051120512205132051420515205162051720518205192052020521205222052320524205252052620527205282052920530205312053220533205342053520536205372053820539205402054120542205432054420545205462054720548205492055020551205522055320554205552055620557205582055920560205612056220563205642056520566205672056820569205702057120572205732057420575205762057720578205792058020581205822058320584205852058620587205882058920590205912059220593205942059520596205972059820599206002060120602206032060420605206062060720608206092061020611206122061320614206152061620617206182061920620206212062220623206242062520626206272062820629206302063120632206332063420635206362063720638206392064020641206422064320644206452064620647206482064920650206512065220653206542065520656206572065820659206602066120662206632066420665206662066720668206692067020671206722067320674206752067620677206782067920680206812068220683206842068520686206872068820689206902069120692206932069420695206962069720698206992070020701207022070320704207052070620707207082070920710207112071220713207142071520716207172071820719207202072120722207232072420725207262072720728207292073020731207322073320734207352073620737207382073920740207412074220743207442074520746207472074820749207502075120752207532075420755207562075720758207592076020761207622076320764207652076620767207682076920770207712077220773207742077520776207772077820779207802078120782207832078420785207862078720788207892079020791207922079320794207952079620797207982079920800208012080220803208042080520806208072080820809208102081120812208132081420815208162081720818208192082020821208222082320824208252082620827208282082920830208312083220833208342083520836208372083820839208402084120842208432084420845208462084720848208492085020851208522085320854208552085620857208582085920860208612086220863208642086520866208672086820869208702087120872208732087420875208762087720878208792088020881208822088320884208852088620887208882088920890208912089220893208942089520896208972089820899209002090120902209032090420905209062090720908209092091020911209122091320914209152091620917209182091920920209212092220923209242092520926209272092820929209302093120932209332093420935209362093720938209392094020941209422094320944209452094620947209482094920950209512095220953209542095520956209572095820959209602096120962209632096420965209662096720968209692097020971209722097320974209752097620977209782097920980209812098220983209842098520986209872098820989209902099120992209932099420995209962099720998209992100021001210022100321004210052100621007210082100921010210112101221013210142101521016210172101821019210202102121022210232102421025210262102721028210292103021031210322103321034210352103621037210382103921040210412104221043210442104521046210472104821049210502105121052210532105421055210562105721058210592106021061210622106321064210652106621067210682106921070210712107221073210742107521076210772107821079210802108121082210832108421085210862108721088210892109021091210922109321094210952109621097210982109921100211012110221103211042110521106211072110821109211102111121112211132111421115211162111721118211192112021121211222112321124211252112621127211282112921130211312113221133211342113521136211372113821139211402114121142211432114421145211462114721148211492115021151211522115321154211552115621157211582115921160211612116221163211642116521166211672116821169211702117121172211732117421175211762117721178211792118021181211822118321184211852118621187211882118921190211912119221193211942119521196211972119821199212002120121202212032120421205212062120721208212092121021211212122121321214212152121621217212182121921220212212122221223212242122521226212272122821229212302123121232212332123421235212362123721238212392124021241212422124321244212452124621247212482124921250212512125221253212542125521256212572125821259212602126121262212632126421265212662126721268212692127021271212722127321274212752127621277212782127921280212812128221283212842128521286212872128821289212902129121292212932129421295212962129721298212992130021301213022130321304213052130621307213082130921310213112131221313213142131521316213172131821319213202132121322213232132421325213262132721328213292133021331213322133321334213352133621337213382133921340213412134221343213442134521346213472134821349213502135121352213532135421355213562135721358213592136021361213622136321364213652136621367213682136921370213712137221373213742137521376213772137821379213802138121382213832138421385213862138721388213892139021391213922139321394213952139621397213982139921400214012140221403214042140521406214072140821409214102141121412214132141421415214162141721418214192142021421214222142321424214252142621427214282142921430214312143221433214342143521436214372143821439214402144121442214432144421445214462144721448214492145021451214522145321454214552145621457214582145921460214612146221463214642146521466214672146821469214702147121472214732147421475214762147721478214792148021481214822148321484214852148621487214882148921490214912149221493214942149521496214972149821499215002150121502215032150421505215062150721508215092151021511215122151321514215152151621517215182151921520215212152221523215242152521526215272152821529215302153121532215332153421535215362153721538215392154021541215422154321544215452154621547215482154921550215512155221553215542155521556215572155821559215602156121562215632156421565215662156721568215692157021571215722157321574215752157621577215782157921580215812158221583215842158521586215872158821589215902159121592215932159421595215962159721598215992160021601216022160321604216052160621607216082160921610216112161221613216142161521616216172161821619216202162121622216232162421625216262162721628216292163021631216322163321634216352163621637216382163921640216412164221643216442164521646216472164821649216502165121652216532165421655216562165721658216592166021661216622166321664216652166621667216682166921670216712167221673216742167521676216772167821679216802168121682216832168421685216862168721688216892169021691216922169321694216952169621697216982169921700217012170221703217042170521706217072170821709217102171121712217132171421715217162171721718217192172021721217222172321724217252172621727217282172921730217312173221733217342173521736217372173821739217402174121742217432174421745217462174721748217492175021751217522175321754217552175621757217582175921760217612176221763217642176521766217672176821769217702177121772217732177421775217762177721778217792178021781217822178321784217852178621787217882178921790217912179221793217942179521796217972179821799218002180121802218032180421805218062180721808218092181021811218122181321814218152181621817218182181921820218212182221823218242182521826218272182821829218302183121832218332183421835218362183721838218392184021841218422184321844218452184621847218482184921850218512185221853218542185521856218572185821859218602186121862218632186421865218662186721868218692187021871218722187321874218752187621877218782187921880218812188221883218842188521886218872188821889218902189121892218932189421895218962189721898218992190021901219022190321904219052190621907219082190921910219112191221913219142191521916219172191821919219202192121922219232192421925219262192721928219292193021931219322193321934219352193621937219382193921940219412194221943219442194521946219472194821949219502195121952219532195421955219562195721958219592196021961219622196321964219652196621967219682196921970219712197221973219742197521976219772197821979219802198121982219832198421985219862198721988219892199021991219922199321994219952199621997219982199922000220012200222003220042200522006220072200822009220102201122012220132201422015220162201722018220192202022021220222202322024220252202622027220282202922030220312203222033220342203522036220372203822039220402204122042220432204422045220462204722048220492205022051220522205322054220552205622057220582205922060220612206222063220642206522066220672206822069220702207122072220732207422075220762207722078220792208022081220822208322084220852208622087220882208922090220912209222093220942209522096220972209822099221002210122102221032210422105221062210722108221092211022111221122211322114221152211622117221182211922120221212212222123221242212522126221272212822129221302213122132221332213422135221362213722138221392214022141221422214322144221452214622147221482214922150221512215222153221542215522156221572215822159221602216122162221632216422165221662216722168221692217022171221722217322174221752217622177221782217922180221812218222183221842218522186221872218822189221902219122192221932219422195221962219722198221992220022201222022220322204222052220622207222082220922210222112221222213222142221522216222172221822219222202222122222222232222422225222262222722228222292223022231222322223322234222352223622237222382223922240222412224222243222442224522246222472224822249222502225122252222532225422255222562225722258222592226022261222622226322264222652226622267222682226922270222712227222273222742227522276222772227822279222802228122282222832228422285222862228722288222892229022291222922229322294222952229622297222982229922300223012230222303223042230522306223072230822309223102231122312223132231422315223162231722318223192232022321223222232322324223252232622327223282232922330223312233222333223342233522336223372233822339223402234122342223432234422345223462234722348223492235022351223522235322354223552235622357223582235922360223612236222363223642236522366223672236822369223702237122372223732237422375223762237722378223792238022381223822238322384223852238622387223882238922390223912239222393223942239522396223972239822399224002240122402224032240422405224062240722408224092241022411224122241322414224152241622417224182241922420224212242222423224242242522426224272242822429224302243122432224332243422435224362243722438224392244022441224422244322444224452244622447224482244922450224512245222453224542245522456224572245822459224602246122462224632246422465224662246722468224692247022471224722247322474224752247622477224782247922480224812248222483224842248522486224872248822489224902249122492224932249422495224962249722498224992250022501225022250322504225052250622507225082250922510225112251222513225142251522516225172251822519225202252122522225232252422525225262252722528225292253022531225322253322534225352253622537225382253922540225412254222543225442254522546225472254822549225502255122552225532255422555225562255722558225592256022561225622256322564225652256622567225682256922570225712257222573225742257522576225772257822579225802258122582225832258422585225862258722588225892259022591225922259322594225952259622597225982259922600226012260222603226042260522606226072260822609226102261122612226132261422615226162261722618226192262022621226222262322624226252262622627226282262922630226312263222633226342263522636226372263822639226402264122642226432264422645226462264722648226492265022651226522265322654226552265622657226582265922660226612266222663226642266522666226672266822669226702267122672226732267422675226762267722678226792268022681226822268322684226852268622687226882268922690226912269222693226942269522696226972269822699227002270122702227032270422705227062270722708227092271022711227122271322714227152271622717227182271922720227212272222723227242272522726227272272822729227302273122732227332273422735227362273722738227392274022741227422274322744227452274622747227482274922750227512275222753227542275522756227572275822759227602276122762227632276422765227662276722768227692277022771227722277322774227752277622777227782277922780227812278222783227842278522786227872278822789227902279122792227932279422795227962279722798227992280022801228022280322804228052280622807228082280922810228112281222813228142281522816228172281822819228202282122822228232282422825228262282722828228292283022831228322283322834228352283622837228382283922840228412284222843228442284522846228472284822849228502285122852228532285422855228562285722858228592286022861228622286322864228652286622867228682286922870228712287222873228742287522876228772287822879228802288122882228832288422885228862288722888228892289022891228922289322894228952289622897228982289922900229012290222903229042290522906229072290822909229102291122912229132291422915229162291722918229192292022921229222292322924229252292622927229282292922930229312293222933229342293522936229372293822939229402294122942229432294422945229462294722948229492295022951229522295322954229552295622957229582295922960229612296222963229642296522966229672296822969229702297122972229732297422975229762297722978229792298022981229822298322984229852298622987229882298922990229912299222993229942299522996229972299822999230002300123002230032300423005230062300723008230092301023011230122301323014230152301623017230182301923020230212302223023230242302523026230272302823029230302303123032230332303423035230362303723038230392304023041230422304323044230452304623047230482304923050230512305223053230542305523056230572305823059230602306123062230632306423065230662306723068230692307023071230722307323074230752307623077230782307923080230812308223083230842308523086230872308823089230902309123092230932309423095230962309723098230992310023101231022310323104231052310623107231082310923110231112311223113231142311523116231172311823119231202312123122231232312423125231262312723128231292313023131231322313323134231352313623137231382313923140231412314223143231442314523146231472314823149231502315123152231532315423155231562315723158231592316023161231622316323164231652316623167231682316923170231712317223173231742317523176231772317823179231802318123182231832318423185231862318723188231892319023191231922319323194231952319623197231982319923200232012320223203232042320523206232072320823209232102321123212232132321423215232162321723218232192322023221232222322323224232252322623227232282322923230232312323223233232342323523236232372323823239232402324123242232432324423245232462324723248232492325023251232522325323254232552325623257232582325923260232612326223263232642326523266232672326823269232702327123272232732327423275232762327723278232792328023281232822328323284232852328623287232882328923290232912329223293232942329523296232972329823299233002330123302233032330423305233062330723308233092331023311233122331323314233152331623317233182331923320233212332223323233242332523326233272332823329233302333123332233332333423335233362333723338233392334023341233422334323344233452334623347233482334923350233512335223353233542335523356233572335823359233602336123362233632336423365233662336723368233692337023371233722337323374233752337623377233782337923380233812338223383233842338523386233872338823389233902339123392233932339423395233962339723398233992340023401234022340323404234052340623407234082340923410234112341223413234142341523416234172341823419234202342123422234232342423425234262342723428234292343023431234322343323434234352343623437234382343923440234412344223443234442344523446234472344823449234502345123452234532345423455234562345723458234592346023461234622346323464234652346623467234682346923470234712347223473234742347523476234772347823479234802348123482234832348423485234862348723488234892349023491234922349323494234952349623497234982349923500235012350223503235042350523506235072350823509235102351123512235132351423515235162351723518235192352023521235222352323524235252352623527235282352923530235312353223533235342353523536235372353823539235402354123542235432354423545235462354723548235492355023551235522355323554235552355623557235582355923560235612356223563235642356523566235672356823569235702357123572235732357423575235762357723578235792358023581235822358323584235852358623587235882358923590235912359223593235942359523596235972359823599236002360123602236032360423605236062360723608236092361023611236122361323614236152361623617236182361923620236212362223623236242362523626236272362823629236302363123632236332363423635236362363723638236392364023641236422364323644236452364623647236482364923650236512365223653236542365523656236572365823659236602366123662236632366423665236662366723668236692367023671236722367323674236752367623677236782367923680236812368223683236842368523686236872368823689236902369123692236932369423695236962369723698236992370023701237022370323704237052370623707237082370923710237112371223713237142371523716237172371823719237202372123722237232372423725237262372723728237292373023731237322373323734237352373623737237382373923740237412374223743237442374523746237472374823749237502375123752237532375423755237562375723758237592376023761237622376323764237652376623767237682376923770237712377223773237742377523776237772377823779237802378123782237832378423785237862378723788237892379023791237922379323794237952379623797237982379923800238012380223803238042380523806238072380823809238102381123812238132381423815238162381723818238192382023821238222382323824238252382623827238282382923830238312383223833238342383523836238372383823839238402384123842238432384423845238462384723848238492385023851238522385323854238552385623857238582385923860238612386223863238642386523866238672386823869238702387123872238732387423875238762387723878238792388023881238822388323884238852388623887238882388923890238912389223893238942389523896238972389823899239002390123902239032390423905239062390723908239092391023911239122391323914239152391623917239182391923920239212392223923239242392523926239272392823929239302393123932239332393423935239362393723938239392394023941239422394323944239452394623947239482394923950239512395223953239542395523956239572395823959239602396123962239632396423965239662396723968239692397023971239722397323974239752397623977239782397923980239812398223983239842398523986239872398823989239902399123992239932399423995239962399723998239992400024001240022400324004240052400624007240082400924010240112401224013240142401524016240172401824019240202402124022240232402424025240262402724028240292403024031240322403324034240352403624037240382403924040240412404224043240442404524046240472404824049240502405124052240532405424055240562405724058240592406024061240622406324064240652406624067240682406924070240712407224073240742407524076240772407824079240802408124082240832408424085240862408724088240892409024091240922409324094240952409624097240982409924100241012410224103241042410524106241072410824109241102411124112241132411424115241162411724118241192412024121241222412324124241252412624127241282412924130241312413224133241342413524136241372413824139241402414124142241432414424145241462414724148241492415024151241522415324154241552415624157241582415924160241612416224163241642416524166241672416824169241702417124172241732417424175241762417724178241792418024181241822418324184241852418624187241882418924190241912419224193241942419524196241972419824199242002420124202242032420424205242062420724208242092421024211242122421324214242152421624217242182421924220242212422224223242242422524226242272422824229242302423124232242332423424235242362423724238242392424024241242422424324244242452424624247242482424924250242512425224253242542425524256242572425824259242602426124262242632426424265242662426724268242692427024271242722427324274242752427624277242782427924280242812428224283242842428524286242872428824289242902429124292242932429424295242962429724298242992430024301243022430324304243052430624307243082430924310243112431224313243142431524316243172431824319243202432124322243232432424325243262432724328243292433024331243322433324334243352433624337243382433924340243412434224343243442434524346243472434824349243502435124352243532435424355243562435724358243592436024361243622436324364243652436624367243682436924370243712437224373243742437524376243772437824379243802438124382243832438424385243862438724388243892439024391243922439324394243952439624397243982439924400244012440224403244042440524406244072440824409244102441124412244132441424415244162441724418244192442024421244222442324424244252442624427244282442924430244312443224433244342443524436244372443824439244402444124442244432444424445244462444724448244492445024451244522445324454244552445624457244582445924460244612446224463244642446524466244672446824469244702447124472244732447424475244762447724478244792448024481244822448324484244852448624487244882448924490244912449224493244942449524496244972449824499245002450124502245032450424505245062450724508245092451024511245122451324514245152451624517245182451924520245212452224523245242452524526245272452824529245302453124532245332453424535245362453724538245392454024541245422454324544245452454624547245482454924550245512455224553245542455524556245572455824559245602456124562245632456424565245662456724568245692457024571245722457324574245752457624577245782457924580245812458224583245842458524586245872458824589245902459124592245932459424595245962459724598245992460024601246022460324604246052460624607246082460924610246112461224613246142461524616246172461824619246202462124622246232462424625246262462724628246292463024631246322463324634246352463624637246382463924640246412464224643246442464524646246472464824649246502465124652246532465424655246562465724658246592466024661246622466324664246652466624667246682466924670246712467224673246742467524676246772467824679246802468124682246832468424685246862468724688246892469024691246922469324694246952469624697246982469924700247012470224703247042470524706247072470824709247102471124712247132471424715247162471724718247192472024721247222472324724247252472624727247282472924730247312473224733247342473524736247372473824739247402474124742247432474424745247462474724748247492475024751247522475324754247552475624757247582475924760247612476224763247642476524766247672476824769247702477124772247732477424775247762477724778247792478024781247822478324784247852478624787247882478924790247912479224793247942479524796247972479824799248002480124802248032480424805248062480724808248092481024811248122481324814248152481624817248182481924820248212482224823248242482524826248272482824829248302483124832248332483424835248362483724838248392484024841248422484324844248452484624847248482484924850248512485224853248542485524856248572485824859248602486124862248632486424865248662486724868248692487024871248722487324874248752487624877248782487924880248812488224883248842488524886248872488824889248902489124892248932489424895248962489724898248992490024901249022490324904249052490624907249082490924910249112491224913249142491524916249172491824919249202492124922249232492424925249262492724928249292493024931249322493324934249352493624937249382493924940249412494224943249442494524946249472494824949249502495124952249532495424955249562495724958249592496024961249622496324964249652496624967249682496924970249712497224973249742497524976249772497824979249802498124982249832498424985249862498724988249892499024991249922499324994249952499624997249982499925000250012500225003250042500525006250072500825009250102501125012250132501425015250162501725018250192502025021250222502325024250252502625027250282502925030250312503225033250342503525036250372503825039250402504125042250432504425045250462504725048250492505025051250522505325054250552505625057250582505925060250612506225063250642506525066250672506825069250702507125072250732507425075250762507725078250792508025081250822508325084250852508625087250882508925090250912509225093250942509525096250972509825099251002510125102251032510425105251062510725108251092511025111251122511325114251152511625117251182511925120251212512225123251242512525126251272512825129251302513125132251332513425135251362513725138251392514025141251422514325144251452514625147251482514925150251512515225153251542515525156251572515825159251602516125162251632516425165251662516725168251692517025171251722517325174251752517625177251782517925180251812518225183251842518525186251872518825189251902519125192251932519425195251962519725198251992520025201252022520325204252052520625207252082520925210252112521225213252142521525216252172521825219252202522125222252232522425225252262522725228252292523025231252322523325234252352523625237252382523925240252412524225243252442524525246252472524825249252502525125252252532525425255252562525725258252592526025261252622526325264252652526625267252682526925270252712527225273252742527525276252772527825279252802528125282252832528425285252862528725288252892529025291252922529325294252952529625297252982529925300253012530225303253042530525306253072530825309253102531125312253132531425315253162531725318253192532025321253222532325324253252532625327253282532925330253312533225333253342533525336253372533825339253402534125342253432534425345253462534725348253492535025351253522535325354253552535625357253582535925360253612536225363253642536525366253672536825369253702537125372253732537425375253762537725378253792538025381253822538325384253852538625387253882538925390253912539225393253942539525396253972539825399254002540125402254032540425405254062540725408254092541025411254122541325414254152541625417254182541925420254212542225423254242542525426254272542825429254302543125432254332543425435254362543725438254392544025441254422544325444254452544625447254482544925450254512545225453254542545525456254572545825459254602546125462254632546425465254662546725468254692547025471254722547325474254752547625477254782547925480254812548225483254842548525486254872548825489254902549125492254932549425495254962549725498254992550025501255022550325504255052550625507255082550925510255112551225513255142551525516255172551825519255202552125522255232552425525255262552725528255292553025531255322553325534255352553625537255382553925540255412554225543255442554525546255472554825549255502555125552255532555425555255562555725558255592556025561255622556325564255652556625567255682556925570255712557225573255742557525576255772557825579255802558125582255832558425585255862558725588255892559025591255922559325594255952559625597255982559925600256012560225603256042560525606256072560825609256102561125612256132561425615256162561725618256192562025621256222562325624256252562625627256282562925630256312563225633256342563525636256372563825639256402564125642256432564425645256462564725648256492565025651256522565325654256552565625657256582565925660256612566225663256642566525666256672566825669256702567125672256732567425675256762567725678256792568025681256822568325684256852568625687256882568925690256912569225693256942569525696256972569825699257002570125702257032570425705257062570725708257092571025711257122571325714257152571625717257182571925720257212572225723257242572525726257272572825729257302573125732257332573425735257362573725738257392574025741257422574325744257452574625747257482574925750257512575225753257542575525756257572575825759257602576125762257632576425765257662576725768257692577025771257722577325774257752577625777257782577925780257812578225783257842578525786257872578825789257902579125792257932579425795257962579725798257992580025801258022580325804258052580625807258082580925810258112581225813258142581525816258172581825819258202582125822258232582425825258262582725828258292583025831258322583325834258352583625837258382583925840258412584225843258442584525846258472584825849258502585125852258532585425855258562585725858258592586025861258622586325864258652586625867258682586925870258712587225873258742587525876258772587825879258802588125882258832588425885258862588725888258892589025891258922589325894258952589625897258982589925900259012590225903259042590525906259072590825909259102591125912259132591425915259162591725918259192592025921259222592325924259252592625927259282592925930259312593225933259342593525936259372593825939259402594125942259432594425945259462594725948259492595025951259522595325954259552595625957259582595925960259612596225963259642596525966259672596825969259702597125972259732597425975259762597725978259792598025981259822598325984259852598625987259882598925990259912599225993259942599525996259972599825999260002600126002260032600426005260062600726008260092601026011260122601326014260152601626017260182601926020260212602226023260242602526026260272602826029260302603126032260332603426035260362603726038260392604026041260422604326044260452604626047260482604926050260512605226053260542605526056260572605826059260602606126062260632606426065260662606726068260692607026071260722607326074260752607626077260782607926080260812608226083260842608526086260872608826089260902609126092260932609426095260962609726098260992610026101261022610326104261052610626107261082610926110261112611226113261142611526116261172611826119261202612126122261232612426125261262612726128261292613026131261322613326134261352613626137261382613926140261412614226143261442614526146261472614826149261502615126152261532615426155261562615726158261592616026161261622616326164261652616626167261682616926170261712617226173261742617526176261772617826179261802618126182261832618426185261862618726188261892619026191261922619326194261952619626197261982619926200262012620226203262042620526206262072620826209262102621126212262132621426215262162621726218262192622026221262222622326224262252622626227262282622926230262312623226233262342623526236262372623826239262402624126242262432624426245262462624726248262492625026251262522625326254262552625626257262582625926260262612626226263262642626526266262672626826269262702627126272262732627426275262762627726278262792628026281262822628326284262852628626287262882628926290262912629226293262942629526296262972629826299263002630126302263032630426305263062630726308263092631026311263122631326314263152631626317263182631926320263212632226323263242632526326263272632826329263302633126332263332633426335263362633726338263392634026341263422634326344263452634626347263482634926350263512635226353263542635526356263572635826359263602636126362263632636426365263662636726368263692637026371263722637326374263752637626377263782637926380263812638226383263842638526386263872638826389263902639126392263932639426395263962639726398263992640026401264022640326404264052640626407264082640926410264112641226413264142641526416264172641826419264202642126422264232642426425264262642726428264292643026431264322643326434264352643626437264382643926440264412644226443264442644526446264472644826449264502645126452264532645426455264562645726458264592646026461264622646326464264652646626467264682646926470264712647226473264742647526476264772647826479264802648126482264832648426485264862648726488264892649026491264922649326494264952649626497264982649926500265012650226503265042650526506265072650826509265102651126512265132651426515265162651726518265192652026521265222652326524265252652626527265282652926530265312653226533265342653526536265372653826539265402654126542265432654426545265462654726548265492655026551265522655326554265552655626557265582655926560265612656226563265642656526566265672656826569265702657126572265732657426575265762657726578265792658026581265822658326584265852658626587265882658926590265912659226593265942659526596265972659826599266002660126602266032660426605266062660726608266092661026611266122661326614266152661626617266182661926620266212662226623266242662526626266272662826629266302663126632266332663426635266362663726638266392664026641266422664326644266452664626647266482664926650266512665226653266542665526656266572665826659266602666126662266632666426665266662666726668266692667026671266722667326674266752667626677266782667926680266812668226683266842668526686266872668826689266902669126692266932669426695266962669726698266992670026701267022670326704267052670626707267082670926710267112671226713267142671526716267172671826719267202672126722267232672426725267262672726728267292673026731267322673326734267352673626737267382673926740267412674226743267442674526746267472674826749267502675126752267532675426755267562675726758267592676026761267622676326764267652676626767267682676926770267712677226773267742677526776267772677826779267802678126782267832678426785267862678726788267892679026791267922679326794267952679626797267982679926800268012680226803268042680526806268072680826809268102681126812268132681426815268162681726818268192682026821268222682326824268252682626827268282682926830268312683226833268342683526836268372683826839268402684126842268432684426845268462684726848268492685026851268522685326854268552685626857268582685926860268612686226863268642686526866268672686826869268702687126872268732687426875268762687726878268792688026881268822688326884268852688626887268882688926890268912689226893268942689526896268972689826899269002690126902269032690426905269062690726908269092691026911269122691326914269152691626917269182691926920269212692226923269242692526926269272692826929269302693126932269332693426935269362693726938269392694026941269422694326944269452694626947269482694926950269512695226953269542695526956269572695826959269602696126962269632696426965269662696726968269692697026971269722697326974269752697626977269782697926980269812698226983269842698526986269872698826989269902699126992269932699426995269962699726998269992700027001270022700327004270052700627007270082700927010270112701227013270142701527016270172701827019270202702127022270232702427025270262702727028270292703027031270322703327034270352703627037270382703927040270412704227043270442704527046270472704827049270502705127052270532705427055270562705727058270592706027061270622706327064270652706627067270682706927070270712707227073270742707527076270772707827079270802708127082270832708427085270862708727088270892709027091270922709327094270952709627097270982709927100271012710227103271042710527106271072710827109271102711127112271132711427115271162711727118271192712027121271222712327124271252712627127271282712927130271312713227133271342713527136271372713827139271402714127142271432714427145271462714727148271492715027151271522715327154271552715627157271582715927160271612716227163271642716527166271672716827169271702717127172271732717427175271762717727178271792718027181271822718327184271852718627187271882718927190271912719227193271942719527196271972719827199272002720127202272032720427205272062720727208272092721027211272122721327214272152721627217272182721927220272212722227223272242722527226272272722827229272302723127232272332723427235272362723727238272392724027241272422724327244272452724627247272482724927250272512725227253272542725527256272572725827259272602726127262272632726427265272662726727268272692727027271272722727327274272752727627277272782727927280272812728227283272842728527286272872728827289272902729127292272932729427295272962729727298272992730027301273022730327304273052730627307273082730927310273112731227313273142731527316273172731827319273202732127322273232732427325273262732727328273292733027331273322733327334273352733627337273382733927340273412734227343273442734527346273472734827349273502735127352273532735427355273562735727358273592736027361273622736327364273652736627367273682736927370273712737227373273742737527376273772737827379273802738127382273832738427385273862738727388273892739027391273922739327394273952739627397273982739927400274012740227403274042740527406274072740827409274102741127412274132741427415274162741727418274192742027421274222742327424274252742627427274282742927430274312743227433274342743527436274372743827439274402744127442274432744427445274462744727448274492745027451274522745327454274552745627457274582745927460274612746227463274642746527466274672746827469274702747127472274732747427475274762747727478274792748027481274822748327484274852748627487274882748927490274912749227493274942749527496274972749827499275002750127502275032750427505275062750727508275092751027511275122751327514275152751627517275182751927520275212752227523275242752527526275272752827529275302753127532275332753427535275362753727538275392754027541275422754327544275452754627547275482754927550275512755227553275542755527556275572755827559275602756127562275632756427565275662756727568275692757027571275722757327574275752757627577275782757927580275812758227583275842758527586275872758827589275902759127592275932759427595275962759727598275992760027601276022760327604276052760627607276082760927610276112761227613276142761527616276172761827619276202762127622276232762427625276262762727628276292763027631276322763327634276352763627637276382763927640276412764227643276442764527646276472764827649276502765127652276532765427655276562765727658276592766027661276622766327664276652766627667276682766927670276712767227673276742767527676276772767827679276802768127682276832768427685276862768727688276892769027691276922769327694276952769627697276982769927700277012770227703277042770527706277072770827709277102771127712277132771427715277162771727718277192772027721277222772327724277252772627727277282772927730277312773227733277342773527736277372773827739277402774127742277432774427745277462774727748277492775027751277522775327754277552775627757277582775927760277612776227763277642776527766277672776827769277702777127772277732777427775277762777727778277792778027781277822778327784277852778627787277882778927790277912779227793277942779527796277972779827799278002780127802278032780427805278062780727808278092781027811278122781327814278152781627817278182781927820278212782227823278242782527826278272782827829278302783127832278332783427835278362783727838278392784027841278422784327844278452784627847278482784927850278512785227853278542785527856278572785827859278602786127862278632786427865278662786727868278692787027871278722787327874278752787627877278782787927880278812788227883278842788527886278872788827889278902789127892278932789427895278962789727898278992790027901279022790327904279052790627907279082790927910279112791227913279142791527916279172791827919279202792127922279232792427925279262792727928279292793027931279322793327934279352793627937279382793927940279412794227943279442794527946279472794827949279502795127952279532795427955279562795727958279592796027961279622796327964279652796627967279682796927970279712797227973279742797527976279772797827979279802798127982279832798427985279862798727988279892799027991279922799327994279952799627997279982799928000280012800228003280042800528006280072800828009280102801128012280132801428015280162801728018280192802028021280222802328024280252802628027280282802928030280312803228033280342803528036280372803828039280402804128042280432804428045280462804728048280492805028051280522805328054280552805628057280582805928060280612806228063280642806528066280672806828069280702807128072280732807428075280762807728078280792808028081280822808328084280852808628087280882808928090280912809228093280942809528096280972809828099281002810128102281032810428105281062810728108281092811028111281122811328114281152811628117281182811928120281212812228123281242812528126281272812828129281302813128132281332813428135281362813728138281392814028141281422814328144281452814628147281482814928150281512815228153281542815528156281572815828159281602816128162281632816428165281662816728168281692817028171281722817328174281752817628177281782817928180281812818228183281842818528186281872818828189281902819128192281932819428195281962819728198281992820028201282022820328204282052820628207282082820928210282112821228213282142821528216282172821828219282202822128222282232822428225282262822728228282292823028231282322823328234282352823628237282382823928240282412824228243282442824528246282472824828249282502825128252282532825428255282562825728258282592826028261282622826328264282652826628267282682826928270282712827228273282742827528276282772827828279282802828128282282832828428285282862828728288282892829028291282922829328294282952829628297282982829928300283012830228303283042830528306283072830828309283102831128312283132831428315283162831728318283192832028321283222832328324283252832628327283282832928330283312833228333283342833528336283372833828339283402834128342283432834428345283462834728348283492835028351283522835328354283552835628357283582835928360283612836228363283642836528366283672836828369283702837128372283732837428375283762837728378283792838028381283822838328384283852838628387283882838928390283912839228393283942839528396283972839828399284002840128402284032840428405284062840728408284092841028411284122841328414284152841628417284182841928420284212842228423284242842528426284272842828429284302843128432284332843428435284362843728438284392844028441284422844328444284452844628447284482844928450284512845228453284542845528456284572845828459284602846128462284632846428465284662846728468284692847028471284722847328474284752847628477284782847928480284812848228483284842848528486284872848828489284902849128492284932849428495284962849728498284992850028501285022850328504285052850628507285082850928510285112851228513285142851528516285172851828519285202852128522285232852428525285262852728528285292853028531285322853328534285352853628537285382853928540285412854228543285442854528546285472854828549285502855128552285532855428555285562855728558285592856028561285622856328564285652856628567285682856928570285712857228573285742857528576285772857828579285802858128582285832858428585285862858728588285892859028591285922859328594285952859628597285982859928600286012860228603286042860528606286072860828609286102861128612286132861428615286162861728618286192862028621286222862328624286252862628627286282862928630286312863228633286342863528636286372863828639286402864128642286432864428645286462864728648286492865028651286522865328654286552865628657286582865928660286612866228663286642866528666286672866828669286702867128672286732867428675286762867728678286792868028681286822868328684286852868628687286882868928690286912869228693286942869528696286972869828699287002870128702287032870428705287062870728708287092871028711287122871328714287152871628717287182871928720287212872228723287242872528726287272872828729287302873128732287332873428735287362873728738287392874028741287422874328744287452874628747287482874928750287512875228753287542875528756287572875828759287602876128762287632876428765287662876728768287692877028771287722877328774287752877628777287782877928780287812878228783287842878528786287872878828789287902879128792287932879428795287962879728798287992880028801288022880328804288052880628807288082880928810288112881228813288142881528816288172881828819288202882128822288232882428825288262882728828288292883028831288322883328834288352883628837288382883928840288412884228843288442884528846288472884828849288502885128852288532885428855288562885728858288592886028861288622886328864288652886628867288682886928870288712887228873288742887528876288772887828879288802888128882288832888428885288862888728888288892889028891288922889328894288952889628897288982889928900289012890228903289042890528906289072890828909289102891128912289132891428915289162891728918289192892028921289222892328924289252892628927289282892928930289312893228933289342893528936289372893828939289402894128942289432894428945289462894728948289492895028951289522895328954289552895628957289582895928960289612896228963289642896528966289672896828969289702897128972289732897428975289762897728978289792898028981289822898328984289852898628987289882898928990289912899228993289942899528996289972899828999290002900129002290032900429005290062900729008290092901029011290122901329014290152901629017290182901929020290212902229023290242902529026290272902829029290302903129032290332903429035290362903729038290392904029041290422904329044290452904629047290482904929050290512905229053290542905529056290572905829059290602906129062290632906429065290662906729068290692907029071290722907329074290752907629077290782907929080290812908229083290842908529086290872908829089290902909129092290932909429095290962909729098290992910029101291022910329104291052910629107291082910929110291112911229113291142911529116291172911829119291202912129122291232912429125291262912729128291292913029131291322913329134291352913629137291382913929140291412914229143291442914529146291472914829149291502915129152291532915429155291562915729158291592916029161291622916329164291652916629167291682916929170291712917229173291742917529176291772917829179291802918129182291832918429185291862918729188291892919029191291922919329194291952919629197291982919929200292012920229203292042920529206292072920829209292102921129212292132921429215292162921729218292192922029221292222922329224292252922629227292282922929230292312923229233292342923529236292372923829239292402924129242292432924429245292462924729248292492925029251292522925329254292552925629257292582925929260292612926229263292642926529266292672926829269292702927129272292732927429275292762927729278292792928029281292822928329284292852928629287292882928929290292912929229293292942929529296292972929829299293002930129302293032930429305293062930729308293092931029311293122931329314293152931629317293182931929320293212932229323293242932529326293272932829329293302933129332293332933429335293362933729338293392934029341293422934329344293452934629347293482934929350293512935229353293542935529356293572935829359293602936129362293632936429365293662936729368293692937029371293722937329374293752937629377293782937929380293812938229383293842938529386293872938829389293902939129392293932939429395293962939729398293992940029401294022940329404294052940629407294082940929410294112941229413294142941529416294172941829419294202942129422294232942429425294262942729428294292943029431294322943329434294352943629437294382943929440294412944229443294442944529446294472944829449294502945129452294532945429455294562945729458294592946029461294622946329464294652946629467294682946929470294712947229473294742947529476294772947829479294802948129482294832948429485294862948729488294892949029491294922949329494294952949629497294982949929500295012950229503295042950529506295072950829509295102951129512295132951429515295162951729518295192952029521295222952329524295252952629527295282952929530295312953229533295342953529536295372953829539295402954129542295432954429545295462954729548295492955029551295522955329554295552955629557295582955929560295612956229563295642956529566295672956829569295702957129572295732957429575295762957729578295792958029581295822958329584295852958629587295882958929590295912959229593295942959529596295972959829599296002960129602296032960429605296062960729608296092961029611296122961329614296152961629617296182961929620296212962229623296242962529626296272962829629296302963129632296332963429635296362963729638296392964029641296422964329644296452964629647296482964929650296512965229653296542965529656296572965829659296602966129662296632966429665296662966729668296692967029671296722967329674296752967629677296782967929680296812968229683296842968529686296872968829689296902969129692296932969429695296962969729698296992970029701297022970329704297052970629707297082970929710297112971229713297142971529716297172971829719297202972129722297232972429725297262972729728297292973029731297322973329734297352973629737297382973929740297412974229743297442974529746297472974829749297502975129752297532975429755297562975729758297592976029761297622976329764297652976629767297682976929770297712977229773297742977529776297772977829779297802978129782297832978429785297862978729788297892979029791297922979329794297952979629797297982979929800298012980229803298042980529806298072980829809298102981129812298132981429815298162981729818298192982029821298222982329824298252982629827298282982929830298312983229833298342983529836298372983829839298402984129842298432984429845298462984729848298492985029851298522985329854298552985629857298582985929860298612986229863298642986529866298672986829869298702987129872298732987429875298762987729878298792988029881298822988329884298852988629887298882988929890298912989229893298942989529896298972989829899299002990129902299032990429905299062990729908299092991029911299122991329914299152991629917299182991929920299212992229923299242992529926299272992829929299302993129932299332993429935299362993729938299392994029941299422994329944299452994629947299482994929950299512995229953299542995529956299572995829959299602996129962299632996429965299662996729968299692997029971299722997329974299752997629977299782997929980299812998229983299842998529986299872998829989299902999129992299932999429995299962999729998299993000030001300023000330004300053000630007300083000930010300113001230013300143001530016300173001830019300203002130022300233002430025300263002730028300293003030031300323003330034300353003630037300383003930040300413004230043300443004530046300473004830049300503005130052300533005430055300563005730058300593006030061300623006330064300653006630067300683006930070300713007230073300743007530076300773007830079300803008130082300833008430085300863008730088300893009030091300923009330094300953009630097300983009930100301013010230103301043010530106301073010830109301103011130112301133011430115301163011730118301193012030121301223012330124301253012630127301283012930130301313013230133301343013530136301373013830139301403014130142301433014430145301463014730148301493015030151301523015330154301553015630157301583015930160301613016230163301643016530166301673016830169301703017130172301733017430175301763017730178301793018030181301823018330184301853018630187301883018930190301913019230193301943019530196301973019830199302003020130202302033020430205302063020730208302093021030211302123021330214302153021630217302183021930220302213022230223302243022530226302273022830229302303023130232302333023430235302363023730238302393024030241302423024330244302453024630247302483024930250302513025230253302543025530256302573025830259302603026130262302633026430265302663026730268302693027030271302723027330274302753027630277302783027930280302813028230283302843028530286302873028830289302903029130292302933029430295302963029730298302993030030301303023030330304303053030630307303083030930310303113031230313303143031530316303173031830319303203032130322303233032430325303263032730328303293033030331303323033330334303353033630337303383033930340303413034230343303443034530346303473034830349303503035130352303533035430355303563035730358303593036030361303623036330364303653036630367303683036930370303713037230373303743037530376303773037830379303803038130382303833038430385303863038730388303893039030391303923039330394303953039630397303983039930400304013040230403304043040530406304073040830409304103041130412304133041430415304163041730418304193042030421304223042330424304253042630427304283042930430304313043230433304343043530436304373043830439304403044130442304433044430445304463044730448304493045030451304523045330454304553045630457304583045930460304613046230463304643046530466304673046830469304703047130472304733047430475304763047730478304793048030481304823048330484304853048630487304883048930490304913049230493304943049530496304973049830499305003050130502305033050430505305063050730508305093051030511305123051330514305153051630517305183051930520305213052230523305243052530526305273052830529305303053130532305333053430535305363053730538305393054030541305423054330544305453054630547305483054930550305513055230553305543055530556305573055830559305603056130562305633056430565305663056730568305693057030571305723057330574305753057630577305783057930580305813058230583305843058530586305873058830589305903059130592305933059430595305963059730598305993060030601306023060330604306053060630607306083060930610306113061230613306143061530616306173061830619306203062130622306233062430625306263062730628306293063030631306323063330634306353063630637306383063930640306413064230643306443064530646306473064830649306503065130652306533065430655306563065730658306593066030661306623066330664306653066630667306683066930670306713067230673306743067530676306773067830679306803068130682306833068430685306863068730688306893069030691306923069330694306953069630697306983069930700307013070230703307043070530706307073070830709307103071130712307133071430715307163071730718307193072030721307223072330724307253072630727307283072930730307313073230733307343073530736307373073830739307403074130742307433074430745307463074730748307493075030751307523075330754307553075630757307583075930760307613076230763307643076530766307673076830769307703077130772307733077430775307763077730778307793078030781307823078330784307853078630787307883078930790307913079230793307943079530796307973079830799308003080130802308033080430805308063080730808308093081030811308123081330814308153081630817308183081930820308213082230823308243082530826308273082830829308303083130832308333083430835308363083730838308393084030841308423084330844308453084630847308483084930850308513085230853308543085530856308573085830859308603086130862308633086430865308663086730868308693087030871308723087330874308753087630877308783087930880308813088230883308843088530886308873088830889308903089130892308933089430895308963089730898308993090030901309023090330904309053090630907309083090930910309113091230913309143091530916309173091830919309203092130922309233092430925309263092730928309293093030931309323093330934309353093630937309383093930940309413094230943309443094530946309473094830949309503095130952309533095430955309563095730958309593096030961309623096330964309653096630967309683096930970309713097230973309743097530976309773097830979309803098130982309833098430985309863098730988309893099030991309923099330994309953099630997309983099931000310013100231003310043100531006310073100831009310103101131012310133101431015310163101731018310193102031021310223102331024310253102631027310283102931030310313103231033310343103531036310373103831039310403104131042310433104431045310463104731048310493105031051310523105331054310553105631057310583105931060310613106231063310643106531066310673106831069310703107131072310733107431075310763107731078310793108031081310823108331084310853108631087310883108931090310913109231093310943109531096310973109831099311003110131102311033110431105311063110731108311093111031111311123111331114311153111631117311183111931120311213112231123311243112531126311273112831129311303113131132311333113431135311363113731138311393114031141311423114331144311453114631147311483114931150311513115231153311543115531156311573115831159311603116131162311633116431165311663116731168311693117031171311723117331174311753117631177311783117931180311813118231183311843118531186311873118831189311903119131192311933119431195311963119731198311993120031201312023120331204312053120631207312083120931210312113121231213312143121531216312173121831219312203122131222312233122431225312263122731228312293123031231312323123331234312353123631237312383123931240312413124231243312443124531246312473124831249312503125131252312533125431255312563125731258312593126031261312623126331264312653126631267312683126931270312713127231273312743127531276312773127831279312803128131282312833128431285312863128731288312893129031291312923129331294312953129631297312983129931300313013130231303313043130531306313073130831309313103131131312313133131431315313163131731318313193132031321313223132331324313253132631327313283132931330313313133231333313343133531336313373133831339313403134131342313433134431345313463134731348313493135031351313523135331354313553135631357313583135931360313613136231363313643136531366313673136831369313703137131372313733137431375313763137731378313793138031381313823138331384313853138631387313883138931390313913139231393313943139531396313973139831399314003140131402314033140431405314063140731408314093141031411314123141331414314153141631417314183141931420314213142231423314243142531426314273142831429314303143131432314333143431435314363143731438314393144031441314423144331444314453144631447314483144931450314513145231453314543145531456314573145831459314603146131462314633146431465314663146731468314693147031471314723147331474314753147631477314783147931480314813148231483314843148531486314873148831489314903149131492314933149431495314963149731498314993150031501315023150331504315053150631507315083150931510315113151231513315143151531516315173151831519315203152131522315233152431525315263152731528315293153031531315323153331534315353153631537315383153931540315413154231543315443154531546315473154831549315503155131552315533155431555315563155731558315593156031561315623156331564315653156631567315683156931570315713157231573315743157531576315773157831579315803158131582315833158431585315863158731588315893159031591315923159331594315953159631597315983159931600316013160231603316043160531606316073160831609316103161131612316133161431615316163161731618316193162031621316223162331624316253162631627316283162931630316313163231633316343163531636316373163831639316403164131642316433164431645316463164731648316493165031651316523165331654316553165631657316583165931660316613166231663316643166531666316673166831669316703167131672316733167431675316763167731678316793168031681316823168331684316853168631687316883168931690316913169231693316943169531696316973169831699317003170131702317033170431705317063170731708317093171031711317123171331714317153171631717317183171931720317213172231723317243172531726317273172831729317303173131732317333173431735317363173731738317393174031741317423174331744317453174631747317483174931750317513175231753317543175531756317573175831759317603176131762317633176431765
  1. <template>
  2. <div class="chat-container">
  3. <!-- 最左侧边栏 -->
  4. <Sidebar />
  5. <!-- 中间历史记录区域 -->
  6. <div class="history-sidebar" :class="{ 'disabled': isProcessing }">
  7. <div class="history-header">
  8. <span class="section-title">历史记录</span>
  9. <img src="@/assets/Chat/2.png" alt="新建任务" class="new-chat-btn" @click="handleNewChatClick"
  10. :class="{ 'disabled': isProcessing }">
  11. <!-- 测试按钮 -->
  12. <!-- <button @click="isProcessing = !isProcessing" style="margin-top: 10px; padding: 5px 10px; font-size: 12px;">
  13. 测试切换: {{ isProcessing ? '处理中' : '空闲' }}
  14. </button> -->
  15. </div>
  16. <div class="history-list">
  17. <!-- 历史记录加载状态 -->
  18. <div v-if="isLoadingHistory && historyTotal === 0" class="history-loading">
  19. <div class="loading-spinner"></div>
  20. <div class="loading-text">正在加载历史记录...</div>
  21. </div>
  22. <!-- 有历史记录时显示 -->
  23. <div v-else-if="historyTotal > 0" v-for="(item, index) in historyData" :key="index"
  24. :class="['history-item', { active: item.isActive, disabled: isProcessing }]"
  25. @click="() => handleHistoryItemClick(item, index)"
  26. :style="{ cursor: (item.isActive || isProcessing) ? 'default' : 'pointer' }">
  27. <div class="history-icon">
  28. <img :src="getHistoryImage(item)" alt="培训图标" class="history-icon-img">
  29. </div>
  30. <div class="history-content">
  31. <div class="history-title">{{ item.title }}</div>
  32. <div class="history-meta">
  33. <span class="history-time">{{ item.time }}</span>
  34. <!-- <span class="history-pages">{{ item.pages }}页</span> -->
  35. </div>
  36. </div>
  37. <div class="delete-btn" @click.stop="deleteHistoryItem(item, index)"
  38. :class="{ 'always-visible': item.isActive }">
  39. <img src="/src/assets/AIWriting/8.png" alt="删除" class="delete-icon" />
  40. </div>
  41. </div>
  42. <!-- 无历史记录时显示空状态 -->
  43. <div v-else class="empty-history">
  44. <img src="@/assets/Chat/22.png" alt="暂无数据" class="empty-icon">
  45. <div class="empty-text">暂无数据</div>
  46. </div>
  47. </div>
  48. </div>
  49. <!-- 右侧工作区域 -->
  50. <div class="main-work">
  51. <!-- 头部 -->
  52. <div class="work-header">
  53. <h2>安全培训</h2>
  54. </div>
  55. <!-- 工作内容区域 -->
  56. <div class="work-content">
  57. <!-- 步骤一:AI聊天界面 -->
  58. <div v-if="currentStep === 'step1'" class="step1-content">
  59. <!-- 初始状态:AI助手介绍和功能卡片 -->
  60. <div v-if="!showChat" class="initial-content">
  61. <!-- AI助手介绍 -->
  62. <div class="ai-intro">
  63. <div class="ai-avatar">
  64. <img src="@/assets/Safety/5.png" alt="AI头像" class="ai-avatar-img">
  65. </div>
  66. <div class="ai-greeting">
  67. <h3>快速生成专业安全培训材料</h3>
  68. <p>输入培训主题,一键生成培训大纲与PPT模板</p>
  69. </div>
  70. </div>
  71. <!-- 功能卡片 -->
  72. <div class="function-cards">
  73. <div v-for="(card, index) in functionCards" :key="card.id || index" class="function-card"
  74. @click="handleFunctionCard(card.function_title)">
  75. <div class="card-header">
  76. <div class="card-icon">
  77. <img :src="getFunctionCardIcon(card.function_title)" :alt="card.function_title"
  78. class="card-icon-img">
  79. </div>
  80. <h4>{{ card.function_title }}</h4>
  81. </div>
  82. <div class="card-description">
  83. <p>{{ card.function_content }}</p>
  84. </div>
  85. </div>
  86. <!-- 如果没有数据,显示默认卡片 -->
  87. <div v-if="functionCards.length === 0" class="function-card"
  88. @click="handleFunctionCard('safety-training')">
  89. <div class="card-header">
  90. <div class="card-icon">
  91. <img src="@/assets/Safety/4.png" alt="安全培训课程" class="card-icon-img">
  92. </div>
  93. <h4>安全培训课程</h4>
  94. </div>
  95. <div class="card-description">
  96. <p>施工安全培训,操作规范学习</p>
  97. </div>
  98. </div>
  99. <div v-if="functionCards.length === 0" class="function-card"
  100. @click="handleFunctionCard('safety-assessment')">
  101. <div class="card-header">
  102. <div class="card-icon">
  103. <img src="@/assets/Safety/3.png" alt="安全评估" class="card-icon-img">
  104. </div>
  105. <h4>安全评估测试</h4>
  106. </div>
  107. <div class="card-description">
  108. <p>安全知识测评,能力水平评估</p>
  109. </div>
  110. </div>
  111. <div v-if="functionCards.length === 0" class="function-card"
  112. @click="handleFunctionCard('safety-regulations')">
  113. <div class="card-header">
  114. <div class="card-icon">
  115. <img src="@/assets/Safety/2.png" alt="安全法规" class="card-icon-img">
  116. </div>
  117. <h4>安全法规查询</h4>
  118. </div>
  119. <div class="card-description">
  120. <p>安全法律法规,标准规范查询</p>
  121. </div>
  122. </div>
  123. <div v-if="functionCards.length === 0" class="function-card"
  124. @click="handleFunctionCard('emergency-procedures')">
  125. <div class="card-header">
  126. <div class="card-icon">
  127. <img src="@/assets/Safety/1.png" alt="应急程序" class="card-icon-img">
  128. </div>
  129. <h4>应急处理程序</h4>
  130. </div>
  131. <div class="card-description">
  132. <p>事故应急预案,处理流程指导</p>
  133. </div>
  134. </div>
  135. </div>
  136. </div>
  137. <!-- 聊天对话区域 -->
  138. <div v-else class="chat-content">
  139. <div class="chat-messages">
  140. <div v-for="(message, index) in chatMessages" :key="index" :class="['message-item', message.type]">
  141. <!-- 用户消息 -->
  142. <div v-if="message.type === 'user'" class="user-message">
  143. <div class="message-content">
  144. <!-- 文件显示 -->
  145. <div v-if="message.file" class="message-file">
  146. <div class="file-display">
  147. <div class="file-icon">
  148. <img v-if="message.file.type === '.doc' || message.file.type === '.docx'" :src="message.file.icon" alt="文档图标" class="file-icon-img">
  149. <span v-else>{{ message.file.icon }}</span>
  150. </div>
  151. <div class="file-details">
  152. <div class="file-name">{{ message.file.name }}</div>
  153. <div class="file-size">{{ formatFileSize(message.file.size) }}</div>
  154. </div>
  155. </div>
  156. </div>
  157. <!-- 文本内容 -->
  158. <div v-if="message.content" class="message-text">{{ message.content }}</div>
  159. </div>
  160. <div class="message-actions">
  161. <button class="action-btn copy-btn" @click="copyUserMessage(message)">
  162. <img src="@/assets/AIWriting/5.png" alt="复制" class="action-icon">
  163. 复制
  164. </button>
  165. <button class="action-btn edit-btn">
  166. <img src="@/assets/AIWriting/6.png" alt="编辑" class="action-icon">
  167. 编辑
  168. </button>
  169. </div>
  170. </div>
  171. <!-- AI消息 -->
  172. <div v-else-if="message.type === 'ai'" class="ai-message">
  173. <div class="ai-avatar-small">
  174. <img src="@/assets/Safety/5.png" alt="AI" class="ai-icon">
  175. </div>
  176. <div class="message-content" :data-message-index="index">
  177. <div class="ai-text">
  178. <div v-if="message.displayContent.length === 0" class="typing-indicator">
  179. <div class="thinking-animation">
  180. <span class="dot"></span>
  181. <span class="dot"></span>
  182. <span class="dot"></span>
  183. </div>
  184. <span>AI正在思考中...</span>
  185. </div>
  186. <div v-else v-html="message.displayContent" class="ai-content"></div>
  187. </div>
  188. <!-- 移除底部按钮,因为用户看不到这些按钮,AI回复完成后直接跳转 -->
  189. </div>
  190. </div>
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. <!-- 步骤二:培训大纲界面 -->
  196. <div v-if="currentStep === 'step2'" class="step2-content">
  197. <!-- 加载状态 -->
  198. <div v-if="isLoadingHistory" class="loading-overlay">
  199. <div class="loading-content">
  200. <div class="loading-spinner"></div>
  201. <div class="loading-text">正在加载历史记录</div>
  202. <div class="loading-subtitle">请稍候,正在为您准备数据...</div>
  203. </div>
  204. </div>
  205. <div class="outline-container">
  206. <!-- 左侧大纲内容 -->
  207. <div class="outline-main">
  208. <div class="outline-header">
  209. <div class="outline-title-container">
  210. <h3 v-if="editingType !== 'title'" class="outline-title"
  211. @click="!isGeneratingOutline && !isGeneratingExam && startEditing(null, 'title', null, outlineTitle || '安全培训大纲')"
  212. :class="{ 'disabled': isGeneratingOutline || isGeneratingExam }">
  213. {{ outlineTitle || '安全培训大纲' }}
  214. </h3>
  215. <div v-else class="edit-input-container">
  216. <textarea v-model="editingContent" class="edit-textarea title-edit-textarea" @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit" ref="titleEditInput" autofocus></textarea>
  217. </div>
  218. </div>
  219. <div class="outline-actions">
  220. <button class="action-btn" @click="copyEntireOutline" :disabled="isGeneratingOutline">
  221. <img src="@/assets/AIWriting/12.png" alt="复制" class="action-icon">
  222. 复制
  223. </button>
  224. <button class="action-btn" @click="downloadOutlineAsWord" :disabled="isGeneratingOutline">
  225. <img src="@/assets/AIWriting/13.png" alt="下载" class="action-icon">
  226. 下载
  227. </button>
  228. </div>
  229. </div>
  230. <div class="outline-content" :class="{ 'disabled': isGeneratingOutline }">
  231. <!-- 生成中遮罩层 -->
  232. <div v-if="isGeneratingOutline" class="generating-overlay">
  233. <div class="generating-content">
  234. <p>AI正在生成新大纲,请稍候...</p>
  235. </div>
  236. </div>
  237. <div v-if="isGeneratingExam" class="generating-overlay">
  238. <div class="generating-content">
  239. <p>AI正在生成考试题目,请稍候...</p>
  240. </div>
  241. </div>
  242. <!-- <div class="zoom-controls">
  243. <button class="zoom-btn">
  244. <img src="@/assets/Safety/7.png" alt="编辑" class="zoom-icon">
  245. </button>
  246. <button class="zoom-btn">
  247. <img src="@/assets/Safety/8.png" alt="新增" class="zoom-icon">
  248. </button>
  249. <button class="zoom-btn">
  250. <img src="@/assets/Safety/9.png" alt="删除" class="zoom-icon">
  251. </button>
  252. </div> -->
  253. <!-- 动态大纲内容 -->
  254. <div v-if="outlineData && outlineData.length > 0" class="outline-content-scrollable">
  255. <template v-for="(chapter, chapterIndex) in outlineData" :key="chapterIndex">
  256. <div v-if="chapter && chapter.sections" class="outline-chapter" :class="{
  257. 'dragging': draggedChapterIndex === chapterIndex,
  258. 'drag-over': dragOverChapterIndex === chapterIndex && draggedChapterIndex !== chapterIndex
  259. }" draggable="true" @dragstart="handleDragStart($event, chapterIndex)" @dragend="handleDragEnd"
  260. @dragover="handleDragOver($event, chapterIndex)" @dragleave="handleDragLeave"
  261. @drop="handleDrop($event, chapterIndex)">
  262. <div class="chapter-header">
  263. <h4 v-if="editingType !== 'chapter' || editingIndex !== chapterIndex" class="chapter-title"
  264. @click="!isGeneratingExam && startEditing(chapter, 'chapter', chapterIndex, getCleanChapterTitle(chapter.title))"
  265. :class="{ 'disabled': isGeneratingExam }">
  266. {{ getDisplayChapterTitle(chapter.title, chapterIndex) }}
  267. </h4>
  268. <div v-else class="edit-input-container">
  269. <div class="edit-input-wrapper">
  270. <textarea v-model="editingContent" class="edit-textarea chapter-edit-textarea"
  271. @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit" ref="chapterEditInput"
  272. autofocus></textarea>
  273. <!-- 编辑选项 - 显示在输入框内 -->
  274. <div class="edit-options-inline">
  275. <button class="edit-option-btn" @click="addNewItem('section', chapterIndex)">
  276. <img src="@/assets/Safety/8.png" alt="添加小节" class="edit-icon">
  277. </button>
  278. <button v-if="outlineData.length > 2" class="edit-option-btn delete-btn"
  279. @click="deleteItem('chapter', chapterIndex)">
  280. <img src="@/assets/AIWriting/8.png" alt="删除" class="edit-icon">
  281. </button>
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. <div class="outline-section">
  287. <template v-for="(section, sectionIndex) in chapter.sections" :key="sectionIndex">
  288. <div v-if="section &&
  289. section.title !== '内容要点' &&
  290. section.title !== '概述' &&
  291. section.title !== '内容详情'" class="section-container">
  292. <div class="section-header">
  293. <div
  294. v-if="editingType !== 'section' || editingIndex !== `${chapterIndex}-${sectionIndex}`"
  295. class="section-title"
  296. @click="!isGeneratingExam && startEditing(section, 'section', `${chapterIndex}-${sectionIndex}`, section.title)"
  297. :class="{ 'disabled': isGeneratingExam }">
  298. {{ section.title }}
  299. </div>
  300. <div v-else class="edit-input-container">
  301. <div class="edit-input-wrapper">
  302. <textarea v-model="editingContent" class="edit-textarea section-edit-textarea"
  303. @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit"
  304. ref="sectionEditInput" autofocus></textarea>
  305. <!-- 编辑选项 - 显示在输入框内 -->
  306. <div class="edit-options-inline">
  307. <button class="edit-option-btn"
  308. @click="addNewItem('subsection', `${chapterIndex}-${sectionIndex}`)">
  309. <img src="@/assets/Safety/8.png" alt="添加子标题" class="edit-icon">
  310. </button>
  311. <button v-if="chapter.sections.length > 1" class="edit-option-btn delete-btn"
  312. @click="deleteItem('section', `${chapterIndex}-${sectionIndex}`)">
  313. <img src="@/assets/AIWriting/8.png" alt="删除" class="edit-icon">
  314. </button>
  315. </div>
  316. </div>
  317. </div>
  318. </div>
  319. <!-- 子小节单独渲染,使用不同的类名 -->
  320. <div v-if="section && section.subsections && section.subsections.length > 0"
  321. class="section-subsection">
  322. <template v-for="(subsection, subsectionIndex) in section.subsections"
  323. :key="subsectionIndex">
  324. <div v-if="subsection &&
  325. subsection.title !== '内容要点' &&
  326. subsection.title !== '概述' &&
  327. subsection.title !== '内容详情' &&
  328. !subsection.title.includes('总章节数') &&
  329. !subsection.title.includes('总小节数') &&
  330. !subsection.title.includes('预计PPT页数') &&
  331. !subsection.title.includes('预计讲解时长')" class="subsection-container">
  332. <div class="subsection-header">
  333. <div
  334. v-if="editingType !== 'subsection' || editingIndex !== `${chapterIndex}-${sectionIndex}-${subsectionIndex}`"
  335. class="subsection-title"
  336. @click="!isGeneratingExam && startEditing(subsection, 'subsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}`, subsection.title)"
  337. :class="{ 'disabled': isGeneratingExam }">
  338. {{ subsection.title }}
  339. </div>
  340. <div v-else class="edit-input-container">
  341. <div class="edit-input-wrapper">
  342. <textarea v-model="editingContent" class="edit-textarea subsection-edit-textarea"
  343. @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit"
  344. ref="subsectionEditInput" autofocus></textarea>
  345. <!-- 编辑选项 - 显示在输入框内 -->
  346. <div class="edit-options-inline">
  347. <button class="edit-option-btn delete-btn"
  348. @click="deleteItem('subsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}`)">
  349. <img src="@/assets/AIWriting/8.png" alt="删除" class="edit-icon">
  350. </button>
  351. </div>
  352. </div>
  353. </div>
  354. </div>
  355. <!-- 具体内容要点(-开头) -->
  356. <div v-if="subsection.subsubsections && subsection.subsubsections.length > 0"
  357. class="subsubsection-container">
  358. <template v-for="(subsubsection, subsubsectionIndex) in subsection.subsubsections"
  359. :key="subsubsectionIndex">
  360. <div class="subsubsection-item">
  361. <div class="subsubsection-header">
  362. <div
  363. v-if="editingType !== 'subsubsection' || editingIndex !== `${chapterIndex}-${sectionIndex}-${subsectionIndex}-${subsubsectionIndex}`"
  364. class="subsubsection-title"
  365. @click="!isGeneratingExam && startEditing(subsubsection, 'subsubsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}-${subsubsectionIndex}`, subsubsection.title)"
  366. :class="{ 'disabled': isGeneratingExam }">
  367. {{ subsubsection.title }}
  368. </div>
  369. <div v-else class="edit-input-container">
  370. <div class="edit-input-wrapper">
  371. <textarea v-model="editingContent"
  372. class="edit-textarea subsubsection-edit-textarea"
  373. @keyup.esc="cancelEdit" @keydown.ctrl.enter="saveEdit" @blur="saveEdit" ref="subsubsectionEditInput"
  374. autofocus></textarea>
  375. <!-- 编辑选项 - 显示在输入框内 -->
  376. <div class="edit-options-inline">
  377. </div>
  378. </div>
  379. </div>
  380. </div>
  381. </div>
  382. </template>
  383. </div>
  384. </div>
  385. </template>
  386. </div>
  387. </div>
  388. </template>
  389. </div>
  390. </div>
  391. </template>
  392. <!-- 添加新章节按钮 -->
  393. <div v-if="outlineData.length < 6" class="add-chapter-container">
  394. <button class="add-chapter-btn" @click="addNewItem('chapter', null)">
  395. <img src="@/assets/Safety/8.png" alt="添加章节" class="add-icon">
  396. <span>添加新章节</span>
  397. </button>
  398. </div>
  399. </div>
  400. <!-- 默认大纲内容(当没有AI生成内容时显示) -->
  401. <div v-else>
  402. <div class="outline-chapter">
  403. <h4>第一章 安全生产基本原则</h4>
  404. <div class="outline-section">
  405. <div class="section-item">1.1 安全生产的重要性</div>
  406. <div class="section-item">1.2 安全生产相关法规</div>
  407. <div class="section-subsection">
  408. <div class="subsection-item">1.2.1 《中华人民共和国安全生产法》解读</div>
  409. <div class="subsection-item">1.2.2 建筑工程安全管理规范</div>
  410. </div>
  411. </div>
  412. </div>
  413. <div class="outline-chapter">
  414. <h4>第二章 施工现场安全管理</h4>
  415. <div class="outline-section">
  416. <div class="section-item">2.1 安全责任制度</div>
  417. <div class="section-item">2.2 安全教育培训</div>
  418. <div class="section-item">2.3 安全检查与隐患排查</div>
  419. </div>
  420. </div>
  421. <div class="outline-chapter">
  422. <h4>第三章 常见安全隐患及防范措施</h4>
  423. <div class="outline-section">
  424. <div class="section-item">3.1 高空作业安全</div>
  425. <div class="section-subsection">
  426. <div class="subsection-item">3.1.1 脚手架搭设及使用安全规范</div>
  427. </div>
  428. <div class="section-item">3.2 用电安全</div>
  429. <div class="section-item">3.3 消防安全</div>
  430. </div>
  431. </div>
  432. <div class="outline-chapter">
  433. <h4>第四章 安全事故案例分析</h4>
  434. <div class="outline-section">
  435. <div class="section-item">4.1 典型事故分析与教训</div>
  436. </div>
  437. </div>
  438. <div class="outline-chapter">
  439. <h4>第五章 总结与展望</h4>
  440. </div>
  441. </div>
  442. </div>
  443. </div>
  444. <!-- 右侧统计信息面板 -->
  445. <div class="outline-sidebar">
  446. <!-- 大纲统计信息 -->
  447. <div class="sidebar-section">
  448. <div class="section-header">
  449. <img src="@/assets/Safety/10.png" alt="统计" class="section-icon">
  450. <h5>大纲统计信息</h5>
  451. </div>
  452. <div class="section-content">
  453. <div class="stat-item">
  454. <span class="stat-label">总章节数:</span>
  455. <span class="stat-value">{{ outlineStats.totalChapters || 0 }}章</span>
  456. </div>
  457. <div class="stat-item">
  458. <span class="stat-label">总小节数:</span>
  459. <span class="stat-value">{{ outlineStats.totalSections || 0 }}小节</span>
  460. </div>
  461. <div class="stat-item">
  462. <span class="stat-label">预计PPT页数:</span>
  463. <span class="stat-value">{{ outlineStats.estimatedPages || '0页' }}</span>
  464. </div>
  465. <div class="stat-item">
  466. <span class="stat-label">预计讲解时长:</span>
  467. <span class="stat-value">{{ outlineStats.estimatedTime || '0分钟' }}</span>
  468. </div>
  469. </div>
  470. </div>
  471. <!-- 大纲编辑提示 -->
  472. <div class="sidebar-section">
  473. <div class="section-header">
  474. <img src="@/assets/Safety/11.png" alt="提示" class="section-icon">
  475. <h5>大纲编辑提示</h5>
  476. </div>
  477. <div class="section-content">
  478. <ul class="tip-list">
  479. <li>点击标题文本可直接在白色区域编辑内容</li>
  480. <li>悬停在章节上将显示编辑选项</li>
  481. <li>编辑完成后系统会自动保存更改</li>
  482. <li>如果内容较多,滚动白色区域可浏览更多大纲内容</li>
  483. <li>章节顺序可以通过拖拽调整</li>
  484. </ul>
  485. </div>
  486. </div>
  487. <!-- 大纲评价 -->
  488. <div class="sidebar-section">
  489. <div class="section-header">
  490. <h5>大纲评价</h5>
  491. </div>
  492. <div class="section-content">
  493. <p class="evaluation-question">这个大纲对您的需求满意度如何?</p>
  494. <div class="evaluation-buttons">
  495. <button class="eval-btn satisfied" :class="{ active: getEvaluationStatus() === 'satisfied' }"
  496. @click="setEvaluation('satisfied')" :disabled="isGeneratingOutline">
  497. <img src="@/assets/AIWriting/10.png" alt="满意" class="eval-icon">
  498. 满意
  499. </button>
  500. <button class="eval-btn unsatisfied" :class="{ active: getEvaluationStatus() === 'unsatisfied' }"
  501. @click="setEvaluation('unsatisfied')" :disabled="isGeneratingOutline">
  502. <img src="@/assets/AIWriting/11.png" alt="不满意" class="eval-icon">
  503. 不满意
  504. </button>
  505. </div>
  506. </div>
  507. </div>
  508. <!-- 操作按钮 -->
  509. <div class="sidebar-actions">
  510. <button class="action-btn secondary" @click="generateNewOutline"
  511. :disabled="isGeneratingOutline || isGeneratingExam">
  512. <img src="@/assets/Safety/12.png" alt="刷新" class="action-icon"
  513. :class="{ 'rotating': isGeneratingOutline }">
  514. 生成新大纲
  515. </button>
  516. <button class="action-btn primary" @click="openWPS" :disabled="isGeneratingOutline || isGeneratingExam"
  517. v-if="!currentPPTInfo">
  518. 继续创作
  519. <img src="@/assets/Safety/13.png" alt="箭头" class="action-icon">
  520. </button>
  521. <!-- <button class="action-btn wps" @click="goToStep3" :disabled="isGeneratingOutline">
  522. <img src="@/assets/Safety/28.png" alt="WPS AI PPT" class="action-icon">
  523. WPS AI PPT
  524. </button> -->
  525. <!-- 动态生成的打开PPT按钮 -->
  526. <button v-if="showOpenPPTButton && currentPPTInfo" class="action-btn primary" @click="openTestPPT"
  527. style="background: #EA580C; color: #fff;">
  528. <!-- <img src="@/assets/Safety/28.png" alt="打开PPT" class="action-icon"> -->
  529. 修改PPT模板
  530. <img src="@/assets/Safety/13.png" alt="箭头" class="action-icon">
  531. </button>
  532. <!-- 测试按钮 - 直接显示用于测试 -->
  533. <!-- <button class="action-btn test-btn" @click="openTestPPT"
  534. style="background: linear-gradient(135deg, #ff7875, #ff9c6e); margin-left: 10px; font-size: 12px;">
  535. 测试PPT查看
  536. </button> -->
  537. </div>
  538. </div>
  539. </div>
  540. </div>
  541. <!-- 步骤三:PPT模板选择界面 -->
  542. <div v-if="currentStep === 'step3'" class="step3-content" :class="{ 'disabled': isApplyingTemplate }">
  543. <!-- 加载状态 -->
  544. <div v-if="isLoadingHistory" class="loading-overlay">
  545. <div class="loading-content">
  546. <div class="loading-spinner"></div>
  547. <div class="loading-text">正在加载历史记录</div>
  548. <div class="loading-subtitle">请稍候,正在为您准备数据...</div>
  549. </div>
  550. </div>
  551. <div class="template-container">
  552. <!-- 应用模板中遮罩层 -->
  553. <div v-if="isApplyingTemplate" class="applying-overlay">
  554. <div class="applying-content">
  555. <p>AI正在填充内容并应用模板,请稍候...</p>
  556. </div>
  557. </div>
  558. <!-- 左侧PPT预览区域 -->
  559. <div class="template-preview">
  560. <div class="preview-container">
  561. <div class="preview-header">
  562. <h3 class="preview-title">预览效果</h3>
  563. <span class="save-status">已保存于 {{ currentTime }}</span>
  564. </div>
  565. <!-- 大图轮播区域 -->
  566. <div class="main-carousel">
  567. <!-- 预览模式:显示轮播图 -->
  568. <div v-if="!showDownloadOptions">
  569. <button class="carousel-btn prev" @click="prevSlide">
  570. <img src="@/assets/Safety/23.png" alt="上一页" class="carousel-icon">
  571. </button>
  572. <div class="main-slide">
  573. <!-- 如果有AI生成的PPT数据,显示AI内容 -->
  574. <div v-if="generatedPPT && generatedPPT.length > 0" class="ai-generated-slide">
  575. <div class="slide-content" :style="{ background: getCurrentPPTSlideBackground() }">
  576. <div v-for="(element, index) in getCurrentPPTSlide().elements" :key="element.id"
  577. class="slide-element" :style="getElementStyle(element)">
  578. <div v-if="element.type === 'text'" v-html="element.content"></div>
  579. <img v-else-if="element.type === 'image' && element.src" :src="element.src"
  580. :alt="element.id">
  581. </div>
  582. </div>
  583. </div>
  584. <!-- 否则显示默认图片 -->
  585. <img v-else :src="currentSlideImage" :alt="`第${currentSlideIndex + 1}页`" class="slide-image">
  586. </div>
  587. <button class="carousel-btn next" @click="nextSlide">
  588. <img src="@/assets/Safety/24.png" alt="下一页" class="carousel-icon">
  589. </button>
  590. </div>
  591. <!-- PPT编辑工具台 -->
  592. <div v-if="showDownloadOptions" class="ppt-editor-workspace">
  593. <div class="editor-canvas">
  594. <!-- PPT预览模式 -->
  595. <div v-if="showDownloadOptions && generatedPPT.length > 0" class="slide-preview" :style="{
  596. transform: `scale(${zoom})`,
  597. background: getCurrentPPTSlideBackground()
  598. }">
  599. <!-- 调试信息 -->
  600. <div
  601. style="position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 5px; border-radius: 4px; font-size: 12px; z-index: 1001;">
  602. {{ generatedPPT.length }}页, 当前第{{ currentPPTSlideIndex + 1 }}页
  603. </div>
  604. <!-- PPT预览提示 -->
  605. <div class="ppt-preview-tip">
  606. <p>💡 点击背景图片、素材图片可更换</p>
  607. </div>
  608. <!-- 显示PPT元素 -->
  609. <div v-for="(element, index) in getCurrentPPTSlideForMainView().elements" :key="element.id"
  610. class="preview-element" :class="{ selected: selectedPPTElementIndex === index }"
  611. :style="getElementStyle(element)" @click.stop="selectPPTElement(index)"
  612. @dblclick="handleDoubleClickDisabled(index)" @mousedown="startPPTDrag($event, index)">
  613. <!-- 缩放手柄 -->
  614. <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-nw"
  615. @mousedown.stop="startPPTResize($event, index, 'nw')"></div>
  616. <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-ne"
  617. @mousedown.stop="startPPTResize($event, index, 'ne')"></div>
  618. <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-sw"
  619. @mousedown.stop="startPPTResize($event, index, 'sw')"></div>
  620. <div v-if="selectedPPTElementIndex === index" class="resize-handle resize-handle-se"
  621. @mousedown.stop="startPPTResize($event, index, 'se')"></div>
  622. <!-- 拖拽手柄 -->
  623. <div v-if="selectedPPTElementIndex === index" class="drag-handle"
  624. @mousedown.stop="startPPTDrag($event, index)">
  625. {{ getElementTypeName(element.type) }}
  626. </div>
  627. <!-- 图片元素 -->
  628. <img v-if="element.type === 'image' && element.src" :src="element.src" :alt="element.id"
  629. :style="{ opacity: element.opacity || 1 }" @click="changePPTImage(index)" @mousedown.stop />
  630. <!-- 文本元素 -->
  631. <template v-else-if="element.type === 'text'">
  632. <!-- 内联编辑态 -->
  633. <div v-if="editingPPTElementIndex === index" class="inline-editor" contenteditable="true"
  634. v-html="editingPPTHtml" :style="{
  635. color: element.defaultColor,
  636. fontFamily: element.defaultFontName,
  637. opacity: element.opacity || 1
  638. }" @input="onPPTInlineInput" @blur="savePPTInlineEdit(index)" @mousedown.stop
  639. @keydown.stop></div>
  640. <!-- 普通显示态 -->
  641. <div v-else v-html="element.content" :style="{
  642. color: element.defaultColor,
  643. fontFamily: element.defaultFontName,
  644. opacity: element.opacity || 1
  645. }"></div>
  646. </template>
  647. <!-- 形状元素 -->
  648. <div v-else-if="element.type === 'shape'" class="shape" :style="getShapeStyle(element)"></div>
  649. </div>
  650. </div>
  651. <!-- 编辑模式:显示编辑界面 -->
  652. <div v-else-if="showDownloadOptions && generatedPPT.length === 0" class="edit-mode">
  653. <div class="slide-editor">
  654. <div class="slide-content" contenteditable="true" @input="updateSlideContent">
  655. <h2 class="slide-title">{{ currentEditingSlide.title || '点击编辑标题' }}</h2>
  656. <div class="slide-body">
  657. <p>{{ currentEditingSlide.content || '点击编辑内容' }}</p>
  658. </div>
  659. </div>
  660. </div>
  661. </div>
  662. </div>
  663. </div>
  664. </div>
  665. <!-- 缩略图导航 -->
  666. <div class="thumbnail-nav">
  667. <!-- 模板预览模式:显示模板缩略图 -->
  668. <div v-if="!showDownloadOptions">
  669. <div class="slide-counter">
  670. 第{{ currentSlideIndex + 1 }}页/共{{ slideImages.length }}页
  671. <div class="progress-dots">
  672. <div v-for="(slide, index) in slideImages" :key="index"
  673. :class="['progress-dot', { active: index === currentSlideIndex }]" @click="goToSlide(index)">
  674. </div>
  675. </div>
  676. </div>
  677. <div class="thumbnail-strip" @wheel="handleThumbnailWheel" ref="thumbnailStrip">
  678. <div v-for="(slide, index) in slideImages" :key="index"
  679. :class="['thumbnail-item', { active: index === currentSlideIndex }]" @click="goToSlide(index)">
  680. <img :src="slide" :alt="`第${index + 1}页`" class="thumbnail-image">
  681. <div class="thumbnail-number">{{ index + 1 }}</div>
  682. </div>
  683. </div>
  684. </div>
  685. <!-- PPT预览模式:显示PPT缩略图 -->
  686. <div v-if="showDownloadOptions && generatedPPT.length > 0">
  687. <div class="slide-counter">
  688. 第{{ currentPPTSlideIndex + 1 }}页/共{{ generatedPPT.length }}页
  689. <div class="progress-dots">
  690. <div v-for="(slide, index) in generatedPPT" :key="index"
  691. :class="['progress-dot', { active: index === currentPPTSlideIndex }]"
  692. @click="goToPPTSlide(index)">
  693. </div>
  694. </div>
  695. </div>
  696. <div class="thumbnail-strip" @wheel="handleThumbnailWheel" ref="thumbnailStrip">
  697. <div v-for="(slide, index) in generatedPPT" :key="index"
  698. :class="['thumbnail-item', { active: index === currentPPTSlideIndex }]"
  699. @click="goToPPTSlide(index)">
  700. <div class="thumbnail-preview" :style="{ background: getSlideBackground(slide) }">
  701. <div class="thumbnail-content">
  702. <div v-for="element in slide.elements" :key="element.id" class="thumbnail-element"
  703. :style="getThumbnailElementStyle(element)">
  704. <div v-if="element.type === 'text'" v-html="element.content"></div>
  705. <img v-else-if="element.type === 'image' && element.src" :src="element.src"
  706. :alt="element.id">
  707. </div>
  708. </div>
  709. </div>
  710. <div class="thumbnail-number">{{ index + 1 }}</div>
  711. </div>
  712. </div>
  713. </div>
  714. </div>
  715. </div>
  716. </div>
  717. <!-- 右侧侧边栏 -->
  718. <div class="template-sidebar">
  719. <!-- 模板样式选择 -->
  720. <div v-if="!showDownloadOptions" class="template-content" :class="{ 'disabled': isApplyingTemplate }">
  721. <h4 class="sidebar-title">模板样式 ({{ templateStyles.length }})</h4>
  722. <div class="template-list">
  723. <div v-for="(template, index) in templateStyles" :key="index"
  724. :class="['template-item', { active: selectedTemplate === index }]"
  725. @click="selectTemplate(index); resetSlidePosition()">
  726. <div class="template-thumbnail">
  727. <img :src="template.thumbnail" :alt="template.title" class="template-img">
  728. <!-- <div v-if="template.type === 'dynamic'" class="dynamic-badge">动态</div> -->
  729. </div>
  730. <div class="template-info">
  731. <h5 class="template-title">{{ template.title }}</h5>
  732. <div class="template-meta">
  733. <span class="update-time">{{ template.updateTime }}</span>
  734. <span class="page-count">{{ template.pageCount }}页</span>
  735. </div>
  736. <div v-if="template.description" class="template-description">
  737. {{ template.description }}
  738. </div>
  739. </div>
  740. </div>
  741. </div>
  742. <!-- 动态模板预览信息 -->
  743. <!-- <div v-if="isDynamicTemplate && templatePreview" class="dynamic-preview">
  744. <h5 class="preview-title">📊 大纲分析</h5>
  745. <div class="preview-stats">
  746. <div class="stat-item">
  747. <span class="stat-label">章节数:</span>
  748. <span class="stat-value">{{ templatePreview.chapterCount }}</span>
  749. </div>
  750. <div class="stat-item">
  751. <span class="stat-label">小节数:</span>
  752. <span class="stat-value">{{ templatePreview.totalSections }}</span>
  753. </div>
  754. <div class="stat-item">
  755. <span class="stat-label">复杂度:</span>
  756. <span class="stat-value" :class="'complexity-' + templatePreview.complexity">
  757. {{ templatePreview.complexity === 'simple' ? '简单' :
  758. templatePreview.complexity === 'medium' ? '中等' : '复杂' }}
  759. </span>
  760. </div>
  761. <div class="stat-item">
  762. <span class="stat-label">预计页数:</span>
  763. <span class="stat-value">{{ templatePreview.estimatedSlides }}</span>
  764. </div>
  765. </div>
  766. <div v-if="templatePreview.recommendations && templatePreview.recommendations.length > 0" class="recommendations">
  767. <h6 class="recommendations-title">💡 优化建议</h6>
  768. <ul class="recommendations-list">
  769. <li v-for="(rec, index) in templatePreview.recommendations" :key="index">
  770. {{ rec }}
  771. </li>
  772. </ul>
  773. </div>
  774. <div v-if="templatePreview.validation && templatePreview.validation.warnings && templatePreview.validation.warnings.length > 0" class="warnings">
  775. <h6 class="warnings-title">⚠️ 注意事项</h6>
  776. <ul class="warnings-list">
  777. <li v-for="(warning, index) in templatePreview.validation.warnings" :key="index">
  778. {{ warning }}
  779. </li>
  780. </ul>
  781. </div>
  782. </div> -->
  783. <!-- 应用模板中遮罩层 -->
  784. <div v-if="isApplyingTemplate" class="applying-overlay">
  785. <div class="applying-content">
  786. <p>AI正在填充内容并应用模板,请稍候...</p>
  787. </div>
  788. </div>
  789. </div>
  790. <!-- 下载选项 -->
  791. <div v-if="showDownloadOptions" class="download-content"
  792. :class="{ 'disabled': isGeneratingTrainingMaterial || isGeneratingExam }">
  793. <h4 class="sidebar-title">下载选项</h4>
  794. <!-- 培训讲义生成中遮罩层 -->
  795. <div v-if="isGeneratingTrainingMaterial" class="applying-overlay">
  796. <div class="applying-content">
  797. <p>AI正在生成培训讲义,请稍候...</p>
  798. </div>
  799. </div>
  800. <!-- 考试工坊生成中遮罩层 -->
  801. <div v-if="isGeneratingExam" class="applying-overlay">
  802. <div class="applying-content">
  803. <p>AI正在生成考试题目,请稍候...</p>
  804. </div>
  805. </div>
  806. <div class="download-options">
  807. <div v-for="(option, index) in downloadOptions" :key="index"
  808. :class="['download-option', { active: selectedDownloadOption === index }]"
  809. @click="selectDownloadOption(index)">
  810. <div class="option-icon">
  811. <img :src="option.icon" :alt="option.title" class="option-img">
  812. </div>
  813. <div class="option-info">
  814. <h5 class="option-title">{{ option.title }}</h5>
  815. <p class="option-description">{{ option.description }}</p>
  816. </div>
  817. <div v-if="selectedDownloadOption === index" class="option-check">
  818. <img src="@/assets/Safety/29.png" alt="选中" class="check-icon">
  819. </div>
  820. </div>
  821. </div>
  822. <div class="download-actions">
  823. <button class="action-btn secondary" @click="goBackToTemplate">
  824. 重新挑选模板
  825. </button>
  826. <button class="action-btn primary" @click="exportPPTX" :disabled="isDownloading">
  827. <img src="@/assets/Safety/30.png" alt="下载" class="download-icon">
  828. {{ isDownloading ? '生成中...' : '立即下载' }}
  829. </button>
  830. </div>
  831. </div>
  832. </div>
  833. </div>
  834. <!-- 操作按钮 -->
  835. <div class="preview-actions" v-if="!showDownloadOptions">
  836. <button class="action-btn secondary" @click="goToStep2">
  837. 返回编辑大纲
  838. </button>
  839. <!-- Mock数据选择器 -->
  840. <!-- <div class="mock-data-selector" style="margin-bottom: 16px;">
  841. <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">选择测试数据:</label>
  842. <select v-model="selectedMockData" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; background: white;">
  843. <option v-for="(option, index) in mockDataOptions" :key="option.id" :value="index">
  844. {{ option.title }} - {{ option.description }}
  845. </option>
  846. </select>
  847. </div> -->
  848. <!-- 使用用户大纲选项 -->
  849. <!-- <div class="user-outline-option" style="margin-bottom: 16px; padding: 16px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;">
  850. <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">使用我之前生成的大纲:</label>
  851. <div style="display: flex; align-items: center; gap: 8px;">
  852. <input
  853. type="checkbox"
  854. id="useUserOutline"
  855. v-model="useUserOutline"
  856. style="margin: 0;"
  857. />
  858. <label for="useUserOutline" style="margin: 0; font-weight: 500; color: #4b5563;">
  859. 使用当前大纲数据 ({{ outlineData.length > 0 ? `${outlineData.length}个章节` : '无大纲数据' }})
  860. </label>
  861. </div>
  862. <div v-if="useUserOutline && outlineData.length === 0" style="margin-top: 8px; padding: 8px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 4px; color: #dc2626; font-size: 14px;">
  863. ⚠️ 当前没有大纲数据,请先生成大纲后再使用此选项
  864. </div>
  865. <div v-else-if="useUserOutline && outlineData.length > 0" style="margin-top: 8px; padding: 8px; background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 4px; color: #0369a1; font-size: 14px;">
  866. ✅ 检测到 {{ outlineData.length }} 个章节的大纲数据,可以用于测试
  867. </div>
  868. </div> -->
  869. <!-- 测试动态模板按钮 -->
  870. <!-- <button class="action-btn test" @click="testDynamicTemplateFill" style="background: #10b981; margin-right: 8px;">
  871. 测试动态模板
  872. </button> -->
  873. <button class="action-btn primary" @click="applyTemplate" :disabled="isApplyingTemplate">
  874. <span v-if="!isApplyingTemplate">应用此模板</span>
  875. <span v-else>正在处理中...</span>
  876. </button>
  877. <!-- <button class="action-btn secondary" @click="testOutlineToAIPPT" style="margin-left: 10px;">
  878. 转换并应用模板5
  879. </button> -->
  880. </div>
  881. </div>
  882. </div>
  883. <!-- 推荐问题 -->
  884. <div v-if="currentStep === 'step1' && !showChat && !selectedFile" class="recommended-questions">
  885. <div v-for="(question, index) in hotQuestions" :key="question.id || index" class="question-tag"
  886. @click="handleRecommendedQuestion(question.question)">
  887. <img :src="getQuestionIcon(question.question)" alt="问题" class="question-icon">
  888. {{ question.question }}
  889. </div>
  890. <!-- 如果没有数据,显示默认问题 -->
  891. <div v-if="hotQuestions.length === 0" class="question-tag"
  892. @click="handleRecommendedQuestion('施工现场安全培训的主要内容有哪些?')">
  893. <img src="@/assets/Chat/12.png" alt="问题" class="question-icon">
  894. 施工现场安全培训的主要内容有哪些?
  895. </div>
  896. <div v-if="hotQuestions.length === 0" class="question-tag"
  897. @click="handleRecommendedQuestion('高空作业安全防护措施有哪些要求?')">
  898. <img src="@/assets/Chat/10.png" alt="问题" class="question-icon">
  899. 高空作业安全防护措施有哪些要求?
  900. </div>
  901. <div v-if="hotQuestions.length === 0" class="question-tag" @click="handleRecommendedQuestion('《建设工程安全生产管理条例》')">
  902. <img src="@/assets/Chat/11.png" alt="文档" class="question-icon">
  903. 《建设工程安全生产管理条例》
  904. </div>
  905. </div>
  906. <!-- 底部输入区域 -->
  907. <div v-if="currentStep === 'step1'" class="chat-input-section">
  908. <div class="input-container">
  909. <!-- 文件预览区域 -->
  910. <div v-if="selectedFile" class="file-preview-section">
  911. <div class="file-preview">
  912. <div class="file-icon">
  913. <img v-if="selectedFile.type === '.doc' || selectedFile.type === '.docx'" :src="selectedFile.icon" alt="文档图标" class="file-icon-img">
  914. <span v-else>{{ selectedFile.icon }}</span>
  915. </div>
  916. <div class="file-info">
  917. <div class="file-name">{{ selectedFile.name }}</div>
  918. <div class="file-size">{{ formatFileSize(selectedFile.size) }}</div>
  919. </div>
  920. <button class="remove-file-btn" @click="removeSelectedFile">
  921. <span class="remove-icon">×</span>
  922. </button>
  923. </div>
  924. </div>
  925. <div class="input-box">
  926. <button class="attach-btn" @click="triggerFileUpload" :disabled="isSending || hasTypingMessage">
  927. <div class="icon-container">
  928. <img src="@/assets/Chat/9.png" alt="附件" class="action-icon"
  929. style="width: 20px; height: 20px; max-width: 20px; max-height: 20px;">
  930. </div>
  931. </button>
  932. <input type="text" placeholder="请在此处发送消息 (Enter键可立即发送)" class="message-input" v-model="messageText"
  933. @keyup.enter="sendMessage" @input="handleInput" :disabled="isSending || hasTypingMessage"
  934. maxlength="2000">
  935. <button class="voice-btn" @click="handleVoiceClick" :disabled="isSending || hasTypingMessage"
  936. :class="{ 'recording': isListening }">
  937. <div class="icon-container">
  938. <img src="@/assets/Chat/18.png" alt="语音" class="action-icon"
  939. style="width: 20px; height: 20px; max-width: 20px; max-height: 20px;">
  940. <div v-if="isListening" class="recording-indicator"></div>
  941. </div>
  942. </button>
  943. <div class="divider"></div>
  944. <button class="send-btn" @click="sendMessage"
  945. :disabled="isSending || hasTypingMessage || !messageText.trim()">
  946. <img :src="messageText.trim() && !isSending && !hasTypingMessage ? sendIconFilled : sendIconEmpty"
  947. alt="发送" class="send-icon">
  948. </button>
  949. </div>
  950. </div>
  951. </div>
  952. <!-- 隐藏的文件输入框 -->
  953. <input ref="fileInput" type="file" accept=".docx" style="display: none" @change="handleFileSelect" />
  954. <!-- 隐藏的图片输入框 -->
  955. <input ref="imageInput" type="file" accept="image/*" style="display: none" @change="handleImageSelect" />
  956. <!-- 删除确认弹窗 -->
  957. <DeleteConfirmModal :visible="showDeleteModal" title="删除历史记录" :message="deleteConfirmMessage"
  958. @confirm="confirmDeleteHistory" @cancel="cancelDeleteHistory" @close="cancelDeleteHistory" />
  959. <!-- 自定义居中提示 -->
  960. <div v-if="showCopyToast" class="copy-toast-overlay"
  961. style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; z-index: 10000; pointer-events: none;">
  962. <div class="copy-toast"
  963. style="background: rgba(0, 0, 0, 0.8); color: white; padding: 16px 24px; border-radius: 8px; font-size: 16px; font-weight: 500; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);">
  964. 已复制大纲
  965. </div>
  966. </div>
  967. <!-- WPS AI PPT集成弹窗 -->
  968. <div v-if="showWPSModal" class="wps-modal-overlay" @click="showWPSModal = false"
  969. style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 9999;">
  970. <div class="wps-modal" @click.stop
  971. style="width: 98%; height: 95%; max-width: none; max-height: none; background: white; border-radius: 8px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; overflow: hidden;">
  972. <div class="wps-modal-header"
  973. style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background: #f8f9fa; border-bottom: 1px solid #e9ecef;">
  974. <h3 style="margin: 0; font-size: 18px; font-weight: 600; color: #333;">
  975. <!-- {{ currentPPTInfo && selectedWPSUrl !== 'https://aippt.wps.cn/aippt/' ? `
  976. ${currentPPTInfo.file_name}` :
  977. '蜀安 AI PPT' }} -->
  978. </h3>
  979. <button class="wps-modal-close" @click="showWPSModal = false"
  980. style="background: none; border: none; font-size: 24px; color: #666; cursor: pointer; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">×</button>
  981. </div>
  982. <div class="wps-modal-content" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
  983. <!-- 下载监听插件控制 -->
  984. <!-- <div style="padding: 8px 24px; flex-shrink: 0; background: #f8f9fa; border-bottom: 1px solid #e9ecef;">
  985. <label style="font-size: 12px; display: flex; align-items: center; gap: 4px;">
  986. <input type="checkbox" v-model="isDownloadListenerActive" @change="toggleDownloadListener"
  987. style="margin: 0;" />
  988. 启用下载监听插件
  989. </label>
  990. </div> -->
  991. <div class="wps-iframe-container" style="flex: 1; min-height: 0; position: relative;">
  992. <iframe :src="selectedWPSUrl" frameborder="0" allowfullscreen class="wps-iframe" title="WPS AI PPT"
  993. @error="handleIframeError" style="width: 100%; height: calc(100% - 30px); border: none;"></iframe>
  994. <!-- 遮罩层:根据PPT创建状态显示不同遮罩 -->
  995. <!-- PPT创建前:显示"蜀安AI PPT"遮罩 -->
  996. <div v-if="!currentPPTInfo" class="wps-iframe-mask-top-right"
  997. style="position: absolute; top: 8px; left: 8px; width: 160px; height: 54px; background: #F8F8F8; border-radius: 8px; z-index: 5; pointer-events: none; opacity: 0.98; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: #333333">
  998. 蜀安AI PPT
  999. </div>
  1000. <!-- PPT创建后:延迟3秒显示另一个遮罩 -->
  1001. <!-- <div
  1002. v-if="currentPPTInfo && showSecondMask"
  1003. class="wps-iframe-mask-top-center"
  1004. style="position: absolute; top: 8px; left: 930px; width: 80px; height: 24px; background: #EEEEEE; border-radius: 8px; z-index: 5; pointer-events: none; opacity: 0.98; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: #333333">
  1005. </div> -->
  1006. </div>
  1007. </div>
  1008. </div>
  1009. </div>
  1010. </div>
  1011. </div>
  1012. </template>
  1013. <script setup>
  1014. import { ref, computed, onMounted, onUnmounted, reactive, nextTick, watch } from 'vue'
  1015. import { useRoute, useRouter } from 'vue-router'
  1016. import Sidebar from '@/components/Sidebar.vue'
  1017. import DeleteConfirmModal from '@/components/DeleteConfirmModal.vue'
  1018. import { apis } from '@/request/apis.js'
  1019. // ===== 已删除:getUserId - 不再需要,改用token =====
  1020. // import { getUserId } from '@/utils/userManager.js'
  1021. // 导入语音识别组件
  1022. import { useSpeechRecognition } from '@/composables/useSpeechRecognition'
  1023. // 导入发送按钮图标
  1024. import sendIconEmpty from '@/assets/Chat/15.png'
  1025. import sendIconFilled from '@/assets/Chat/16.png'
  1026. // 导入其他图片资源
  1027. import safetyTrainingIcon from '@/assets/Safety/4.png'
  1028. import safetyAssessmentIcon from '@/assets/Safety/3.png'
  1029. import safetyRegulationsIcon from '@/assets/Safety/2.png'
  1030. import emergencyProceduresIcon from '@/assets/Safety/1.png'
  1031. import defaultHistoryIcon from '@/assets/Safety/6.png'
  1032. import copyIcon from '@/assets/AIWriting/5.png'
  1033. import editIcon from '@/assets/AIWriting/6.png'
  1034. import regenerateIcon from '@/assets/AIWriting/7.png'
  1035. import deleteIcon from '@/assets/AIWriting/8.png'
  1036. import voiceIcon from '@/assets/AIWriting/9.png'
  1037. import likeIcon from '@/assets/AIWriting/10.png'
  1038. import dislikeIcon from '@/assets/AIWriting/11.png'
  1039. import questionIcon1 from '@/assets/Chat/12.png'
  1040. import questionIcon2 from '@/assets/Chat/10.png'
  1041. import questionIcon3 from '@/assets/Chat/11.png'
  1042. // 导入template5图片
  1043. import template5Slide1 from '@/assets/template5/template5-slide-1.png'
  1044. import template5Slide2 from '@/assets/template5/template5-slide-2.png'
  1045. import template5Slide3 from '@/assets/template5/template5-slide-3.png'
  1046. import template5Slide4 from '@/assets/template5/template5-slide-4.png'
  1047. import template5Slide5 from '@/assets/template5/template5-slide-5.png'
  1048. // 导入template7图片
  1049. import template7Slide1 from '@/assets/template7/template7-slide-1.png'
  1050. import template7Slide2 from '@/assets/template7/template7-slide-2.png'
  1051. import template7Slide3 from '@/assets/template7/template7-slide-3.png'
  1052. import template7Slide4 from '@/assets/template7/template7-slide-4.png'
  1053. import template7Slide5 from '@/assets/template7/template7-slide-5.png'
  1054. // 导入template8图片
  1055. import template8Slide1 from '@/assets/template8/template8-slide-1.png'
  1056. import template8Slide2 from '@/assets/template8/template8-slide-2.png'
  1057. import template8Slide3 from '@/assets/template8/template8-slide-3.png'
  1058. import template8Slide4 from '@/assets/template8/template8-slide-4.png'
  1059. import template8Slide5 from '@/assets/template8/template8-slide-5.png'
  1060. // 导入JSON文件
  1061. import template5JsonData from '@/assets/mocks/template_5.json'
  1062. import template7JsonData from '@/assets/mocks/template_7.json'
  1063. import template8JsonData from '@/assets/mocks/template_8.json'
  1064. import aipptJsonData from '@/assets/mocks/AIPPT.json'
  1065. // 导入动态模板生成器
  1066. import {
  1067. generateDynamicTemplate,
  1068. selectOptimalTemplate,
  1069. validateOutlineForDynamicTemplate,
  1070. getAvailableTemplateStyles
  1071. } from '@/utils/dynamicTemplateGenerator.js'
  1072. import {
  1073. matchOutlineAndGeneratePPT,
  1074. previewOutlineMatch
  1075. } from '@/utils/outlineMatchingAlgorithm.js'
  1076. // 导入Element Plus组件
  1077. import { ElMessage, ElMessageBox } from 'element-plus'
  1078. // 路由
  1079. const router = useRouter()
  1080. const route = useRoute()
  1081. // 响应式数据
  1082. const messageText = ref('')
  1083. const selectedHistoryItem = ref(null) // 选中的历史记录项
  1084. const ai_conversation_id = ref(0) // 对话ID
  1085. // 保存状态管理
  1086. const lastSavedOutlineData = ref(null) // 上次保存的大纲数据
  1087. const lastSavedPPTData = ref(null) // 上次保存的PPT数据
  1088. const isSaving = ref(false) // 是否正在保存中
  1089. const isSwitchingHistory = ref(false) // 是否正在切换历史记录
  1090. // 删除相关状态
  1091. const showDeleteModal = ref(false) // 控制是否显示删除确认弹窗
  1092. const deleteTargetItem = ref(null) // 要删除的目标项
  1093. const currentStep = ref('step1') // 当前步骤:step1-步骤一,step2-步骤二,step3-步骤三
  1094. const showOutline = ref(false) // 控制是否显示大纲
  1095. const evaluation = ref('') // 大纲评价状态
  1096. const outlineFeedback = ref(null) // 大纲的用户反馈状态(从后端获取)
  1097. const currentAiMessageId = ref(null) // 当前AI消息的ID
  1098. const outlineId = ref(null) // 大纲的ID
  1099. const isGeneratingOutline = ref(false) // 控制是否正在生成新大纲
  1100. const isGeneratingExam = ref(false) // 控制是否正在生成考题
  1101. const isApplyingTemplate = ref(false) // 控制是否正在应用模板
  1102. const isLoadingHistory = ref(false) // 控制是否正在加载历史记录
  1103. const isGeneratingTrainingMaterial = ref(false) // 控制是否正在生成培训讲义
  1104. const isProcessing = ref(false) // 控制是否正在处理中(生成PPT或下载时禁用其他操作)
  1105. // 功能卡片和热点问题数据
  1106. const functionCards = ref([])
  1107. const hotQuestions = ref([])
  1108. // 聊天相关状态
  1109. const showChat = ref(false) // 控制是否显示聊天界面
  1110. const chatMessages = ref([]) // 聊天消息历史
  1111. const isSending = ref(false) // 控制发送状态
  1112. const showWPSModal = ref(false) // 控制是否显示WPS AI PPT集成弹窗
  1113. const showCopyToast = ref(false) // 控制是否显示复制大纲提示
  1114. const selectedWPSUrl = ref('https://aippt.wps.cn/aippt/') // 当前选择的WPS URL,默认进入WPS AI PPT
  1115. // 下载监听插件相关
  1116. const isDownloadListenerActive = ref(true) // 是否启用下载监听,默认选中
  1117. const downloadHistory = ref([]) // 下载历史记录
  1118. const lastDownloadTime = ref(null) // 最后一次下载时间
  1119. // PPT打开功能相关
  1120. const currentPPTInfo = ref(null) // 当前检测到的PPT信息
  1121. const showOpenPPTButton = ref(false) // 是否显示打开PPT按钮
  1122. const showSecondMask = ref(false) // 是否显示第二个遮罩(延迟3秒)
  1123. // 文件上传相关
  1124. const selectedFile = ref(null)
  1125. const isUploadingFile = ref(false)
  1126. const fileContent = ref('') // 存储文件内容
  1127. // 文件处理配置
  1128. const fileConfig = reactive({
  1129. maxSize: 20 * 1024 * 1024, // 20MB
  1130. allowedTypes: ['.docx'] // 只允许.docx格式的Word文档
  1131. })
  1132. // OSS上传配置
  1133. const uploadData = reactive({
  1134. OssAccessKeyId: '',
  1135. policy: '',
  1136. Signature: '',
  1137. host: '',
  1138. dir: '',
  1139. key: ''
  1140. })
  1141. // 语音功能
  1142. const {
  1143. isSupported: speechSupported,
  1144. isListening,
  1145. isSpeaking: speechIsSpeaking,
  1146. transcript,
  1147. error: speechError,
  1148. startListening,
  1149. stopListening,
  1150. speakText,
  1151. stopSpeaking
  1152. } = useSpeechRecognition()
  1153. // 记录正在朗读的消息ID
  1154. const speakingMessageId = ref(null)
  1155. // 计算属性 - 是否有正在打字的AI消息
  1156. const hasTypingMessage = computed(() => {
  1157. return chatMessages.value.some(message => message.type === 'ai' && message.isTyping)
  1158. })
  1159. // 删除确认消息
  1160. const deleteConfirmMessage = computed(() => {
  1161. const title = deleteTargetItem.value?.item?.title || ''
  1162. return `确定要删除历史记录"${title}"吗?删除后将无法恢复。`
  1163. })
  1164. // PPT生成相关数据
  1165. const outlineData = ref(null) // 大纲数据
  1166. const outlineStats = ref({}) // 大纲统计信息
  1167. const outlineTitle = ref('') // 大纲标题
  1168. // 计算大纲统计信息
  1169. const calculateOutlineStats = (chapters) => {
  1170. if (!chapters || !Array.isArray(chapters)) {
  1171. return {
  1172. totalChapters: 0,
  1173. totalSections: 0,
  1174. estimatedPages: '0页',
  1175. estimatedTime: '0分钟'
  1176. }
  1177. }
  1178. const totalChapters = chapters.length
  1179. let totalSections = 0
  1180. // 计算总小节数
  1181. chapters.forEach((chapter, index) => {
  1182. if (chapter.sections && Array.isArray(chapter.sections)) {
  1183. totalSections += chapter.sections.length
  1184. }
  1185. })
  1186. // 根据章节和小节数预估PPT页数和讲解时长
  1187. const estimatedPages = estimatePPTPages(chapters, totalSections)
  1188. const estimatedTime = estimatePresentationTime(chapters, totalSections)
  1189. return {
  1190. totalChapters,
  1191. totalSections,
  1192. estimatedPages,
  1193. estimatedTime
  1194. }
  1195. }
  1196. // 预估PPT页数
  1197. const estimatePPTPages = (chapters, sections) => {
  1198. // 基础页面:封面(1) + 目录(1) + 结束页(1) = 3页
  1199. let basePages = 3
  1200. // 每个章节:过渡页(1) + 内容页(根据小节数计算)
  1201. let contentPages = 0
  1202. chapters.forEach(chapter => {
  1203. if (chapter.sections && chapter.sections.length > 0) {
  1204. // 每个章节至少1个过渡页
  1205. contentPages += 1
  1206. // 每个小节1-2页内容页
  1207. chapter.sections.forEach(section => {
  1208. if (section.subsections && section.subsections.length > 0) {
  1209. // 根据子小节数量决定页数
  1210. const subsectionCount = section.subsections.length
  1211. if (subsectionCount <= 2) {
  1212. contentPages += 1 // 1-2个子小节用1页
  1213. } else if (subsectionCount <= 4) {
  1214. contentPages += 2 // 3-4个子小节用2页
  1215. } else {
  1216. contentPages += 3 // 5个以上子小节用3页
  1217. }
  1218. } else {
  1219. contentPages += 1 // 没有子小节,默认1页
  1220. }
  1221. })
  1222. } else {
  1223. // 章节没有小节,默认1页过渡页 + 1页内容页
  1224. contentPages += 2
  1225. }
  1226. })
  1227. const totalPages = basePages + contentPages
  1228. return `${totalPages}页`
  1229. }
  1230. // 预估讲解时长
  1231. const estimatePresentationTime = (chapters, sections) => {
  1232. // 基础时间:开场(2分钟) + 结束(3分钟) = 5分钟
  1233. let baseTime = 5
  1234. // 每个章节的讲解时间
  1235. let contentTime = 0
  1236. chapters.forEach(chapter => {
  1237. if (chapter.sections && chapter.sections.length > 0) {
  1238. // 每个章节过渡页:1-2分钟
  1239. contentTime += 2
  1240. // 每个小节:3-5分钟
  1241. chapter.sections.forEach(section => {
  1242. if (section.subsections && section.subsections.length > 0) {
  1243. const subsectionCount = section.subsections.length
  1244. if (subsectionCount <= 2) {
  1245. contentTime += 3 // 1-2个子小节用3分钟
  1246. } else if (subsectionCount <= 4) {
  1247. contentTime += 5 // 3-4个子小节用5分钟
  1248. } else {
  1249. contentTime += 7 // 5个以上子小节用7分钟
  1250. }
  1251. } else {
  1252. contentTime += 4 // 没有子小节,默认4分钟
  1253. }
  1254. })
  1255. } else {
  1256. // 章节没有小节,默认2分钟过渡 + 4分钟内容
  1257. contentTime += 6
  1258. }
  1259. })
  1260. const totalTime = baseTime + contentTime
  1261. return `${totalTime}分钟`
  1262. }
  1263. // 更新大纲统计信息
  1264. const updateOutlineStats = () => {
  1265. if (outlineData.value) {
  1266. outlineStats.value = calculateOutlineStats(outlineData.value)
  1267. }
  1268. }
  1269. // 编辑相关状态
  1270. const editingItem = ref(null) // 当前正在编辑的项目
  1271. const editingType = ref('') // 编辑类型:'title', 'chapter', 'section', 'subsection'
  1272. const editingIndex = ref(null) // 编辑项目的索引
  1273. const editingContent = ref('') // 编辑内容
  1274. // 拖拽相关状态
  1275. const draggedChapterIndex = ref(null) // 正在拖拽的章节索引
  1276. const dragOverChapterIndex = ref(null) // 拖拽悬停的章节索引
  1277. const showEditOptions = ref(null) // 显示编辑选项的项目ID
  1278. // 步骤三相关数据
  1279. const currentSlideIndex = ref(0) // 当前幻灯片索引
  1280. const selectedTemplate = ref(0) // 选中的模板索引
  1281. // PPT编辑相关数据
  1282. const currentEditingSlide = ref({ title: '', content: '' }) // 当前编辑的幻灯片内容
  1283. const pptSlides = ref([]) // PPT幻灯片数据
  1284. // 步骤四相关数据
  1285. const selectedDownloadOption = ref(0) // 选中的下载选项索引
  1286. const showDownloadOptions = ref(false) // 控制是否显示下载选项
  1287. // 预览模式相关数据
  1288. const previewMode = ref('edit') // 预览模式:'edit' - 编辑模式, 'preview' - 预览模式
  1289. // PPT预览相关数据
  1290. const isPPTPreviewMode = ref(false) // 是否处于PPT预览模式
  1291. const generatedPPT = ref([]) // 生成的PPT数据
  1292. const currentPPTSlideIndex = ref(0) // 当前PPT幻灯片索引
  1293. const selectedPPTElementIndex = ref(-1) // 选中的PPT元素索引
  1294. const editingPPTElementIndex = ref(-1) // 正在编辑的PPT元素索引
  1295. const editingPPTHtml = ref('') // 正在编辑的PPT HTML内容
  1296. const zoom = ref(1) // 缩放比例
  1297. const selectedImageIndex = ref(null) // 当前选中的图片索引
  1298. // 幻灯片图片数组
  1299. const slideImages = ref([
  1300. template5Slide1, // 第1页
  1301. template5Slide2, // 第2页
  1302. template5Slide3, // 第3页
  1303. template5Slide4, // 第4页
  1304. template5Slide5 // 第5页
  1305. ])
  1306. // 模板样式数据
  1307. const templateStyles = ref([
  1308. // {
  1309. // thumbnail: template5Slide1,
  1310. // title: '通用类PPT',
  1311. // updateTime: '2025-09-01 14:23 更新',
  1312. // pageCount: 5,
  1313. // type: 'static'
  1314. // },
  1315. {
  1316. thumbnail: template5Slide1,
  1317. title: '通用类PPT',
  1318. updateTime: '2025-01-15 10:00 更新',
  1319. pageCount: '5',
  1320. type: 'dynamic', // 恢复dynamic类型
  1321. // description: '支持2-6章节,2-4小节的灵活组合',
  1322. style: 'default'
  1323. },
  1324. {
  1325. thumbnail: template7Slide1, // 使用相同的缩略图,实际会显示红色主题
  1326. title: '红色主题PPT',
  1327. updateTime: '2025-01-15 10:00 更新',
  1328. pageCount: '5',
  1329. type: 'static', // 改回static类型,使用我们设计的template_7.json
  1330. // description: '红色主题的PPT模板',
  1331. style: 'red', // 恢复红色主题样式
  1332. templateData: template7JsonData // 使用我们精心设计的红色主题结构
  1333. },
  1334. // {
  1335. // thumbnail: template8Slide1, // 使用模板8的缩略图
  1336. // title: '蓝色科技主题PPT',
  1337. // updateTime: '2025-01-15 10:00 更新',
  1338. // pageCount: '5',
  1339. // type: 'static', // 使用我们设计的template_8.json
  1340. // description: '蓝色科技风格的PPT模板',
  1341. // style: 'blueTech', // 蓝色科技主题样式
  1342. // templateData: template8JsonData // 使用我们精心设计的蓝色科技主题结构
  1343. // }
  1344. ])
  1345. // 动态模板相关状态
  1346. const isDynamicTemplate = ref(false)
  1347. const templatePreview = ref(null)
  1348. const templateAnalysis = ref(null)
  1349. // 填充动态模板内容(使用AI填充详细内容)
  1350. const fillDynamicTemplateContentWithAI = async (template, outlineData, title) => {
  1351. try {
  1352. console.log('开始填充动态模板内容(AI增强)...')
  1353. console.log('模板数据:', template)
  1354. console.log('大纲数据:', outlineData)
  1355. console.log('标题:', title)
  1356. // 第一步:填充基本内容(标题等)
  1357. const basicFilledTemplate = fillDynamicTemplateBasicContent(template, outlineData, title)
  1358. // 第二步:使用AI填充详细内容
  1359. console.log('开始使用AI填充详细内容...')
  1360. const aiFilledTemplate = await fillDynamicTemplateWithAI(basicFilledTemplate, outlineData, title)
  1361. console.log('动态模板内容填充完成(AI增强):', aiFilledTemplate)
  1362. return aiFilledTemplate
  1363. } catch (error) {
  1364. console.error('填充动态模板内容失败(AI增强):', error)
  1365. throw error
  1366. }
  1367. }
  1368. // 填充动态模板基本内容
  1369. const fillDynamicTemplateBasicContent = (template, outlineData, title) => {
  1370. try {
  1371. console.log('开始填充动态模板基本内容...')
  1372. console.log('模板数据:', template)
  1373. console.log('大纲数据:', outlineData)
  1374. console.log('标题:', title)
  1375. const filledTemplate = JSON.parse(JSON.stringify(template)) // 深拷贝
  1376. // 填充封面页
  1377. if (filledTemplate[0] && filledTemplate[0].type === 'cover') {
  1378. filledTemplate[0].elements.forEach(element => {
  1379. if (element.textType === 'title') {
  1380. element.content = `<p style="text-align: center;"><strong><span style="font-size: ${getResponsiveFontSize(48)}px; color: ${element.defaultColor}; text-shadow: 2px 2px 8px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  1381. } else if (element.textType === 'content') {
  1382. element.content = `<p style="text-align: center;"><span style="font-size: ${getResponsiveFontSize(24)}px; color: ${element.defaultColor};">基于AI生成的培训大纲,包含相关内容</span></p>`
  1383. }
  1384. })
  1385. }
  1386. // 填充目录页
  1387. if (filledTemplate[1] && filledTemplate[1].type === 'contents') {
  1388. const chapterTitles = outlineData.map(chapter => chapter.title)
  1389. let itemIndex = 0
  1390. filledTemplate[1].elements.forEach(element => {
  1391. if (element.textType === 'item' && itemIndex < chapterTitles.length) {
  1392. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${itemIndex + 1}. ${chapterTitles[itemIndex]}</span></p>`
  1393. itemIndex++
  1394. }
  1395. })
  1396. }
  1397. // 填充章节内容页
  1398. let slideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  1399. outlineData.forEach((chapter, chapterIndex) => {
  1400. // 章节过渡页
  1401. if (filledTemplate[slideIndex] && filledTemplate[slideIndex].type === 'transition') {
  1402. filledTemplate[slideIndex].elements.forEach(element => {
  1403. if (element.textType === 'title') {
  1404. element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${chapter.title}</span></strong></p>`
  1405. } else if (element.textType === 'content') {
  1406. element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">${chapter.content || `本章将介绍${chapter.title}的相关内容`}</span></p>`
  1407. }
  1408. })
  1409. slideIndex++
  1410. }
  1411. // 小节内容页
  1412. if (chapter.sections && chapter.sections.length > 0) {
  1413. chapter.sections.forEach((section, sectionIndex) => {
  1414. if (filledTemplate[slideIndex] && filledTemplate[slideIndex].type === 'content') {
  1415. // 填充小节标题
  1416. filledTemplate[slideIndex].elements.forEach(element => {
  1417. if (element.textType === 'title') {
  1418. element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${section.title}</span></strong></p>`
  1419. }
  1420. })
  1421. // 填充小节内容(只填充标题,详细内容留给AI)
  1422. let contentIndex = 0
  1423. const subsections = section.subsections || []
  1424. filledTemplate[slideIndex].elements.forEach(element => {
  1425. if (element.textType === 'itemTitle' && contentIndex < subsections.length) {
  1426. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${subsections[contentIndex].title}</span></strong></p>`
  1427. } else if (element.textType === 'itemContent' && contentIndex < subsections.length) {
  1428. element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">待AI填充</span></p>`
  1429. contentIndex++
  1430. }
  1431. })
  1432. // 确保所有itemContent都有内容,即使没有对应的subsection
  1433. filledTemplate[slideIndex].elements.forEach(element => {
  1434. if (element.textType === 'itemContent' && element.content.includes('待AI填充')) {
  1435. // 保持"待AI填充"标记,让AI来处理
  1436. }
  1437. })
  1438. slideIndex++
  1439. }
  1440. })
  1441. } else {
  1442. // 如果章节没有小节,填充默认内容
  1443. if (filledTemplate[slideIndex] && filledTemplate[slideIndex].type === 'content') {
  1444. filledTemplate[slideIndex].elements.forEach(element => {
  1445. if (element.textType === 'title') {
  1446. element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${chapter.title}</span></strong></p>`
  1447. } else if (element.textType === 'itemTitle') {
  1448. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">概述</span></strong></p>`
  1449. } else if (element.textType === 'itemContent') {
  1450. element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">待AI填充</span></p>`
  1451. }
  1452. })
  1453. slideIndex++
  1454. }
  1455. }
  1456. })
  1457. // 填充结束页(最后一张)
  1458. const lastSlide = filledTemplate[filledTemplate.length - 1]
  1459. if (lastSlide && lastSlide.type === 'end') {
  1460. lastSlide.elements.forEach(element => {
  1461. if (element.textType === 'title') {
  1462. element.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: ${element.defaultColor}; text-shadow: 2px 2px 8px rgba(0,0,0,0.5);">谢谢聆听</span></strong></p>`
  1463. } else if (element.textType === 'content') {
  1464. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">感谢您的时间与关注</span></p>`
  1465. }
  1466. })
  1467. }
  1468. console.log('动态模板基本内容填充完成:', filledTemplate)
  1469. return filledTemplate
  1470. } catch (error) {
  1471. console.error('填充动态模板基本内容失败:', error)
  1472. throw error
  1473. }
  1474. }
  1475. // 使用AI填充动态模板详细内容(借用6目录模板的填充方式)
  1476. const fillDynamicTemplateWithAI = async (template, outlineData, title) => {
  1477. try {
  1478. console.log('开始使用AI填充动态模板详细内容...')
  1479. console.log('接收到的template类型:', typeof template, '是否为数组:', Array.isArray(template))
  1480. console.log('template内容:', template)
  1481. // 验证template参数
  1482. if (!Array.isArray(template)) {
  1483. throw new Error(`模板数据格式错误,期望数组但得到: ${typeof template}`)
  1484. }
  1485. // 统计需要填充的内容数量
  1486. let pendingFillCount = 0
  1487. template.forEach(slide => {
  1488. if (slide.elements) {
  1489. slide.elements.forEach(element => {
  1490. if (element.textType === 'itemContent' && element.content && element.content.includes('待AI填充')) {
  1491. pendingFillCount++
  1492. }
  1493. })
  1494. }
  1495. })
  1496. console.log(`需要AI填充的内容项数量: ${pendingFillCount}`)
  1497. // 借用6目录模板的填充方式:直接填充itemContent内容
  1498. const filledTemplate = JSON.parse(JSON.stringify(template)) // 深拷贝
  1499. // 为每个需要填充的itemContent生成内容
  1500. let contentIndex = 0
  1501. for (let slideIndex = 0; slideIndex < filledTemplate.length; slideIndex++) {
  1502. const slide = filledTemplate[slideIndex]
  1503. if (slide.elements) {
  1504. for (let elementIndex = 0; elementIndex < slide.elements.length; elementIndex++) {
  1505. const element = slide.elements[elementIndex]
  1506. if (element.textType === 'itemContent' && element.content && element.content.includes('待AI填充')) {
  1507. // 显示进度提示
  1508. console.log(`📝 正在生成第${contentIndex + 1}/${pendingFillCount}项内容...`)
  1509. try {
  1510. // 获取对应的标题用于AI生成
  1511. const titleForAI = getTitleForAIContent(outlineData, slideIndex, contentIndex)
  1512. console.log(`🤖 正在为第${contentIndex + 1}项内容调用AI,标题: ${titleForAI}`)
  1513. // 调用AI API生成内容
  1514. const aiResponse = await apis.reProduceSingleQuestion({
  1515. message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.要有独特性和创新性,避免与其他内容重复 6.从不同角度阐述主题。这是关于"${title}"的PPT演示文稿,当前章节是"${titleForAI}"`
  1516. })
  1517. console.log(`🤖 AI响应:`, aiResponse)
  1518. if (aiResponse && aiResponse.data) {
  1519. // 提取AI回复的内容,参考其他地方的解析方式
  1520. const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data || 'AI生成的内容为空'
  1521. // 添加动态填充效果
  1522. await animateTextFill(element, aiContent, element.defaultColor)
  1523. console.log(`✅ AI生成内容成功: ${aiContent.substring(0, 50)}...`)
  1524. } else {
  1525. // AI调用失败,使用备用内容
  1526. const fallbackContent = generateContentFromOutline(outlineData, slideIndex, contentIndex)
  1527. await animateTextFill(element, fallbackContent, element.defaultColor)
  1528. console.log(`⚠️ AI调用失败,使用备用内容: ${fallbackContent}`)
  1529. }
  1530. } catch (aiError) {
  1531. console.error(`❌ AI调用失败 (第${contentIndex + 1}项):`, aiError)
  1532. // AI调用失败,使用备用内容
  1533. const fallbackContent = generateContentFromOutline(outlineData, slideIndex, contentIndex)
  1534. await animateTextFill(element, fallbackContent, element.defaultColor)
  1535. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  1536. }
  1537. contentIndex++
  1538. }
  1539. }
  1540. }
  1541. }
  1542. console.log(`动态模板内容填充完成,共填充 ${contentIndex} 项内容`)
  1543. return filledTemplate
  1544. } catch (error) {
  1545. console.error('AI填充动态模板详细内容失败:', error)
  1546. throw new Error('AI填充动态模板详细内容失败: ' + error.message)
  1547. }
  1548. }
  1549. // 逐页生成PPT效果(类似AIPPT)
  1550. const generatePPTWithAnimation = async (template, outlineData, title) => {
  1551. console.log('🎬 开始逐页生成PPT效果...')
  1552. // 初始化空的PPT
  1553. generatedPPT.value = []
  1554. // 逐页生成效果
  1555. for (let slideIndex = 0; slideIndex < template.length; slideIndex++) {
  1556. const slide = template[slideIndex]
  1557. console.log(`📄 正在生成第${slideIndex + 1}页: ${slide.type || '未知类型'}`)
  1558. // 1. 先显示空白页面(缩略图条显示实际内容,大图显示"正在生成...")
  1559. generatedPPT.value.push({
  1560. ...slide,
  1561. elements: slide.elements ? slide.elements.map(el => ({
  1562. ...el,
  1563. content: el.content, // 缩略图条显示实际内容
  1564. // 确保位置信息不被修改
  1565. left: el.left,
  1566. top: el.top,
  1567. width: el.width,
  1568. height: el.height
  1569. })) : []
  1570. })
  1571. // 强制更新预览
  1572. generatedPPT.value = [...generatedPPT.value]
  1573. // 调试信息:检查当前页面的背景
  1574. console.log(`第${slideIndex + 1}页背景信息:`, slide.background)
  1575. console.log(`第${slideIndex + 1}页元素数量:`, slide.elements?.length)
  1576. // 强制Vue重新渲染缩略图条
  1577. await nextTick()
  1578. // 强制更新generatedPPT以触发缩略图重新渲染
  1579. generatedPPT.value = [...generatedPPT.value]
  1580. // 2. 等待一下让用户看到页面出现
  1581. await new Promise(resolve => setTimeout(resolve, 300))
  1582. // 3. 异步填充页面内容(逐元素渲染)
  1583. const filledSlide = await fillSlideContentWithAnimation(slide, outlineData, title, slideIndex)
  1584. // 4. 更新当前页面
  1585. generatedPPT.value[slideIndex] = filledSlide
  1586. generatedPPT.value = [...generatedPPT.value]
  1587. // 强制Vue重新渲染缩略图条
  1588. await nextTick()
  1589. // 强制更新generatedPPT以触发缩略图重新渲染
  1590. generatedPPT.value = [...generatedPPT.value]
  1591. // 5. 确保大图显示当前页面
  1592. currentPPTSlideIndex.value = slideIndex
  1593. // 6. 等待一下让用户看到内容填充
  1594. await new Promise(resolve => setTimeout(resolve, 400))
  1595. console.log(`✅ 第${slideIndex + 1}页生成完成`)
  1596. }
  1597. console.log('🎉 所有页面生成完成!')
  1598. // 生成完成后回到第一页
  1599. currentPPTSlideIndex.value = 0
  1600. console.log('📄 已回到第一页')
  1601. }
  1602. // 填充单个页面内容(带动画效果)
  1603. const fillSlideContentWithAnimation = async (slide, outlineData, title, slideIndex) => {
  1604. const filledSlide = JSON.parse(JSON.stringify(slide))
  1605. if (filledSlide.elements) {
  1606. console.log(`🔍 第${slideIndex + 1}页有${filledSlide.elements.length}个元素`)
  1607. // 逐个处理元素,实现逐元素渲染效果
  1608. for (let elementIndex = 0; elementIndex < filledSlide.elements.length; elementIndex++) {
  1609. const element = filledSlide.elements[elementIndex]
  1610. console.log(`🔍 处理元素${elementIndex + 1}:`, {
  1611. textType: element.textType,
  1612. content: element.content,
  1613. id: element.id,
  1614. type: element.type
  1615. })
  1616. // 处理封面页标题
  1617. if (slideIndex === 0 && element.textType === 'title' && element.content && element.content.includes('标题占位符')) {
  1618. element.content = `<p style="text-align: center;"><strong><span style="font-size: 48px; color: #ffffff; text-shadow: 2px 2px 8px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  1619. console.log(`📝 填充封面标题: ${title}`)
  1620. // 强制更新PPT预览,触发响应式更新
  1621. nextTick(() => {
  1622. generatedPPT.value = [...generatedPPT.value]
  1623. })
  1624. // 等待一下让用户看到元素出现
  1625. await new Promise(resolve => setTimeout(resolve, 200))
  1626. }
  1627. // 处理封面页副标题
  1628. else if (slideIndex === 0 && element.textType === 'content' && element.content && element.content.includes('副标题占位符')) {
  1629. // 使用完整的描述信息作为副标题
  1630. const subtitle = await getFullDescriptionForCover(outlineData, title)
  1631. element.content = `<p style="text-align: center;"><span style="font-size: 24px; color: #f0f0f0;">${subtitle}</span></p>`
  1632. console.log(`📝 填充封面副标题: ${subtitle}`)
  1633. // 强制更新PPT预览,触发响应式更新
  1634. nextTick(() => {
  1635. generatedPPT.value = [...generatedPPT.value]
  1636. })
  1637. // 等待一下让用户看到元素出现
  1638. await new Promise(resolve => setTimeout(resolve, 200))
  1639. }
  1640. // 处理目录页内容
  1641. else if (slideIndex === 1 && element.textType === 'item' && element.content && element.content.includes('目录项')) {
  1642. // 从元素ID中提取目录项索引
  1643. const itemMatch = element.id.match(/item-(\d+)/)
  1644. if (itemMatch) {
  1645. const itemIndex = parseInt(itemMatch[1]) - 1 // 转换为0基索引
  1646. if (itemIndex < outlineData.length) {
  1647. const chapterTitle = outlineData[itemIndex].title
  1648. element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">${chapterTitle}</span></p>`
  1649. console.log(`📝 填充目录项${itemIndex + 1}: ${chapterTitle}`)
  1650. } else {
  1651. element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">&nbsp;</span></p>`
  1652. }
  1653. }
  1654. // 强制更新PPT预览,触发响应式更新
  1655. nextTick(() => {
  1656. generatedPPT.value = [...generatedPPT.value]
  1657. })
  1658. // 等待一下让用户看到元素出现
  1659. await new Promise(resolve => setTimeout(resolve, 200))
  1660. }
  1661. // 处理过渡页标题
  1662. else if (element.textType === 'title' && element.content && element.content.includes('章节标题')) {
  1663. const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
  1664. element.content = `<p style="text-align: center;"><strong><span style="font-size: 36px; color: ${element.defaultColor};">${chapterTitle}</span></strong></p>`
  1665. console.log(`📝 填充过渡页标题: ${chapterTitle}`)
  1666. // 强制更新PPT预览,触发响应式更新
  1667. nextTick(() => {
  1668. generatedPPT.value = [...generatedPPT.value]
  1669. })
  1670. // 等待一下让用户看到元素出现
  1671. await new Promise(resolve => setTimeout(resolve, 200))
  1672. }
  1673. // 处理过渡页内容(使用AI生成)
  1674. else if (element.textType === 'content' && element.content && element.content.includes('章节介绍')) {
  1675. // 获取当前章节标题用于AI生成
  1676. const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
  1677. try {
  1678. console.log(`🤖 正在为过渡页生成章节介绍内容: ${chapterTitle}`)
  1679. // 调用AI API生成章节介绍内容
  1680. const aiResponse = await apis.reProduceSingleQuestion({
  1681. message: `请为PPT章节"${chapterTitle}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
  1682. })
  1683. if (aiResponse && aiResponse.data) {
  1684. const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  1685. console.log(`✅ 过渡页章节介绍生成完成: ${aiContent}`)
  1686. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${aiContent}</span></p>`
  1687. } else {
  1688. // AI调用失败,使用备用内容
  1689. const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
  1690. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  1691. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${fallbackContent}</span></p>`
  1692. }
  1693. } catch (aiError) {
  1694. console.error(`❌ AI生成过渡页内容失败:`, aiError)
  1695. // AI生成失败,使用备用内容
  1696. const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
  1697. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  1698. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: ${element.defaultColor};">${fallbackContent}</span></p>`
  1699. }
  1700. // 强制更新PPT预览,触发响应式更新
  1701. nextTick(() => {
  1702. generatedPPT.value = [...generatedPPT.value]
  1703. })
  1704. // 等待一下让用户看到元素出现
  1705. await new Promise(resolve => setTimeout(resolve, 200))
  1706. }
  1707. // 处理内容页标题
  1708. else if (element.textType === 'title' && element.content && element.content.includes('内容页标题')) {
  1709. const contentTitle = getContentPageTitle(outlineData, slideIndex)
  1710. element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${contentTitle}</span></strong></p>`
  1711. console.log(`📝 填充内容页标题: ${contentTitle}`)
  1712. // 强制更新PPT预览,触发响应式更新
  1713. nextTick(() => {
  1714. generatedPPT.value = [...generatedPPT.value]
  1715. })
  1716. // 等待一下让用户看到元素出现
  1717. await new Promise(resolve => setTimeout(resolve, 200))
  1718. }
  1719. // 处理需要填充的元素
  1720. else if (element.textType === 'itemContent' && element.content && (element.content.includes('正在生成...') || element.content.includes('待AI填充'))) {
  1721. console.log(`🤖 找到需要AI填充的元素:`, {
  1722. textType: element.textType,
  1723. content: element.content,
  1724. id: element.id
  1725. })
  1726. // 获取对应的标题用于AI生成
  1727. const titleForAI = getTitleForAIContent(outlineData, slideIndex, elementIndex)
  1728. try {
  1729. console.log(`🤖 正在为第${slideIndex + 1}页生成内容: ${titleForAI}`)
  1730. console.log(`🤖 元素信息:`, element)
  1731. // 调用AI API生成内容
  1732. const aiResponse = await apis.reProduceSingleQuestion({
  1733. message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.不要包含任何编号(如"子小节3:"、"要点1:"等)6.直接返回内容,不要添加前缀 7.要有独特性和创新性,避免与其他内容重复 8.从不同角度阐述主题。这是关于"${title}"的PPT演示文稿,当前章节是"${titleForAI}"`
  1734. })
  1735. console.log(`🤖 AI响应:`, aiResponse)
  1736. if (aiResponse && aiResponse.data) {
  1737. const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data || 'AI生成的内容为空'
  1738. console.log(`🤖 AI生成内容:`, aiContent)
  1739. // 逐字符显示效果
  1740. await animateTextFill(element, aiContent, element.defaultColor)
  1741. console.log(`✅ 第${slideIndex + 1}页内容生成完成`)
  1742. } else {
  1743. // AI调用失败,使用备用内容
  1744. const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
  1745. console.log(`🔄 使用备用内容:`, fallbackContent)
  1746. await animateTextFill(element, fallbackContent, element.defaultColor)
  1747. }
  1748. } catch (aiError) {
  1749. console.error(`❌ AI生成内容失败:`, aiError)
  1750. // AI生成失败,使用备用内容
  1751. const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
  1752. console.log(`🔄 使用备用内容:`, fallbackContent)
  1753. await animateTextFill(element, fallbackContent, element.defaultColor)
  1754. }
  1755. }
  1756. // 处理标题元素
  1757. else if (element.textType === 'itemTitle' && element.content && (element.content.includes('要点标题') || element.content.includes('待AI生成子小节'))) {
  1758. // 获取对应的标题
  1759. const titleForElement = getTitleForElement(outlineData, slideIndex, elementIndex)
  1760. // 检查是否需要AI生成标题 - 统一使用通用PPT的方式
  1761. if (titleForElement.includes('待AI生成子小节')) {
  1762. // 需要AI生成子小节标题
  1763. try {
  1764. console.log(`🤖 正在为子小节生成标题: ${titleForElement}`)
  1765. // 获取小节标题用于AI生成
  1766. const sectionTitle = getSectionTitleForAI(outlineData, slideIndex, elementIndex)
  1767. // 调用AI生成子小节标题
  1768. const aiResponse = await apis.reProduceSingleQuestion({
  1769. message: `请为PPT幻灯片的小节"${sectionTitle}"生成3-4个简洁的子标题。要求:1.每个标题都要不同 2.标题简洁明了,控制在6-12字 3.专业准确 4.适合PPT展示 5.直接返回标题,用换行分隔,不要编号,不要重复,不要解释,只要标题`
  1770. })
  1771. if (aiResponse && aiResponse.data) {
  1772. const aiTitles = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  1773. const titles = aiTitles.split('\n').filter(title => title.trim()).map(title => title.trim())
  1774. if (titles.length > 0) {
  1775. // 根据元素索引选择不同的标题
  1776. const titleIndex = elementIndex % titles.length
  1777. const finalTitle = titles[titleIndex]
  1778. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${finalTitle}</span></strong></p>`
  1779. console.log(`✅ AI生成子小节标题[${titleIndex}]: ${finalTitle}`)
  1780. } else {
  1781. // AI生成失败,使用默认标题
  1782. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  1783. console.log(`⚠️ AI生成标题失败,使用默认标题`)
  1784. }
  1785. } else {
  1786. // AI调用失败,使用默认标题
  1787. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  1788. console.log(`⚠️ AI调用失败,使用默认标题`)
  1789. }
  1790. } catch (aiError) {
  1791. console.error(`❌ AI生成标题失败:`, aiError)
  1792. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  1793. }
  1794. } else {
  1795. // 使用现有标题
  1796. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  1797. console.log(`📝 填充标题: ${titleForElement}`)
  1798. }
  1799. // 强制更新PPT预览,触发响应式更新
  1800. nextTick(() => {
  1801. generatedPPT.value = [...generatedPPT.value]
  1802. })
  1803. // 等待一下让用户看到元素出现
  1804. await new Promise(resolve => setTimeout(resolve, 200))
  1805. }
  1806. // 处理其他需要填充的元素(但不要覆盖已经AI生成的内容)
  1807. else if (element.content && element.content.includes('待AI填充') && !element.content.includes('正在生成...')) {
  1808. // 获取对应的内容
  1809. const contentForElement = getContentForElement(outlineData, slideIndex, elementIndex)
  1810. element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">${contentForElement}</span></p>`
  1811. console.log(`📝 填充内容: ${contentForElement}`)
  1812. // 强制更新PPT预览,触发响应式更新
  1813. nextTick(() => {
  1814. generatedPPT.value = [...generatedPPT.value]
  1815. })
  1816. // 等待一下让用户看到元素出现
  1817. await new Promise(resolve => setTimeout(resolve, 200))
  1818. }
  1819. }
  1820. }
  1821. return filledSlide
  1822. }
  1823. // 填充单个页面内容
  1824. const fillSlideContent = async (slide, outlineData, title, slideIndex) => {
  1825. const filledSlide = JSON.parse(JSON.stringify(slide))
  1826. if (filledSlide.elements) {
  1827. console.log(`🔍 第${slideIndex + 1}页有${filledSlide.elements.length}个元素`)
  1828. // 逐个处理元素,实现逐元素渲染效果
  1829. for (let elementIndex = 0; elementIndex < filledSlide.elements.length; elementIndex++) {
  1830. const element = filledSlide.elements[elementIndex]
  1831. console.log(`🔍 处理元素${elementIndex + 1}:`, {
  1832. textType: element.textType,
  1833. content: element.content,
  1834. id: element.id,
  1835. type: element.type
  1836. })
  1837. // 处理封面页标题
  1838. if (slideIndex === 0 && element.textType === 'title' && element.content && element.content.includes('标题占位符')) {
  1839. element.content = `<p style="text-align: center;"><strong><span style="font-size: 48px; color: #ffffff; text-shadow: 2px 2px 8px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  1840. console.log(`📝 填充封面标题: ${title}`)
  1841. // 强制更新PPT预览,触发响应式更新
  1842. nextTick(() => {
  1843. generatedPPT.value = [...generatedPPT.value]
  1844. })
  1845. }
  1846. // 处理封面页副标题
  1847. else if (slideIndex === 0 && element.textType === 'content' && element.content && element.content.includes('副标题占位符')) {
  1848. // 使用完整的描述信息作为副标题
  1849. const subtitle = await getFullDescriptionForCover(outlineData, title)
  1850. element.content = `<p style="text-align: center;"><span style="font-size: 24px; color: #f0f0f0;">${subtitle}</span></p>`
  1851. console.log(`📝 填充封面副标题: ${subtitle}`)
  1852. // 强制更新PPT预览,触发响应式更新
  1853. nextTick(() => {
  1854. generatedPPT.value = [...generatedPPT.value]
  1855. })
  1856. }
  1857. // 处理目录页内容
  1858. else if (slideIndex === 1 && element.textType === 'item' && element.content && element.content.includes('目录项')) {
  1859. // 从元素ID中提取目录项索引
  1860. const itemMatch = element.id.match(/item-(\d+)/)
  1861. if (itemMatch) {
  1862. const itemIndex = parseInt(itemMatch[1]) - 1 // 转换为0-based索引
  1863. if (itemIndex < outlineData.length) {
  1864. const chapter = outlineData[itemIndex]
  1865. const tocItem = `${itemIndex + 1}. ${chapter.title}`
  1866. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: #4a5568;">${tocItem}</span></p>`
  1867. console.log(`📝 填充目录项${itemIndex + 1}: ${tocItem}`)
  1868. } else {
  1869. // 如果章节数量少于目录项数量,隐藏多余的目录项
  1870. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: #4a5568;"></span></p>`
  1871. console.log(`📝 隐藏多余的目录项${itemIndex + 1}`)
  1872. }
  1873. } else {
  1874. // 如果无法解析ID,使用默认内容
  1875. element.content = `<p style="text-align: center;"><span style="font-size: 20px; color: #4a5568;">目录项</span></p>`
  1876. console.log(`📝 使用默认目录项内容`)
  1877. }
  1878. // 强制更新PPT预览,触发响应式更新
  1879. nextTick(() => {
  1880. generatedPPT.value = [...generatedPPT.value]
  1881. })
  1882. }
  1883. // 处理过渡页标题
  1884. else if (slide.type === 'transition' && element.textType === 'title' && element.content && element.content.includes('章节标题')) {
  1885. // 获取当前章节标题
  1886. const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
  1887. element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #2d3748;">${chapterTitle}</span></strong></p>`
  1888. console.log(`📝 填充过渡页标题: ${chapterTitle}`)
  1889. // 强制更新PPT预览,触发响应式更新
  1890. nextTick(() => {
  1891. generatedPPT.value = [...generatedPPT.value]
  1892. })
  1893. }
  1894. // 处理过渡页章节介绍内容
  1895. else if (slide.type === 'transition' && element.textType === 'content' && element.content && element.content.includes('章节介绍内容')) {
  1896. // 获取当前章节标题用于AI生成
  1897. const chapterTitle = getChapterTitleForTransition(outlineData, slideIndex)
  1898. try {
  1899. console.log(`🤖 正在为过渡页生成章节介绍内容: ${chapterTitle}`)
  1900. // 调用AI API生成章节介绍内容
  1901. const aiResponse = await apis.reProduceSingleQuestion({
  1902. message: `请为PPT章节"${chapterTitle}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
  1903. })
  1904. if (aiResponse && aiResponse.data) {
  1905. const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  1906. console.log(`✅ 过渡页章节介绍生成完成: ${aiContent}`)
  1907. // 添加动态填充效果
  1908. await animateTextFill(element, aiContent, element.defaultColor)
  1909. } else {
  1910. // AI调用失败,使用备用内容
  1911. const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
  1912. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  1913. await animateTextFill(element, fallbackContent, element.defaultColor)
  1914. }
  1915. } catch (aiError) {
  1916. console.error(`❌ AI生成过渡页内容失败:`, aiError)
  1917. // AI生成失败,使用备用内容
  1918. const fallbackContent = getChapterContentForTransition(outlineData, slideIndex)
  1919. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  1920. await animateTextFill(element, fallbackContent, element.defaultColor)
  1921. }
  1922. }
  1923. // 处理内容页标题
  1924. else if (element.textType === 'title' && element.content && element.content.includes('内容页标题')) {
  1925. // 获取当前内容页标题
  1926. const contentTitle = getContentPageTitle(outlineData, slideIndex)
  1927. element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #2d3748;">${contentTitle}</span></strong></p>`
  1928. console.log(`📝 填充内容页标题: ${contentTitle}`)
  1929. // 强制更新PPT预览,触发响应式更新
  1930. nextTick(() => {
  1931. generatedPPT.value = [...generatedPPT.value]
  1932. })
  1933. }
  1934. // 处理需要填充的元素
  1935. else if (element.textType === 'itemContent' && element.content && (element.content.includes('正在生成...') || element.content.includes('待AI填充'))) {
  1936. console.log(`🤖 找到需要AI填充的元素:`, {
  1937. textType: element.textType,
  1938. content: element.content,
  1939. id: element.id
  1940. })
  1941. // 获取对应的标题用于AI生成
  1942. const titleForAI = getTitleForAIContent(outlineData, slideIndex, elementIndex)
  1943. try {
  1944. console.log(`🤖 正在为第${slideIndex + 1}页生成内容: ${titleForAI}`)
  1945. console.log(`🤖 元素信息:`, element)
  1946. // 调用AI API生成内容
  1947. const aiResponse = await apis.reProduceSingleQuestion({
  1948. message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.不要包含任何编号(如"子小节3:"、"要点1:"等)6.直接返回内容,不要添加前缀 7.要有独特性和创新性,避免与其他内容重复 8.从不同角度阐述主题。这是关于"${title}"的PPT演示文稿,当前章节是"${titleForAI}"`
  1949. })
  1950. console.log(`🤖 AI响应:`, aiResponse)
  1951. if (aiResponse && aiResponse.data) {
  1952. const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data || 'AI生成的内容为空'
  1953. console.log(`🤖 AI生成内容:`, aiContent)
  1954. // 逐字符显示效果
  1955. await animateTextFill(element, aiContent, element.defaultColor)
  1956. console.log(`✅ 第${slideIndex + 1}页内容生成完成`)
  1957. } else {
  1958. // AI调用失败,使用备用内容
  1959. const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
  1960. console.log(`⚠️ AI调用失败,使用备用内容:`, fallbackContent)
  1961. await animateTextFill(element, fallbackContent, element.defaultColor)
  1962. }
  1963. } catch (aiError) {
  1964. console.error(`❌ AI调用失败:`, aiError)
  1965. const fallbackContent = generateContentFromOutline(outlineData, slideIndex, elementIndex)
  1966. console.log(`🔄 使用备用内容:`, fallbackContent)
  1967. await animateTextFill(element, fallbackContent, element.defaultColor)
  1968. }
  1969. }
  1970. // 处理标题元素
  1971. else if (element.textType === 'itemTitle' && element.content && (element.content.includes('要点标题') || element.content.includes('待AI生成子小节'))) {
  1972. // 获取对应的标题
  1973. const titleForElement = getTitleForElement(outlineData, slideIndex, elementIndex)
  1974. // 检查是否需要AI生成标题 - 统一使用通用PPT的方式
  1975. if (titleForElement.includes('待AI生成子小节')) {
  1976. // 需要AI生成子小节标题
  1977. try {
  1978. console.log(`🤖 正在为子小节生成标题: ${titleForElement}`)
  1979. // 获取小节标题用于AI生成
  1980. const sectionTitle = getSectionTitleForAI(outlineData, slideIndex, elementIndex)
  1981. // 调用AI生成子小节标题
  1982. const aiResponse = await apis.reProduceSingleQuestion({
  1983. message: `请为PPT幻灯片的小节"${sectionTitle}"生成3-4个简洁的子标题。要求:1.每个标题都要不同 2.标题简洁明了,控制在6-12字 3.专业准确 4.适合PPT展示 5.直接返回标题,用换行分隔,不要编号,不要重复,不要解释,只要标题`
  1984. })
  1985. if (aiResponse && aiResponse.data) {
  1986. const aiTitles = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  1987. const titles = aiTitles.split('\n').filter(title => title.trim()).map(title => title.trim())
  1988. if (titles.length > 0) {
  1989. // 根据元素索引选择不同的标题
  1990. const titleIndex = elementIndex % titles.length
  1991. const finalTitle = titles[titleIndex]
  1992. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${finalTitle}</span></strong></p>`
  1993. console.log(`✅ AI生成子小节标题[${titleIndex}]: ${finalTitle}`)
  1994. } else {
  1995. // AI生成失败,使用默认标题
  1996. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  1997. console.log(`⚠️ AI生成标题失败,使用默认标题`)
  1998. }
  1999. } else {
  2000. // AI调用失败,使用默认标题
  2001. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  2002. console.log(`⚠️ AI调用失败,使用默认标题`)
  2003. }
  2004. } catch (aiError) {
  2005. console.error(`❌ AI生成标题失败:`, aiError)
  2006. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  2007. }
  2008. } else {
  2009. // 使用现有标题
  2010. element.content = `<p style="text-align: center;"><strong><span style="font-size: 20px; color: ${element.defaultColor};">${titleForElement}</span></strong></p>`
  2011. console.log(`📝 填充标题: ${titleForElement}`)
  2012. }
  2013. // 强制更新PPT预览,触发响应式更新
  2014. nextTick(() => {
  2015. generatedPPT.value = [...generatedPPT.value]
  2016. })
  2017. }
  2018. // 处理其他需要填充的元素(但不要覆盖已经AI生成的内容)
  2019. else if (element.content && element.content.includes('待AI填充') && !element.content.includes('正在生成...')) {
  2020. // 获取对应的内容
  2021. const contentForElement = getContentForElement(outlineData, slideIndex, elementIndex)
  2022. element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">${contentForElement}</span></p>`
  2023. console.log(`📝 填充内容: ${contentForElement}`)
  2024. // 强制更新PPT预览,触发响应式更新
  2025. nextTick(() => {
  2026. generatedPPT.value = [...generatedPPT.value]
  2027. })
  2028. }
  2029. // 每个元素处理完成后稍作延迟,让用户看到逐元素渲染效果
  2030. await new Promise(resolve => setTimeout(resolve, 100))
  2031. }
  2032. }
  2033. return filledSlide
  2034. }
  2035. // 动态文本填充效果
  2036. const animateTextFill = async (element, content, color) => {
  2037. return new Promise((resolve) => {
  2038. // 先显示加载状态
  2039. element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${color};">正在生成内容...</span></p>`
  2040. // 强制更新PPT预览 - 使用nextTick确保DOM更新
  2041. nextTick(() => {
  2042. generatedPPT.value = [...generatedPPT.value]
  2043. })
  2044. // 延迟一下让用户看到加载状态
  2045. setTimeout(() => {
  2046. // 逐步显示内容
  2047. let currentText = ''
  2048. const words = content.split('')
  2049. let index = 0
  2050. const typeWriter = () => {
  2051. if (index < words.length) {
  2052. currentText += words[index]
  2053. element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${color};">${currentText}</span></p>`
  2054. // 强制更新PPT预览,触发响应式更新
  2055. nextTick(() => {
  2056. generatedPPT.value = [...generatedPPT.value]
  2057. })
  2058. index++
  2059. setTimeout(typeWriter, 30) // 每30ms显示一个字符,加快速度
  2060. } else {
  2061. resolve()
  2062. }
  2063. }
  2064. typeWriter()
  2065. }, 500) // 500ms后开始打字效果
  2066. })
  2067. }
  2068. // 获取元素标题
  2069. const getTitleForElement = (outlineData, slideIndex, elementIndex) => {
  2070. console.log(`获取元素标题 - 幻灯片索引: ${slideIndex}, 元素索引: ${elementIndex}`)
  2071. console.log(`大纲数据:`, outlineData)
  2072. // 跳过封面页和目录页(前2页)
  2073. if (slideIndex < 2) {
  2074. return '默认标题'
  2075. }
  2076. // 计算当前内容页对应的章节和小节
  2077. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2078. let chapterIndex = 0
  2079. let sectionIndex = 0
  2080. // 找到当前幻灯片对应的章节和小节
  2081. for (let i = 0; i < outlineData.length; i++) {
  2082. const chapter = outlineData[i]
  2083. // Each chapter has a transition slide
  2084. currentSlideIndex++ // Account for chapter transition slide
  2085. if (chapter.sections && chapter.sections.length > 0) {
  2086. for (let j = 0; j < chapter.sections.length; j++) {
  2087. if (currentSlideIndex === slideIndex) {
  2088. chapterIndex = i
  2089. sectionIndex = j
  2090. break
  2091. }
  2092. currentSlideIndex++ // Account for section content slide
  2093. }
  2094. if (currentSlideIndex === slideIndex) break
  2095. } else {
  2096. // If a chapter has no sections, it still gets one content slide
  2097. if (currentSlideIndex === slideIndex) {
  2098. chapterIndex = i
  2099. sectionIndex = 0
  2100. break
  2101. }
  2102. currentSlideIndex++
  2103. }
  2104. }
  2105. console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
  2106. // 获取对应的章节和小节数据
  2107. const chapter = outlineData[chapterIndex]
  2108. const section = chapter && chapter.sections && chapter.sections[sectionIndex]
  2109. if (section && section.subsections && section.subsections.length > 0) {
  2110. // 根据子小节生成标题
  2111. const subsection = section.subsections[elementIndex % section.subsections.length]
  2112. if (subsection && subsection.title) {
  2113. // 如果是"待AI生成子小节"标记,直接返回
  2114. if (subsection.title.includes('待AI生成子小节')) {
  2115. return subsection.title
  2116. }
  2117. return subsection.title
  2118. }
  2119. }
  2120. // 如果没有子小节,根据小节标题生成标题
  2121. if (section && section.title) {
  2122. return section.title
  2123. }
  2124. // 最后根据章节标题生成标题
  2125. if (chapter && chapter.title) {
  2126. return chapter.title
  2127. }
  2128. // 默认标题
  2129. return '默认标题'
  2130. }
  2131. // 获取元素内容
  2132. const getContentForElement = (outlineData, slideIndex, elementIndex) => {
  2133. console.log(`获取元素内容 - 幻灯片索引: ${slideIndex}, 元素索引: ${elementIndex}`)
  2134. // 跳过封面页和目录页(前2页)
  2135. if (slideIndex < 2) {
  2136. return '默认内容'
  2137. }
  2138. // 计算当前内容页对应的章节和小节
  2139. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2140. let chapterIndex = 0
  2141. let sectionIndex = 0
  2142. // 找到当前幻灯片对应的章节和小节
  2143. for (let i = 0; i < outlineData.length; i++) {
  2144. const chapter = outlineData[i]
  2145. // Each chapter has a transition slide
  2146. currentSlideIndex++ // Account for chapter transition slide
  2147. if (chapter.sections && chapter.sections.length > 0) {
  2148. for (let j = 0; j < chapter.sections.length; j++) {
  2149. if (currentSlideIndex === slideIndex) {
  2150. chapterIndex = i
  2151. sectionIndex = j
  2152. break
  2153. }
  2154. currentSlideIndex++ // Account for section content slide
  2155. }
  2156. if (currentSlideIndex === slideIndex) break
  2157. } else {
  2158. // If a chapter has no sections, it still gets one content slide
  2159. if (currentSlideIndex === slideIndex) {
  2160. chapterIndex = i
  2161. sectionIndex = 0
  2162. break
  2163. }
  2164. currentSlideIndex++
  2165. }
  2166. }
  2167. console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
  2168. // 获取对应的章节和小节数据
  2169. const chapter = outlineData[chapterIndex]
  2170. const section = chapter && chapter.sections && chapter.sections[sectionIndex]
  2171. if (section && section.subsections && section.subsections.length > 0) {
  2172. // 根据子小节生成内容
  2173. const subsection = section.subsections[elementIndex % section.subsections.length]
  2174. if (subsection && subsection.title) {
  2175. return generateProfessionalContent(subsection.title, elementIndex)
  2176. }
  2177. }
  2178. // 如果没有子小节,根据小节标题生成内容
  2179. if (section && section.title) {
  2180. return generateProfessionalContent(section.title, elementIndex)
  2181. }
  2182. // 最后根据章节标题生成内容
  2183. if (chapter && chapter.title) {
  2184. return generateProfessionalContent(chapter.title, elementIndex)
  2185. }
  2186. // 默认内容
  2187. return generateProfessionalContent('默认内容', elementIndex)
  2188. }
  2189. // 获取内容页标题
  2190. const getContentPageTitle = (outlineData, slideIndex) => {
  2191. console.log(`获取内容页标题 - 幻灯片索引: ${slideIndex}`)
  2192. // 跳过封面页和目录页(前2页)
  2193. if (slideIndex < 2) {
  2194. return '默认内容页标题'
  2195. }
  2196. // 计算当前内容页对应的章节和小节
  2197. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2198. let chapterIndex = 0
  2199. let sectionIndex = 0
  2200. // 找到当前内容页对应的章节和小节
  2201. for (let i = 0; i < outlineData.length; i++) {
  2202. const chapter = outlineData[i]
  2203. // Each chapter has a transition slide
  2204. currentSlideIndex++ // Account for chapter transition slide
  2205. if (chapter.sections && chapter.sections.length > 0) {
  2206. for (let j = 0; j < chapter.sections.length; j++) {
  2207. if (currentSlideIndex === slideIndex) {
  2208. chapterIndex = i
  2209. sectionIndex = j
  2210. break
  2211. }
  2212. currentSlideIndex++ // Account for section content slide
  2213. }
  2214. if (currentSlideIndex === slideIndex) break
  2215. } else {
  2216. // If a chapter has no sections, it still gets one content slide
  2217. if (currentSlideIndex === slideIndex) {
  2218. chapterIndex = i
  2219. sectionIndex = 0
  2220. break
  2221. }
  2222. currentSlideIndex++
  2223. }
  2224. }
  2225. console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
  2226. // 获取对应的章节和小节数据
  2227. const chapter = outlineData[chapterIndex]
  2228. const section = chapter && chapter.sections && chapter.sections[sectionIndex]
  2229. // 优先使用小节标题
  2230. if (section && section.title) {
  2231. return section.title
  2232. }
  2233. // 如果没有小节,使用章节标题
  2234. if (chapter && chapter.title) {
  2235. return chapter.title
  2236. }
  2237. // 默认标题
  2238. return '默认内容页标题'
  2239. }
  2240. // 转换用户大纲数据为兼容格式
  2241. const convertUserOutlineToCompatibleFormat = async (userOutline) => {
  2242. console.log('转换用户大纲数据为兼容格式...')
  2243. console.log('原始用户大纲数据:', userOutline)
  2244. if (!userOutline || !Array.isArray(userOutline)) {
  2245. console.log('用户大纲数据无效,返回空数组')
  2246. return []
  2247. }
  2248. const convertedOutline = []
  2249. for (let chapterIndex = 0; chapterIndex < userOutline.length; chapterIndex++) {
  2250. const chapter = userOutline[chapterIndex]
  2251. // 生成章节内容
  2252. let chapterContent = chapter.content
  2253. if (!chapterContent) {
  2254. try {
  2255. console.log(`🤖 正在为章节"${chapter.title}"生成内容...`)
  2256. const aiResponse = await apis.reProduceSingleQuestion({
  2257. message: `请为PPT章节"${chapter.title}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
  2258. })
  2259. if (aiResponse && aiResponse.data) {
  2260. chapterContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  2261. console.log(`✅ 章节内容生成完成: ${chapterContent}`)
  2262. } else {
  2263. chapterContent = `第${chapterIndex + 1}章节的内容`
  2264. }
  2265. } catch (error) {
  2266. console.error(`❌ 章节内容生成失败:`, error)
  2267. chapterContent = `第${chapterIndex + 1}章节的内容`
  2268. }
  2269. }
  2270. const convertedChapter = {
  2271. title: chapter.title || `章节${chapterIndex + 1}`,
  2272. content: chapterContent,
  2273. sections: []
  2274. }
  2275. if (chapter.sections && Array.isArray(chapter.sections)) {
  2276. for (let sectionIndex = 0; sectionIndex < chapter.sections.length; sectionIndex++) {
  2277. const section = chapter.sections[sectionIndex]
  2278. // 生成小节内容
  2279. let sectionContent = section.content
  2280. if (!sectionContent) {
  2281. try {
  2282. console.log(`🤖 正在为小节"${section.title}"生成内容...`)
  2283. const aiResponse = await apis.reProduceSingleQuestion({
  2284. message: `请为PPT小节"${section.title}"生成一个简洁的小节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
  2285. })
  2286. if (aiResponse && aiResponse.data) {
  2287. sectionContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  2288. console.log(`✅ 小节内容生成完成: ${sectionContent}`)
  2289. } else {
  2290. sectionContent = `第${sectionIndex + 1}小节的内容`
  2291. }
  2292. } catch (error) {
  2293. console.error(`❌ 小节内容生成失败:`, error)
  2294. sectionContent = `第${sectionIndex + 1}小节的内容`
  2295. }
  2296. }
  2297. const convertedSection = {
  2298. title: section.title || `小节${sectionIndex + 1}`,
  2299. content: sectionContent,
  2300. subsections: []
  2301. }
  2302. if (section.subsections && Array.isArray(section.subsections) && section.subsections.length > 0) {
  2303. // 如果用户大纲有子小节,使用用户的子小节
  2304. convertedSection.subsections = section.subsections.map((subsection, subsectionIndex) => {
  2305. return {
  2306. title: subsection.title || `子小节${subsectionIndex + 1}`
  2307. // 注意:不添加content字段,与mock数据结构保持一致
  2308. }
  2309. })
  2310. } else {
  2311. // 如果用户大纲没有子小节,让AI生成子小节标题
  2312. // 随机生成2-4个子小节
  2313. const subsectionCount = Math.floor(Math.random() * 3) + 2 // 2-4个子小节
  2314. convertedSection.subsections = []
  2315. // 创建占位符,稍后由AI填充
  2316. for (let i = 0; i < subsectionCount; i++) {
  2317. convertedSection.subsections.push({
  2318. title: `待AI生成子小节${i + 1}` // 占位符,稍后由AI填充
  2319. })
  2320. }
  2321. console.log(`为小节"${section.title}"创建了${subsectionCount}个待AI生成的子小节`)
  2322. }
  2323. convertedChapter.sections.push(convertedSection)
  2324. }
  2325. } else {
  2326. // 如果没有小节,创建默认的小节(与mock数据结构一致)
  2327. convertedChapter.sections = [
  2328. {
  2329. title: '主要内容',
  2330. content: '主要内容描述',
  2331. subsections: [
  2332. { title: `${convertedChapter.title} - 要点1` },
  2333. { title: `${convertedChapter.title} - 要点2` },
  2334. { title: `${convertedChapter.title} - 要点3` },
  2335. { title: `${convertedChapter.title} - 要点4` }
  2336. ]
  2337. }
  2338. ]
  2339. console.log(`为章节"${convertedChapter.title}"创建了默认小节和子小节`)
  2340. }
  2341. convertedOutline.push(convertedChapter)
  2342. }
  2343. console.log('转换后的兼容格式数据:', convertedOutline)
  2344. return convertedOutline
  2345. }
  2346. // 获取封面页的完整描述信息
  2347. const getFullDescriptionForCover = async (outlineData, title) => {
  2348. console.log(`获取封面页完整描述 - 标题: ${title}`)
  2349. // 如果是用户大纲数据(检查是否包含用户大纲的特征)
  2350. if (title !== '用户生成的大纲' && outlineData && outlineData.length > 0) {
  2351. if (outlineData && outlineData.length > 0) {
  2352. try {
  2353. // 使用AI生成专业的描述
  2354. console.log(`🤖 正在为用户大纲生成专业描述...`)
  2355. const aiResponse = await apis.reProduceSingleQuestion({
  2356. message: `请为PPT演示文稿生成一个专业的副标题描述。大纲包含${outlineData.length}个章节,每个章节都有多个小节和子小节。要求:1.描述专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在30-45字以内 5.突出培训的专业性和系统性`
  2357. })
  2358. if (aiResponse && aiResponse.data) {
  2359. const aiDescription = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  2360. console.log(`✅ 用户大纲描述生成完成: ${aiDescription}`)
  2361. return aiDescription
  2362. }
  2363. } catch (error) {
  2364. console.error(`❌ 用户大纲描述生成失败:`, error)
  2365. }
  2366. // AI生成失败,使用默认描述
  2367. const chapterCount = outlineData.length
  2368. let description = `${chapterCount}章节`
  2369. // 添加每个章节的小节信息
  2370. outlineData.forEach((chapter, index) => {
  2371. if (chapter.sections && chapter.sections.length > 0) {
  2372. const sectionCount = chapter.sections.length
  2373. description += `,第${index + 1}章节${sectionCount}小节`
  2374. // 添加子小节信息
  2375. const subsectionCounts = chapter.sections.map(section =>
  2376. section.subsections ? section.subsections.length : 0
  2377. )
  2378. if (subsectionCounts.length > 0 && subsectionCounts.some(count => count > 0)) {
  2379. description += `(${subsectionCounts.join('+')}子小节)`
  2380. }
  2381. }
  2382. })
  2383. console.log(`生成用户大纲描述: ${description}`)
  2384. return description
  2385. }
  2386. return '用户生成的大纲结构'
  2387. }
  2388. // 根据标题找到对应的mock数据选项
  2389. const selectedOption = mockDataOptions.value.find(option => option.title === title)
  2390. if (selectedOption && selectedOption.description) {
  2391. console.log(`找到对应的描述: ${selectedOption.description}`)
  2392. return selectedOption.description
  2393. }
  2394. // 如果没有找到对应的选项,根据大纲数据生成描述
  2395. if (outlineData && outlineData.length > 0) {
  2396. const chapterCount = outlineData.length
  2397. let description = `${chapterCount}章节`
  2398. // 添加每个章节的小节信息
  2399. outlineData.forEach((chapter, index) => {
  2400. if (chapter.sections && chapter.sections.length > 0) {
  2401. const sectionCount = chapter.sections.length
  2402. description += `,第${index + 1}章节${sectionCount}小节`
  2403. // 添加子小节信息
  2404. const subsectionCounts = chapter.sections.map(section =>
  2405. section.subsections ? section.subsections.length : 0
  2406. )
  2407. if (subsectionCounts.length > 0 && subsectionCounts.some(count => count > 0)) {
  2408. description += `(${subsectionCounts.join('+')}子小节)`
  2409. }
  2410. }
  2411. })
  2412. console.log(`生成的描述: ${description}`)
  2413. return description
  2414. }
  2415. // 默认描述
  2416. return '安全培训演示文稿'
  2417. }
  2418. // 获取过渡页的章节介绍内容
  2419. const getChapterContentForTransition = (outlineData, slideIndex) => {
  2420. console.log(`获取过渡页章节介绍内容 - 幻灯片索引: ${slideIndex}`)
  2421. // 跳过封面页和目录页(前2页)
  2422. if (slideIndex < 2) {
  2423. return '默认章节介绍'
  2424. }
  2425. // 计算当前过渡页对应的章节
  2426. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2427. let chapterIndex = 0
  2428. // 找到当前过渡页对应的章节
  2429. for (let i = 0; i < outlineData.length; i++) {
  2430. const chapter = outlineData[i]
  2431. // Each chapter has a transition slide
  2432. if (currentSlideIndex === slideIndex) {
  2433. chapterIndex = i
  2434. break
  2435. }
  2436. currentSlideIndex++ // Account for chapter transition slide
  2437. // Then account for all section content slides in this chapter
  2438. if (chapter.sections && chapter.sections.length > 0) {
  2439. currentSlideIndex += chapter.sections.length
  2440. } else {
  2441. // If a chapter has no sections, it still gets one content slide
  2442. currentSlideIndex++
  2443. }
  2444. }
  2445. console.log(`当前过渡页对应 - 章节: ${chapterIndex}`)
  2446. // 获取对应的章节数据
  2447. const chapter = outlineData[chapterIndex]
  2448. // 返回章节的介绍内容
  2449. if (chapter && chapter.content) {
  2450. return chapter.content
  2451. }
  2452. // 如果没有content字段,生成一个基于章节标题的介绍
  2453. if (chapter && chapter.title) {
  2454. return `本章将介绍${chapter.title}的相关内容,包括核心概念、重要知识点和实践应用。`
  2455. }
  2456. // 默认内容
  2457. return '默认章节介绍'
  2458. }
  2459. // 获取过渡页的章节标题
  2460. const getChapterTitleForTransition = (outlineData, slideIndex) => {
  2461. console.log(`获取过渡页章节标题 - 幻灯片索引: ${slideIndex}`)
  2462. // 跳过封面页和目录页(前2页)
  2463. if (slideIndex < 2) {
  2464. return '默认章节'
  2465. }
  2466. // 计算当前过渡页对应的章节
  2467. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2468. let chapterIndex = 0
  2469. // 找到当前过渡页对应的章节
  2470. for (let i = 0; i < outlineData.length; i++) {
  2471. const chapter = outlineData[i]
  2472. // Each chapter has a transition slide
  2473. if (currentSlideIndex === slideIndex) {
  2474. chapterIndex = i
  2475. break
  2476. }
  2477. currentSlideIndex++ // Account for chapter transition slide
  2478. // 为每个小节生成内容页
  2479. if (chapter.sections && chapter.sections.length > 0) {
  2480. currentSlideIndex += chapter.sections.length // Account for section content slides
  2481. } else {
  2482. currentSlideIndex++ // Account for default content slide
  2483. }
  2484. }
  2485. console.log(`当前过渡页对应 - 章节: ${chapterIndex}`)
  2486. // 获取对应的章节数据
  2487. const chapter = outlineData[chapterIndex]
  2488. if (chapter && chapter.title) {
  2489. // 使用干净的标题(去掉第X章前缀)
  2490. return getCleanChapterTitle(chapter.title)
  2491. }
  2492. // 默认标题
  2493. return '默认章节'
  2494. }
  2495. // 获取小节标题用于AI生成子小节标题
  2496. const getSectionTitleForAI = (outlineData, slideIndex, elementIndex) => {
  2497. console.log(`获取小节标题用于AI生成 - 幻灯片索引: ${slideIndex}, 元素索引: ${elementIndex}`)
  2498. // 跳过封面页和目录页(前2页)
  2499. if (slideIndex < 2) {
  2500. return '默认小节'
  2501. }
  2502. // 计算当前内容页对应的章节和小节
  2503. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2504. let chapterIndex = 0
  2505. let sectionIndex = 0
  2506. // 找到当前内容页对应的章节和小节
  2507. for (let i = 0; i < outlineData.length; i++) {
  2508. const chapter = outlineData[i]
  2509. // Each chapter has a transition slide
  2510. currentSlideIndex++ // Account for chapter transition slide
  2511. if (chapter.sections && chapter.sections.length > 0) {
  2512. for (let j = 0; j < chapter.sections.length; j++) {
  2513. if (currentSlideIndex === slideIndex) {
  2514. chapterIndex = i
  2515. sectionIndex = j
  2516. break
  2517. }
  2518. currentSlideIndex++ // Account for section content slide
  2519. }
  2520. if (currentSlideIndex === slideIndex) break
  2521. } else {
  2522. // If a chapter has no sections, it still gets one content slide
  2523. if (currentSlideIndex === slideIndex) {
  2524. chapterIndex = i
  2525. sectionIndex = 0
  2526. break
  2527. }
  2528. currentSlideIndex++
  2529. }
  2530. }
  2531. console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
  2532. // 获取对应的小节数据
  2533. const chapter = outlineData[chapterIndex]
  2534. const section = chapter && chapter.sections && chapter.sections[sectionIndex]
  2535. // 返回小节标题
  2536. if (section && section.title) {
  2537. return section.title
  2538. }
  2539. // 默认标题
  2540. return '默认小节'
  2541. }
  2542. // 获取用于AI内容生成的标题
  2543. const getTitleForAIContent = (outlineData, slideIndex, contentIndex) => {
  2544. console.log(`获取AI标题 - 幻灯片索引: ${slideIndex}, 内容索引: ${contentIndex}`)
  2545. // 跳过封面页和目录页(前2页)
  2546. if (slideIndex < 2) {
  2547. return '默认内容'
  2548. }
  2549. // 计算当前内容页对应的章节和小节
  2550. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2551. let chapterIndex = 0
  2552. let sectionIndex = 0
  2553. // 找到当前幻灯片对应的章节和小节
  2554. for (let i = 0; i < outlineData.length; i++) {
  2555. const chapter = outlineData[i]
  2556. // Each chapter has a transition slide
  2557. currentSlideIndex++ // Account for chapter transition slide
  2558. if (chapter.sections && chapter.sections.length > 0) {
  2559. for (let j = 0; j < chapter.sections.length; j++) {
  2560. if (currentSlideIndex === slideIndex) {
  2561. chapterIndex = i
  2562. sectionIndex = j
  2563. break
  2564. }
  2565. currentSlideIndex++ // Account for section content slide
  2566. }
  2567. if (currentSlideIndex === slideIndex) break
  2568. } else {
  2569. // If a chapter has no sections, it still gets one content slide
  2570. if (currentSlideIndex === slideIndex) {
  2571. chapterIndex = i
  2572. sectionIndex = 0
  2573. break
  2574. }
  2575. currentSlideIndex++
  2576. }
  2577. }
  2578. console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
  2579. // 获取对应的章节和小节数据
  2580. const chapter = outlineData[chapterIndex]
  2581. const section = chapter && chapter.sections && chapter.sections[sectionIndex]
  2582. if (section && section.subsections && section.subsections.length > 0) {
  2583. // 根据子小节生成标题
  2584. const subsection = section.subsections[contentIndex % section.subsections.length]
  2585. if (subsection && subsection.title) {
  2586. return `${chapter.title} - ${section.title} - ${subsection.title}`
  2587. }
  2588. }
  2589. // 如果没有子小节,根据小节标题生成标题
  2590. if (section && section.title) {
  2591. return `${chapter.title} - ${section.title}`
  2592. }
  2593. // 最后根据章节标题生成标题
  2594. if (chapter && chapter.title) {
  2595. return chapter.title
  2596. }
  2597. // 默认标题
  2598. return '默认内容'
  2599. }
  2600. // 生成专业内容(使用mock数据测试)
  2601. const generateProfessionalContent = (title, index) => {
  2602. // Mock数据:根据大纲结构生成对应的内容
  2603. const mockContentMap = {
  2604. '信息上报与指挥体系建立': [
  2605. '建立完善的信息收集机制,确保各类安全信息及时准确上报',
  2606. '构建统一的指挥调度平台,实现各部门协调联动',
  2607. '制定标准化的上报流程,提高信息处理效率',
  2608. '建立应急响应机制,确保突发事件快速处置'
  2609. ],
  2610. '安全风险评估与控制': [
  2611. '开展全面的安全风险识别,建立风险清单',
  2612. '制定风险等级评估标准,实施分级管控',
  2613. '建立风险监测预警系统,实现动态监控',
  2614. '完善风险控制措施,确保风险可控'
  2615. ],
  2616. '应急预案与处置': [
  2617. '制定完善的应急预案体系,覆盖各类突发事件',
  2618. '建立应急响应队伍,提高应急处置能力',
  2619. '开展应急演练,检验预案有效性',
  2620. '完善应急物资储备,确保应急保障'
  2621. ],
  2622. '培训教育与能力提升': [
  2623. '制定系统化的培训计划,提高全员安全意识',
  2624. '开展专业技能培训,提升操作能力',
  2625. '建立培训考核机制,确保培训效果',
  2626. '持续改进培训方式,提高培训质量'
  2627. ],
  2628. '监督检查与持续改进': [
  2629. '建立监督检查机制,确保制度有效执行',
  2630. '开展定期检查评估,发现问题及时整改',
  2631. '建立问题跟踪机制,确保整改到位',
  2632. '持续改进工作方法,提升管理水平'
  2633. ]
  2634. }
  2635. // 根据标题匹配内容
  2636. for (const [key, contents] of Object.entries(mockContentMap)) {
  2637. if (title.includes(key) || key.includes(title)) {
  2638. return contents[index % contents.length]
  2639. }
  2640. }
  2641. // 默认内容模板
  2642. const defaultTemplates = [
  2643. '建立完善的管理体系,确保各项工作有序开展',
  2644. '制定详细的操作规范,提高工作效率和质量',
  2645. '加强人员培训,提升专业技能和综合素质',
  2646. '建立监督检查机制,确保制度有效执行',
  2647. '完善应急预案,提高应对突发事件的能力',
  2648. '加强沟通协调,促进部门间有效合作',
  2649. '持续改进优化,不断提升管理水平',
  2650. '注重细节管理,确保工作质量稳定可靠',
  2651. '强化责任意识,确保各项工作落实到位',
  2652. '创新工作方法,提高工作效率和效果'
  2653. ]
  2654. return defaultTemplates[index % defaultTemplates.length]
  2655. }
  2656. // 根据大纲数据生成对应的内容
  2657. const generateContentFromOutline = (outlineData, slideIndex, contentIndex) => {
  2658. console.log(`生成内容 - 幻灯片索引: ${slideIndex}, 内容索引: ${contentIndex}`)
  2659. // 跳过封面页和目录页(前2页)
  2660. if (slideIndex < 2) {
  2661. return generateProfessionalContent('默认内容', contentIndex)
  2662. }
  2663. // 计算当前内容页对应的章节和小节
  2664. let currentSlideIndex = 2 // 从第3张幻灯片开始(封面+目录后)
  2665. let chapterIndex = 0
  2666. let sectionIndex = 0
  2667. // 找到当前幻灯片对应的章节和小节
  2668. for (let i = 0; i < outlineData.length; i++) {
  2669. const chapter = outlineData[i]
  2670. if (chapter.sections && chapter.sections.length > 0) {
  2671. for (let j = 0; j < chapter.sections.length; j++) {
  2672. if (currentSlideIndex === slideIndex) {
  2673. chapterIndex = i
  2674. sectionIndex = j
  2675. break
  2676. }
  2677. currentSlideIndex++
  2678. }
  2679. if (currentSlideIndex === slideIndex) break
  2680. } else {
  2681. if (currentSlideIndex === slideIndex) {
  2682. chapterIndex = i
  2683. sectionIndex = 0
  2684. break
  2685. }
  2686. currentSlideIndex++
  2687. }
  2688. }
  2689. console.log(`当前内容页对应 - 章节: ${chapterIndex}, 小节: ${sectionIndex}`)
  2690. // 获取对应的章节和小节数据
  2691. const chapter = outlineData[chapterIndex]
  2692. const section = chapter && chapter.sections && chapter.sections[sectionIndex]
  2693. if (section && section.subsections && section.subsections.length > 0) {
  2694. // 根据子小节生成内容
  2695. const subsection = section.subsections[contentIndex % section.subsections.length]
  2696. if (subsection && subsection.title) {
  2697. return generateProfessionalContent(subsection.title, contentIndex)
  2698. }
  2699. }
  2700. // 如果没有子小节,根据小节标题生成内容
  2701. if (section && section.title) {
  2702. return generateProfessionalContent(section.title, contentIndex)
  2703. }
  2704. // 最后根据章节标题生成内容
  2705. if (chapter && chapter.title) {
  2706. return generateProfessionalContent(chapter.title, contentIndex)
  2707. }
  2708. // 默认内容
  2709. return generateProfessionalContent('默认内容', contentIndex)
  2710. }
  2711. // Mock数据配置选项
  2712. const mockDataOptions = ref([
  2713. {
  2714. id: 'safety-basic',
  2715. title: '基础安全培训',
  2716. description: '2章节,每章节2小节,每小节4子小节',
  2717. data: [
  2718. {
  2719. title: '信息上报与指挥体系建立',
  2720. content: '建立完善的信息上报和指挥体系',
  2721. sections: [
  2722. {
  2723. title: '信息收集机制',
  2724. content: '建立完善的信息收集机制',
  2725. subsections: [
  2726. { title: '信息收集渠道' },
  2727. { title: '信息收集标准' },
  2728. { title: '信息收集流程' },
  2729. { title: '信息质量控制' }
  2730. ]
  2731. },
  2732. {
  2733. title: '指挥调度平台',
  2734. content: '构建统一的指挥调度平台',
  2735. subsections: [
  2736. { title: '平台功能设计' },
  2737. { title: '系统架构规划' },
  2738. { title: '数据集成方案' },
  2739. { title: '用户权限管理' }
  2740. ]
  2741. }
  2742. ]
  2743. },
  2744. {
  2745. title: '安全风险评估与控制',
  2746. content: '开展全面的安全风险评估与控制',
  2747. sections: [
  2748. {
  2749. title: '风险识别与评估',
  2750. content: '建立风险识别与评估体系',
  2751. subsections: [
  2752. { title: '风险识别方法' },
  2753. { title: '风险评估标准' },
  2754. { title: '风险等级划分' },
  2755. { title: '风险评估流程' }
  2756. ]
  2757. },
  2758. {
  2759. title: '风险控制措施',
  2760. content: '制定有效的风险控制措施',
  2761. subsections: [
  2762. { title: '预防控制措施' },
  2763. { title: '监测预警机制' },
  2764. { title: '应急处置方案' },
  2765. { title: '持续改进机制' }
  2766. ]
  2767. }
  2768. ]
  2769. }
  2770. ]
  2771. },
  2772. {
  2773. id: 'comprehensive-training',
  2774. title: '综合培训体系',
  2775. description: '3章节,每章节3小节,每小节3子小节',
  2776. data: [
  2777. {
  2778. title: '培训体系建设',
  2779. content: '建立完善的培训体系',
  2780. sections: [
  2781. {
  2782. title: '培训需求分析',
  2783. content: '分析培训需求',
  2784. subsections: [
  2785. { title: '需求调研方法' },
  2786. { title: '需求分析工具' },
  2787. { title: '需求确认流程' }
  2788. ]
  2789. },
  2790. {
  2791. title: '培训计划制定',
  2792. content: '制定培训计划',
  2793. subsections: [
  2794. { title: '计划制定原则' },
  2795. { title: '计划执行方案' },
  2796. { title: '计划调整机制' }
  2797. ]
  2798. },
  2799. {
  2800. title: '培训效果评估',
  2801. content: '评估培训效果',
  2802. subsections: [
  2803. { title: '评估指标体系' },
  2804. { title: '评估方法选择' },
  2805. { title: '评估结果应用' }
  2806. ]
  2807. }
  2808. ]
  2809. },
  2810. {
  2811. title: '师资队伍建设',
  2812. content: '建设专业师资队伍',
  2813. sections: [
  2814. {
  2815. title: '师资选拔标准',
  2816. content: '制定师资选拔标准',
  2817. subsections: [
  2818. { title: '专业能力要求' },
  2819. { title: '教学经验要求' },
  2820. { title: '综合素质要求' }
  2821. ]
  2822. },
  2823. {
  2824. title: '师资培训体系',
  2825. content: '建立师资培训体系',
  2826. subsections: [
  2827. { title: '培训内容设计' },
  2828. { title: '培训方式选择' },
  2829. { title: '培训效果跟踪' }
  2830. ]
  2831. },
  2832. {
  2833. title: '师资激励机制',
  2834. content: '建立师资激励机制',
  2835. subsections: [
  2836. { title: '激励政策制定' },
  2837. { title: '激励措施实施' },
  2838. { title: '激励效果评估' }
  2839. ]
  2840. }
  2841. ]
  2842. },
  2843. {
  2844. title: '培训资源管理',
  2845. content: '管理培训资源',
  2846. sections: [
  2847. {
  2848. title: '培训设施建设',
  2849. content: '建设培训设施',
  2850. subsections: [
  2851. { title: '设施规划布局' },
  2852. { title: '设施设备配置' },
  2853. { title: '设施维护管理' }
  2854. ]
  2855. },
  2856. {
  2857. title: '培训教材开发',
  2858. content: '开发培训教材',
  2859. subsections: [
  2860. { title: '教材编写标准' },
  2861. { title: '教材审核流程' },
  2862. { title: '教材更新机制' }
  2863. ]
  2864. },
  2865. {
  2866. title: '培训技术支持',
  2867. content: '提供技术支持',
  2868. subsections: [
  2869. { title: '技术平台建设' },
  2870. { title: '技术维护服务' },
  2871. { title: '技术培训支持' }
  2872. ]
  2873. }
  2874. ]
  2875. }
  2876. ]
  2877. },
  2878. {
  2879. id: 'emergency-management',
  2880. title: '应急管理体系',
  2881. description: '4章节,每章节2小节,每小节2子小节',
  2882. data: [
  2883. {
  2884. title: '应急预案制定',
  2885. content: '制定应急预案',
  2886. sections: [
  2887. {
  2888. title: '预案编制流程',
  2889. content: '编制应急预案',
  2890. subsections: [
  2891. { title: '预案编制标准' },
  2892. { title: '预案审核程序' }
  2893. ]
  2894. },
  2895. {
  2896. title: '预案演练实施',
  2897. content: '实施预案演练',
  2898. subsections: [
  2899. { title: '演练计划制定' },
  2900. { title: '演练效果评估' }
  2901. ]
  2902. }
  2903. ]
  2904. },
  2905. {
  2906. title: '应急响应机制',
  2907. content: '建立应急响应机制',
  2908. sections: [
  2909. {
  2910. title: '响应流程设计',
  2911. content: '设计响应流程',
  2912. subsections: [
  2913. { title: '响应级别划分' },
  2914. { title: '响应时间要求' }
  2915. ]
  2916. },
  2917. {
  2918. title: '响应队伍建设',
  2919. content: '建设响应队伍',
  2920. subsections: [
  2921. { title: '队伍组建标准' },
  2922. { title: '队伍培训体系' }
  2923. ]
  2924. }
  2925. ]
  2926. },
  2927. {
  2928. title: '应急物资保障',
  2929. content: '保障应急物资',
  2930. sections: [
  2931. {
  2932. title: '物资储备管理',
  2933. content: '管理物资储备',
  2934. subsections: [
  2935. { title: '储备标准制定' },
  2936. { title: '储备检查制度' }
  2937. ]
  2938. },
  2939. {
  2940. title: '物资调配机制',
  2941. content: '建立调配机制',
  2942. subsections: [
  2943. { title: '调配流程设计' },
  2944. { title: '调配效率优化' }
  2945. ]
  2946. }
  2947. ]
  2948. },
  2949. {
  2950. title: '应急信息管理',
  2951. content: '管理应急信息',
  2952. sections: [
  2953. {
  2954. title: '信息收集系统',
  2955. content: '建设收集系统',
  2956. subsections: [
  2957. { title: '系统功能设计' },
  2958. { title: '系统运行维护' }
  2959. ]
  2960. },
  2961. {
  2962. title: '信息发布机制',
  2963. content: '建立发布机制',
  2964. subsections: [
  2965. { title: '发布渠道建设' },
  2966. { title: '发布效果监控' }
  2967. ]
  2968. }
  2969. ]
  2970. }
  2971. ]
  2972. },
  2973. {
  2974. id: 'quality-management',
  2975. title: '质量管理体系',
  2976. description: '5章节,每章节2小节,每小节3子小节',
  2977. data: [
  2978. {
  2979. title: '质量方针制定',
  2980. content: '制定质量方针',
  2981. sections: [
  2982. {
  2983. title: '方针内容设计',
  2984. content: '设计方针内容',
  2985. subsections: [
  2986. { title: '方针目标设定' },
  2987. { title: '方针实施策略' },
  2988. { title: '方针效果评估' }
  2989. ]
  2990. },
  2991. {
  2992. title: '方针宣传推广',
  2993. content: '推广质量方针',
  2994. subsections: [
  2995. { title: '宣传渠道建设' },
  2996. { title: '推广活动组织' },
  2997. { title: '推广效果跟踪' }
  2998. ]
  2999. }
  3000. ]
  3001. },
  3002. {
  3003. title: '质量目标管理',
  3004. content: '管理质量目标',
  3005. sections: [
  3006. {
  3007. title: '目标设定方法',
  3008. content: '设定质量目标',
  3009. subsections: [
  3010. { title: '目标分解原则' },
  3011. { title: '目标量化标准' },
  3012. { title: '目标调整机制' }
  3013. ]
  3014. },
  3015. {
  3016. title: '目标监控体系',
  3017. content: '监控目标实现',
  3018. subsections: [
  3019. { title: '监控指标设计' },
  3020. { title: '监控频率设定' },
  3021. { title: '监控结果分析' }
  3022. ]
  3023. }
  3024. ]
  3025. },
  3026. {
  3027. title: '质量过程控制',
  3028. content: '控制质量过程',
  3029. sections: [
  3030. {
  3031. title: '过程识别分析',
  3032. content: '识别分析过程',
  3033. subsections: [
  3034. { title: '过程流程图绘制' },
  3035. { title: '过程关键点识别' },
  3036. { title: '过程风险分析' }
  3037. ]
  3038. },
  3039. {
  3040. title: '过程改进优化',
  3041. content: '改进优化过程',
  3042. subsections: [
  3043. { title: '改进机会识别' },
  3044. { title: '改进方案设计' },
  3045. { title: '改进效果验证' }
  3046. ]
  3047. }
  3048. ]
  3049. },
  3050. {
  3051. title: '质量审核评估',
  3052. content: '审核评估质量',
  3053. sections: [
  3054. {
  3055. title: '审核计划制定',
  3056. content: '制定审核计划',
  3057. subsections: [
  3058. { title: '审核范围确定' },
  3059. { title: '审核标准制定' },
  3060. { title: '审核人员安排' }
  3061. ]
  3062. },
  3063. {
  3064. title: '审核实施管理',
  3065. content: '管理审核实施',
  3066. subsections: [
  3067. { title: '审核流程执行' },
  3068. { title: '审核记录管理' },
  3069. { title: '审核结果处理' }
  3070. ]
  3071. }
  3072. ]
  3073. },
  3074. {
  3075. title: '质量持续改进',
  3076. content: '持续改进质量',
  3077. sections: [
  3078. {
  3079. title: '改进机会识别',
  3080. content: '识别改进机会',
  3081. subsections: [
  3082. { title: '问题分析方法' },
  3083. { title: '改进需求评估' },
  3084. { title: '改进优先级排序' }
  3085. ]
  3086. },
  3087. {
  3088. title: '改进措施实施',
  3089. content: '实施改进措施',
  3090. subsections: [
  3091. { title: '改进方案制定' },
  3092. { title: '改进资源保障' },
  3093. { title: '改进效果跟踪' }
  3094. ]
  3095. }
  3096. ]
  3097. }
  3098. ]
  3099. },
  3100. {
  3101. id: 'innovation-system',
  3102. title: '创新管理体系',
  3103. description: '6章节,每章节1小节,每小节4子小节',
  3104. data: [
  3105. {
  3106. title: '创新战略规划',
  3107. content: '规划创新战略',
  3108. sections: [
  3109. {
  3110. title: '战略分析制定',
  3111. content: '制定创新战略',
  3112. subsections: [
  3113. { title: '内外部环境分析' },
  3114. { title: '创新机会识别' },
  3115. { title: '战略目标设定' },
  3116. { title: '战略实施路径' }
  3117. ]
  3118. }
  3119. ]
  3120. },
  3121. {
  3122. title: '创新文化建设',
  3123. content: '建设创新文化',
  3124. sections: [
  3125. {
  3126. title: '文化理念塑造',
  3127. content: '塑造创新文化',
  3128. subsections: [
  3129. { title: '创新价值观建立' },
  3130. { title: '创新氛围营造' },
  3131. { title: '创新激励机制' },
  3132. { title: '创新成果分享' }
  3133. ]
  3134. }
  3135. ]
  3136. },
  3137. {
  3138. title: '创新团队建设',
  3139. content: '建设创新团队',
  3140. sections: [
  3141. {
  3142. title: '团队组建管理',
  3143. content: '管理创新团队',
  3144. subsections: [
  3145. { title: '团队成员选拔' },
  3146. { title: '团队能力建设' },
  3147. { title: '团队协作机制' },
  3148. { title: '团队绩效管理' }
  3149. ]
  3150. }
  3151. ]
  3152. },
  3153. {
  3154. title: '创新项目管理',
  3155. content: '管理创新项目',
  3156. sections: [
  3157. {
  3158. title: '项目全生命周期',
  3159. content: '管理项目全周期',
  3160. subsections: [
  3161. { title: '项目立项评估' },
  3162. { title: '项目执行监控' },
  3163. { title: '项目风险控制' },
  3164. { title: '项目成果转化' }
  3165. ]
  3166. }
  3167. ]
  3168. },
  3169. {
  3170. title: '创新资源保障',
  3171. content: '保障创新资源',
  3172. sections: [
  3173. {
  3174. title: '资源统筹配置',
  3175. content: '配置创新资源',
  3176. subsections: [
  3177. { title: '资金投入保障' },
  3178. { title: '技术平台建设' },
  3179. { title: '人才资源开发' },
  3180. { title: '信息资源整合' }
  3181. ]
  3182. }
  3183. ]
  3184. },
  3185. {
  3186. title: '创新成果转化',
  3187. content: '转化创新成果',
  3188. sections: [
  3189. {
  3190. title: '成果产业化',
  3191. content: '实现成果产业化',
  3192. subsections: [
  3193. { title: '成果评估筛选' },
  3194. { title: '产业化路径设计' },
  3195. { title: '市场推广策略' },
  3196. { title: '经济效益评估' }
  3197. ]
  3198. }
  3199. ]
  3200. }
  3201. ]
  3202. },
  3203. {
  3204. id: 'mixed-structure',
  3205. title: '混合结构演示',
  3206. description: '2章节,第一章节2小节(3+4子小节),第二章节3小节(2+3+4子小节)',
  3207. data: [
  3208. {
  3209. title: '第一章节:基础管理',
  3210. content: '建立基础管理体系',
  3211. sections: [
  3212. {
  3213. title: '制度建设',
  3214. content: '建立完善的管理制度',
  3215. subsections: [
  3216. { title: '制度框架设计' },
  3217. { title: '制度内容制定' },
  3218. { title: '制度执行监督' }
  3219. ]
  3220. },
  3221. {
  3222. title: '流程优化',
  3223. content: '优化工作流程',
  3224. subsections: [
  3225. { title: '流程梳理分析' },
  3226. { title: '流程改进设计' },
  3227. { title: '流程实施推广' },
  3228. { title: '流程效果评估' }
  3229. ]
  3230. }
  3231. ]
  3232. },
  3233. {
  3234. title: '第二章节:运营管理',
  3235. content: '提升运营管理水平',
  3236. sections: [
  3237. {
  3238. title: '资源配置',
  3239. content: '优化资源配置',
  3240. subsections: [
  3241. { title: '资源需求分析' },
  3242. { title: '资源配置方案' }
  3243. ]
  3244. },
  3245. {
  3246. title: '绩效管理',
  3247. content: '建立绩效管理体系',
  3248. subsections: [
  3249. { title: '绩效指标设定' },
  3250. { title: '绩效评估方法' },
  3251. { title: '绩效改进措施' }
  3252. ]
  3253. },
  3254. {
  3255. title: '风险控制',
  3256. content: '加强风险控制',
  3257. subsections: [
  3258. { title: '风险识别评估' },
  3259. { title: '风险控制措施' },
  3260. { title: '风险监测预警' },
  3261. { title: '风险应急处置' }
  3262. ]
  3263. }
  3264. ]
  3265. }
  3266. ]
  3267. }
  3268. ])
  3269. // 当前选中的mock数据
  3270. const selectedMockData = ref(0)
  3271. // 是否使用用户大纲数据
  3272. const useUserOutline = ref(false)
  3273. // 模板风格选择
  3274. const selectedTemplateStyle = ref('default')
  3275. const availableTemplateStyles = ref(getAvailableTemplateStyles())
  3276. // 测试动态模板填充功能
  3277. const testDynamicTemplateFill = async () => {
  3278. try {
  3279. console.log('开始测试动态模板填充功能...')
  3280. let testOutline, testTitle, testDescription
  3281. // 根据用户选择决定使用哪个数据源
  3282. if (useUserOutline.value && outlineData.value.length > 0) {
  3283. // 使用用户大纲数据,并转换为兼容格式
  3284. testOutline = await convertUserOutlineToCompatibleFormat(outlineData.value)
  3285. testTitle = outlineTitle.value || '用户生成的大纲' // 使用大纲标题作为PPT标题
  3286. testDescription = `${testOutline.length}章节结构演示`
  3287. console.log('使用用户大纲数据:', testTitle, `共${testOutline.length}个章节`)
  3288. console.log('转换后的用户大纲数据结构:', JSON.stringify(testOutline, null, 2))
  3289. } else {
  3290. // 使用选中的mock数据
  3291. const selectedOption = mockDataOptions.value[selectedMockData.value]
  3292. testOutline = selectedOption.data
  3293. testTitle = selectedOption.title
  3294. testDescription = selectedOption.description
  3295. console.log('使用mock数据:', selectedOption.title, selectedOption.description)
  3296. }
  3297. // 生成动态模板
  3298. const result = await matchOutlineAndGeneratePPT(testOutline, testTitle, selectedTemplateStyle.value)
  3299. console.log('生成的动态模板结果:', result)
  3300. if (!result.success) {
  3301. throw new Error(result.error || '动态模板生成失败')
  3302. }
  3303. // 获取模板数据
  3304. const dynamicTemplate = result.data.template
  3305. console.log('提取的模板数据:', dynamicTemplate)
  3306. // 验证模板数据格式
  3307. if (!Array.isArray(dynamicTemplate)) {
  3308. console.error('模板数据不是数组格式:', typeof dynamicTemplate, dynamicTemplate)
  3309. throw new Error('模板数据格式不正确,期望数组但得到: ' + typeof dynamicTemplate)
  3310. }
  3311. // 启用预览模式
  3312. showDownloadOptions.value = true
  3313. currentPPTSlideIndex.value = 0
  3314. isPPTPreviewMode.value = true
  3315. console.log('已启用PPT预览模式,开始逐页生成效果...')
  3316. // 实现逐页生成效果
  3317. await generatePPTWithAnimation(dynamicTemplate, testOutline, testTitle)
  3318. const successMessage = useUserOutline.value ?
  3319. `动态模板测试完成!使用用户大纲数据 (${testOutline.length}个章节)` :
  3320. `动态模板测试完成!使用数据: ${testTitle} (${testDescription})`
  3321. ElMessage.success(successMessage)
  3322. } catch (error) {
  3323. console.error('测试动态模板填充失败:', error)
  3324. ElMessage.error('测试失败: ' + error.message)
  3325. }
  3326. }
  3327. // 导入下载选项图标
  3328. import pptxIcon from '@/assets/Safety/28.png'
  3329. import examIcon from '@/assets/Safety/27.png'
  3330. import documentIcon from '@/assets/Safety/26.png'
  3331. import wordDocIcon from '@/assets/Chat/26.png'
  3332. // 下载选项数据
  3333. const downloadOptions = ref([
  3334. {
  3335. icon: pptxIcon,
  3336. title: 'PowerPoint (PPTX)',
  3337. description: '可编辑的演示文稿'
  3338. },
  3339. {
  3340. icon: examIcon,
  3341. title: '考试工坊',
  3342. description: '基于该文档,生成考试题'
  3343. },
  3344. {
  3345. icon: documentIcon,
  3346. title: '培训讲义文档',
  3347. description: '基于PPT内容,提取文档文字'
  3348. }
  3349. ])
  3350. // 计算属性
  3351. const currentSlideImage = computed(() => slideImages.value[currentSlideIndex.value])
  3352. const currentTime = computed(() => {
  3353. const now = new Date()
  3354. return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
  3355. })
  3356. // 历史记录数据
  3357. const historyData = ref([])
  3358. const historyTotal = ref(0) // 历史记录总数
  3359. // 获取历史记录列表
  3360. const getHistoryRecordList = async () => {
  3361. try {
  3362. console.log('📋 开始获取安全培训历史记录列表...')
  3363. const startTime = performance.now()
  3364. const response = await apis.getHistoryRecord({
  3365. // ===== 已删除:user_id - 后端从token解析 =====
  3366. ai_conversation_id: 0, // 0表示获取对话列表
  3367. business_type: 1 // 安全培训类型
  3368. })
  3369. const endTime = performance.now()
  3370. console.log(`📋 历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
  3371. console.log('📋 安全培训历史记录列表响应:', response)
  3372. if (response.statusCode === 200) {
  3373. // 设置历史记录总数
  3374. historyTotal.value = response.total || 0
  3375. // 转换后端数据为前端格式
  3376. historyData.value = response.data.map(conversation => ({
  3377. id: conversation.id,
  3378. title: generateConversationTitle(conversation.content),
  3379. time: formatTime(conversation.updated_at),
  3380. businessType: conversation.business_type,
  3381. step: conversation.step || 0, // 添加步骤字段
  3382. cover_image: conversation.cover_image || '', // 添加封面图字段
  3383. ppt_json_url: conversation.ppt_json_url || '', // 添加PPT JSON URL字段
  3384. ppt_json_content: conversation.ppt_json_content || '', // 添加PPT JSON内容字段
  3385. isActive: false,
  3386. // 保存原始数据用于后续查询
  3387. rawData: conversation
  3388. }))
  3389. console.log(`✅ 安全培训历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
  3390. } else {
  3391. console.error('❌ 获取安全培训历史记录列表失败:', response.statusCode)
  3392. }
  3393. } catch (error) {
  3394. console.error('❌ 获取安全培训历史记录列表失败:', error)
  3395. }
  3396. }
  3397. // 获取指定对话的详细消息
  3398. const getConversationMessages = async (conversationId) => {
  3399. try {
  3400. console.log('开始获取安全培训对话消息,conversationId:', conversationId)
  3401. const response = await apis.getHistoryRecord({
  3402. // ===== 已删除:user_id - 后端从token解析 =====
  3403. ai_conversation_id: conversationId,
  3404. business_type: 1 // 安全培训类型
  3405. })
  3406. console.log('安全培训对话消息响应:', response)
  3407. if (response.statusCode === 200) {
  3408. // 转换后端消息数据为前端格式
  3409. const messages = response.data.map(message => {
  3410. const userFeedback = convertUserFeedback(message.user_feedback)
  3411. console.log(`安全培训消息 ${message.id} 的反馈状态:`, {
  3412. raw: message.user_feedback,
  3413. converted: userFeedback
  3414. })
  3415. // 如果是用户消息且包含文件标签,提取文件信息
  3416. let file = null
  3417. let userContent = message.content
  3418. if (message.type === 'user' && message.content.includes('</filesize>')) {
  3419. // 提取文件信息
  3420. const filenameMatch = message.content.match(/<filename>(.*?)<\/filename>/)
  3421. const filesizeMatch = message.content.match(/<filesize>(.*?)<\/filesize>/)
  3422. const wordMatch = message.content.match(/<word>(.*?)<\/word>/s)
  3423. if (filenameMatch && filesizeMatch) {
  3424. const filename = filenameMatch[1]
  3425. const filesize = parseInt(filesizeMatch[1])
  3426. const fileContent = wordMatch ? wordMatch[1].trim() : ''
  3427. // 创建文件对象
  3428. file = {
  3429. name: filename,
  3430. size: filesize,
  3431. type: filename.endsWith('.docx') ? '.docx' : filename.endsWith('.doc') ? '.doc' : '.docx',
  3432. icon: getFileIcon(filename.endsWith('.docx') ? '.docx' : filename.endsWith('.doc') ? '.doc' : '.docx'),
  3433. content: fileContent
  3434. }
  3435. // 提取用户实际说的话(</filesize>标签后的内容)
  3436. const userMessageMatch = message.content.split('</filesize>')[1]
  3437. userContent = userMessageMatch ? userMessageMatch.trim() : ''
  3438. }
  3439. }
  3440. return {
  3441. type: message.type, // 'user' 或 'ai'
  3442. content: userContent, // 使用提取的用户消息
  3443. displayContent: message.type === 'ai' ? markdownToHtml(message.content) : userContent,
  3444. file: file, // 添加文件对象
  3445. isTyping: false,
  3446. id: message.id,
  3447. userFeedback: userFeedback,
  3448. // 保存原始数据
  3449. rawData: message
  3450. }
  3451. })
  3452. // 设置聊天消息
  3453. chatMessages.value = messages
  3454. console.log('安全培训对话消息已设置:', chatMessages.value)
  3455. // 更新对话ID
  3456. ai_conversation_id.value = conversationId
  3457. // 直接使用AI回复内容解析大纲(不再使用ppt_outline字段)
  3458. const aiMessage = messages.find(msg => msg.type === 'ai')
  3459. if (aiMessage && aiMessage.content) {
  3460. console.log('找到AI回复内容,直接解析大纲:', aiMessage.content)
  3461. // 检查是否正在切换历史记录,如果是则强制更新数据
  3462. const isCurrentlySwitching = isSwitchingHistory.value
  3463. if (!outlineData.value || outlineData.value.length === 0 || isCurrentlySwitching) {
  3464. // 解析AI回复中的大纲
  3465. const parsedOutline = parseOutlineFromAI(aiMessage.content)
  3466. if (parsedOutline && parsedOutline.chapters && parsedOutline.chapters.length > 0) {
  3467. outlineData.value = parsedOutline.chapters
  3468. outlineTitle.value = parsedOutline.title || '安全培训大纲'
  3469. outlineStats.value = calculateOutlineStats(parsedOutline.chapters)
  3470. console.log('从AI回复解析大纲数据成功', isCurrentlySwitching ? '(强制更新)' : '')
  3471. } else {
  3472. console.log('AI回复中未找到有效的大纲内容')
  3473. }
  3474. } else {
  3475. console.log('已有大纲数据,跳过设置(避免覆盖用户编辑结果)')
  3476. }
  3477. } else {
  3478. console.log('未找到AI回复内容')
  3479. }
  3480. // 设置大纲反馈状态(从AI消息中获取)
  3481. if (aiMessage && aiMessage.rawData && aiMessage.rawData.user_feedback !== undefined) {
  3482. outlineFeedback.value = aiMessage.rawData.user_feedback
  3483. currentAiMessageId.value = aiMessage.id
  3484. console.log('设置大纲反馈状态:', outlineFeedback.value, 'AI消息ID:', currentAiMessageId.value)
  3485. console.log('AI消息原始数据:', aiMessage.rawData)
  3486. } else {
  3487. outlineFeedback.value = null
  3488. currentAiMessageId.value = null
  3489. console.log('未找到AI消息或反馈状态,重置大纲反馈状态')
  3490. }
  3491. return true
  3492. } else {
  3493. console.error('获取安全培训对话消息失败:', response.statusCode)
  3494. return false
  3495. }
  3496. } catch (error) {
  3497. console.error('获取安全培训对话消息失败:', error)
  3498. return false
  3499. }
  3500. }
  3501. // 生成对话标题(从内容中提取)
  3502. const generateConversationTitle = (content) => {
  3503. if (!content) return '未知对话'
  3504. // 检查是否包含文件标签格式
  3505. if (content.includes('</filesize>')) {
  3506. // 提取</filesize>标签后面的内容
  3507. const userMessage = content.split('</filesize>')[1]
  3508. if (userMessage && userMessage.trim()) {
  3509. // 清理多余的空格和换行
  3510. const cleanMessage = userMessage.replace(/\s+/g, ' ').trim()
  3511. // 提取第一句话作为标题
  3512. const firstSentence = cleanMessage.split(/[。!?\n]/)[0]
  3513. // 限制标题长度
  3514. if (firstSentence.length > 30) {
  3515. return firstSentence.substring(0, 30) + '...'
  3516. }
  3517. return firstSentence || '新对话'
  3518. }
  3519. }
  3520. // 如果没有文件标签,使用原来的方法
  3521. let cleanContent = content.replace(/<[^>]*>/g, '')
  3522. cleanContent = cleanContent.replace(/\s+/g, ' ').trim()
  3523. // 提取第一句话作为标题
  3524. const firstSentence = cleanContent.split(/[。!?\n]/)[0]
  3525. // 限制标题长度
  3526. if (firstSentence.length > 30) {
  3527. return firstSentence.substring(0, 30) + '...'
  3528. }
  3529. return firstSentence || '新对话'
  3530. }
  3531. // 格式化时间
  3532. const formatTime = (timestamp) => {
  3533. if (!timestamp) return '未知时间'
  3534. // 处理时间戳
  3535. let date
  3536. if (typeof timestamp === 'string') {
  3537. // 如果是ISO字符串格式,直接创建Date对象
  3538. date = new Date(timestamp)
  3539. } else {
  3540. // 如果是数字时间戳
  3541. let timestampMs = timestamp
  3542. if (timestamp.toString().length === 10) {
  3543. timestampMs = timestamp * 1000
  3544. } else if (timestamp.toString().length === 11) {
  3545. timestampMs = timestamp * 1000
  3546. } else if (timestamp.toString().length === 13) {
  3547. // 13位时间戳,直接使用
  3548. } else {
  3549. timestampMs = timestamp * 1000
  3550. }
  3551. date = new Date(timestampMs)
  3552. }
  3553. const now = new Date()
  3554. // 获取今天的开始时间(0点0分0秒)
  3555. const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  3556. // 获取昨天的开始时间
  3557. const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000)
  3558. // 今天的对话(日期相同)
  3559. if (date >= todayStart) {
  3560. return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  3561. }
  3562. // 昨天的对话(日期是昨天)
  3563. if (date >= yesterdayStart && date < todayStart) {
  3564. return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  3565. }
  3566. // 更早的对话:显示为 "8月30日 15:30" 格式
  3567. const month = date.getMonth() + 1 // getMonth() 返回 0-11
  3568. const day = date.getDate()
  3569. const time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
  3570. return `${month}月${day}日 ${time}`
  3571. }
  3572. // 转换用户反馈状态
  3573. const convertUserFeedback = (feedback) => {
  3574. console.log('转换用户反馈状态:', feedback, '类型:', typeof feedback)
  3575. switch (parseInt(feedback)) {
  3576. case 2: return 'like' // 满意(赞)
  3577. case 3: return 'dislike' // 不满意(踩)
  3578. case 0:
  3579. default: return null // 无反馈
  3580. }
  3581. }
  3582. // 滚动到底部
  3583. const scrollToBottom = () => {
  3584. nextTick(() => {
  3585. const chatContent = document.querySelector('.chat-content')
  3586. console.log('滚动函数执行,找到聊天区域:', chatContent)
  3587. if (chatContent) {
  3588. console.log('滚动前 - scrollTop:', chatContent.scrollTop, 'scrollHeight:', chatContent.scrollHeight, 'clientHeight:', chatContent.clientHeight)
  3589. // 强制滚动到底部
  3590. chatContent.scrollTop = chatContent.scrollHeight
  3591. // 如果上面的方法不工作,尝试其他方法
  3592. setTimeout(() => {
  3593. chatContent.scrollTop = chatContent.scrollHeight
  3594. console.log('延迟滚动后 - scrollTop:', chatContent.scrollTop)
  3595. }, 10)
  3596. // 使用scrollIntoView方法作为备选
  3597. const lastMessage = chatContent.lastElementChild
  3598. if (lastMessage) {
  3599. lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' })
  3600. }
  3601. console.log('滚动后 - scrollTop:', chatContent.scrollTop)
  3602. } else {
  3603. console.warn('未找到聊天内容区域')
  3604. }
  3605. })
  3606. }
  3607. // 检查是否在底部
  3608. const isAtBottom = () => {
  3609. const chatContent = document.querySelector('.chat-content')
  3610. if (!chatContent) return true
  3611. const threshold = 3.125 // 50px的容差 (50px)
  3612. return chatContent.scrollHeight - chatContent.scrollTop - chatContent.clientHeight < threshold
  3613. }
  3614. // 删除历史记录
  3615. const deleteHistoryItem = (historyItem, index) => {
  3616. console.log('准备删除安全培训历史记录:', historyItem)
  3617. // 设置要删除的项目并显示确认弹窗
  3618. deleteTargetItem.value = { item: historyItem, index: index }
  3619. showDeleteModal.value = true
  3620. }
  3621. // 确认删除历史记录
  3622. const confirmDeleteHistory = async () => {
  3623. if (!deleteTargetItem.value) return
  3624. const { item: historyItem, index } = deleteTargetItem.value
  3625. try {
  3626. // 调用删除接口
  3627. const response = await apis.deleteHistoryRecord({
  3628. ai_conversation_id: historyItem.id
  3629. })
  3630. if (response.statusCode === 200) {
  3631. // 删除成功,从列表中移除
  3632. historyData.value.splice(index, 1)
  3633. // 如果删除的是当前激活的历史记录,需要清空界面并调用新建任务
  3634. if (historyItem.isActive) {
  3635. await createNewChat()
  3636. }
  3637. console.log('安全培训历史记录删除成功')
  3638. ElMessage.success('删除成功')
  3639. } else {
  3640. console.error('删除安全培训历史记录失败:', response.msg)
  3641. ElMessage.error(response.msg || '删除失败')
  3642. }
  3643. } catch (error) {
  3644. console.error('删除安全培训历史记录失败:', error)
  3645. ElMessage.error('删除失败,请稍后重试')
  3646. } finally {
  3647. // 关闭弹窗并清除目标项
  3648. showDeleteModal.value = false
  3649. deleteTargetItem.value = null
  3650. }
  3651. }
  3652. // 取消删除
  3653. const cancelDeleteHistory = () => {
  3654. showDeleteModal.value = false
  3655. deleteTargetItem.value = null
  3656. }
  3657. // 处理新建任务点击
  3658. const handleNewChatClick = () => {
  3659. console.log('点击新建任务按钮,isProcessing:', isProcessing.value)
  3660. if (isProcessing.value) {
  3661. console.log('正在处理中,无法新建任务')
  3662. return
  3663. }
  3664. createNewChat()
  3665. }
  3666. // 处理历史记录项点击
  3667. const handleHistoryItemClick = (item, index) => {
  3668. console.log('点击历史记录项,isProcessing:', isProcessing.value, 'isGeneratingOutline:', isGeneratingOutline.value, 'item.isActive:', item.isActive, 'isSwitchingHistory:', isSwitchingHistory.value)
  3669. if (item.isActive || isProcessing.value || isGeneratingOutline.value || isSwitchingHistory.value) {
  3670. console.log('正在处理中、正在生成大纲、已激活或正在切换历史记录,无法切换')
  3671. return
  3672. }
  3673. handleHistoryItem(item)
  3674. }
  3675. // 方法
  3676. const createNewChat = async () => {
  3677. console.log('创建新安全培训任务')
  3678. // 重置所有状态
  3679. ai_conversation_id.value = 0
  3680. chatMessages.value = []
  3681. messageText.value = ''
  3682. selectedFile.value = null
  3683. // 重置保存状态
  3684. lastSavedOutlineData.value = null
  3685. lastSavedPPTData.value = null
  3686. isSaving.value = false
  3687. showChat.value = false
  3688. // 清除所有历史记录的选中状态
  3689. historyData.value.forEach((item) => {
  3690. item.isActive = false
  3691. })
  3692. // 重置步骤状态,回到第一步
  3693. currentStep.value = 'step1'
  3694. // 清除大纲相关数据
  3695. outlineData.value = null
  3696. outlineStats.value = {}
  3697. outlineTitle.value = ''
  3698. // 清除评价状态
  3699. evaluation.value = ''
  3700. outlineFeedback.value = null
  3701. currentAiMessageId.value = null
  3702. outlineId.value = null
  3703. // 清除编辑状态
  3704. editingItem.value = null
  3705. editingType.value = ''
  3706. editingIndex.value = null
  3707. editingContent.value = ''
  3708. // 清除PPT相关状态
  3709. currentSlideIndex.value = 0
  3710. selectedTemplate.value = 0
  3711. showDownloadOptions.value = false
  3712. selectedDownloadOption.value = 0
  3713. pptSlides.value = []
  3714. currentEditingSlide.value = { title: '', content: '' }
  3715. // 清除PPT预览相关状态
  3716. generatedPPT.value = []
  3717. currentPPTSlideIndex.value = 0
  3718. selectedPPTElementIndex.value = -1
  3719. editingPPTElementIndex.value = -1
  3720. editingPPTHtml.value = ''
  3721. zoom.value = 1
  3722. selectedImageIndex.value = null
  3723. // 重新初始化幻灯片图片数组
  3724. slideImages.value = [
  3725. template5Slide1, // 第1页
  3726. template5Slide2, // 第2页
  3727. template5Slide3, // 第3页
  3728. template5Slide4, // 第4页
  3729. template5Slide5 // 第5页
  3730. ]
  3731. // 刷新历史记录列表
  3732. await getHistoryRecordList()
  3733. }
  3734. // 获取历史记录图片
  3735. const getHistoryImage = (item) => {
  3736. // 如果有封面图且不为空,使用封面图
  3737. if (item.cover_image && item.cover_image.trim()) {
  3738. return item.cover_image
  3739. }
  3740. // 否则使用默认图片
  3741. return defaultHistoryIcon
  3742. }
  3743. // 从URL加载PPT数据
  3744. const loadPPTFromUrl = async (pptJsonUrl) => {
  3745. try {
  3746. console.log('开始从URL加载PPT数据:', pptJsonUrl)
  3747. // 使用后端API代理获取JSON数据
  3748. const response = await apis.getPPTJson({ url: pptJsonUrl })
  3749. console.log('从API获取PPT数据响应:', response)
  3750. if (response.statusCode === 200 && response.data) {
  3751. const pptData = response.data
  3752. console.log('从URL加载的PPT数据:', pptData)
  3753. // 设置PPT数据
  3754. generatedPPT.value = pptData
  3755. // 启用PPT预览模式
  3756. showDownloadOptions.value = true
  3757. console.log('PPT数据加载完成,已启用预览模式')
  3758. return pptData
  3759. } else {
  3760. throw new Error(`API返回错误: ${response.msg || '未知错误'}`)
  3761. }
  3762. } catch (error) {
  3763. console.error('从URL加载PPT数据失败:', error)
  3764. // 如果API也失败,尝试重新生成PPT
  3765. try {
  3766. console.log('API加载失败,尝试重新生成PPT数据...')
  3767. await loadAIPPTData()
  3768. showDownloadOptions.value = true
  3769. console.log('重新生成PPT数据完成')
  3770. } catch (regenerateError) {
  3771. console.error('重新生成PPT也失败:', regenerateError)
  3772. throw error
  3773. }
  3774. }
  3775. }
  3776. // 保存步骤信息到后端
  3777. const saveStepToBackend = async (updateCoverImage = true, forceSave = false) => {
  3778. try {
  3779. if (isSaving.value) {
  3780. console.log('正在保存中,跳过重复保存请求')
  3781. return false
  3782. }
  3783. // 检查PPT数据是否有变化
  3784. if (!forceSave && !hasPPTDataChanged()) {
  3785. console.log('PPT数据未发生变化,跳过保存')
  3786. return false
  3787. }
  3788. isSaving.value = true
  3789. console.log('开始保存步骤信息到后端...')
  3790. // 生成PPT JSON数据
  3791. const pptJsonData = {
  3792. slides: generatedPPT.value,
  3793. title: outlineTitle.value || '安全培训演示文稿',
  3794. generatedAt: new Date().toISOString()
  3795. }
  3796. // 直接使用JSON数据,不再上传到OSS
  3797. const pptJsonContent = JSON.stringify(pptJsonData, null, 2)
  3798. console.log('PPT JSON数据已准备,长度:', pptJsonContent.length)
  3799. // 封面图处理逻辑
  3800. let coverImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0909_1757403783.png' // 默认封面图
  3801. // 如果使用的是模板7(红色主题),使用指定的封面图
  3802. if (selectedTemplateStyle.value === 'red') {
  3803. coverImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0913_1757730132.png'
  3804. console.log('使用模板7专用封面图:', coverImage)
  3805. }
  3806. // 如果使用的是模板8(蓝色科技主题),使用指定的封面图
  3807. if (selectedTemplateStyle.value === 'blueTech') {
  3808. coverImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0913_1757730132.png'
  3809. console.log('使用模板8专用封面图:', coverImage)
  3810. }
  3811. if (updateCoverImage) {
  3812. // 只有在需要更新封面图时才查找新的封面图
  3813. // 遍历所有幻灯片,查找第一张用户上传的图片(排除模板默认图片)
  3814. for (const slide of generatedPPT.value) {
  3815. if (slide.elements && slide.elements.length > 0) {
  3816. for (const element of slide.elements) {
  3817. if (element.type === 'image' && element.src && element.src.startsWith('http')) {
  3818. // 跳过模板中的默认图片(unsplash图片)
  3819. if (element.src.includes('unsplash.com') || element.src.includes('placeholder')) {
  3820. console.log('跳过模板默认图片:', element.src)
  3821. continue
  3822. }
  3823. // 找到第一张用户上传的图片,使用它作为封面图
  3824. coverImage = element.src
  3825. console.log('使用用户上传的图片作为封面图:', coverImage)
  3826. break
  3827. }
  3828. }
  3829. if (coverImage !== 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0909_1757403783.png' &&
  3830. coverImage !== 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0913_1757730132.png') {
  3831. break // 已经找到用户上传的图片,跳出外层循环
  3832. }
  3833. }
  3834. }
  3835. } else {
  3836. // 不更新封面图,保持使用现有的封面图
  3837. if (ai_conversation_id.value) {
  3838. const currentHistoryItem = historyData.value.find(item => item.id === ai_conversation_id.value)
  3839. if (currentHistoryItem && currentHistoryItem.cover_image && currentHistoryItem.cover_image.trim()) {
  3840. coverImage = currentHistoryItem.cover_image
  3841. console.log('保持使用现有封面图:', coverImage)
  3842. }
  3843. }
  3844. }
  3845. // 调用保存步骤接口
  3846. const saveStepData = {
  3847. ai_conversation_id: ai_conversation_id.value,
  3848. step: 1, // 设置为步骤1,表示PPT已生成
  3849. ppt_json_url: '', // 不再使用OSS URL
  3850. ppt_json_content: JSON.stringify(generatedPPT.value), // 存储完整的PPT JSON内容
  3851. cover_image: coverImage // 使用检测到的封面图
  3852. }
  3853. console.log('正在保存步骤信息:', saveStepData)
  3854. const saveResponse = await apis.saveStep(saveStepData)
  3855. if (saveResponse.statusCode === 200) {
  3856. console.log('步骤信息保存成功')
  3857. // 更新上次保存的PPT数据
  3858. lastSavedPPTData.value = JSON.parse(JSON.stringify(generatedPPT.value)) // 深拷贝
  3859. ElMessage.success('PPT生成完成并已保存!')
  3860. return true
  3861. } else {
  3862. console.error('保存步骤信息失败:', saveResponse.msg)
  3863. ElMessage.warning('PPT生成完成,但保存步骤信息失败')
  3864. return false
  3865. }
  3866. } catch (error) {
  3867. console.error('保存步骤信息时发生错误:', error)
  3868. ElMessage.warning('PPT生成完成,但保存步骤信息时发生错误')
  3869. return false
  3870. } finally {
  3871. isSaving.value = false
  3872. }
  3873. }
  3874. // 点击历史记录
  3875. const handleHistoryItem = async (historyItem) => {
  3876. console.log('点击安全培训历史记录:', historyItem)
  3877. // 设置切换状态,防止快速点击
  3878. isSwitchingHistory.value = true
  3879. try {
  3880. // 立即更新激活状态,让用户看到反馈
  3881. selectedHistoryItem.value = historyItem
  3882. // 保存当前的历史记录ID,避免在异步保存过程中被覆盖
  3883. const currentConversationId = ai_conversation_id.value
  3884. const currentOutlineId = outlineId.value
  3885. // 保存当前的数据快照,避免在异步保存过程中被覆盖
  3886. const currentOutlineData = outlineData.value ? JSON.parse(JSON.stringify(outlineData.value)) : null
  3887. const currentOutlineTitle = outlineTitle.value
  3888. const currentOutlineStats = outlineStats.value ? JSON.parse(JSON.stringify(outlineStats.value)) : null
  3889. const currentGeneratedPPT = generatedPPT.value ? JSON.parse(JSON.stringify(generatedPPT.value)) : null
  3890. // 更新数据层的active状态
  3891. historyData.value.forEach((item) => {
  3892. item.isActive = item.id === historyItem.id
  3893. })
  3894. // 设置加载状态
  3895. isLoadingHistory.value = true
  3896. // 异步保存当前数据(不阻塞UI更新)
  3897. const saveCurrentData = async () => {
  3898. // 只有在数据真正发生变化时才保存
  3899. let hasChanges = false
  3900. // 检查大纲数据是否有变化(使用数据快照)
  3901. if (currentOutlineData && currentOutlineData.length > 0 && currentOutlineId) {
  3902. // 检查数据快照是否有变化
  3903. const hasChangesInSnapshot = !lastSavedOutlineData.value ||
  3904. JSON.stringify({
  3905. title: currentOutlineTitle,
  3906. stats: currentOutlineStats,
  3907. chapters: currentOutlineData
  3908. }) !== JSON.stringify(lastSavedOutlineData.value)
  3909. if (hasChangesInSnapshot) {
  3910. console.log('检测到当前大纲数据有变化,保存当前修改到历史记录:', currentOutlineId)
  3911. try {
  3912. // 临时设置ai_conversation_id为当前要保存的历史记录ID
  3913. const originalConversationId = ai_conversation_id.value
  3914. ai_conversation_id.value = currentOutlineId
  3915. // 临时设置数据快照
  3916. const originalOutlineData = outlineData.value
  3917. const originalOutlineTitle = outlineTitle.value
  3918. const originalOutlineStats = outlineStats.value
  3919. outlineData.value = currentOutlineData
  3920. outlineTitle.value = currentOutlineTitle
  3921. outlineStats.value = currentOutlineStats
  3922. const saved = await saveOutlineToBackend()
  3923. // 恢复ai_conversation_id
  3924. ai_conversation_id.value = originalConversationId
  3925. // 检查是否需要恢复数据(如果当前数据不是目标历史记录的数据)
  3926. if (ai_conversation_id.value !== historyItem.id) {
  3927. // 如果当前不是目标历史记录,恢复原始数据
  3928. outlineData.value = originalOutlineData
  3929. outlineTitle.value = originalOutlineTitle
  3930. outlineStats.value = originalOutlineStats
  3931. }
  3932. if (saved) {
  3933. hasChanges = true
  3934. console.log('当前大纲修改已保存到历史记录:', currentOutlineId)
  3935. // 更新历史记录列表中的大纲数据
  3936. const currentHistoryItem = historyData.value.find(item => item.id === currentOutlineId)
  3937. if (currentHistoryItem) {
  3938. currentHistoryItem.rawData = {
  3939. ...currentHistoryItem.rawData,
  3940. ppt_outline: JSON.stringify({
  3941. title: currentOutlineTitle,
  3942. stats: currentOutlineStats,
  3943. chapters: currentOutlineData,
  3944. timestamp: Date.now()
  3945. })
  3946. }
  3947. console.log('已更新历史记录列表中的大纲数据')
  3948. }
  3949. }
  3950. } catch (error) {
  3951. console.error('保存当前大纲修改失败:', error)
  3952. }
  3953. } else {
  3954. console.log('大纲数据快照无变化,跳过保存')
  3955. }
  3956. }
  3957. // 检查PPT数据是否有变化(使用数据快照)
  3958. if (currentGeneratedPPT && currentGeneratedPPT.length > 0 && currentConversationId) {
  3959. // 检查PPT数据快照是否有变化
  3960. const hasChangesInPPTSnapshot = !lastSavedPPTData.value ||
  3961. JSON.stringify(currentGeneratedPPT) !== JSON.stringify(lastSavedPPTData.value)
  3962. if (hasChangesInPPTSnapshot) {
  3963. console.log('检测到当前PPT数据有变化,保存当前修改到历史记录:', currentConversationId)
  3964. try {
  3965. // 临时设置ai_conversation_id为当前要保存的历史记录ID
  3966. const originalConversationId = ai_conversation_id.value
  3967. ai_conversation_id.value = currentConversationId
  3968. // 临时设置PPT数据快照
  3969. const originalGeneratedPPT = generatedPPT.value
  3970. generatedPPT.value = currentGeneratedPPT
  3971. const saved = await saveStepToBackend()
  3972. // 恢复ai_conversation_id
  3973. ai_conversation_id.value = originalConversationId
  3974. // 检查是否需要恢复数据(如果当前数据不是目标历史记录的数据)
  3975. if (currentConversationId !== historyItem.id) {
  3976. // 如果当前不是目标历史记录,恢复原始数据
  3977. generatedPPT.value = originalGeneratedPPT
  3978. }
  3979. if (saved) {
  3980. hasChanges = true
  3981. console.log('当前PPT修改已保存到历史记录:', currentConversationId)
  3982. // 更新历史记录列表中的PPT数据
  3983. const currentHistoryItem = historyData.value.find(item => item.id === currentConversationId)
  3984. if (currentHistoryItem) {
  3985. currentHistoryItem.rawData = {
  3986. ...currentHistoryItem.rawData,
  3987. ppt_json_content: JSON.stringify(currentGeneratedPPT),
  3988. timestamp: Date.now()
  3989. }
  3990. console.log('已更新历史记录列表中的PPT数据')
  3991. }
  3992. }
  3993. } catch (error) {
  3994. console.error('保存当前PPT修改失败:', error)
  3995. }
  3996. } else {
  3997. console.log('PPT数据快照无变化,跳过保存')
  3998. }
  3999. }
  4000. if (!hasChanges) {
  4001. console.log('当前数据无变化,跳过保存')
  4002. }
  4003. }
  4004. // 更新ai_conversation_id(在保存任务启动前)
  4005. ai_conversation_id.value = historyItem.id
  4006. // 启动异步保存任务(不等待完成,避免阻塞UI)
  4007. saveCurrentData()
  4008. // 根据step字段决定跳转到哪个页面
  4009. if (historyItem.step === 0) {
  4010. // step为0,跳转到步骤二(大纲页)
  4011. console.log('历史记录step为0,跳转到步骤二(大纲页)')
  4012. // 清除PPT预览相关状态
  4013. generatedPPT.value = []
  4014. currentPPTSlideIndex.value = 0
  4015. selectedPPTElementIndex.value = -1
  4016. editingPPTElementIndex.value = -1
  4017. editingPPTHtml.value = ''
  4018. zoom.value = 1
  4019. selectedImageIndex.value = null
  4020. showDownloadOptions.value = false
  4021. // 重置为模板预览状态
  4022. currentSlideIndex.value = 0
  4023. // 显示加载状态
  4024. const loadingMessage = {
  4025. type: 'ai',
  4026. content: '正在加载历史对话...',
  4027. displayContent: '正在加载历史对话...',
  4028. isTyping: true,
  4029. id: Date.now() + 1,
  4030. userFeedback: null
  4031. }
  4032. chatMessages.value = [loadingMessage]
  4033. try {
  4034. // 获取该对话的详细消息
  4035. const success = await getConversationMessages(historyItem.id)
  4036. if (success) {
  4037. console.log('安全培训历史对话加载成功', historyItem)
  4038. // 移除加载消息
  4039. chatMessages.value = chatMessages.value.filter(msg => msg.id !== loadingMessage.id)
  4040. // 数据加载完成后再设置步骤
  4041. currentStep.value = 'step2'
  4042. // 清除加载状态
  4043. isLoadingHistory.value = false
  4044. // 检查是否有大纲数据(优先使用outlineData.value,因为getConversationMessages可能已经设置了)
  4045. // 但是要确保这是当前历史记录的大纲数据,不是上一个历史记录的
  4046. if (outlineData.value && outlineData.value.length > 0 && ai_conversation_id.value === historyItem.id) {
  4047. console.log('检测到已有大纲数据,直接使用(来自getConversationMessages)')
  4048. // 设置大纲ID
  4049. outlineId.value = historyItem.id
  4050. // 更新保存状态,避免重复保存
  4051. lastSavedOutlineData.value = {
  4052. title: outlineTitle.value,
  4053. stats: outlineStats.value,
  4054. chapters: JSON.parse(JSON.stringify(outlineData.value)) // 深拷贝
  4055. }
  4056. console.log('设置大纲ID:', outlineId.value)
  4057. // 跳转到步骤二
  4058. currentStep.value = 'step2'
  4059. console.log('已跳转到步骤二:培训大纲界面(使用getConversationMessages数据)')
  4060. // 确保模板预览数据已加载
  4061. loadLocalPPT()
  4062. updateSlideThumbnails()
  4063. } else {
  4064. console.log('未找到大纲数据,显示聊天界面')
  4065. showChat.value = true
  4066. }
  4067. } else {
  4068. // 加载失败,显示错误消息
  4069. chatMessages.value = [{
  4070. type: 'ai',
  4071. content: '抱歉,加载历史对话失败,请稍后重试。',
  4072. displayContent: '抱歉,加载历史对话失败,请稍后重试。',
  4073. isTyping: false,
  4074. id: Date.now() + 1,
  4075. userFeedback: null
  4076. }]
  4077. showChat.value = true
  4078. }
  4079. } catch (error) {
  4080. console.error('加载安全培训历史对话失败:', error)
  4081. chatMessages.value = [{
  4082. type: 'ai',
  4083. content: '抱歉,加载历史对话时发生错误,请稍后重试。',
  4084. displayContent: '抱歉,加载历史对话时发生错误,请稍后重试。',
  4085. isTyping: false,
  4086. id: Date.now() + 1,
  4087. userFeedback: null
  4088. }]
  4089. showChat.value = true
  4090. // 清除加载状态
  4091. isLoadingHistory.value = false
  4092. }
  4093. } else if (historyItem.step === 1) {
  4094. // step为1,直接跳转到PPT预览页面
  4095. console.log('历史记录step为1,直接跳转到PPT预览页面')
  4096. // 清除之前的PPT预览状态,准备加载新的PPT数据
  4097. generatedPPT.value = []
  4098. currentPPTSlideIndex.value = 0
  4099. selectedPPTElementIndex.value = -1
  4100. editingPPTElementIndex.value = -1
  4101. editingPPTHtml.value = ''
  4102. zoom.value = 1
  4103. selectedImageIndex.value = null
  4104. // 显示加载状态
  4105. const loadingMessage = {
  4106. type: 'ai',
  4107. content: '正在加载PPT数据...',
  4108. displayContent: '正在加载PPT数据...',
  4109. isTyping: true,
  4110. id: Date.now() + 1,
  4111. userFeedback: null
  4112. }
  4113. chatMessages.value = [loadingMessage]
  4114. try {
  4115. // 获取该对话的详细消息
  4116. const success = await getConversationMessages(historyItem.id)
  4117. if (success) {
  4118. console.log('安全培训历史对话加载成功')
  4119. // 移除加载消息
  4120. chatMessages.value = chatMessages.value.filter(msg => msg.id !== loadingMessage.id)
  4121. // 数据加载完成后再设置步骤
  4122. currentStep.value = 'step3'
  4123. // 清除加载状态
  4124. isLoadingHistory.value = false
  4125. // 获取最新的AI回复消息并解析大纲
  4126. const aiMessages = chatMessages.value.filter(msg => msg.type === 'ai')
  4127. const aiMessage = aiMessages.length > 1 ? aiMessages[1] : (aiMessages.length > 0 ? aiMessages[0] : null)
  4128. if (aiMessage && aiMessage.content) {
  4129. console.log('找到AI回复,开始解析大纲并直接生成PPT预览')
  4130. // 设置大纲ID
  4131. outlineId.value = historyItem.id
  4132. // 解析AI回复获取大纲数据
  4133. const parsedOutline = parseOutlineFromAI(aiMessage.content)
  4134. if (parsedOutline && parsedOutline.chapters && parsedOutline.chapters.length > 0) {
  4135. outlineData.value = parsedOutline.chapters
  4136. outlineTitle.value = parsedOutline.title || '安全培训大纲'
  4137. // 使用新的统计计算函数
  4138. outlineStats.value = calculateOutlineStats(parsedOutline.chapters)
  4139. // step=1时,直接从数据库读取已保存的PPT数据
  4140. try {
  4141. console.log('step=1,从数据库读取已保存的PPT数据...')
  4142. // 检查是否有保存的PPT JSON内容
  4143. if (historyItem.ppt_json_content && historyItem.ppt_json_content.trim()) {
  4144. console.log('找到已保存的PPT JSON内容,直接加载...')
  4145. // 解析JSON内容
  4146. const savedPPTData = JSON.parse(historyItem.ppt_json_content)
  4147. console.log('解析的PPT数据:', savedPPTData)
  4148. // 直接设置PPT数据(后端数据已经是最新的)
  4149. generatedPPT.value = savedPPTData
  4150. showDownloadOptions.value = true
  4151. // 更新保存状态,避免重复保存
  4152. lastSavedPPTData.value = JSON.parse(JSON.stringify(savedPPTData)) // 深拷贝
  4153. console.log('PPT数据加载完成,共', savedPPTData.length, '张幻灯片')
  4154. // 确保历史记录列表中的数据也是最新的
  4155. const updatedHistoryItem = historyData.value.find(item => item.id === historyItem.id)
  4156. if (updatedHistoryItem) {
  4157. updatedHistoryItem.ppt_json_content = historyItem.ppt_json_content
  4158. console.log('已同步历史记录列表中的PPT数据')
  4159. }
  4160. } else {
  4161. console.log('未找到保存的PPT内容,重新生成...')
  4162. // 如果没有保存的内容,则重新生成
  4163. const aipptData = convertOutlineToAIPPT(outlineData.value, outlineTitle.value)
  4164. const filledAIPPTData = await fillAIPPTContent(aipptData, outlineTitle.value)
  4165. await loadAIPPTData(filledAIPPTData, false)
  4166. showDownloadOptions.value = true
  4167. console.log('PPT数据重新生成完成')
  4168. }
  4169. } catch (error) {
  4170. console.error('加载PPT失败:', error)
  4171. ElMessage.error('加载PPT失败: ' + error.message)
  4172. }
  4173. } else {
  4174. console.log('解析大纲失败,显示聊天界面')
  4175. showChat.value = true
  4176. }
  4177. } else {
  4178. console.log('未找到AI回复,显示聊天界面')
  4179. showChat.value = true
  4180. }
  4181. } else {
  4182. // 加载失败,显示错误消息
  4183. chatMessages.value = [{
  4184. type: 'ai',
  4185. content: '抱歉,加载历史对话失败,请稍后重试。',
  4186. displayContent: '抱歉,加载历史对话失败,请稍后重试。',
  4187. isTyping: false,
  4188. id: Date.now() + 1,
  4189. userFeedback: null
  4190. }]
  4191. showChat.value = true
  4192. }
  4193. } catch (error) {
  4194. console.error('加载安全培训历史对话失败:', error)
  4195. chatMessages.value = [{
  4196. type: 'ai',
  4197. content: '抱歉,加载历史对话时发生错误,请稍后重试。',
  4198. displayContent: '抱歉,加载历史对话时发生错误,请稍后重试。',
  4199. isTyping: false,
  4200. id: Date.now() + 1,
  4201. userFeedback: null
  4202. }]
  4203. showChat.value = true
  4204. // 清除加载状态
  4205. isLoadingHistory.value = false
  4206. }
  4207. } else {
  4208. // 其他情况,显示聊天界面
  4209. console.log('历史记录step未知,显示聊天界面')
  4210. showChat.value = true
  4211. }
  4212. } catch (error) {
  4213. console.error('处理历史记录切换失败:', error)
  4214. ElMessage.error('切换历史记录失败,请稍后重试')
  4215. } finally {
  4216. // 重置切换状态
  4217. isSwitchingHistory.value = false
  4218. isLoadingHistory.value = false
  4219. }
  4220. }
  4221. // 功能卡片图标计数器
  4222. let functionCardIconIndex = 0
  4223. // 推荐问题图标计数器
  4224. let questionIconIndex = 0
  4225. // 根据功能卡片标题返回对应的图标
  4226. const getFunctionCardIcon = (title) => {
  4227. // 按顺序循环使用4个图标
  4228. const icons = [safetyTrainingIcon, safetyAssessmentIcon, safetyRegulationsIcon, emergencyProceduresIcon]
  4229. const icon = icons[functionCardIconIndex % icons.length]
  4230. functionCardIconIndex++
  4231. return icon
  4232. }
  4233. // 根据问题标题返回对应的图标
  4234. const getQuestionIcon = (question) => {
  4235. // 按顺序循环使用3个图标
  4236. const icons = [questionIcon1, questionIcon2, questionIcon3]
  4237. const icon = icons[questionIconIndex % icons.length]
  4238. questionIconIndex++
  4239. return icon
  4240. }
  4241. // 点击功能卡片
  4242. const handleFunctionCard = (cardType) => {
  4243. console.log('点击功能卡片:', cardType)
  4244. // 显示聊天界面
  4245. showChat.value = true
  4246. // 根据卡片类型设置不同的消息
  4247. let message = ''
  4248. // 如果是后端数据,直接使用卡片标题作为消息
  4249. if (typeof cardType === 'string' && cardType.length > 0) {
  4250. message = `请详细介绍${cardType}的相关内容`
  4251. } else {
  4252. // 兼容原有的硬编码逻辑
  4253. switch (cardType) {
  4254. case 'safety-training':
  4255. message = '请详细介绍安全培训课程的相关内容'
  4256. break
  4257. case 'safety-assessment':
  4258. message = '请介绍安全评估测试的关键要点'
  4259. break
  4260. case 'safety-regulations':
  4261. message = '请查询相关的安全法规和标准'
  4262. break
  4263. case 'emergency-procedures':
  4264. message = '请介绍应急处理程序的关键步骤'
  4265. break
  4266. default:
  4267. message = `请详细介绍${cardType}的相关内容`
  4268. }
  4269. }
  4270. // 自动发送消息
  4271. messageText.value = message
  4272. sendMessage()
  4273. }
  4274. // 发送消息
  4275. const sendMessage = async () => {
  4276. if (messageText.value.trim() && !isSending.value) {
  4277. console.log('开始发送消息:', messageText.value, '文件:', selectedFile.value)
  4278. // 设置发送状态
  4279. isSending.value = true
  4280. isProcessing.value = true
  4281. // 显示聊天界面
  4282. showChat.value = true
  4283. // 如果是新对话(没有历史记录或当前没有激活的历史记录),清除所有历史记录的选中状态
  4284. if (chatMessages.value.length === 0) {
  4285. historyData.value.forEach((item) => {
  4286. item.isActive = false;
  4287. });
  4288. console.log('新对话开始,清除所有历史记录的选中状态');
  4289. }
  4290. // 添加用户消息
  4291. const userMessage = {
  4292. type: 'user',
  4293. content: messageText.value,
  4294. file: selectedFile.value, // 添加文件信息
  4295. id: Date.now() // 添加唯一ID
  4296. }
  4297. chatMessages.value.push(userMessage)
  4298. // 添加AI消息(初始状态为思考中)
  4299. const aiMessage = {
  4300. type: 'ai',
  4301. content: '',
  4302. displayContent: '',
  4303. isTyping: true,
  4304. id: Date.now() + 1, // 添加唯一ID
  4305. userFeedback: null // 用户反馈状态:null, 'like', 'dislike'
  4306. }
  4307. chatMessages.value.push(aiMessage)
  4308. // 清空输入框
  4309. const currentMessage = messageText.value
  4310. messageText.value = ''
  4311. // 立即清理选中的文件
  4312. if (selectedFile.value) {
  4313. removeSelectedFile()
  4314. }
  4315. // 发送消息后滚动到底部
  4316. scrollToBottom()
  4317. console.log('当前聊天消息:', chatMessages.value)
  4318. try {
  4319. // 构建发送给AI的消息
  4320. let messageToAI = currentMessage
  4321. // 如果有文件,使用标签格式
  4322. if (userMessage.file && userMessage.file.content) {
  4323. messageToAI = `<word>${userMessage.file.content}</word><filename>${userMessage.file.name}</filename><filesize>${userMessage.file.size}</filesize>${currentMessage}`
  4324. }
  4325. // 调用后端DeepSeek接口
  4326. const response = await apis.sendDeepseekMessage({
  4327. // ai_conversation_id: ai_conversation_id.value,
  4328. // ===== 已删除:user_id - 后端从token解析 =====
  4329. business_type: 1,
  4330. message: messageToAI
  4331. })
  4332. console.log('DeepSeek API响应:', response)
  4333. if (response.statusCode === 200) {
  4334. const aiReply = response.data.reply
  4335. ai_conversation_id.value = response.data.ai_conversation_id
  4336. // 处理特殊字符和表情符号
  4337. const processedReply = processAIResponse(aiReply)
  4338. // 处理文件标签格式的回显
  4339. const processedReplyWithFileDisplay = processFileDisplay(processedReply, userMessage.file)
  4340. // 将Markdown格式转换为HTML格式
  4341. const htmlReply = markdownToHtml(processedReplyWithFileDisplay)
  4342. // 开始打字效果 - 按完整HTML标签或文本块显示,避免标签分割
  4343. const textBlocks = []
  4344. let currentBlock = ''
  4345. let inTag = false
  4346. let tagContent = ''
  4347. // 将HTML内容分割成完整的块
  4348. for (let i = 0; i < htmlReply.length; i++) {
  4349. const char = htmlReply[i]
  4350. if (char === '<') {
  4351. // 如果之前有文本内容,先保存
  4352. if (currentBlock && !inTag) {
  4353. textBlocks.push({ type: 'text', content: currentBlock })
  4354. currentBlock = ''
  4355. }
  4356. inTag = true
  4357. tagContent = char
  4358. } else if (char === '>') {
  4359. // 标签结束
  4360. tagContent += char
  4361. textBlocks.push({ type: 'tag', content: tagContent })
  4362. inTag = false
  4363. tagContent = ''
  4364. } else if (inTag) {
  4365. // 在标签内
  4366. tagContent += char
  4367. } else {
  4368. // 普通文本
  4369. currentBlock += char
  4370. }
  4371. }
  4372. // 保存最后一个文本块
  4373. if (currentBlock) {
  4374. textBlocks.push({ type: 'text', content: currentBlock })
  4375. }
  4376. console.log('分割后的文本块:', textBlocks)
  4377. let currentBlockIndex = 0
  4378. let currentCharIndex = 0
  4379. const typeInterval = setInterval(() => {
  4380. if (currentBlockIndex < textBlocks.length) {
  4381. const currentBlock = textBlocks[currentBlockIndex]
  4382. if (currentBlock.type === 'tag') {
  4383. // 标签直接显示,不分字符
  4384. aiMessage.displayContent += currentBlock.content
  4385. currentBlockIndex++
  4386. currentCharIndex = 0
  4387. } else {
  4388. // 文本按字符显示
  4389. if (currentCharIndex < currentBlock.content.length) {
  4390. const newContent = aiMessage.displayContent + currentBlock.content[currentCharIndex]
  4391. aiMessage.displayContent = newContent
  4392. currentCharIndex++
  4393. } else {
  4394. // 当前文本块完成,移动到下一个
  4395. currentBlockIndex++
  4396. currentCharIndex = 0
  4397. }
  4398. }
  4399. // 强制触发Vue响应式更新
  4400. chatMessages.value = [...chatMessages.value]
  4401. // 逐步扩展宽度
  4402. const messageElement = document.querySelector(`[data-message-index="${chatMessages.value.length - 1}"] .message-content`)
  4403. if (messageElement) {
  4404. messageElement.style.width = 'fit-content'
  4405. }
  4406. // 每显示一个字符都滚动到底部,确保长回复时滚动条始终跟随
  4407. scrollToBottom()
  4408. // 强制触发DOM更新后再滚动一次
  4409. nextTick(() => {
  4410. scrollToBottom()
  4411. })
  4412. } else {
  4413. // 所有块都显示完成
  4414. aiMessage.isTyping = false
  4415. aiMessage.content = processedReply // 保存完整内容
  4416. clearInterval(typeInterval)
  4417. console.log('打字完成')
  4418. // 打字完成后设置最终宽度
  4419. const messageElement = document.querySelector(`[data-message-index="${chatMessages.value.length - 1}"] .message-content`)
  4420. if (messageElement) {
  4421. messageElement.style.width = '100%'
  4422. }
  4423. // 强制触发Vue响应式更新
  4424. setTimeout(async () => {
  4425. chatMessages.value = [...chatMessages.value]
  4426. console.log('打字完成,AI回复内容已全部显示')
  4427. // AI回复完成后,获取最新的历史记录
  4428. await getHistoryRecordList()
  4429. // 如果是新对话,将最新的历史记录设为激活状态
  4430. if (ai_conversation_id.value > 0) {
  4431. historyData.value.forEach((item) => {
  4432. item.isActive = item.id === ai_conversation_id.value;
  4433. });
  4434. console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
  4435. // 设置大纲ID(使用对话ID作为大纲ID)
  4436. outlineId.value = ai_conversation_id.value
  4437. console.log('设置新对话的大纲ID:', outlineId.value);
  4438. }
  4439. // 打字完成后强制滚动到底部,确保长回复完全可见
  4440. scrollToBottom()
  4441. // AI回复完全结束后,解禁历史记录和新建任务
  4442. isProcessing.value = false
  4443. console.log('AI回复完成,解禁历史记录和新建任务')
  4444. // 立即检查是否为PPT生成需求,如果是则跳转到步骤二
  4445. console.log('AI回复完成,开始检查是否为PPT需求')
  4446. checkAndNavigateToOutline(processedReply)
  4447. }, 100)
  4448. }
  4449. }, 20) // 每20ms显示一个字符,更频繁地滚动
  4450. } else {
  4451. // API调用失败
  4452. aiMessage.isTyping = false
  4453. aiMessage.content = '抱歉,我暂时无法回答您的问题,请稍后重试。'
  4454. aiMessage.displayContent = aiMessage.content
  4455. console.error('DeepSeek API调用失败:', response)
  4456. // API调用失败时也要解禁历史记录和新建任务
  4457. isProcessing.value = false
  4458. }
  4459. } catch (error) {
  4460. console.error('发送消息失败:', error)
  4461. // 显示错误消息
  4462. aiMessage.isTyping = false
  4463. aiMessage.content = '抱歉,网络连接出现问题,请检查网络后重试。'
  4464. aiMessage.displayContent = aiMessage.content
  4465. // 网络错误时也要解禁历史记录和新建任务
  4466. isProcessing.value = false
  4467. } finally {
  4468. // 重置发送状态
  4469. isSending.value = false
  4470. }
  4471. }
  4472. }
  4473. // 生成安全培训智能提示词
  4474. const generateSafetyTrainingPrompt = () => {
  4475. return `请根据用户的需求进行回复,无论用户说什么都要提取关键词生成安全培训PPT大纲,请严格按照以下格式回复:
  4476. 以下是为您准备的PPT大纲,包含相关内容:
  4477. [在这里生成大纲内容,使用Markdown格式,包含:
  4478. 1. 章节标题(如:第一章 xxxx)
  4479. 2. 小节内容(如:1.1 xxxx)
  4480. 3. 子小节内容(如:1.1.1 xxxx)
  4481. 大纲统计信息:
  4482. - 总章节数:X章
  4483. - 总小节数:X小节
  4484. - 预计PPT页数:X-X页
  4485. - 预计讲解时长:X-X分钟]
  4486. 请确保:
  4487. 1. 每个章节都以"第X章"开头
  4488. 2. 每个小节都以"X.X"开头
  4489. 3. 每个子小节都以"X.X.X"开头
  4490. 4. 统计信息严格按照上述格式
  4491. 5. 不要添加其他无关内容或占位符
  4492. 6. 内容要符合安全培训的主题,包括但不限于:安全生产法规、安全操作规程、事故预防、应急处理、安全文化建设等`;
  4493. }
  4494. // 生成新大纲
  4495. const generateNewOutline = async () => {
  4496. try {
  4497. // 设置生成状态
  4498. isGeneratingOutline.value = true
  4499. isProcessing.value = true
  4500. // 锁定当前的ai_conversation_id,防止在生成过程中被切换
  4501. const lockedConversationId = ai_conversation_id.value
  4502. // 获取当前大纲标题作为请求内容
  4503. const currentTitle = outlineTitle.value || '安全培训大纲'
  4504. const requestMessage = `${currentTitle}`
  4505. console.log('开始生成新大纲111:', requestMessage)
  4506. console.log("锁定的ai_conversation_id:", lockedConversationId)
  4507. console.log("outlineTitle.value:", outlineTitle.value)
  4508. // 调用后端DeepSeek接口
  4509. const response = await apis.sendDeepseekMessage({
  4510. business_type: 1,
  4511. message: outlineTitle.value,
  4512. ai_conversation_id: lockedConversationId
  4513. })
  4514. console.log('新大纲生成API响应:', response)
  4515. if (response.statusCode === 200) {
  4516. const aiReply = response.data.reply
  4517. // 处理特殊字符和表情符号
  4518. const processedReply = processAIResponse(aiReply)
  4519. // 解析新大纲
  4520. const newOutline = parseOutlineFromAI(processedReply)
  4521. if (newOutline && newOutline.chapters && newOutline.chapters.length > 0) {
  4522. console.log('新大纲解析成功:', newOutline)
  4523. // 更新大纲数据
  4524. outlineData.value = newOutline.chapters
  4525. outlineStats.value = newOutline.stats
  4526. outlineTitle.value = newOutline.title
  4527. // 重置反馈状态(新大纲没有反馈)
  4528. outlineFeedback.value = null
  4529. evaluation.value = ''
  4530. // currentAiMessageId.value = null
  4531. outlineId.value = null
  4532. // 立即保存新生成的大纲到后端,防止切换历史记录时数据被覆盖
  4533. // 直接调用保存接口,不依赖全局的ai_conversation_id
  4534. await saveOutlineToBackendDirectly(lockedConversationId, newOutline.chapters, newOutline.title, newOutline.stats)
  4535. ElMessage.success('大纲已生成')
  4536. } else {
  4537. console.log('新大纲解析失败')
  4538. ElMessage.error('新大纲生成失败,请重试')
  4539. }
  4540. } else {
  4541. // API调用失败
  4542. console.error('新大纲生成API调用失败:', response)
  4543. ElMessage.error('生成失败,请重试')
  4544. }
  4545. } catch (error) {
  4546. console.error('生成新大纲失败:', error)
  4547. ElMessage.error('生成失败,请重试')
  4548. } finally {
  4549. // 重置生成状态
  4550. isGeneratingOutline.value = false
  4551. isProcessing.value = false
  4552. }
  4553. }
  4554. // 确保题目初始值正确
  4555. const ensureQuestionInitialValues = (examData) => {
  4556. // 单选题
  4557. if (examData.singleChoice && examData.singleChoice.questions) {
  4558. examData.singleChoice.questions.forEach(question => {
  4559. if (!question.selectedAnswer) {
  4560. question.selectedAnswer = ""
  4561. }
  4562. if (!question.options || question.options.length === 0) {
  4563. question.options = [
  4564. { key: "A", text: "选项A" },
  4565. { key: "B", text: "选项B" },
  4566. { key: "C", text: "选项C" },
  4567. { key: "D", text: "选项D" }
  4568. ]
  4569. }
  4570. })
  4571. }
  4572. // 判断题
  4573. if (examData.judge && examData.judge.questions) {
  4574. examData.judge.questions.forEach(question => {
  4575. if (!question.selectedAnswer) {
  4576. question.selectedAnswer = ""
  4577. }
  4578. })
  4579. }
  4580. // 多选题
  4581. if (examData.multiple && examData.multiple.questions) {
  4582. examData.multiple.questions.forEach(question => {
  4583. if (!question.selectedAnswers) {
  4584. question.selectedAnswers = []
  4585. }
  4586. if (!question.options || question.options.length === 0) {
  4587. question.options = [
  4588. { key: "A", text: "选项A" },
  4589. { key: "B", text: "选项B" },
  4590. { key: "C", text: "选项C" },
  4591. { key: "D", text: "选项D" }
  4592. ]
  4593. }
  4594. })
  4595. }
  4596. // 简答题
  4597. if (examData.short && examData.short.questions) {
  4598. examData.short.questions.forEach(question => {
  4599. if (!question.outline) {
  4600. question.outline = { keyFactors: "答题要点、关键因素、示例答案" }
  4601. }
  4602. })
  4603. }
  4604. }
  4605. // 生成默认考试
  4606. const generateDefaultExam = () => {
  4607. return {
  4608. title: "安全培训考试",
  4609. totalScore: 100,
  4610. totalQuestions: 17,
  4611. singleChoice: {
  4612. scorePerQuestion: 5,
  4613. totalScore: 25,
  4614. count: 5,
  4615. questions: []
  4616. },
  4617. judge: {
  4618. scorePerQuestion: 3,
  4619. totalScore: 15,
  4620. count: 5,
  4621. questions: []
  4622. },
  4623. multiple: {
  4624. scorePerQuestion: 8,
  4625. totalScore: 40,
  4626. count: 5,
  4627. questions: []
  4628. },
  4629. short: {
  4630. scorePerQuestion: 10,
  4631. totalScore: 20,
  4632. count: 2,
  4633. questions: []
  4634. }
  4635. }
  4636. }
  4637. // 导出考试文件为Word格式
  4638. const exportExamToFile = (examData) => {
  4639. try {
  4640. // 创建Word文档内容(使用HTML格式,兼容WPS和Word)
  4641. const wordContent = createExamWordContent(examData)
  4642. // 创建Blob对象 - 使用Word兼容的MIME类型
  4643. const blob = new Blob([wordContent], {
  4644. type: 'application/msword'
  4645. })
  4646. // 下载文件
  4647. const url = URL.createObjectURL(blob)
  4648. const link = document.createElement('a')
  4649. link.setAttribute('href', url)
  4650. link.setAttribute('download', `${examData.title}_${new Date().toISOString().split('T')[0]}.doc`)
  4651. link.style.visibility = 'hidden'
  4652. document.body.appendChild(link)
  4653. link.click()
  4654. document.body.removeChild(link)
  4655. URL.revokeObjectURL(url)
  4656. ElMessage.success('考试文件已下载!')
  4657. } catch (error) {
  4658. console.error('导出考试文件失败:', error)
  4659. ElMessage.error('导出考试文件失败,请重试')
  4660. }
  4661. }
  4662. // 创建Word格式的考试文档内容(兼容WPS和Word)
  4663. const createExamWordContent = (examData) => {
  4664. const currentTime = new Date().toLocaleString('zh-CN')
  4665. // HTML文档内容,使用Word兼容的格式
  4666. let htmlContent = `<!DOCTYPE html>
  4667. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  4668. xmlns:w="urn:schemas-microsoft-com:office:word"
  4669. xmlns="http://www.w3.org/TR/REC-html40">
  4670. <head>
  4671. <meta charset="utf-8">
  4672. <meta name="ProgId" content="Word.Document">
  4673. <meta name="Generator" content="Microsoft Word 15">
  4674. <meta name="Originator" content="Microsoft Word 15">
  4675. <title>${examData.title || '考试试卷'}</title>
  4676. <!--[if gte mso 9]>
  4677. <xml>
  4678. <w:WordDocument>
  4679. <w:View>Print</w:View>
  4680. <w:Zoom>100</w:Zoom>
  4681. <w:DoNotPromptForConvert/>
  4682. <w:DoNotShowRevisions/>
  4683. <w:DoNotPrintRevisions/>
  4684. <w:DoNotShowComments/>
  4685. <w:DoNotShowInsertionsAndDeletions/>
  4686. <w:DoNotShowPropertyChanges/>
  4687. <w:Compatibility>
  4688. <w:BreakWrappedTables/>
  4689. <w:SnapToGridInCell/>
  4690. <w:WrapTextWithPunct/>
  4691. <w:UseAsianBreakRules/>
  4692. <w:DontGrowAutofit/>
  4693. </w:Compatibility>
  4694. </w:WordDocument>
  4695. </xml>
  4696. <![endif]-->
  4697. <style>
  4698. body {
  4699. font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
  4700. font-size: 14px;
  4701. line-height: 1.6;
  4702. margin: 24px;
  4703. color: #000;
  4704. }
  4705. .header {
  4706. text-align: center;
  4707. margin-bottom: 14px;
  4708. }
  4709. .exam-title {
  4710. font-size: 24px;
  4711. font-weight: bold;
  4712. margin-bottom: 14px;
  4713. color: #000;
  4714. }
  4715. .exam-info {
  4716. font-size: 14px;
  4717. color: #666;
  4718. margin-bottom: 14px;
  4719. }
  4720. .section {
  4721. margin-bottom: 14px;
  4722. }
  4723. .section-title {
  4724. font-size: 18px;
  4725. font-weight: bold;
  4726. margin-bottom: 14px;
  4727. color: #000;
  4728. border-bottom: 2px solid #3e7bfa;
  4729. padding-bottom: 5px;
  4730. }
  4731. .question {
  4732. margin-bottom: 14px;
  4733. padding: 10px;
  4734. background-color: #f9f9f9;
  4735. border-left: 4px solid #3e7bfa;
  4736. }
  4737. .question-header {
  4738. margin-bottom: 14px;
  4739. line-height: 1.6;
  4740. }
  4741. .question-number {
  4742. font-weight: bold;
  4743. color: #3e7bfa;
  4744. }
  4745. .options {
  4746. margin-left: 12px;
  4747. }
  4748. .option {
  4749. margin-bottom: 5px;
  4750. }
  4751. .answer {
  4752. margin-top: 10px;
  4753. padding: 8px;
  4754. background: #e8f4fd;
  4755. border-radius: 4px;
  4756. font-weight: bold;
  4757. color: #2c5aa0;
  4758. }
  4759. </style>
  4760. </head>
  4761. <body>
  4762. <div class="header">
  4763. <div class="exam-title">${examData.title || '考试试卷'}</div>
  4764. <div class="exam-info">
  4765. 总分:${examData.totalScore || 0}分 | 总题数:${examData.totalQuestions || 0}题 | 生成时间:${currentTime}
  4766. </div>
  4767. </div>`
  4768. // 单选题
  4769. if (examData.singleChoice && examData.singleChoice.questions.length > 0) {
  4770. htmlContent += `
  4771. <div class="section">
  4772. <div class="section-title">一、单选题(${examData.singleChoice.count}题,每题${examData.singleChoice.scorePerQuestion}分,共${examData.singleChoice.totalScore}分)</div>`
  4773. examData.singleChoice.questions.forEach((question, index) => {
  4774. htmlContent += `
  4775. <div class="question">
  4776. <div class="question-header">
  4777. <span class="question-number">${index + 1}.</span> ${question.text}
  4778. </div>
  4779. <div class="options">`
  4780. question.options.forEach(option => {
  4781. htmlContent += `
  4782. <div class="option">${option.key}. ${option.text}</div>`
  4783. })
  4784. htmlContent += `
  4785. </div>
  4786. <div class="answer">正确答案:${question.selectedAnswer}</div>
  4787. </div>`
  4788. })
  4789. htmlContent += `
  4790. </div>`
  4791. }
  4792. // 判断题
  4793. if (examData.judge && examData.judge.questions.length > 0) {
  4794. htmlContent += `
  4795. <div class="section">
  4796. <div class="section-title">二、判断题(${examData.judge.count}题,每题${examData.judge.scorePerQuestion}分,共${examData.judge.totalScore}分)</div>`
  4797. examData.judge.questions.forEach((question, index) => {
  4798. htmlContent += `
  4799. <div class="question">
  4800. <div class="question-header">
  4801. <span class="question-number">${index + 1}.</span> ${question.text}
  4802. </div>
  4803. <div class="answer">正确答案:${question.selectedAnswer}</div>
  4804. </div>`
  4805. })
  4806. htmlContent += `
  4807. </div>`
  4808. }
  4809. // 多选题
  4810. if (examData.multiple && examData.multiple.questions.length > 0) {
  4811. htmlContent += `
  4812. <div class="section">
  4813. <div class="section-title">三、多选题(${examData.multiple.count}题,每题${examData.multiple.scorePerQuestion}分,共${examData.multiple.totalScore}分)</div>`
  4814. examData.multiple.questions.forEach((question, index) => {
  4815. htmlContent += `
  4816. <div class="question">
  4817. <div class="question-header">
  4818. <span class="question-number">${index + 1}.</span> ${question.text}
  4819. </div>
  4820. <div class="options">`
  4821. question.options.forEach(option => {
  4822. htmlContent += `
  4823. <div class="option">${option.key}. ${option.text}</div>`
  4824. })
  4825. htmlContent += `
  4826. </div>
  4827. <div class="answer">正确答案:${question.selectedAnswers.join(', ')}</div>
  4828. </div>`
  4829. })
  4830. htmlContent += `
  4831. </div>`
  4832. }
  4833. // 简答题
  4834. if (examData.short && examData.short.questions.length > 0) {
  4835. htmlContent += `
  4836. <div class="section">
  4837. <div class="section-title">四、简答题(${examData.short.count}题,每题${examData.short.scorePerQuestion}分,共${examData.short.totalScore}分)</div>`
  4838. examData.short.questions.forEach((question, index) => {
  4839. htmlContent += `
  4840. <div class="question">
  4841. <div class="question-header">
  4842. <span class="question-number">${index + 1}.</span> ${question.text}
  4843. </div>
  4844. <div class="answer">答题要点:${question.outline.keyFactors}</div>
  4845. </div>`
  4846. })
  4847. htmlContent += `
  4848. </div>`
  4849. }
  4850. htmlContent += `
  4851. </body>
  4852. </html>`
  4853. return htmlContent
  4854. }
  4855. // 检查并跳转到大纲页面
  4856. const checkAndNavigateToOutline = (aiReply) => {
  4857. console.log('AI回复完成,直接解析并跳转到步骤二')
  4858. // 直接解析AI回复,提取大纲信息
  4859. const parsedOutline = parseOutlineFromAI(aiReply)
  4860. if (parsedOutline && parsedOutline.chapters && parsedOutline.chapters.length > 0) {
  4861. console.log('解析成功,更新大纲数据:', parsedOutline)
  4862. // 更新大纲数据
  4863. outlineData.value = parsedOutline.chapters
  4864. outlineStats.value = parsedOutline.stats
  4865. outlineTitle.value = parsedOutline.title
  4866. // 只有在没有现有反馈状态时才重置(新生成的大纲)
  4867. if (outlineFeedback.value === null && currentAiMessageId.value === null) {
  4868. console.log('新生成的大纲,重置反馈状态')
  4869. outlineFeedback.value = null
  4870. evaluation.value = ''
  4871. outlineId.value = null
  4872. // 立即跳转到步骤二
  4873. currentStep.value = 'step2'
  4874. console.log('已跳转到步骤二:培训大纲界面')
  4875. console.log('当前大纲数据:', outlineData.value)
  4876. console.log('当前统计信息:', outlineStats.value)
  4877. console.log('当前标题:', outlineTitle.value)
  4878. // 确保模板预览数据已加载
  4879. loadLocalPPT()
  4880. updateSlideThumbnails()
  4881. // 不需要自动保存,后台会自动保存AI回复内容
  4882. } else {
  4883. console.log('从历史记录加载的大纲,保持现有反馈状态:', outlineFeedback.value)
  4884. // 立即跳转到步骤二
  4885. currentStep.value = 'step2'
  4886. console.log('已跳转到步骤二:培训大纲界面')
  4887. console.log('当前大纲数据:', outlineData.value)
  4888. console.log('当前统计信息:', outlineStats.value)
  4889. console.log('当前标题:', outlineTitle.value)
  4890. // 确保模板预览数据已加载
  4891. loadLocalPPT()
  4892. updateSlideThumbnails()
  4893. // 从历史记录加载的大纲不需要保存
  4894. }
  4895. } else {
  4896. console.log('解析失败或没有章节数据')
  4897. }
  4898. }
  4899. // 从AI回复中解析大纲信息
  4900. const parseOutlineFromAI = (aiReply) => {
  4901. try {
  4902. console.log('开始解析AI回复中的大纲信息')
  4903. console.log('AI回复内容:', aiReply)
  4904. // 提取大纲标题 - 从AI回复的第一行或标题行提取
  4905. let title = '安全培训大纲' // 默认标题
  4906. // 尝试从第一行提取标题
  4907. const lines = aiReply.split('\n')
  4908. for (let line of lines) {
  4909. const trimmedLine = line.trim()
  4910. // 查找包含"以下是为您准备的PPT大纲"的行,下一行通常是标题
  4911. if (trimmedLine.includes('以下是为您准备的PPT大纲')) {
  4912. // 查找下一行作为标题
  4913. const nextLineIndex = lines.indexOf(line) + 1
  4914. if (nextLineIndex < lines.length) {
  4915. const nextLine = lines[nextLineIndex].trim()
  4916. if (nextLine && nextLine.length > 0 && !nextLine.includes('以下') && !nextLine.includes('大纲统计信息')) {
  4917. title = nextLine
  4918. break
  4919. }
  4920. }
  4921. }
  4922. // 或者直接查找以#开头的标题行
  4923. if (trimmedLine.startsWith('#') && trimmedLine.length > 1) {
  4924. title = trimmedLine.replace(/^#+\s*/, '').trim()
  4925. break
  4926. }
  4927. }
  4928. // 先解析章节内容,然后自动计算统计信息
  4929. // 不再从AI回复中提取统计信息,而是根据解析的章节内容自动计算
  4930. // 提取章节内容 - 使用基于井号数量的解析逻辑
  4931. const chapters = []
  4932. const contentLines = aiReply.split('\n')
  4933. let currentChapter = null
  4934. console.log('开始解析行数:', lines.length)
  4935. for (let i = 0; i < lines.length; i++) {
  4936. const line = lines[i]
  4937. const trimmedLine = line.trim()
  4938. console.log(`第${i}行: "${trimmedLine}"`)
  4939. // 根据井号数量来解析不同层级的标题
  4940. const hashCount = (trimmedLine.match(/^#+/) || [''])[0].length
  4941. if (hashCount === 1) {
  4942. // # 开头的大标题,作为整个大纲的标题
  4943. console.log('找到大标题:', trimmedLine)
  4944. let mainTitle = trimmedLine.replace(/^#\s*/, '').trim()
  4945. title = mainTitle
  4946. continue
  4947. } else if (hashCount === 2) {
  4948. // ## 开头的标题作为章节
  4949. console.log('找到章节:', trimmedLine)
  4950. let chapterTitle = trimmedLine.replace(/^##\s*/, '').trim()
  4951. currentChapter = {
  4952. title: chapterTitle,
  4953. sections: []
  4954. }
  4955. chapters.push(currentChapter)
  4956. continue
  4957. } else if (hashCount === 3) {
  4958. // ### 开头的标题作为小节
  4959. if (currentChapter) {
  4960. console.log('找到小节:', trimmedLine)
  4961. let sectionTitle = trimmedLine.replace(/^###\s*/, '').trim()
  4962. currentChapter.sections.push({
  4963. title: sectionTitle,
  4964. subsections: []
  4965. })
  4966. continue
  4967. }
  4968. } else if (hashCount === 4) {
  4969. // #### 开头的标题作为子标题
  4970. if (currentChapter && currentChapter.sections.length > 0) {
  4971. console.log('找到子标题:', trimmedLine)
  4972. let subsectionTitle = trimmedLine.replace(/^####\s*/, '').trim()
  4973. const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
  4974. lastSection.subsections.push({
  4975. title: subsectionTitle,
  4976. subsubsections: []
  4977. })
  4978. continue
  4979. }
  4980. } else if (trimmedLine.startsWith('-')) {
  4981. // - 开头的作为具体内容要点
  4982. if (currentChapter && currentChapter.sections.length > 0) {
  4983. console.log('找到具体内容要点:', trimmedLine)
  4984. let contentTitle = trimmedLine.replace(/^-\s*/, '').trim()
  4985. const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
  4986. if (lastSection.subsections.length > 0) {
  4987. const lastSubsection = lastSection.subsections[lastSection.subsections.length - 1]
  4988. if (!lastSubsection.subsubsections) {
  4989. lastSubsection.subsubsections = []
  4990. }
  4991. lastSubsection.subsubsections.push({
  4992. title: contentTitle,
  4993. content: ''
  4994. })
  4995. }
  4996. continue
  4997. }
  4998. } else if (hashCount === 0 && trimmedLine.match(/^\d+\.\d+\.\d+/)) {
  4999. // 没有井号但有三级数字格式的作为子小节
  5000. if (currentChapter && currentChapter.sections.length > 0) {
  5001. console.log('找到子小节(数字格式):', trimmedLine)
  5002. const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
  5003. lastSection.subsections.push({
  5004. title: trimmedLine
  5005. })
  5006. continue
  5007. }
  5008. } else if (hashCount === 0 && trimmedLine.match(/^\d+\.\d+/)) {
  5009. // 没有井号但有二级数字格式的作为小节
  5010. if (currentChapter) {
  5011. console.log('找到小节(数字格式):', trimmedLine)
  5012. currentChapter.sections.push({
  5013. title: trimmedLine,
  5014. subsections: []
  5015. })
  5016. continue
  5017. }
  5018. }
  5019. // 如果没有找到结构化内容,尝试将内容作为章节处理
  5020. if (!currentChapter && trimmedLine && trimmedLine.length > 5 &&
  5021. !trimmedLine.includes('以下') &&
  5022. !trimmedLine.includes('以上') &&
  5023. !trimmedLine.startsWith('#') &&
  5024. !trimmedLine.includes('大纲统计信息')) {
  5025. console.log('将内容作为章节处理:', trimmedLine)
  5026. currentChapter = {
  5027. title: trimmedLine,
  5028. sections: []
  5029. }
  5030. chapters.push(currentChapter)
  5031. }
  5032. // 如果当前行看起来像内容,但不是结构化的,添加到当前章节
  5033. if (currentChapter && trimmedLine && trimmedLine.length > 3 &&
  5034. !trimmedLine.match(/^第[一二三四五六七八九十]+章/) &&
  5035. !trimmedLine.match(/^第\d+章/) &&
  5036. !trimmedLine.match(/^\d+\.\d+/) &&
  5037. !trimmedLine.match(/^[一二三四五六七八九十]+\.\d+/) &&
  5038. !trimmedLine.match(/^\d+\.\d+\.\d+/) &&
  5039. !trimmedLine.match(/^[一二三四五六七八九十]+\.\d+\.\d+/) &&
  5040. !trimmedLine.includes('以下') &&
  5041. !trimmedLine.includes('以上') &&
  5042. !trimmedLine.includes('大纲统计信息') &&
  5043. !trimmedLine.includes('预计PPT页数') &&
  5044. !trimmedLine.includes('预计讲解时长') &&
  5045. !trimmedLine.includes('总章节数') &&
  5046. !trimmedLine.includes('总小节数') &&
  5047. !trimmedLine.startsWith('#')) {
  5048. // 如果当前章节没有小节,创建一个默认小节
  5049. if (currentChapter.sections.length === 0) {
  5050. currentChapter.sections.push({
  5051. title: '内容详情',
  5052. subsections: []
  5053. })
  5054. }
  5055. // 将内容添加到最后一个小节
  5056. const lastSection = currentChapter.sections[currentChapter.sections.length - 1]
  5057. if (!lastSection.subsections) {
  5058. lastSection.subsections = []
  5059. }
  5060. // 过滤掉一些无用的占位符文本和统计信息
  5061. if (trimmedLine &&
  5062. trimmedLine !== '内容要点' &&
  5063. trimmedLine !== '概述' &&
  5064. trimmedLine !== '内容详情' &&
  5065. !trimmedLine.includes('总章节数') &&
  5066. !trimmedLine.includes('总小节数') &&
  5067. !trimmedLine.includes('预计PPT页数') &&
  5068. !trimmedLine.includes('预计讲解时长') &&
  5069. !trimmedLine.startsWith('-') &&
  5070. trimmedLine.length > 2) {
  5071. // 检查是否应该作为具体内容要点的正文内容
  5072. const lastSubsection = lastSection.subsections[lastSection.subsections.length - 1]
  5073. if (lastSubsection && lastSubsection.subsubsections && lastSubsection.subsubsections.length > 0) {
  5074. // 如果最后一个子标题有具体内容要点,将内容添加到最后一个具体内容要点
  5075. const lastSubsubsection = lastSubsection.subsubsections[lastSubsection.subsubsections.length - 1]
  5076. if (lastSubsubsection && !lastSubsubsection.content) {
  5077. lastSubsubsection.content = trimmedLine
  5078. continue
  5079. }
  5080. }
  5081. // 否则作为普通子小节
  5082. lastSection.subsections.push({
  5083. title: trimmedLine,
  5084. subsubsections: []
  5085. })
  5086. }
  5087. }
  5088. }
  5089. // 确保所有数据结构完整
  5090. chapters.forEach(chapter => {
  5091. if (chapter && chapter.sections) {
  5092. chapter.sections.forEach(section => {
  5093. if (section && !section.subsections) {
  5094. section.subsections = []
  5095. }
  5096. // 确保每个子小节都有subsubsections数组
  5097. if (section.subsections) {
  5098. section.subsections.forEach(subsection => {
  5099. if (!subsection.subsubsections) {
  5100. subsection.subsubsections = []
  5101. }
  5102. })
  5103. }
  5104. })
  5105. }
  5106. })
  5107. // 根据解析的章节内容自动计算统计信息
  5108. const stats = calculateOutlineStats(chapters)
  5109. return {
  5110. title,
  5111. stats,
  5112. chapters
  5113. }
  5114. } catch (error) {
  5115. console.error('解析大纲信息失败:', error)
  5116. return null
  5117. }
  5118. }
  5119. // 测试大纲转换为AIPPT.json格式并应用模板5
  5120. const testOutlineToAIPPT = async () => {
  5121. try {
  5122. console.log('开始测试大纲转换为AIPPT.json格式并应用模板5')
  5123. if (outlineData.value && outlineData.value.length > 0) {
  5124. console.log('当前大纲数据:', outlineData.value)
  5125. console.log('当前标题:', outlineTitle.value)
  5126. // 显示loading效果
  5127. const loadingInstance = ElMessage({
  5128. message: '正在填充大纲内容,请稍候...',
  5129. type: 'info',
  5130. duration: 0,
  5131. showClose: false
  5132. })
  5133. try {
  5134. // 第一步:填充大纲内容
  5135. console.log('开始填充大纲内容...')
  5136. const enrichedOutlineData = await enrichOutlineContent(outlineData.value, outlineTitle.value)
  5137. // 检查是否所有内容都填充完成
  5138. let allContentFilled = true
  5139. let missingItems = []
  5140. for (let chapterIndex = 0; chapterIndex < enrichedOutlineData.length; chapterIndex++) {
  5141. const chapter = enrichedOutlineData[chapterIndex]
  5142. if (!chapter.content || chapter.content.trim() === '') {
  5143. allContentFilled = false
  5144. missingItems.push(`章节: ${chapter.title}`)
  5145. }
  5146. if (chapter.sections && chapter.sections.length > 0) {
  5147. for (let sectionIndex = 0; sectionIndex < chapter.sections.length; sectionIndex++) {
  5148. const section = chapter.sections[sectionIndex]
  5149. if (!section.content || section.content.trim() === '') {
  5150. allContentFilled = false
  5151. missingItems.push(`小节: ${section.title}`)
  5152. }
  5153. if (section.subsections && section.subsections.length > 0) {
  5154. for (let subsectionIndex = 0; subsectionIndex < section.subsections.length; subsectionIndex++) {
  5155. const subsection = section.subsections[subsectionIndex]
  5156. if (!subsection.content || subsection.content.trim() === '') {
  5157. allContentFilled = false
  5158. missingItems.push(`子小节: ${subsection.title}`)
  5159. }
  5160. }
  5161. }
  5162. }
  5163. }
  5164. }
  5165. if (!allContentFilled) {
  5166. loadingInstance.close()
  5167. console.warn('部分内容填充失败:', missingItems)
  5168. ElMessage.warning(`部分内容填充失败,请重试。失败项目: ${missingItems.slice(0, 3).join(', ')}${missingItems.length > 3 ? '...' : ''}`)
  5169. return
  5170. }
  5171. // 更新loading消息
  5172. loadingInstance.message = '正在转换大纲为PPT格式...'
  5173. // 第二步:转换大纲为AIPPT.json格式
  5174. const aipptData = convertOutlineToAIPPT(enrichedOutlineData, outlineTitle.value)
  5175. console.log('转换结果:')
  5176. console.log(JSON.stringify(aipptData, null, 2))
  5177. // 更新loading消息
  5178. loadingInstance.message = '正在应用模板5...'
  5179. // 第三步:应用模板5
  5180. await loadAIPPTData(aipptData)
  5181. // 确保数据被正确保存到本地存储
  5182. if (generatedPPT.value && generatedPPT.value.length > 0) {
  5183. localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
  5184. console.log('PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
  5185. }
  5186. // 大纲数据现在只存储在后端
  5187. // 关闭loading
  5188. loadingInstance.close()
  5189. // 显示成功消息
  5190. ElMessage.success('转换完成并已应用模板5,正在跳转到PPT预览...')
  5191. // 自动跳转到步骤三(PPT预览)
  5192. setTimeout(() => {
  5193. currentStep.value = 'step3'
  5194. }, 1000)
  5195. } catch (error) {
  5196. // 关闭loading
  5197. loadingInstance.close()
  5198. throw error
  5199. }
  5200. } else {
  5201. ElMessage.warning('没有大纲数据,无法进行转换')
  5202. }
  5203. } catch (error) {
  5204. console.error('测试转换失败:', error)
  5205. ElMessage.error('转换失败: ' + error.message)
  5206. }
  5207. }
  5208. // 监听语音识别结果
  5209. watch(transcript, (newVal) => {
  5210. if (!newVal || isListening.value) return
  5211. messageText.value = newVal
  5212. })
  5213. // 监听语音识别错误
  5214. watch(speechError, (newVal) => {
  5215. if (newVal) {
  5216. console.error('语音识别错误:', newVal)
  5217. ElMessage.error(newVal)
  5218. }
  5219. })
  5220. // 生命周期钩子
  5221. onMounted(async () => {
  5222. console.log('🚀 页面初始化开始,优先加载历史记录...')
  5223. // 检查URL中是否有id参数,如果有,将其设置为当前的 ai_conversation_id
  5224. if (route.query.id) {
  5225. const id = parseInt(route.query.id);
  5226. if (!isNaN(id) && id > 0) {
  5227. ai_conversation_id.value = id;
  5228. console.log('从URL获取到对话ID:', id);
  5229. // 等待历史记录加载完成后,自动选中并触发点击事件
  5230. const checkHistory = setInterval(() => {
  5231. if (historyData.value && historyData.value.length > 0) {
  5232. clearInterval(checkHistory);
  5233. const targetItem = historyData.value.find(item => item.id === id);
  5234. if (targetItem) {
  5235. handleHistoryItem(targetItem);
  5236. } else {
  5237. handleHistoryItem({ id }); // 降级处理
  5238. }
  5239. }
  5240. }, 500);
  5241. // 5秒后自动清除定时器,避免死循环
  5242. setTimeout(() => clearInterval(checkHistory), 5000);
  5243. }
  5244. }
  5245. // 设置初始加载状态
  5246. isLoadingHistory.value = true
  5247. try {
  5248. // 1. 首先获取历史记录列表(最高优先级)
  5249. await getHistoryRecordList()
  5250. console.log('✅ 历史记录加载完成')
  5251. // 2. 并行获取其他非关键数据(不阻塞历史记录显示)
  5252. // 注释掉功能卡片和热点问题获取,因为版本没有步骤三和四
  5253. const otherDataPromise = Promise.all([
  5254. getFunctionCards(),
  5255. getHotQuestions()
  5256. ])
  5257. // 3. 初始化大纲统计信息
  5258. // if (outlineData.value && outlineData.value.length > 0) {
  5259. // updateOutlineStats()
  5260. // }
  5261. // 4. 初始化模板选择 - 注释掉,因为版本没有步骤三
  5262. // if (templateStyles.value.length > 0) {
  5263. // selectTemplate(0) // 选择第一个模板
  5264. // console.log('初始化模板选择完成,当前模板:', templateStyles.value[0].title)
  5265. // }
  5266. // 5. 等待其他数据加载完成(后台进行) - 注释掉
  5267. // try {
  5268. // await otherDataPromise
  5269. // console.log('✅ 其他数据加载完成')
  5270. // } catch (error) {
  5271. // console.warn('⚠️ 其他数据加载失败,但不影响主要功能:', error)
  5272. // }
  5273. console.log('🎉 页面初始化完成')
  5274. } catch (error) {
  5275. console.error('❌ 页面初始化失败:', error)
  5276. } finally {
  5277. // 清除加载状态
  5278. isLoadingHistory.value = false
  5279. }
  5280. })
  5281. // 点击推荐问题
  5282. const handleRecommendedQuestion = (question) => {
  5283. messageText.value = question
  5284. console.log('选择推荐问题:', question)
  5285. // 显示聊天界面
  5286. showChat.value = true
  5287. // 自动发送问题
  5288. sendMessage()
  5289. }
  5290. // 获取评价状态(优先使用后端数据,如果没有则使用本地状态)
  5291. const getEvaluationStatus = () => {
  5292. console.log('getEvaluationStatus - outlineFeedback:', outlineFeedback.value, 'evaluation:', evaluation.value)
  5293. if (outlineFeedback.value !== null) {
  5294. // 根据后端数据判断状态
  5295. switch (outlineFeedback.value) {
  5296. case 2: return 'satisfied' // 满意
  5297. case 3: return 'unsatisfied' // 不满意
  5298. case 0: return '' // 无反馈(取消评价)
  5299. default: return '' // 无反馈
  5300. }
  5301. }
  5302. return evaluation.value // 如果没有后端数据,使用本地状态
  5303. }
  5304. // 设置大纲评价
  5305. const setEvaluation = async (value) => {
  5306. try {
  5307. console.log('设置评价:', value)
  5308. // 检查当前状态,如果点击的是当前已激活的状态,则取消评价
  5309. const currentStatus = getEvaluationStatus()
  5310. let feedbackValue
  5311. if (currentStatus === value) {
  5312. // 如果点击的是当前已激活的状态,取消评价
  5313. feedbackValue = 0
  5314. console.log('取消评价,发送0')
  5315. } else {
  5316. // 否则设置新的评价
  5317. feedbackValue = value === 'satisfied' ? 2 : 3
  5318. console.log('设置新评价:', feedbackValue)
  5319. }
  5320. console.log('currentAiMessageId.value', currentAiMessageId.value)
  5321. // 调用后端API保存评价
  5322. const response = await apis.likeAndDislike({
  5323. id: currentAiMessageId.value, // 优先使用大纲ID,如果没有则使用AI消息ID
  5324. user_feedback: feedbackValue
  5325. })
  5326. if (response.statusCode === 200) {
  5327. console.log('评价保存成功')
  5328. // 更新本地状态
  5329. if (feedbackValue === 0) {
  5330. // 取消评价
  5331. evaluation.value = ''
  5332. outlineFeedback.value = 0
  5333. ElMessage.success('评价已取消')
  5334. } else {
  5335. // 设置新评价
  5336. evaluation.value = value
  5337. outlineFeedback.value = feedbackValue
  5338. ElMessage.success('评价已保存')
  5339. }
  5340. } else {
  5341. console.error('评价保存失败:', response)
  5342. ElMessage.error('评价保存失败,请重试')
  5343. }
  5344. } catch (error) {
  5345. console.error('设置评价失败:', error)
  5346. ElMessage.error('评价设置失败,请重试')
  5347. }
  5348. }
  5349. // 大纲编辑相关方法
  5350. const startEditing = (item, type, index, content) => {
  5351. editingItem.value = item
  5352. editingType.value = type
  5353. editingIndex.value = index
  5354. // 提取纯文字内容,去掉编号部分
  5355. let pureContent = content
  5356. if (type === 'chapter') {
  5357. // 章节:去掉"第一章"、"第二章"等前缀
  5358. pureContent = content.replace(/^第[一二三四五六七八九十\d]+章\s*/, '')
  5359. } else if (type === 'section') {
  5360. // 小节:去掉"1.1"、"2.1"等前缀
  5361. pureContent = content.replace(/^\d+\.\d+\s*/, '')
  5362. } else if (type === 'subsection') {
  5363. // 四级标题:去掉"#### "前缀
  5364. pureContent = content.replace(/^####\s*/, '')
  5365. } else if (type === 'subsubsection') {
  5366. // 具体内容要点:去掉"- "前缀
  5367. pureContent = content.replace(/^-\s*/, '')
  5368. }
  5369. editingContent.value = pureContent
  5370. console.log('开始编辑:', { type, index, content: pureContent })
  5371. }
  5372. const saveEdit = async () => {
  5373. if (!editingContent.value.trim()) {
  5374. console.log('编辑内容为空,取消保存')
  5375. cancelEdit()
  5376. return
  5377. }
  5378. try {
  5379. // 根据编辑类型更新对应的数据
  5380. if (editingType.value === 'title') {
  5381. outlineTitle.value = editingContent.value.trim()
  5382. } else if (editingType.value === 'chapter') {
  5383. const chapterIndex = editingIndex.value
  5384. // 保持原有的章节编号格式,不重新生成
  5385. const originalTitle = outlineData.value[chapterIndex].title
  5386. const match = originalTitle.match(/^(第[一二三四五六七八九十\d]+章)\s*/)
  5387. if (match) {
  5388. // 保持原有的编号格式
  5389. const chapterNumber = match[1]
  5390. const chapterTitle = `${chapterNumber} ${editingContent.value.trim()}`
  5391. outlineData.value[chapterIndex].title = chapterTitle
  5392. } else {
  5393. // 如果没有匹配到编号格式,使用中文数字格式
  5394. const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
  5395. const chapterNumber = chapterIndex < chineseNumbers.length ? chineseNumbers[chapterIndex] : (chapterIndex + 1).toString()
  5396. const chapterTitle = `第${chapterNumber}章 ${editingContent.value.trim()}`
  5397. outlineData.value[chapterIndex].title = chapterTitle
  5398. }
  5399. } else if (editingType.value === 'section') {
  5400. const [chapterIndex, sectionIndex] = editingIndex.value.split('-')
  5401. // 直接保存用户编辑的内容,不添加数字前缀
  5402. outlineData.value[chapterIndex].sections[sectionIndex].title = editingContent.value.trim()
  5403. } else if (editingType.value === 'subsection') {
  5404. const [chapterIndex, sectionIndex, subsectionIndex] = editingIndex.value.split('-')
  5405. // 直接保存用户编辑的内容,不添加数字前缀
  5406. outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].title = editingContent.value.trim()
  5407. } else if (editingType.value === 'subsubsection') {
  5408. const [chapterIndex, sectionIndex, subsectionIndex, subsubsectionIndex] = editingIndex.value.split('-')
  5409. // 直接保存用户编辑的内容,不添加数字前缀
  5410. outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections[subsubsectionIndex].title = editingContent.value.trim()
  5411. } else if (editingType.value === 'subsubsection-content') {
  5412. const [chapterIndex, sectionIndex, subsectionIndex, subsubsectionIndex] = editingIndex.value.split('-')
  5413. // 保存具体内容要点(-开头)下的正文内容
  5414. outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections[subsubsectionIndex].content = editingContent.value.trim()
  5415. }
  5416. console.log('保存成功:', editingContent.value.trim())
  5417. // 更新大纲统计信息
  5418. updateOutlineStats()
  5419. // 自动保存到后端
  5420. await saveOutlineToBackend(true) // 强制保存编辑后的内容
  5421. // 清除编辑状态
  5422. cancelEdit()
  5423. // 显示成功提示
  5424. ElMessage.success('保存成功!')
  5425. } catch (error) {
  5426. console.error('保存失败:', error)
  5427. ElMessage.error('保存失败,请重试')
  5428. }
  5429. }
  5430. const cancelEdit = () => {
  5431. editingItem.value = null
  5432. editingType.value = ''
  5433. editingIndex.value = null
  5434. editingContent.value = ''
  5435. console.log('取消编辑')
  5436. }
  5437. const toggleEditOptions = (itemId) => {
  5438. showEditOptions.value = showEditOptions.value === itemId ? null : itemId
  5439. }
  5440. const deleteItem = async (type, index) => {
  5441. try {
  5442. if (type === 'chapter') {
  5443. // 限制章节数量不能少于2章
  5444. if (outlineData.value.length <= 2) {
  5445. ElMessage.warning('至少需要保留2个章节')
  5446. return
  5447. }
  5448. outlineData.value.splice(index, 1)
  5449. } else if (type === 'section') {
  5450. const [chapterIndex, sectionIndex] = index.split('-')
  5451. // 限制每个章节至少保留1个小节
  5452. if (outlineData.value[chapterIndex].sections.length <= 1) {
  5453. ElMessage.warning('每个章节至少需要保留1个小节')
  5454. return
  5455. }
  5456. outlineData.value[chapterIndex].sections.splice(sectionIndex, 1)
  5457. } else if (type === 'subsection') {
  5458. const [chapterIndex, sectionIndex, subsectionIndex] = index.split('-')
  5459. outlineData.value[chapterIndex].sections[sectionIndex].subsections.splice(subsectionIndex, 1)
  5460. } else if (type === 'subsubsection') {
  5461. const [chapterIndex, sectionIndex, subsectionIndex, subsubsectionIndex] = index.split('-')
  5462. outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections.splice(subsubsectionIndex, 1)
  5463. }
  5464. // 更新统计信息
  5465. updateOutlineStats()
  5466. // 自动保存到后端
  5467. await saveOutlineToBackend(true) // 强制保存删除后的内容
  5468. ElMessage.success('删除成功!')
  5469. } catch (error) {
  5470. console.error('删除失败:', error)
  5471. ElMessage.error('删除失败,请重试')
  5472. }
  5473. }
  5474. // 智能编号生成函数
  5475. const generateNextNumber = (type, parentIndex) => {
  5476. try {
  5477. if (type === 'chapter') {
  5478. // 生成下一个章节编号
  5479. const existingChapters = outlineData.value || []
  5480. let maxChapterNum = 0
  5481. // 查找现有的最大章节编号
  5482. const chineseNumbers = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
  5483. existingChapters.forEach(chapter => {
  5484. const match = chapter.title.match(/^第([一二三四五六七八九十\d]+)章/)
  5485. if (match) {
  5486. let num = 0
  5487. const matchedText = match[1]
  5488. // 如果是阿拉伯数字
  5489. if (/^\d+$/.test(matchedText)) {
  5490. num = parseInt(matchedText)
  5491. } else {
  5492. // 如果是中文数字,转换为阿拉伯数字
  5493. const index = chineseNumbers.indexOf(matchedText)
  5494. if (index > 0) {
  5495. num = index
  5496. }
  5497. }
  5498. maxChapterNum = Math.max(maxChapterNum, num)
  5499. }
  5500. })
  5501. const nextChapterNum = maxChapterNum + 1
  5502. // 最多6章,直接使用中文数字
  5503. return `第${chineseNumbers[nextChapterNum]}章`
  5504. } else if (type === 'section') {
  5505. // 生成下一个小节编号
  5506. const chapterIndex = parentIndex
  5507. const existingSections = outlineData.value[chapterIndex]?.sections || []
  5508. let maxSectionNum = 0
  5509. // 查找现有的最大小节编号
  5510. existingSections.forEach(section => {
  5511. const match = section.title.match(/^\d+\.(\d+)/)
  5512. if (match) {
  5513. const num = parseInt(match[1]) || 0
  5514. maxSectionNum = Math.max(maxSectionNum, num)
  5515. }
  5516. })
  5517. const nextSectionNum = maxSectionNum + 1
  5518. return `${chapterIndex + 1}.${nextSectionNum}`
  5519. } else if (type === 'subsection') {
  5520. // 生成下一个子小节编号
  5521. const [chapterIndex, sectionIndex] = parentIndex.split('-')
  5522. const existingSubsections = outlineData.value[chapterIndex]?.sections[sectionIndex]?.subsections || []
  5523. let maxSubsectionNum = 0
  5524. // 查找现有的最大子小节编号
  5525. existingSubsections.forEach(subsection => {
  5526. const match = subsection.title.match(/^\d+\.\d+\.(\d+)/)
  5527. if (match) {
  5528. const num = parseInt(match[1]) || 0
  5529. maxSubsectionNum = Math.max(maxSubsectionNum, num)
  5530. }
  5531. })
  5532. const nextSubsectionNum = maxSubsectionNum + 1
  5533. return `${parseInt(chapterIndex) + 1}.${parseInt(sectionIndex) + 1}.${nextSubsectionNum}`
  5534. } else if (type === 'subsubsection') {
  5535. // 生成下一个具体内容要点(-开头)编号
  5536. const [chapterIndex, sectionIndex, subsectionIndex] = parentIndex.split('-')
  5537. const existingSubsubsections = outlineData.value[chapterIndex]?.sections[sectionIndex]?.subsections[subsectionIndex]?.subsubsections || []
  5538. let maxSubsubsectionNum = 0
  5539. // 查找现有的最大具体内容要点(-开头)编号
  5540. existingSubsubsections.forEach(subsubsection => {
  5541. const match = subsubsection.title.match(/^\d+\.\d+\.\d+\.(\d+)/)
  5542. if (match) {
  5543. const num = parseInt(match[1]) || 0
  5544. maxSubsubsectionNum = Math.max(maxSubsubsectionNum, num)
  5545. }
  5546. })
  5547. const nextSubsubsectionNum = maxSubsubsectionNum + 1
  5548. return `${parseInt(chapterIndex) + 1}.${parseInt(sectionIndex) + 1}.${parseInt(subsectionIndex) + 1}.${nextSubsubsectionNum}`
  5549. }
  5550. return ''
  5551. } catch (error) {
  5552. console.error('生成编号失败:', error)
  5553. return ''
  5554. }
  5555. }
  5556. // 拖拽相关函数
  5557. const handleDragStart = (event, chapterIndex) => {
  5558. // 如果事件来自编辑框内部,不启动拖拽
  5559. if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT' ||
  5560. event.target.closest('.edit-input-container') || event.target.closest('.edit-input-wrapper')) {
  5561. event.preventDefault()
  5562. return
  5563. }
  5564. // 如果正在编辑状态,不启动拖拽
  5565. if (editingType.value !== '') {
  5566. event.preventDefault()
  5567. return
  5568. }
  5569. draggedChapterIndex.value = chapterIndex
  5570. event.dataTransfer.effectAllowed = 'move'
  5571. event.dataTransfer.setData('text/html', event.target.outerHTML)
  5572. event.target.style.opacity = '0.5'
  5573. }
  5574. const handleDragEnd = (event) => {
  5575. event.target.style.opacity = '1'
  5576. draggedChapterIndex.value = null
  5577. dragOverChapterIndex.value = null
  5578. }
  5579. const handleDragOver = (event, chapterIndex) => {
  5580. event.preventDefault()
  5581. event.dataTransfer.dropEffect = 'move'
  5582. dragOverChapterIndex.value = chapterIndex
  5583. }
  5584. const handleDragLeave = (event) => {
  5585. // 只有当鼠标真正离开元素时才清除悬停状态
  5586. if (!event.currentTarget.contains(event.relatedTarget)) {
  5587. dragOverChapterIndex.value = null
  5588. }
  5589. }
  5590. const handleDrop = async (event, targetChapterIndex) => {
  5591. event.preventDefault()
  5592. const sourceIndex = draggedChapterIndex.value
  5593. const targetIndex = targetChapterIndex
  5594. if (sourceIndex === null || sourceIndex === targetIndex) {
  5595. return
  5596. }
  5597. try {
  5598. // 移动章节
  5599. const chapters = [...outlineData.value]
  5600. const [movedChapter] = chapters.splice(sourceIndex, 1)
  5601. chapters.splice(targetIndex, 0, movedChapter)
  5602. // 重新生成所有章节的编号
  5603. const chineseNumbers = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
  5604. chapters.forEach((chapter, index) => {
  5605. // 提取章节标题中的内容部分(去掉"第X章"前缀)
  5606. const contentMatch = chapter.title.match(/^第[一二三四五六七八九十\d]+章\s*(.+)$/)
  5607. if (contentMatch) {
  5608. const content = contentMatch[1]
  5609. chapter.title = `第${chineseNumbers[index + 1]}章 ${content}`
  5610. } else {
  5611. // 如果没有匹配到标准格式,直接使用新编号
  5612. chapter.title = `第${chineseNumbers[index + 1]}章 ${chapter.title}`
  5613. }
  5614. // 保持小节和子小节无序号格式
  5615. if (chapter.sections && chapter.sections.length > 0) {
  5616. chapter.sections.forEach((section, sectionIndex) => {
  5617. // 移除小节标题中的序号,只保留内容
  5618. const sectionContentMatch = section.title.match(/^\d+\.\d+\s*(.+)$/)
  5619. if (sectionContentMatch) {
  5620. section.title = sectionContentMatch[1]
  5621. }
  5622. // 如果已经是无序号格式,保持不变
  5623. // 保持子小节无序号格式
  5624. if (section.subsections && section.subsections.length > 0) {
  5625. section.subsections.forEach((subsection, subsectionIndex) => {
  5626. // 移除子小节标题中的序号,只保留内容
  5627. const subsectionContentMatch = subsection.title.match(/^\d+\.\d+\.\d+\s*(.+)$/)
  5628. if (subsectionContentMatch) {
  5629. subsection.title = subsectionContentMatch[1]
  5630. }
  5631. // 如果已经是无序号格式,保持不变
  5632. })
  5633. }
  5634. })
  5635. }
  5636. })
  5637. // 更新数据
  5638. outlineData.value = chapters
  5639. // 更新统计信息
  5640. updateOutlineStats()
  5641. // 保存到后端
  5642. await saveOutlineToBackend(true) // 强制保存调整后的内容
  5643. ElMessage.success('章节顺序已调整,编号已重新排序')
  5644. } catch (error) {
  5645. console.error('调整章节顺序失败:', error)
  5646. ElMessage.error('调整章节顺序失败,请重试')
  5647. }
  5648. // 清除拖拽状态
  5649. draggedChapterIndex.value = null
  5650. dragOverChapterIndex.value = null
  5651. }
  5652. const addNewItem = async (type, parentIndex) => {
  5653. try {
  5654. if (type === 'chapter') {
  5655. // 限制章节数量不能超过6章
  5656. if (outlineData.value.length >= 6) {
  5657. ElMessage.warning('最多只能添加6个章节')
  5658. return
  5659. }
  5660. const nextNumber = generateNextNumber('chapter', null)
  5661. const newChapter = {
  5662. title: `${nextNumber} 新章节`,
  5663. sections: [{
  5664. title: `新小节`,
  5665. subsections: []
  5666. }]
  5667. }
  5668. outlineData.value.push(newChapter)
  5669. // 自动开始编辑新章节
  5670. startEditing(newChapter, 'chapter', outlineData.value.length - 1, `${nextNumber} 新章节`)
  5671. } else if (type === 'section') {
  5672. const nextNumber = generateNextNumber('section', parentIndex)
  5673. const newSection = {
  5674. title: `${nextNumber} 新小节`,
  5675. subsections: []
  5676. }
  5677. outlineData.value[parentIndex].sections.push(newSection)
  5678. // 自动开始编辑新小节
  5679. startEditing(newSection, 'section', `${parentIndex}-${outlineData.value[parentIndex].sections.length - 1}`, `新小节`)
  5680. } else if (type === 'subsection') {
  5681. const [chapterIndex, sectionIndex] = parentIndex.split('-')
  5682. const nextNumber = generateNextNumber('subsection', parentIndex)
  5683. const newSubsection = {
  5684. title: `新子标题`,
  5685. subsubsections: []
  5686. }
  5687. outlineData.value[chapterIndex].sections[sectionIndex].subsections.push(newSubsection)
  5688. // 自动开始编辑新子标题
  5689. startEditing(newSubsection, 'subsection', `${chapterIndex}-${sectionIndex}-${outlineData.value[chapterIndex].sections[sectionIndex].subsections.length - 1}`, `新子标题`)
  5690. } else if (type === 'subsubsection') {
  5691. const [chapterIndex, sectionIndex, subsectionIndex] = parentIndex.split('-')
  5692. const nextNumber = generateNextNumber('subsubsection', parentIndex)
  5693. const newSubsubsection = {
  5694. title: `${nextNumber} 新具体内容要点`,
  5695. content: ''
  5696. }
  5697. // 确保subsection有subsubsections数组
  5698. if (!outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections) {
  5699. outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections = []
  5700. }
  5701. outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections.push(newSubsubsection)
  5702. // 自动开始编辑新具体内容要点(-开头)
  5703. startEditing(newSubsubsection, 'subsubsection', `${chapterIndex}-${sectionIndex}-${subsectionIndex}-${outlineData.value[chapterIndex].sections[sectionIndex].subsections[subsectionIndex].subsubsections.length - 1}`, `${nextNumber} 新具体内容要点`)
  5704. }
  5705. // 更新统计信息
  5706. updateOutlineStats()
  5707. // 自动保存到后端
  5708. await saveOutlineToBackend(true) // 强制保存添加后的内容
  5709. } catch (error) {
  5710. console.error('添加失败:', error)
  5711. ElMessage.error('添加失败,请重试')
  5712. }
  5713. }
  5714. // 填充大纲内容 - 整体处理方案
  5715. const enrichOutlineContent = async (outlineData, outlineTitle) => {
  5716. console.log('开始填充大纲内容...')
  5717. // 深拷贝大纲数据,避免修改原始数据
  5718. const enrichedData = JSON.parse(JSON.stringify(outlineData))
  5719. // 最大重试次数
  5720. const maxRetries = 3
  5721. let retryCount = 0
  5722. while (retryCount < maxRetries) {
  5723. try {
  5724. console.log(`第 ${retryCount + 1} 次尝试填充内容...`)
  5725. // 构建整体提示词
  5726. const overallPrompt = `请为以下大纲数据填充内容,要求:
  5727. 1. title字段:15字以内
  5728. 2. text字段:20-50字以内
  5729. 3. 内容要专业、实用、简洁
  5730. 4. 保持JSON格式不变,填充空的内容
  5731. 5. 请自由发挥,生成丰富多样的标题和内容
  5732. 6. 重要:请确保所有空的内容都被填充,不要遗漏任何项目
  5733. 7. 如果数据量较大,请耐心处理,确保完整性
  5734. 大纲主题:${outlineTitle}
  5735. 大纲数据:${JSON.stringify(enrichedData, null, 2)}
  5736. 请直接返回完整的JSON数据,不要添加任何说明文字。确保所有content字段都有内容。`
  5737. console.log('发送整体填充请求...')
  5738. const response = await apis.reProduceSingleQuestion({ message: overallPrompt })
  5739. console.log('API响应:', response)
  5740. // 解析AI返回的JSON数据
  5741. let aiResponse = null
  5742. if (response && response.data) {
  5743. if (response.data && typeof response.data === 'object' && response.data.reply) {
  5744. aiResponse = response.data.reply
  5745. } else if (response.data && typeof response.data === 'string') {
  5746. aiResponse = response.data
  5747. } else {
  5748. aiResponse = JSON.stringify(response.data)
  5749. }
  5750. } else if (response && response.message) {
  5751. aiResponse = response.message
  5752. } else if (response && response.content) {
  5753. aiResponse = response.content
  5754. } else if (response && response.reply) {
  5755. aiResponse = response.reply
  5756. } else if (response && typeof response === 'string') {
  5757. aiResponse = response
  5758. } else if (response && typeof response === 'object') {
  5759. aiResponse = JSON.stringify(response)
  5760. }
  5761. if (aiResponse && typeof aiResponse === 'string' && aiResponse.trim() !== '') {
  5762. try {
  5763. // 尝试解析JSON
  5764. const parsedData = JSON.parse(aiResponse.trim())
  5765. console.log('AI返回的JSON数据解析成功:', parsedData)
  5766. // 验证数据结构
  5767. if (Array.isArray(parsedData)) {
  5768. // 验证内容完整性
  5769. let totalItems = 0
  5770. let filledItems = 0
  5771. parsedData.forEach(chapter => {
  5772. if (chapter.content && chapter.content.trim() !== '') {
  5773. filledItems++
  5774. }
  5775. totalItems++
  5776. if (chapter.sections && chapter.sections.length > 0) {
  5777. chapter.sections.forEach(section => {
  5778. if (section.content && section.content.trim() !== '') {
  5779. filledItems++
  5780. }
  5781. totalItems++
  5782. if (section.subsections && section.subsections.length > 0) {
  5783. section.subsections.forEach(subsection => {
  5784. if (subsection.content && subsection.content.trim() !== '') {
  5785. filledItems++
  5786. }
  5787. totalItems++
  5788. })
  5789. }
  5790. })
  5791. }
  5792. })
  5793. console.log(`内容填充统计: ${filledItems}/${totalItems} 项已填充`)
  5794. if (filledItems < totalItems * 0.8) {
  5795. console.warn('内容填充不完整,尝试重新填充...')
  5796. retryCount++
  5797. if (retryCount >= maxRetries) {
  5798. throw new Error('多次尝试后内容填充仍不完整')
  5799. }
  5800. continue
  5801. }
  5802. console.log('大纲内容填充完成')
  5803. return parsedData
  5804. } else {
  5805. throw new Error('AI返回的数据不是数组格式')
  5806. }
  5807. } catch (parseError) {
  5808. console.error('解析AI返回的JSON失败:', parseError)
  5809. console.log('AI返回的原始内容:', aiResponse)
  5810. retryCount++
  5811. if (retryCount >= maxRetries) {
  5812. throw new Error('AI返回的数据格式不正确')
  5813. }
  5814. continue
  5815. }
  5816. } else {
  5817. retryCount++
  5818. if (retryCount >= maxRetries) {
  5819. throw new Error('AI返回内容为空或格式不正确')
  5820. }
  5821. continue
  5822. }
  5823. } catch (error) {
  5824. retryCount++
  5825. if (retryCount >= maxRetries) {
  5826. throw error
  5827. }
  5828. console.warn(`第 ${retryCount} 次尝试失败,准备重试...`)
  5829. // 等待一段时间后重试
  5830. await new Promise(resolve => setTimeout(resolve, 2000))
  5831. }
  5832. }
  5833. }
  5834. // 直接填充AIPPT格式的内容
  5835. const fillAIPPTContent = async (aipptData, outlineTitle) => {
  5836. console.log('开始填充AIPPT内容...')
  5837. try {
  5838. // 构建提示词
  5839. const prompt = `请为以下PPT数据填充内容,要求:
  5840. 1. title字段:15字以内
  5841. 2. text字段:20-50字以内
  5842. 3. 内容要专业、实用、简洁
  5843. 4. 保持JSON格式不变,只填充空的内容
  5844. 5. 请自由发挥,生成丰富多样的标题和内容
  5845. 6. 重要:必须保持原有的幻灯片数量和结构,不能删除或合并任何幻灯片
  5846. 7. 对于每个content类型的幻灯片,确保items数组有4个元素
  5847. PPT主题:${outlineTitle}
  5848. PPT数据:${JSON.stringify(aipptData, null, 2)}
  5849. 请直接返回完整的JSON数据,不要添加任何说明文字。确保所有空的内容都被填充,并且保持原有的幻灯片数量。`
  5850. console.log('发送AIPPT填充请求...')
  5851. const response = await apis.reProduceSingleQuestion({ message: prompt })
  5852. console.log('API响应:', response)
  5853. // 解析AI返回的JSON数据
  5854. let aiResponse = null
  5855. if (response && response.data) {
  5856. if (response.data && typeof response.data === 'object' && response.data.reply) {
  5857. aiResponse = response.data.reply
  5858. } else if (response.data && typeof response.data === 'string') {
  5859. aiResponse = response.data
  5860. } else {
  5861. aiResponse = JSON.stringify(response.data)
  5862. }
  5863. } else if (response && response.message) {
  5864. aiResponse = response.message
  5865. } else if (response && response.content) {
  5866. aiResponse = response.content
  5867. } else if (response && response.reply) {
  5868. aiResponse = response.reply
  5869. } else if (response && typeof response === 'string') {
  5870. aiResponse = response
  5871. } else if (response && typeof response === 'object') {
  5872. aiResponse = JSON.stringify(response)
  5873. }
  5874. if (aiResponse && typeof aiResponse === 'string' && aiResponse.trim() !== '') {
  5875. try {
  5876. // 尝试解析JSON
  5877. const parsedData = JSON.parse(aiResponse.trim())
  5878. console.log('AI返回的AIPPT数据解析成功:', parsedData)
  5879. // 验证数据结构
  5880. if (Array.isArray(parsedData)) {
  5881. console.log('AIPPT内容填充完成,原始数量:', aipptData.length, '填充后数量:', parsedData.length)
  5882. // 检查数量是否一致
  5883. if (parsedData.length !== aipptData.length) {
  5884. console.warn('警告:AI返回的幻灯片数量与原始数量不一致!')
  5885. console.warn('原始数量:', aipptData.length, 'AI返回数量:', parsedData.length)
  5886. }
  5887. return parsedData
  5888. } else {
  5889. throw new Error('AI返回的数据不是数组格式')
  5890. }
  5891. } catch (parseError) {
  5892. console.error('解析AI返回的JSON失败:', parseError)
  5893. console.log('AI返回的原始内容:', aiResponse)
  5894. throw new Error('AI返回的数据格式不正确')
  5895. }
  5896. } else {
  5897. throw new Error('AI返回内容为空或格式不正确')
  5898. }
  5899. } catch (error) {
  5900. console.error('填充AIPPT内容失败:', error)
  5901. throw new Error('填充AIPPT内容失败: ' + error.message)
  5902. }
  5903. }
  5904. // 转换字符串数组为大纲对象格式
  5905. const convertStringArrayToOutline = (stringArray) => {
  5906. const outline = []
  5907. let currentChapter = null
  5908. let currentSection = null
  5909. stringArray.forEach((item, index) => {
  5910. if (item.startsWith('章节:')) {
  5911. // 创建新章节
  5912. currentChapter = {
  5913. title: item.replace('章节: ', ''),
  5914. content: '',
  5915. sections: []
  5916. }
  5917. outline.push(currentChapter)
  5918. currentSection = null
  5919. } else if (item.startsWith('小节:')) {
  5920. // 创建新小节
  5921. if (currentChapter) {
  5922. currentSection = {
  5923. title: item.replace('小节: ', ''),
  5924. content: '',
  5925. subsections: []
  5926. }
  5927. currentChapter.sections.push(currentSection)
  5928. }
  5929. } else if (item.startsWith('子小节:')) {
  5930. // 创建新子小节
  5931. if (currentSection) {
  5932. const subsection = {
  5933. title: item.replace('子小节: ', ''),
  5934. content: ''
  5935. }
  5936. currentSection.subsections.push(subsection)
  5937. }
  5938. }
  5939. })
  5940. // 确保每个小节至少有4个子小节
  5941. outline.forEach(chapter => {
  5942. chapter.sections.forEach(section => {
  5943. while (section.subsections.length < 4) {
  5944. // 让AI自由发挥,不预设标题模式
  5945. const additionalSubsections = [
  5946. {
  5947. title: '',
  5948. content: ''
  5949. },
  5950. {
  5951. title: '',
  5952. content: ''
  5953. },
  5954. {
  5955. title: '',
  5956. content: ''
  5957. },
  5958. {
  5959. title: '',
  5960. content: ''
  5961. }
  5962. ]
  5963. const index = section.subsections.length
  5964. if (index < additionalSubsections.length) {
  5965. section.subsections.push(additionalSubsections[index])
  5966. }
  5967. }
  5968. })
  5969. })
  5970. return outline
  5971. }
  5972. // 检查大纲数据是否有变化
  5973. const hasOutlineDataChanged = () => {
  5974. if (!lastSavedOutlineData.value) return true
  5975. const currentData = {
  5976. title: outlineTitle.value,
  5977. stats: outlineStats.value,
  5978. chapters: outlineData.value
  5979. }
  5980. return JSON.stringify(currentData) !== JSON.stringify(lastSavedOutlineData.value)
  5981. }
  5982. // 检查PPT数据是否有变化
  5983. const hasPPTDataChanged = () => {
  5984. if (!lastSavedPPTData.value) return true
  5985. return JSON.stringify(generatedPPT.value) !== JSON.stringify(lastSavedPPTData.value)
  5986. }
  5987. // 获取章节的显示标题(加上第X章前缀)
  5988. const getDisplayChapterTitle = (chapterTitle, chapterIndex) => {
  5989. // 如果标题已经包含"第X章",直接返回
  5990. if (chapterTitle.match(/^第[一二三四五六七八九十\d]+章/)) {
  5991. return chapterTitle
  5992. }
  5993. // 否则加上"第X章"前缀,使用中文数字
  5994. const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
  5995. const chapterNumber = chapterIndex < chineseNumbers.length ? chineseNumbers[chapterIndex] : (chapterIndex + 1).toString()
  5996. return `第${chapterNumber}章 ${chapterTitle}`
  5997. }
  5998. // 获取章节的干净标题(去掉第X章前缀,用于编辑)
  5999. const getCleanChapterTitle = (chapterTitle) => {
  6000. // 去掉章节标题中的"第X章"前缀
  6001. return chapterTitle.replace(/^第[一二三四五六七八九十\d]+章\s*/, '')
  6002. }
  6003. // 将大纲数据转换为markdown格式
  6004. const convertOutlineToMarkdown = (chapters, title) => {
  6005. let markdown = `# ${title}\n\n`
  6006. chapters.forEach((chapter, index) => {
  6007. // 直接使用原始标题,不要修改
  6008. markdown += `## ${chapter.title}\n\n`
  6009. if (chapter.sections && chapter.sections.length > 0) {
  6010. chapter.sections.forEach((section, sectionIndex) => {
  6011. markdown += `### ${section.title}\n\n`
  6012. // 处理子小节
  6013. if (section.subsections && section.subsections.length > 0) {
  6014. section.subsections.forEach((subsection, subsectionIndex) => {
  6015. // 四级标题添加 #### 前缀
  6016. markdown += `#### ${subsection.title}\n\n`
  6017. // 处理具体内容要点(-开头)
  6018. if (subsection.subsubsections && subsection.subsubsections.length > 0) {
  6019. subsection.subsubsections.forEach((subsubsection, subsubsectionIndex) => {
  6020. // 具体内容要点添加 - 前缀
  6021. markdown += `- ${subsubsection.title}\n\n`
  6022. // 添加具体内容要点(-开头)下的正文内容
  6023. if (subsubsection.content && subsubsection.content.trim()) {
  6024. markdown += `${subsubsection.content}\n\n`
  6025. }
  6026. })
  6027. }
  6028. })
  6029. }
  6030. })
  6031. }
  6032. markdown += '\n'
  6033. })
  6034. return markdown
  6035. }
  6036. // 直接保存大纲到后端(不依赖全局状态,用于生成大纲时)
  6037. const saveOutlineToBackendDirectly = async (conversationId, chapters, title, stats) => {
  6038. try {
  6039. if (isSaving.value) {
  6040. console.log('正在保存中,跳过重复保存请求')
  6041. return false
  6042. }
  6043. if (!conversationId || !chapters) {
  6044. console.log('缺少conversationId或大纲数据,跳过后端保存')
  6045. return false
  6046. }
  6047. isSaving.value = true
  6048. // 将大纲数据转换为markdown格式
  6049. const markdownContent = convertOutlineToMarkdown(chapters, title)
  6050. console.log('直接保存大纲到后端:', {
  6051. ai_conversation_id: conversationId,
  6052. markdownContent: markdownContent
  6053. })
  6054. const response = await apis.savePPTOutline({
  6055. ai_conversation_id: conversationId,
  6056. ppt_content: markdownContent
  6057. })
  6058. if (response.statusCode === 200) {
  6059. console.log('大纲已直接保存到后端服务器,conversation_id:', conversationId)
  6060. return true
  6061. } else {
  6062. console.error('直接保存到后端服务器失败:', response)
  6063. return false
  6064. }
  6065. } catch (error) {
  6066. console.error('直接保存大纲失败:', error)
  6067. return false
  6068. } finally {
  6069. isSaving.value = false
  6070. }
  6071. }
  6072. // 保存大纲到后端(用户编辑时才调用)
  6073. const saveOutlineToBackend = async (forceSave = false) => {
  6074. try {
  6075. if (isSaving.value) {
  6076. console.log('正在保存中,跳过重复保存请求')
  6077. return false
  6078. }
  6079. if (!ai_conversation_id.value || !outlineData.value) {
  6080. console.log('缺少ai_conversation_id或大纲数据,跳过后端保存')
  6081. return false
  6082. }
  6083. // 检查数据是否有变化
  6084. if (!forceSave && !hasOutlineDataChanged()) {
  6085. console.log('大纲数据未发生变化,跳过保存')
  6086. return false
  6087. }
  6088. isSaving.value = true
  6089. // 将大纲数据转换为markdown格式
  6090. const markdownContent = convertOutlineToMarkdown(outlineData.value, outlineTitle.value)
  6091. console.log('准备保存大纲到后端:', {
  6092. ai_conversation_id: ai_conversation_id.value,
  6093. markdownContent: markdownContent
  6094. })
  6095. const response = await apis.savePPTOutline({
  6096. ai_conversation_id: ai_conversation_id.value,
  6097. ppt_content: markdownContent
  6098. })
  6099. if (response.statusCode === 200) {
  6100. console.log('大纲已保存到后端服务器,conversation_id:', ai_conversation_id.value)
  6101. // 更新上次保存的数据
  6102. lastSavedOutlineData.value = {
  6103. title: outlineTitle.value,
  6104. stats: outlineStats.value,
  6105. chapters: JSON.parse(JSON.stringify(outlineData.value)) // 深拷贝
  6106. }
  6107. return true
  6108. } else {
  6109. console.error('保存到后端服务器失败:', response)
  6110. return false
  6111. }
  6112. } catch (error) {
  6113. console.error('保存大纲失败:', error)
  6114. return false
  6115. } finally {
  6116. isSaving.value = false
  6117. }
  6118. }
  6119. // 步骤三相关方法
  6120. const goToStep3 = () => {
  6121. currentStep.value = 'step3'
  6122. console.log('进入步骤三:PPT模板选择')
  6123. // 确保处于模板预览模式
  6124. showDownloadOptions.value = false
  6125. selectedDownloadOption.value = 0
  6126. // 清理PPT预览相关状态(保留generatedPPT数据)
  6127. currentPPTSlideIndex.value = 0
  6128. selectedPPTElementIndex.value = -1
  6129. editingPPTElementIndex.value = -1
  6130. editingPPTHtml.value = ''
  6131. zoom.value = 1
  6132. selectedImageIndex.value = null
  6133. // 加载模板预览数据
  6134. loadLocalPPT()
  6135. updateSlideThumbnails()
  6136. // 重置当前幻灯片索引
  6137. currentSlideIndex.value = 0
  6138. console.log('模板预览已加载,共', slideImages.value.length, '张图片')
  6139. }
  6140. // 显示复制大纲提示
  6141. const showCopyOutlineToast = () => {
  6142. showCopyToast.value = true
  6143. setTimeout(() => {
  6144. showCopyToast.value = false
  6145. }, 1000)
  6146. }
  6147. // WPS AI PPT集成方法
  6148. const openWPS = () => {
  6149. try {
  6150. // 检查是否有大纲数据
  6151. if (!outlineData.value || outlineData.value.length === 0) {
  6152. ElMessage.warning('请先生成大纲后再使用WPS AI PPT')
  6153. return
  6154. }
  6155. // 默认调用复制大纲功能
  6156. copyEntireOutline()
  6157. // 显示复制大纲提示
  6158. showCopyOutlineToast()
  6159. // 重置到WPS AI PPT页面
  6160. selectedWPSUrl.value = 'https://aippt.wps.cn/aippt/'
  6161. // 显示WPS AI PPT集成页面
  6162. showWPSModal.value = true
  6163. ElMessage.success('正在加载WPS AI PPT...')
  6164. console.log('WPS AI PPT集成页面已打开')
  6165. } catch (error) {
  6166. console.error('WPS AI PPT加载失败:', error)
  6167. ElMessage.error('WPS AI PPT加载失败,请稍后重试')
  6168. }
  6169. }
  6170. // 将大纲转换为文本格式
  6171. const convertOutlineToText = () => {
  6172. if (!outlineData.value || outlineData.value.length === 0) {
  6173. return '暂无大纲内容'
  6174. }
  6175. let text = ''
  6176. outlineData.value.forEach((chapter, chapterIndex) => {
  6177. if (chapter && chapter.title) {
  6178. text += `${chapterIndex + 1}. ${chapter.title}\n`
  6179. if (chapter.sections && chapter.sections.length > 0) {
  6180. chapter.sections.forEach((section, sectionIndex) => {
  6181. if (section && section.title &&
  6182. section.title !== '内容要点' &&
  6183. section.title !== '概述') {
  6184. text += ` ${chapterIndex + 1}.${sectionIndex + 1} ${section.title}\n`
  6185. if (section.subsections && section.subsections.length > 0) {
  6186. section.subsections.forEach((subsection, subsectionIndex) => {
  6187. if (subsection && subsection.title &&
  6188. subsection.title !== '内容要点' &&
  6189. subsection.title !== '概述' &&
  6190. subsection.title !== '内容详情') {
  6191. text += ` ${chapterIndex + 1}.${sectionIndex + 1}.${subsectionIndex + 1} ${subsection.title}\n`
  6192. }
  6193. })
  6194. }
  6195. }
  6196. })
  6197. }
  6198. text += '\n'
  6199. }
  6200. })
  6201. return text
  6202. }
  6203. // 复制大纲到剪贴板
  6204. const copyOutlineToClipboard = () => {
  6205. const outlineText = convertOutlineToText()
  6206. navigator.clipboard.writeText(outlineText).then(() => {
  6207. ElMessage.success('大纲已复制到剪贴板!')
  6208. }).catch(() => {
  6209. // 降级方案
  6210. const textArea = document.createElement('textarea')
  6211. textArea.value = outlineText
  6212. document.body.appendChild(textArea)
  6213. textArea.select()
  6214. document.execCommand('copy')
  6215. document.body.removeChild(textArea)
  6216. ElMessage.success('大纲已复制到剪贴板!')
  6217. })
  6218. }
  6219. // 更新iframe源地址
  6220. const updateIframeSrc = () => {
  6221. console.log('切换到WPS页面:', selectedWPSUrl.value)
  6222. ElMessage.info(`正在切换到${selectedWPSUrl.value}`)
  6223. }
  6224. // 刷新iframe
  6225. const refreshIframe = () => {
  6226. console.log('刷新WPS页面')
  6227. ElMessage.info('正在刷新页面...')
  6228. // 通过改变src来刷新iframe
  6229. const currentSrc = selectedWPSUrl.value
  6230. selectedWPSUrl.value = ''
  6231. setTimeout(() => {
  6232. selectedWPSUrl.value = currentSrc
  6233. }, 100)
  6234. }
  6235. // 处理iframe错误
  6236. const handleIframeError = () => {
  6237. console.log('iframe加载失败')
  6238. ElMessage.warning('页面加载失败,请尝试其他选项或检查网络连接')
  6239. }
  6240. // ==================== 下载监听插件功能 ====================
  6241. // 切换下载监听状态
  6242. const toggleDownloadListener = () => {
  6243. if (isDownloadListenerActive.value) {
  6244. startDownloadListener()
  6245. ElMessage.success('下载监听插件已启用')
  6246. } else {
  6247. stopDownloadListener()
  6248. ElMessage.info('下载监听插件已关闭')
  6249. }
  6250. }
  6251. // 启动下载监听
  6252. const startDownloadListener = () => {
  6253. console.log('启动下载监听插件...')
  6254. // 方法1: 监听浏览器的下载事件
  6255. setupBrowserDownloadListener()
  6256. // 方法2: 监听iframe的postMessage事件
  6257. setupIframeMessageListener()
  6258. // 方法3: 定期检查下载文件夹变化(如果可能)
  6259. setupDownloadFolderMonitor()
  6260. // 方法4: 监听网络请求中的下载链接
  6261. setupNetworkRequestListener()
  6262. }
  6263. // 停止下载监听
  6264. const stopDownloadListener = () => {
  6265. console.log('停止下载监听插件...')
  6266. // 移除所有事件监听器
  6267. window.removeEventListener('beforeunload', handleBeforeUnload)
  6268. window.removeEventListener('message', handleIframeMessage)
  6269. // 清除定时器
  6270. if (downloadMonitorTimer.value) {
  6271. clearInterval(downloadMonitorTimer.value)
  6272. downloadMonitorTimer.value = null
  6273. }
  6274. }
  6275. // 方法1: 监听浏览器下载事件
  6276. const setupBrowserDownloadListener = () => {
  6277. // 监听页面卸载前的下载事件
  6278. window.addEventListener('beforeunload', handleBeforeUnload)
  6279. // 监听点击事件,检测可能的下载链接
  6280. document.addEventListener('click', handleClickEvent, true)
  6281. }
  6282. // 方法2: 监听iframe的postMessage事件
  6283. const setupIframeMessageListener = () => {
  6284. window.addEventListener('message', handleIframeMessage)
  6285. }
  6286. // 方法3: 定期检查下载状态
  6287. const downloadMonitorTimer = ref(null)
  6288. const setupDownloadFolderMonitor = () => {
  6289. // 由于浏览器安全限制,无法直接访问下载文件夹
  6290. // 这里使用模拟的方式,定期检查是否有新的下载
  6291. // downloadMonitorTimer.value = setInterval(() => {
  6292. // checkForNewDownloads()
  6293. // }, 2000) // 每2秒检查一次
  6294. }
  6295. // 方法4: 监听网络请求
  6296. const setupNetworkRequestListener = () => {
  6297. // 拦截fetch请求
  6298. const originalFetch = window.fetch
  6299. window.fetch = async (...args) => {
  6300. const response = await originalFetch(...args)
  6301. // 检查响应头中是否包含下载相关的信息
  6302. const contentType = response.headers.get('content-type')
  6303. const contentDisposition = response.headers.get('content-disposition')
  6304. if (contentType && (
  6305. contentType.includes('application/vnd.openxmlformats-officedocument.presentationml.presentation') ||
  6306. contentType.includes('application/vnd.ms-powerpoint') ||
  6307. contentType.includes('application/pdf')
  6308. )) {
  6309. console.log('检测到可能的PPT下载请求:', args[0])
  6310. handleDetectedDownload(args[0], contentType, contentDisposition)
  6311. }
  6312. return response
  6313. }
  6314. }
  6315. // 处理页面卸载前的下载检测
  6316. const handleBeforeUnload = (event) => {
  6317. console.log('页面即将卸载,检查是否有下载活动')
  6318. // 这里可以记录一些状态信息
  6319. }
  6320. // 处理iframe消息
  6321. const handleIframeMessage = (event) => {
  6322. console.log('收到iframe消息 - 来源:', event.origin, '数据:', event.data)
  6323. // 检查消息来源 - 放宽限制,支持多个WPS域名
  6324. const allowedOrigins = [
  6325. 'https://aippt.wps.cn',
  6326. 'https://web.wps.cn',
  6327. 'https://www.kdocs.cn',
  6328. 'https://docs.wps.cn'
  6329. ]
  6330. if (!allowedOrigins.includes(event.origin)) {
  6331. console.log('消息来源不在允许列表中:', event.origin)
  6332. return
  6333. }
  6334. console.log('消息来源验证通过,处理消息:', event.data)
  6335. console.log('消息类型:', JSON.parse(event.data))
  6336. // 处理WPS AI PPT创建完成的消息
  6337. let messageData = JSON.parse(event.data)
  6338. if (messageData.type === 'AIPPT_BEFORE_CREATEPPT' && messageData.data && messageData.data.fileInfo) {
  6339. const fileInfo = messageData.data.fileInfo
  6340. console.log('文件信息:', fileInfo)
  6341. currentPPTInfo.value = fileInfo
  6342. showOpenPPTButton.value = true
  6343. // 延迟3秒显示第二个遮罩
  6344. setTimeout(() => {
  6345. showSecondMask.value = true
  6346. console.log('延迟3秒后显示第二个遮罩')
  6347. }, 3000)
  6348. }
  6349. }
  6350. // 处理点击事件,检测下载链接
  6351. const handleClickEvent = (event) => {
  6352. const target = event.target
  6353. const link = target.closest('a')
  6354. if (link && link.href) {
  6355. const href = link.href.toLowerCase()
  6356. if (href.includes('.pptx') || href.includes('.ppt') || href.includes('download')) {
  6357. console.log('检测到可能的下载链接:', link.href)
  6358. handleDetectedDownload(link.href, 'application/vnd.openxmlformats-officedocument.presentationml.presentation')
  6359. }
  6360. }
  6361. }
  6362. // 检查新下载
  6363. const checkForNewDownloads = () => {
  6364. // 由于浏览器安全限制,这里使用模拟的方式
  6365. // 在实际应用中,可能需要通过其他方式来实现
  6366. console.log('检查新下载...')
  6367. }
  6368. // 处理检测到的下载
  6369. const handleDetectedDownload = (url, contentType, filename) => {
  6370. const now = new Date()
  6371. const downloadInfo = {
  6372. id: Date.now(),
  6373. url: url,
  6374. contentType: contentType,
  6375. filename: filename || `PPT_${now.getTime()}.pptx`,
  6376. timestamp: now.toLocaleString(),
  6377. source: 'WPS AI PPT'
  6378. }
  6379. console.log('检测到下载:', downloadInfo)
  6380. // 添加到下载历史
  6381. downloadHistory.value.unshift(downloadInfo)
  6382. // 限制历史记录数量
  6383. if (downloadHistory.value.length > 10) {
  6384. downloadHistory.value = downloadHistory.value.slice(0, 10)
  6385. }
  6386. // 更新最后下载时间
  6387. lastDownloadTime.value = now
  6388. // 显示通知
  6389. ElMessage.success(`检测到PPT下载: ${downloadInfo.filename}`)
  6390. // 自动下载到本地
  6391. autoDownloadToLocal(downloadInfo)
  6392. }
  6393. // 自动下载到本地
  6394. const autoDownloadToLocal = async (downloadInfo) => {
  6395. try {
  6396. console.log('开始自动下载到本地:', downloadInfo)
  6397. // 创建下载链接
  6398. const link = document.createElement('a')
  6399. link.href = downloadInfo.url
  6400. link.download = downloadInfo.filename
  6401. link.style.display = 'none'
  6402. // 添加到页面并触发下载
  6403. document.body.appendChild(link)
  6404. link.click()
  6405. document.body.removeChild(link)
  6406. ElMessage.success(`已自动下载: ${downloadInfo.filename}`)
  6407. } catch (error) {
  6408. console.error('自动下载失败:', error)
  6409. ElMessage.error('自动下载失败,请手动下载')
  6410. }
  6411. }
  6412. // 下载WPS文件
  6413. const downloadWPSFile = async (fileInfo) => {
  6414. try {
  6415. console.log('开始下载WPS文件:', fileInfo)
  6416. // 构建下载URL - 使用link_url作为下载地址
  6417. const downloadUrl = fileInfo.link_url
  6418. const fileName = fileInfo.file_name || `WPS_PPT_${Date.now()}.pptx`
  6419. // 创建下载信息记录
  6420. const downloadInfo = {
  6421. id: Date.now(),
  6422. url: downloadUrl,
  6423. contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  6424. filename: fileName,
  6425. timestamp: new Date().toLocaleString(),
  6426. source: 'WPS AI PPT',
  6427. fileId: fileInfo.id,
  6428. linkId: fileInfo.link_id
  6429. }
  6430. // 添加到下载历史
  6431. downloadHistory.value.unshift(downloadInfo)
  6432. // 限制历史记录数量
  6433. if (downloadHistory.value.length > 10) {
  6434. downloadHistory.value = downloadHistory.value.slice(0, 10)
  6435. }
  6436. // 更新最后下载时间
  6437. lastDownloadTime.value = new Date()
  6438. // 显示通知
  6439. ElMessage.success(`检测到WPS PPT创建完成,正在下载: ${fileName}`)
  6440. // 尝试多种下载方式
  6441. await tryMultipleDownloadMethods(downloadUrl, fileName, downloadInfo)
  6442. } catch (error) {
  6443. console.error('WPS文件下载失败:', error)
  6444. ElMessage.error('WPS文件下载失败,请手动下载')
  6445. }
  6446. }
  6447. // 尝试多种下载方式
  6448. const tryMultipleDownloadMethods = async (downloadUrl, fileName, downloadInfo) => {
  6449. const methods = [
  6450. () => downloadViaDirectLink(downloadUrl, fileName),
  6451. () => downloadViaProxy(downloadUrl, fileName),
  6452. () => downloadViaNewWindow(downloadUrl, fileName),
  6453. () => downloadViaIframe(downloadUrl, fileName)
  6454. ]
  6455. for (let i = 0; i < methods.length; i++) {
  6456. try {
  6457. console.log(`尝试下载方式 ${i + 1}:`, methods[i].name)
  6458. await methods[i]()
  6459. ElMessage.success(`已自动下载WPS文件: ${fileName}`)
  6460. console.log('WPS文件下载完成:', downloadInfo)
  6461. return // 成功则退出
  6462. } catch (error) {
  6463. console.warn(`下载方式 ${i + 1} 失败:`, error)
  6464. if (i === methods.length - 1) {
  6465. throw error // 所有方式都失败
  6466. }
  6467. }
  6468. }
  6469. }
  6470. // 方式1: 直接链接下载
  6471. const downloadViaDirectLink = (downloadUrl, fileName) => {
  6472. return new Promise((resolve, reject) => {
  6473. const link = document.createElement('a')
  6474. link.href = downloadUrl
  6475. link.download = fileName
  6476. link.style.display = 'none'
  6477. link.onload = () => resolve()
  6478. link.onerror = () => reject(new Error('直接链接下载失败'))
  6479. document.body.appendChild(link)
  6480. link.click()
  6481. document.body.removeChild(link)
  6482. // 给一点时间让下载开始
  6483. setTimeout(resolve, 1000)
  6484. })
  6485. }
  6486. // 方式2: 通过代理下载
  6487. const downloadViaProxy = async (downloadUrl, fileName) => {
  6488. try {
  6489. // 尝试通过后端代理下载
  6490. const response = await fetch('/api/proxy-download', {
  6491. method: 'POST',
  6492. headers: {
  6493. 'Content-Type': 'application/json',
  6494. },
  6495. body: JSON.stringify({
  6496. url: downloadUrl,
  6497. filename: fileName
  6498. })
  6499. })
  6500. if (!response.ok) {
  6501. throw new Error('代理下载失败')
  6502. }
  6503. const blob = await response.blob()
  6504. const url = URL.createObjectURL(blob)
  6505. const link = document.createElement('a')
  6506. link.href = url
  6507. link.download = fileName
  6508. link.style.display = 'none'
  6509. document.body.appendChild(link)
  6510. link.click()
  6511. document.body.removeChild(link)
  6512. URL.revokeObjectURL(url)
  6513. } catch (error) {
  6514. throw new Error('代理下载失败: ' + error.message)
  6515. }
  6516. }
  6517. // 方式3: 新窗口下载
  6518. const downloadViaNewWindow = (downloadUrl, fileName) => {
  6519. return new Promise((resolve, reject) => {
  6520. const newWindow = window.open(downloadUrl, '_blank')
  6521. if (!newWindow) {
  6522. reject(new Error('无法打开新窗口'))
  6523. return
  6524. }
  6525. // 监听窗口关闭
  6526. const checkClosed = setInterval(() => {
  6527. if (newWindow.closed) {
  6528. clearInterval(checkClosed)
  6529. resolve()
  6530. }
  6531. }, 1000)
  6532. // 5秒后超时
  6533. setTimeout(() => {
  6534. clearInterval(checkClosed)
  6535. resolve()
  6536. }, 5000)
  6537. })
  6538. }
  6539. // 方式4: iframe下载
  6540. const downloadViaIframe = (downloadUrl, fileName) => {
  6541. return new Promise((resolve, reject) => {
  6542. const iframe = document.createElement('iframe')
  6543. iframe.style.display = 'none'
  6544. iframe.src = downloadUrl
  6545. iframe.onload = () => {
  6546. setTimeout(() => {
  6547. document.body.removeChild(iframe)
  6548. resolve()
  6549. }, 2000)
  6550. }
  6551. iframe.onerror = () => {
  6552. document.body.removeChild(iframe)
  6553. reject(new Error('iframe下载失败'))
  6554. }
  6555. document.body.appendChild(iframe)
  6556. })
  6557. }
  6558. // ==================== PPT查看器功能 ====================
  6559. // 打开PPT查看器
  6560. const openPPTViewer = () => {
  6561. if (!currentPPTInfo.value) {
  6562. ElMessage.error('没有可查看的PPT信息')
  6563. return
  6564. }
  6565. try {
  6566. console.log('打开PPT查看器:', currentPPTInfo.value)
  6567. // 构建PPT查看器URL
  6568. // 使用WPS的在线查看器
  6569. const viewerUrl = `https://www.kdocs.cn/l/${currentPPTInfo.value.linkId}`
  6570. // 直接在当前WPS弹窗中切换iframe内容
  6571. selectedWPSUrl.value = viewerUrl
  6572. ElMessage.success(`正在打开PPT: ${currentPPTInfo.value.file_name}`)
  6573. } catch (error) {
  6574. console.error('打开PPT查看器失败:', error)
  6575. ElMessage.error('打开PPT查看器失败,请重试')
  6576. }
  6577. }
  6578. // 下载当前PPT
  6579. const downloadCurrentPPT = () => {
  6580. if (!currentPPTInfo.value) {
  6581. ElMessage.error('没有可下载的PPT信息')
  6582. return
  6583. }
  6584. try {
  6585. console.log('下载当前PPT:', currentPPTInfo.value)
  6586. // 创建下载信息
  6587. const downloadInfo = {
  6588. id: Date.now(),
  6589. url: currentPPTInfo.value.link_url,
  6590. contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  6591. filename: currentPPTInfo.value.file_name,
  6592. timestamp: new Date().toLocaleString(),
  6593. source: 'PPT查看器',
  6594. fileId: currentPPTInfo.value.fileId,
  6595. linkId: currentPPTInfo.value.linkId
  6596. }
  6597. // 添加到下载历史
  6598. downloadHistory.value.unshift(downloadInfo)
  6599. // 限制历史记录数量
  6600. if (downloadHistory.value.length > 10) {
  6601. downloadHistory.value = downloadHistory.value.slice(0, 10)
  6602. }
  6603. // 尝试下载
  6604. downloadViaDirectLink(currentPPTInfo.value.link_url, currentPPTInfo.value.file_name)
  6605. .then(() => {
  6606. ElMessage.success(`已下载PPT: ${currentPPTInfo.value.file_name}`)
  6607. })
  6608. .catch((error) => {
  6609. console.error('下载失败:', error)
  6610. ElMessage.error('下载失败,请手动下载')
  6611. })
  6612. } catch (error) {
  6613. console.error('下载当前PPT失败:', error)
  6614. ElMessage.error('下载失败,请重试')
  6615. }
  6616. }
  6617. // 处理PPT iframe错误
  6618. const handlePPTIframeError = () => {
  6619. console.log('PPT iframe加载失败')
  6620. ElMessage.warning('PPT加载失败,请检查网络连接或尝试刷新')
  6621. }
  6622. // 测试PPT检测功能
  6623. const testPPTDetection = () => {
  6624. console.log('开始测试PPT检测功能...')
  6625. // 模拟您提供的消息数据
  6626. const testMessage = {
  6627. type: "AIPPT_BEFORE_CREATEPPT",
  6628. data: {
  6629. infoId: "0d51bbc5f7cde18652340593cccc1305",
  6630. fileInfo: {
  6631. id: 450721083297,
  6632. link_id: "ckNZFi6F3Gtv",
  6633. link_url: "https://www.kdocs.cn/l/ckNZFi6F3Gtv",
  6634. cache: 1,
  6635. branch_id: 0,
  6636. file_name: "课堂互动小游戏设计PPT.pptx"
  6637. }
  6638. }
  6639. }
  6640. console.log('模拟消息数据:', testMessage)
  6641. // 直接调用消息处理函数
  6642. const mockEvent = {
  6643. origin: 'https://aippt.wps.cn',
  6644. data: testMessage
  6645. }
  6646. handleIframeMessage(mockEvent)
  6647. ElMessage.success('测试消息已发送,请检查是否出现"打开PPT"按钮')
  6648. }
  6649. // 返回WPS AI PPT
  6650. const backToWPSAI = () => {
  6651. try {
  6652. console.log('返回WPS AI PPT')
  6653. // 切换回WPS AI PPT页面
  6654. selectedWPSUrl.value = 'https://aippt.wps.cn/aippt/'
  6655. ElMessage.success('已返回WPS AI PPT页面')
  6656. } catch (error) {
  6657. console.error('返回WPS AI PPT失败:', error)
  6658. ElMessage.error('返回失败,请重试')
  6659. }
  6660. }
  6661. // 打开测试PPT
  6662. const openTestPPT = () => {
  6663. try {
  6664. console.log('打开测试PPT')
  6665. // 直接打开WPS弹窗并显示测试PPT
  6666. showWPSModal.value = true
  6667. selectedWPSUrl.value = currentPPTInfo.value.link_url
  6668. ElMessage.success('正在打开测试PPT...')
  6669. } catch (error) {
  6670. console.error('打开测试PPT失败:', error)
  6671. ElMessage.error('打开测试PPT失败,请重试')
  6672. }
  6673. }
  6674. const goToStep2 = () => {
  6675. console.log('返回步骤二:编辑大纲')
  6676. // 检查大纲数据是否已加载,如果没有则重新加载
  6677. if (!outlineData.value || outlineData.value.length === 0) {
  6678. console.log('大纲数据未加载,重新加载历史记录数据')
  6679. // 获取当前历史记录
  6680. const currentHistoryItem = historyData.value.find(item => item.id === ai_conversation_id.value)
  6681. if (currentHistoryItem) {
  6682. // 重新加载大纲数据
  6683. if (currentHistoryItem.rawData && currentHistoryItem.rawData.ppt_outline && currentHistoryItem.rawData.ppt_outline.trim()) {
  6684. try {
  6685. const savedOutlineData = JSON.parse(currentHistoryItem.rawData.ppt_outline)
  6686. outlineData.value = savedOutlineData.chapters
  6687. outlineTitle.value = savedOutlineData.title || '安全培训大纲'
  6688. outlineStats.value = calculateOutlineStats(savedOutlineData.chapters)
  6689. outlineId.value = currentHistoryItem.id
  6690. // 更新保存状态
  6691. lastSavedOutlineData.value = {
  6692. title: outlineTitle.value,
  6693. stats: outlineStats.value,
  6694. chapters: JSON.parse(JSON.stringify(savedOutlineData.chapters))
  6695. }
  6696. console.log('重新加载大纲数据成功:', outlineTitle.value)
  6697. } catch (error) {
  6698. console.error('重新加载大纲数据失败:', error)
  6699. }
  6700. } else {
  6701. console.log('当前历史记录没有大纲数据')
  6702. }
  6703. }
  6704. } else {
  6705. console.log('大纲数据已存在,直接跳转')
  6706. }
  6707. currentStep.value = 'step2'
  6708. }
  6709. const prevSlide = () => {
  6710. if (currentSlideIndex.value > 0) {
  6711. currentSlideIndex.value--
  6712. } else {
  6713. currentSlideIndex.value = slideImages.value.length - 1
  6714. }
  6715. }
  6716. const nextSlide = () => {
  6717. if (currentSlideIndex.value < slideImages.value.length - 1) {
  6718. currentSlideIndex.value++
  6719. } else {
  6720. currentSlideIndex.value = 0
  6721. }
  6722. }
  6723. const goToSlide = (index) => {
  6724. currentSlideIndex.value = index
  6725. // 如果在编辑模式下,更新当前编辑的幻灯片
  6726. if (showDownloadOptions.value && pptSlides.value[index]) {
  6727. currentEditingSlide.value = { ...pptSlides.value[index] }
  6728. }
  6729. }
  6730. const goToPPTSlide = (index) => {
  6731. currentPPTSlideIndex.value = index
  6732. console.log('切换到PPT幻灯片:', index)
  6733. // 确保缩略图条滚动到正确位置
  6734. nextTick(() => {
  6735. const thumbnailStripEl = thumbnailStrip.value
  6736. if (thumbnailStripEl) {
  6737. // 计算需要滚动的位置,确保当前缩略图可见
  6738. const thumbnailWidth = 135 + 11 // 缩略图宽度 + 间距
  6739. const scrollPosition = index * thumbnailWidth - thumbnailStripEl.clientWidth / 2 + thumbnailWidth / 2
  6740. thumbnailStripEl.scrollLeft = Math.max(0, scrollPosition)
  6741. console.log(`缩略图条已滚动到第${index + 1}页,位置:${scrollPosition}`)
  6742. }
  6743. })
  6744. }
  6745. // 缩略图滚轮滑动处理
  6746. const handleThumbnailWheel = (event) => {
  6747. event.preventDefault() // 阻止默认滚动行为
  6748. const thumbnailStrip = event.currentTarget
  6749. const scrollAmount = event.deltaY > 0 ? 200 : -200 // 向下滚动时向右,向上滚动时向左
  6750. thumbnailStrip.scrollLeft += scrollAmount
  6751. }
  6752. // PPT编辑相关方法
  6753. const initializePPTEditor = () => {
  6754. // 使用你的本地1.pptx文件内容(2页)
  6755. pptSlides.value = [
  6756. {
  6757. title: '第1页标题',
  6758. content: '第1页内容',
  6759. type: 'cover'
  6760. },
  6761. {
  6762. title: '第2页标题',
  6763. content: '第2页内容',
  6764. type: 'content'
  6765. }
  6766. ]
  6767. // 设置当前编辑的幻灯片
  6768. if (pptSlides.value.length > 0) {
  6769. currentSlideIndex.value = 0
  6770. currentEditingSlide.value = { ...pptSlides.value[0] }
  6771. }
  6772. // 更新缩略图
  6773. updateSlideThumbnails()
  6774. console.log('PPT编辑器初始化完成,页数:', pptSlides.value.length)
  6775. }
  6776. const loadLocalPPT = () => {
  6777. console.log('开始加载本地PPT数据')
  6778. // 优先检查是否有生成的PPT数据
  6779. if (generatedPPT.value && generatedPPT.value.length > 0) {
  6780. console.log('使用已生成的PPT数据:', generatedPPT.value.length, '张幻灯片')
  6781. // 将generatedPPT数据转换为pptSlides格式
  6782. pptSlides.value = generatedPPT.value.map((slide, index) => ({
  6783. title: slide.data?.title || `第${index + 1}页`,
  6784. content: slide.data?.text || slide.data?.items?.map(item => item.title).join('\n') || '内容',
  6785. type: slide.type
  6786. }))
  6787. // 更新缩略图
  6788. updateSlideThumbnails()
  6789. // 重置当前幻灯片索引
  6790. currentSlideIndex.value = 0
  6791. currentEditingSlide.value = { ...pptSlides.value[0] }
  6792. ElMessage.success('生成的PPT数据加载成功')
  6793. console.log('加载的PPT页数:', pptSlides.value.length)
  6794. return
  6795. }
  6796. // 如果没有生成的PPT数据,加载默认的template5内容
  6797. console.log('加载默认template5内容')
  6798. const localPPTData = [
  6799. {
  6800. title: '第1页标题',
  6801. content: '第1页内容',
  6802. type: 'cover'
  6803. },
  6804. {
  6805. title: '第2页标题',
  6806. content: '第2页内容',
  6807. type: 'content'
  6808. },
  6809. {
  6810. title: '第3页标题',
  6811. content: '第3页内容',
  6812. type: 'content'
  6813. },
  6814. {
  6815. title: '第4页标题',
  6816. content: '第4页内容',
  6817. type: 'content'
  6818. },
  6819. {
  6820. title: '第5页标题',
  6821. content: '第5页内容',
  6822. type: 'content'
  6823. }
  6824. ]
  6825. // 更新PPT数据
  6826. pptSlides.value = localPPTData
  6827. // 更新缩略图,显示5页
  6828. updateSlideThumbnails()
  6829. // 重置当前幻灯片索引
  6830. currentSlideIndex.value = 0
  6831. currentEditingSlide.value = { ...localPPTData[0] }
  6832. // ElMessage.success('默认PPT文件加载成功')
  6833. console.log('加载的PPT页数:', localPPTData.length)
  6834. }
  6835. // 从大纲数据生成PPT幻灯片
  6836. const generateSlidesFromOutline = () => {
  6837. const slides = []
  6838. if (outlineData.value) {
  6839. // 封面幻灯片
  6840. slides.push({
  6841. title: outlineTitle.value || '安全培训演示文稿',
  6842. content: '基于AI生成的培训大纲',
  6843. type: 'cover'
  6844. })
  6845. // 目录幻灯片
  6846. const tocContent = outlineData.value.map((chapter, index) =>
  6847. `${index + 1}. ${chapter.title}`
  6848. ).join('\n')
  6849. slides.push({
  6850. title: '目录',
  6851. content: tocContent,
  6852. type: 'toc'
  6853. })
  6854. // 章节内容幻灯片
  6855. outlineData.value.forEach((chapter, chapterIndex) => {
  6856. // 章节标题幻灯片
  6857. slides.push({
  6858. title: chapter.title,
  6859. content: `第${chapterIndex + 1}章`,
  6860. type: 'chapter'
  6861. })
  6862. // 小节内容幻灯片
  6863. chapter.sections.forEach((section, sectionIndex) => {
  6864. if (section.subsections && section.subsections.length > 0) {
  6865. const content = section.subsections.map(sub => sub.title).join('\n')
  6866. slides.push({
  6867. title: section.title,
  6868. content: content,
  6869. type: 'content'
  6870. })
  6871. } else {
  6872. slides.push({
  6873. title: section.title,
  6874. content: '详细内容待补充',
  6875. type: 'content'
  6876. })
  6877. }
  6878. })
  6879. })
  6880. // 总结幻灯片
  6881. slides.push({
  6882. title: '总结与展望',
  6883. content: '培训要点回顾\n安全知识巩固\n持续改进建议',
  6884. type: 'summary'
  6885. })
  6886. }
  6887. return slides
  6888. }
  6889. const updateSlideContent = (event) => {
  6890. const target = event.target
  6891. if (target.classList.contains('slide-title')) {
  6892. currentEditingSlide.value.title = target.textContent
  6893. } else if (target.classList.contains('slide-body')) {
  6894. currentEditingSlide.value.content = target.textContent
  6895. }
  6896. // 更新PPT数据
  6897. if (pptSlides.value[currentSlideIndex.value]) {
  6898. pptSlides.value[currentSlideIndex.value] = { ...currentEditingSlide.value }
  6899. }
  6900. // 实时更新缩略图标题
  6901. updateSlideThumbnails()
  6902. }
  6903. const addNewSlide = () => {
  6904. // 限制最大页数为5页
  6905. if (pptSlides.value.length >= 5) {
  6906. ElMessage.warning('最多只能添加5页')
  6907. return
  6908. }
  6909. const newSlide = {
  6910. title: '新页面',
  6911. content: '点击编辑内容',
  6912. type: 'content'
  6913. }
  6914. pptSlides.value.push(newSlide)
  6915. currentSlideIndex.value = pptSlides.value.length - 1
  6916. currentEditingSlide.value = { ...newSlide }
  6917. updateSlideThumbnails()
  6918. ElMessage.success('新增页面成功')
  6919. }
  6920. const deleteCurrentSlide = async () => {
  6921. if (pptSlides.value.length <= 1) {
  6922. ElMessage.warning('至少保留一个页面')
  6923. return
  6924. }
  6925. try {
  6926. await ElMessageBox.confirm('确定要删除当前页面吗?', '确认删除', {
  6927. confirmButtonText: '确定',
  6928. cancelButtonText: '取消',
  6929. type: 'warning'
  6930. })
  6931. } catch {
  6932. return // 用户取消删除
  6933. }
  6934. pptSlides.value.splice(currentSlideIndex.value, 1)
  6935. if (currentSlideIndex.value >= pptSlides.value.length) {
  6936. currentSlideIndex.value = pptSlides.value.length - 1
  6937. }
  6938. currentEditingSlide.value = { ...pptSlides.value[currentSlideIndex.value] }
  6939. updateSlideThumbnails()
  6940. ElMessage.success('删除页面成功')
  6941. }
  6942. const savePPT = () => {
  6943. // 保存PPT数据
  6944. localStorage.setItem('safety-training-ppt', JSON.stringify(pptSlides.value))
  6945. ElMessage.success('PPT保存成功')
  6946. console.log('PPT数据已保存:', pptSlides.value)
  6947. }
  6948. // 下载状态
  6949. const isDownloading = ref(false)
  6950. const exportPPTX = async () => {
  6951. if (isDownloading.value) {
  6952. return // 防止重复点击
  6953. }
  6954. try {
  6955. isDownloading.value = true
  6956. isProcessing.value = true
  6957. console.log('exportPPTX: 设置 isProcessing = true')
  6958. // 根据选中的下载选项执行不同的操作
  6959. switch (selectedDownloadOption.value) {
  6960. case 0: // PowerPoint (PPTX)
  6961. await exportAsPPTX()
  6962. break
  6963. case 1: // 考试工坊
  6964. await generateExamQuestions()
  6965. break
  6966. case 2: // 培训讲义文档
  6967. await generateTrainingDocument()
  6968. break
  6969. default:
  6970. ElMessage.warning('未知的下载选项')
  6971. }
  6972. } catch (error) {
  6973. console.error('导出失败:', error)
  6974. ElMessage.error('导出失败,请重试')
  6975. } finally {
  6976. isDownloading.value = false
  6977. isProcessing.value = false
  6978. console.log('exportPPTX: 重置 isProcessing = false')
  6979. }
  6980. }
  6981. // 生成考试题目
  6982. const generateExamQuestions = async () => {
  6983. try {
  6984. console.log('开始生成考试题目...')
  6985. // 设置loading状态
  6986. isGeneratingExam.value = true
  6987. isProcessing.value = true
  6988. // 构建提示词
  6989. const prompt = `请基于以下安全培训内容生成完整的考试题目:
  6990. 培训主题:${outlineTitle.value || '安全培训'}
  6991. 培训内容:
  6992. ${generatedPPT.value.map((slide, index) => {
  6993. const content = slide.elements?.map(element => {
  6994. if (element.type === 'text') {
  6995. return element.content.replace(/<[^>]*>/g, '') // 移除HTML标签
  6996. }
  6997. return ''
  6998. }).filter(text => text.trim()).join(' ')
  6999. return `第${index + 1}页:${content}`
  7000. }).join('\n')}
  7001. 请严格按照以下格式生成考试题目:
  7002. 一、单选题(每题4分,共60分)
  7003. 1. 题目内容
  7004. A. 选项A
  7005. B. 选项B
  7006. C. 选项C
  7007. D. 选项D
  7008. 正确答案:X
  7009. 解析:详细解析内容
  7010. 2. 题目内容
  7011. A. 选项A
  7012. B. 选项B
  7013. C. 选项C
  7014. D. 选项D
  7015. 正确答案:X
  7016. 解析:详细解析内容
  7017. 二、多选题(每题4分,共20分)
  7018. 1. 题目内容
  7019. A. 选项A
  7020. B. 选项B
  7021. C. 选项C
  7022. D. 选项D
  7023. 正确答案:AB
  7024. 解析:详细解析内容
  7025. 三、判断题(每题2分,共20分)
  7026. 1. 题目内容
  7027. 正确答案:正确/错误
  7028. 解析:详细解析内容
  7029. 四、简答题(每题10分,共20分)
  7030. 1. 题目内容
  7031. 答题要点:详细答案内容和评分标准
  7032. 2. 题目内容
  7033. 答题要点:详细答案内容和评分标准
  7034. 3. 题目内容
  7035. 答案:详细答案内容和评分标准
  7036. 重要要求:
  7037. 1. 必须严格按照上述格式输出,不能省略任何内容
  7038. 2. 单选题15道(每题2分,共30分),多选题10道(每题3分,共30分),判断题10道(每题2分,共20分),简答题2道(每题10分,共20分)
  7039. 3. 总分控制在100分,不包含填空题
  7040. 4. 题目要全面覆盖培训内容的主要知识点
  7041. 5. 每道题都要包含正确答案和详细解析
  7042. 6. 简答题的答案必须详细具体,不能写"未设置"或"待补充"
  7043. 7. 所有答案都要具体详细,不能省略或留空
  7044. 8. 严格按照示例格式,每道简答题后面必须跟"答题要点:"开头的详细内容
  7045. 9. 简答题答案必须基于题目内容提供具体的知识点和实际应用示例
  7046. 10. 答案内容要丰富详实,至少包含3-5个要点,每个要点都要有具体说明
  7047. 11. 每道简答题的答题要点必须包含:核心概念解释、关键步骤分析、实际应用举例、注意事项说明
  7048. 12. 答题要点内容要具体可操作,不能是空泛的指导性语言
  7049. 13. 必须为每道简答题提供完整的答题要点,不能留空或写"未设置"`
  7050. // 调用AI接口
  7051. const response = await apis.reProduceSingleQuestion({
  7052. message: prompt
  7053. })
  7054. if (response && response.data) {
  7055. console.log('AI返回的数据:', response.data)
  7056. // 提取AI回复的内容
  7057. const aiContent = response.data.reply || response.data.content || response.data.message || response.data || 'AI生成的内容为空'
  7058. console.log('AI生成的内容:', aiContent)
  7059. // 将AI回复转换为Word文档并下载
  7060. await downloadAsWord(aiContent, `考试题目-${outlineTitle.value || '安全培训'}`)
  7061. ElMessage.success('考试题目生成成功!')
  7062. } else {
  7063. throw new Error('AI生成考试题目失败')
  7064. }
  7065. } catch (error) {
  7066. console.error('生成考试题目失败:', error)
  7067. ElMessage.error('生成考试题目失败: ' + error.message)
  7068. } finally {
  7069. // 重置loading状态
  7070. isGeneratingExam.value = false
  7071. isProcessing.value = false
  7072. }
  7073. }
  7074. // 生成培训讲义文档
  7075. const generateTrainingDocument = async () => {
  7076. try {
  7077. console.log('开始生成培训讲义文档...')
  7078. // 设置loading状态
  7079. isGeneratingTrainingMaterial.value = true
  7080. isProcessing.value = true
  7081. // 构建提示词
  7082. const prompt = `请基于以下安全培训内容生成培训讲义:
  7083. 培训主题:${outlineTitle.value || '安全培训'}
  7084. 培训内容:
  7085. ${generatedPPT.value.map((slide, index) => {
  7086. const content = slide.elements?.map(element => {
  7087. if (element.type === 'text') {
  7088. return element.content.replace(/<[^>]*>/g, '') // 移除HTML标签
  7089. }
  7090. return ''
  7091. }).filter(text => text.trim()).join(' ')
  7092. return `第${index + 1}页:${content}`
  7093. }).join('\n')}
  7094. 要求:
  7095. 1. 生成完整的培训讲义,使用Markdown格式
  7096. 2. 包含以下结构:
  7097. - 封面页(标题、副标题、日期)
  7098. - 目录页
  7099. - 各章节内容(使用# ## ###等标题层级)
  7100. - 要点列表(使用- 或 1. 2. 等)
  7101. - 重要概念(使用**粗体**标记)
  7102. - 注意事项(使用> 引用格式)
  7103. - 总结页
  7104. 3. 内容要详细、专业、易懂
  7105. 4. 适合作为培训教材使用
  7106. 5. 使用标准的Markdown语法
  7107. 请生成完整的培训讲义文档,使用规范的Markdown格式。`
  7108. // 调用AI接口
  7109. const response = await apis.reProduceSingleQuestion({
  7110. message: prompt
  7111. })
  7112. if (response && response.data) {
  7113. console.log('AI返回的数据:', response.data)
  7114. // 提取AI回复的内容
  7115. const aiContent = response.data.reply || response.data.content || response.data.message || response.data || 'AI生成的内容为空'
  7116. // 将AI回复转换为Word文档并下载
  7117. await downloadAsWord(aiContent, `培训讲义-${outlineTitle.value || '安全培训'}`)
  7118. ElMessage.success('培训讲义生成成功!')
  7119. } else {
  7120. throw new Error('AI生成培训讲义失败')
  7121. }
  7122. } catch (error) {
  7123. console.error('生成培训讲义失败:', error)
  7124. ElMessage.error('生成培训讲义失败: ' + error.message)
  7125. } finally {
  7126. // 重置loading状态
  7127. isGeneratingTrainingMaterial.value = false
  7128. isProcessing.value = false
  7129. }
  7130. }
  7131. // 下载为Word文档 - 培训讲义专用(简洁格式)
  7132. const downloadAsWord = async (content, fileName) => {
  7133. try {
  7134. console.log('开始生成Word文档,内容:', content)
  7135. // 确保content是字符串
  7136. const contentStr = String(content)
  7137. console.log('转换后的内容字符串:', contentStr)
  7138. // 清理内容,移除复杂的格式
  7139. let cleanedContent = contentStr
  7140. // 移除HTML标签
  7141. .replace(/<[^>]*>/g, '')
  7142. // 移除颜色相关的文本
  7143. .replace(/颜色[::].*?[,,。]/g, '')
  7144. .replace(/红色|蓝色|绿色|黄色|紫色|橙色|灰色|黑色|白色/g, '')
  7145. .replace(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g, '')
  7146. .replace(/rgb\([^)]*\)|rgba\([^)]*\)/g, '')
  7147. // 移除代码块标记
  7148. .replace(/```[\s\S]*?```/g, '')
  7149. .replace(/`[^`]*`/g, '')
  7150. // 移除引用标记
  7151. .replace(/^>\s*/gm, '')
  7152. // 移除表格标记
  7153. .replace(/\|.*\|/g, '')
  7154. .replace(/^[-:\s|]+$/gm, '')
  7155. // 清理多余的空行
  7156. .replace(/\n\s*\n\s*\n/g, '\n\n')
  7157. // 创建简洁的HTML文档内容
  7158. const wordContent = createSimpleWordContent(cleanedContent, fileName)
  7159. // 创建Blob对象 - 使用Word兼容的MIME类型
  7160. const blob = new Blob([wordContent], {
  7161. type: 'application/msword'
  7162. })
  7163. // 下载文件
  7164. const url = URL.createObjectURL(blob)
  7165. const link = document.createElement('a')
  7166. link.setAttribute('href', url)
  7167. link.setAttribute('download', `${fileName}-${new Date().toISOString().split('T')[0]}.doc`)
  7168. link.style.visibility = 'hidden'
  7169. document.body.appendChild(link)
  7170. link.click()
  7171. document.body.removeChild(link)
  7172. URL.revokeObjectURL(url)
  7173. console.log('Word文档已下载')
  7174. } catch (error) {
  7175. console.error('下载Word文档失败:', error)
  7176. // 备用方案:使用简单的文本下载
  7177. const contentStr = String(content)
  7178. const blob = new Blob([contentStr], { type: 'text/plain;charset=utf-8' })
  7179. const url = window.URL.createObjectURL(blob)
  7180. const link = document.createElement('a')
  7181. link.href = url
  7182. link.download = `${fileName}-${new Date().toISOString().split('T')[0]}.txt`
  7183. document.body.appendChild(link)
  7184. link.click()
  7185. document.body.removeChild(link)
  7186. window.URL.revokeObjectURL(url)
  7187. console.log('已降级为文本文件下载')
  7188. }
  7189. }
  7190. // 创建简洁的Word文档内容(参考AI写作的格式)
  7191. const createSimpleWordContent = (content, fileName) => {
  7192. // 处理内容,转换为简洁格式
  7193. let processedContent = content
  7194. // 处理Markdown标题
  7195. .replace(/^# (.*?)$/gm, '<h1>$1</h1>')
  7196. .replace(/^## (.*?)$/gm, '<h2>$1</h2>')
  7197. .replace(/^### (.*?)$/gm, '<h3>$1</h3>')
  7198. .replace(/^#### (.*?)$/gm, '<h4>$1</h4>')
  7199. // 处理Markdown加粗
  7200. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  7201. // 处理Markdown斜体
  7202. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  7203. // 处理Markdown列表
  7204. .replace(/^\- (.*$)/gim, '<div class="list-item">- $1</div>')
  7205. .replace(/^(\d+)\. (.*$)/gim, '<div class="list-item">$1. $2</div>')
  7206. // 处理换行符
  7207. .replace(/\n/g, '<br>')
  7208. // 创建HTML格式的文档内容(兼容Microsoft Office Word)
  7209. return `<!DOCTYPE html>
  7210. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  7211. xmlns:w="urn:schemas-microsoft-com:office:word"
  7212. xmlns="http://www.w3.org/TR/REC-html40">
  7213. <head>
  7214. <meta charset="utf-8">
  7215. <meta name="ProgId" content="Word.Document">
  7216. <meta name="Generator" content="Microsoft Word 15">
  7217. <meta name="Originator" content="Microsoft Word 15">
  7218. <title>${fileName || '培训讲义'}</title>
  7219. <style>
  7220. body {
  7221. font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
  7222. font-size: 14px;
  7223. line-height: 1.6;
  7224. margin: 24px;
  7225. color: #000;
  7226. }
  7227. .header {
  7228. text-align: center;
  7229. margin-bottom: 14px;
  7230. }
  7231. .doc-title {
  7232. font-size: 24px;
  7233. font-weight: bold;
  7234. margin-bottom: 14px;
  7235. color: #000;
  7236. }
  7237. .content {
  7238. font-family: "Microsoft YaHei", "宋体", Arial, sans-serif;
  7239. font-size: 14px;
  7240. line-height: 1.6;
  7241. color: #000;
  7242. margin: 0;
  7243. padding: 0;
  7244. }
  7245. .header {
  7246. text-align: center;
  7247. margin-bottom: 30px;
  7248. }
  7249. .doc-title {
  7250. font-size: 24px;
  7251. font-weight: bold;
  7252. margin-bottom: 20px;
  7253. color: #333;
  7254. }
  7255. h1 {
  7256. font-size: 20px;
  7257. font-weight: bold;
  7258. margin: 20px 0 15px 0;
  7259. color: #333;
  7260. }
  7261. h2 {
  7262. font-size: 18px;
  7263. font-weight: bold;
  7264. margin: 18px 0 12px 0;
  7265. color: #333;
  7266. }
  7267. h3 {
  7268. font-size: 16px;
  7269. font-weight: bold;
  7270. margin: 15px 0 10px 0;
  7271. color: #333;
  7272. }
  7273. h4 {
  7274. font-size: 14px;
  7275. font-weight: bold;
  7276. margin: 12px 0 8px 0;
  7277. color: #333;
  7278. }
  7279. p {
  7280. margin: 10px 0;
  7281. text-align: justify;
  7282. }
  7283. .list-item {
  7284. margin: 5px 0;
  7285. padding-left: 20px;
  7286. }
  7287. strong {
  7288. font-weight: bold;
  7289. }
  7290. em {
  7291. font-style: italic;
  7292. }
  7293. </style>
  7294. </head>
  7295. <body>
  7296. <div class='header'>
  7297. <div class='doc-title'>${fileName || '培训讲义'}</div>
  7298. </div>
  7299. <div class='content'>
  7300. ${processedContent}
  7301. </div>
  7302. </body>
  7303. </html>
  7304. `
  7305. }
  7306. // 解析AI生成的内容,提取题目信息
  7307. const parseExamContent = (content, fileName) => {
  7308. const contentStr = String(content)
  7309. console.log('开始解析内容,原始内容长度:', contentStr.length)
  7310. const lines = contentStr.split('\n').filter(line => line.trim())
  7311. console.log('分割后的行数:', lines.length)
  7312. console.log('前10行内容:', lines.slice(0, 10))
  7313. const examData = {
  7314. title: fileName,
  7315. totalScore: 0,
  7316. totalQuestions: 0,
  7317. singleChoice: {
  7318. questions: [],
  7319. scorePerQuestion: 2,
  7320. totalScore: 0
  7321. },
  7322. multiple: {
  7323. questions: [],
  7324. scorePerQuestion: 3,
  7325. totalScore: 0
  7326. },
  7327. judge: {
  7328. questions: [],
  7329. scorePerQuestion: 2,
  7330. totalScore: 0
  7331. },
  7332. short: {
  7333. questions: [],
  7334. scorePerQuestion: 10,
  7335. totalScore: 0
  7336. }
  7337. }
  7338. let currentSection = ''
  7339. let questionIndex = 0
  7340. for (let i = 0; i < lines.length; i++) {
  7341. const line = lines[i].trim()
  7342. if (!line) continue
  7343. // 检测章节
  7344. if (line.includes('一、单选题') || line.includes('单选题') || line.includes('选择题') || line.includes('一、')) {
  7345. currentSection = 'singleChoice'
  7346. continue
  7347. } else if (line.includes('二、多选题') || line.includes('多选题') || line.includes('多项选择题') || line.includes('二、')) {
  7348. currentSection = 'multiple'
  7349. continue
  7350. } else if (line.includes('三、判断题') || line.includes('判断题') || line.includes('三、')) {
  7351. currentSection = 'judge'
  7352. continue
  7353. } else if (line.includes('四、简答题') || line.includes('简答题') || line.includes('问答题') || line.includes('四、')) {
  7354. console.log('检测到简答题章节:', line)
  7355. currentSection = 'short'
  7356. continue
  7357. }
  7358. // 检测题目
  7359. if (line.match(/^\d+[.、]/) || line.match(/^第\d+题/)) {
  7360. questionIndex++
  7361. // 检查是否是填空题(包含空白处)
  7362. const isFillQuestion = line.includes('_______') || line.includes('____') || line.includes('空白')
  7363. // 如果当前是判断题章节但题目是填空题,则归类为填空题
  7364. if (currentSection === 'judge' && isFillQuestion) {
  7365. currentSection = 'fill'
  7366. }
  7367. const question = {
  7368. text: line.replace(/^\d+[.、]/, '').replace(/^第\d+题/, '').trim()
  7369. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // 转换粗体
  7370. .replace(/\*(.*?)\*/g, '<em>$1</em>'), // 转换斜体
  7371. options: [],
  7372. selectedAnswer: '',
  7373. selectedAnswers: [],
  7374. outline: { keyFactors: '' }
  7375. }
  7376. // 收集选项和答案
  7377. let j = i + 1
  7378. while (j < lines.length) {
  7379. const nextLine = lines[j].trim()
  7380. if (!nextLine) {
  7381. j++
  7382. continue
  7383. }
  7384. // 检测选项
  7385. if (nextLine.match(/^[A-D][.、]/)) {
  7386. question.options.push({
  7387. key: nextLine.charAt(0),
  7388. text: nextLine.substring(2).trim()
  7389. })
  7390. }
  7391. // 检测答案
  7392. else if (nextLine.includes('正确答案:') || nextLine.includes('答案:')) {
  7393. const answerText = nextLine.replace(/正确答案[::]?/, '').replace(/答案[::]?/, '').trim()
  7394. if (currentSection === 'multiple') {
  7395. question.selectedAnswers = answerText.split('').filter(char => /[A-D]/.test(char))
  7396. } else if (currentSection === 'judge') {
  7397. question.selectedAnswer = answerText.includes('正确') ? '正确' : '错误'
  7398. } else if (currentSection === 'fill') {
  7399. question.selectedAnswer = answerText
  7400. } else {
  7401. question.selectedAnswer = answerText
  7402. }
  7403. }
  7404. // 检测解析
  7405. else if (nextLine.includes('解析:') || nextLine.includes('说明:')) {
  7406. let analysisText = nextLine.replace(/解析[::]?/, '').replace(/说明[::]?/, '').trim()
  7407. // 收集多行解析内容
  7408. let k = j + 1
  7409. while (k < lines.length) {
  7410. const nextAnalysisLine = lines[k]?.trim()
  7411. // 如果遇到下一题或新章节,停止收集
  7412. if (nextAnalysisLine.match(/^\d+[.、]/) || nextAnalysisLine.match(/^第\d+题/) ||
  7413. nextAnalysisLine.includes('一、单选题') || nextAnalysisLine.includes('二、多选题') ||
  7414. nextAnalysisLine.includes('三、判断题') || nextAnalysisLine.includes('四、简答题') ||
  7415. nextAnalysisLine.includes('单选题') || nextAnalysisLine.includes('多选题') ||
  7416. nextAnalysisLine.includes('判断题') || nextAnalysisLine.includes('简答题') ||
  7417. nextAnalysisLine.includes('正确答案:') || nextAnalysisLine.includes('答题要点:')) {
  7418. break
  7419. }
  7420. // 如果遇到空行,跳过
  7421. if (nextAnalysisLine === '') {
  7422. k++
  7423. continue
  7424. }
  7425. // 收集解析内容
  7426. if (analysisText) {
  7427. analysisText += '\n' + nextAnalysisLine
  7428. } else {
  7429. analysisText = nextAnalysisLine
  7430. }
  7431. k++
  7432. }
  7433. question.outline.keyFactors = analysisText
  7434. console.log('收集到解析内容:', analysisText)
  7435. j = k - 1 // 更新j的位置
  7436. }
  7437. // 检测答题要点或答案
  7438. else if (nextLine.includes('答题要点:') || nextLine.includes('答案:') || nextLine.includes('答案')) {
  7439. console.log('找到简答题答案:', nextLine)
  7440. let answerText = nextLine.replace(/答题要点[::]?/, '').replace(/答案[::]?/, '').trim()
  7441. // 无论是否包含"未设置",都尝试收集多行答案内容
  7442. let k = j + 1
  7443. while (k < lines.length) {
  7444. const nextAnswerLine = lines[k]?.trim()
  7445. // 如果遇到下一题或新章节,停止收集
  7446. if (nextAnswerLine.match(/^\d+[.、]/) || nextAnswerLine.match(/^第\d+题/) ||
  7447. nextAnswerLine.includes('一、单选题') || nextAnswerLine.includes('二、多选题') ||
  7448. nextAnswerLine.includes('三、判断题') || nextAnswerLine.includes('四、简答题') ||
  7449. nextAnswerLine.includes('单选题') || nextAnswerLine.includes('多选题') ||
  7450. nextAnswerLine.includes('判断题') || nextAnswerLine.includes('简答题') ||
  7451. nextAnswerLine.includes('正确答案:') || nextAnswerLine.includes('解析:')) {
  7452. break
  7453. }
  7454. // 如果遇到空行,跳过
  7455. if (nextAnswerLine === '') {
  7456. k++
  7457. continue
  7458. }
  7459. // 收集答案内容
  7460. if (answerText) {
  7461. answerText += '\n' + nextAnswerLine
  7462. } else {
  7463. answerText = nextAnswerLine
  7464. }
  7465. k++
  7466. }
  7467. // 如果答案包含"未设置",则清空
  7468. if (answerText.includes('未设置')) {
  7469. answerText = ''
  7470. }
  7471. question.outline.keyFactors = answerText
  7472. console.log('收集到完整答题要点:', answerText)
  7473. j = k - 1 // 更新j的位置
  7474. }
  7475. // 如果没有找到答题要点,尝试从题目后面直接收集答案内容
  7476. else if (currentSection === 'short' && nextLine.trim() !== '' && !nextLine.match(/^\d+[.、]/) &&
  7477. !nextLine.includes('一、') && !nextLine.includes('二、') && !nextLine.includes('三、') &&
  7478. !nextLine.includes('四、') && !nextLine.includes('五、') &&
  7479. !nextLine.includes('单选题') && !nextLine.includes('多选题') &&
  7480. !nextLine.includes('判断题') && !nextLine.includes('简答题')) {
  7481. // 如果当前题目还没有答题要点,则收集这行作为答案
  7482. if (!question.outline.keyFactors || question.outline.keyFactors === '') {
  7483. question.outline.keyFactors = nextLine.trim()
  7484. console.log('收集到简答题答案内容:', nextLine.trim())
  7485. }
  7486. }
  7487. // 如果遇到下一题或新章节,停止收集
  7488. else if (nextLine.match(/^\d+[.、]/) || nextLine.match(/^第\d+题/) ||
  7489. nextLine.includes('一、单选题') || nextLine.includes('二、多选题') ||
  7490. nextLine.includes('三、判断题') || nextLine.includes('四、简答题') ||
  7491. nextLine.includes('单选题') || nextLine.includes('多选题') ||
  7492. nextLine.includes('判断题') || nextLine.includes('简答题')) {
  7493. break
  7494. }
  7495. j++
  7496. }
  7497. if (currentSection && examData[currentSection]) {
  7498. examData[currentSection].questions.push(question)
  7499. examData.totalQuestions++
  7500. }
  7501. i = j - 1 // 跳过已处理的行
  7502. }
  7503. }
  7504. // 计算分数
  7505. examData.singleChoice.totalScore = examData.singleChoice.questions.length * examData.singleChoice.scorePerQuestion
  7506. examData.multiple.totalScore = examData.multiple.questions.length * examData.multiple.scorePerQuestion
  7507. examData.judge.totalScore = examData.judge.questions.length * examData.judge.scorePerQuestion
  7508. examData.short.totalScore = examData.short.questions.length * examData.short.scorePerQuestion
  7509. examData.totalScore = examData.singleChoice.totalScore + examData.multiple.totalScore +
  7510. examData.judge.totalScore + examData.short.totalScore
  7511. return examData
  7512. }
  7513. // 自动保存修改后的PPT数据
  7514. const saveModifiedPPTData = () => {
  7515. try {
  7516. // 将修改后的PPT数据保存到localStorage
  7517. const modifiedPPTData = {
  7518. slides: generatedPPT.value,
  7519. timestamp: Date.now(),
  7520. title: outlineTitle.value || '安全培训演示文稿'
  7521. }
  7522. localStorage.setItem('safetyHazardModifiedPPT', JSON.stringify(modifiedPPTData))
  7523. console.log('PPT修改已自动保存')
  7524. } catch (error) {
  7525. console.error('保存PPT数据失败:', error)
  7526. }
  7527. }
  7528. // 检查并跳转到指定步骤
  7529. // 加载修改后的PPT数据
  7530. const loadModifiedPPTData = () => {
  7531. try {
  7532. const savedData = localStorage.getItem('safetyHazardModifiedPPT')
  7533. if (savedData) {
  7534. const data = JSON.parse(savedData)
  7535. // 检查数据是否过期(24小时)
  7536. if (Date.now() - data.timestamp < 24 * 60 * 60 * 1000) {
  7537. generatedPPT.value = data.slides || []
  7538. console.log('已加载修改后的PPT数据')
  7539. return true
  7540. } else {
  7541. localStorage.removeItem('safetyHazardModifiedPPT')
  7542. console.log('修改后的PPT数据已过期,已清除')
  7543. }
  7544. }
  7545. } catch (error) {
  7546. console.error('加载修改后的PPT数据失败:', error)
  7547. }
  7548. return false
  7549. }
  7550. // 导出为真正的PPTX文件(使用修改后的数据)
  7551. const exportAsPPTX = async () => {
  7552. if (generatedPPT.value.length === 0) {
  7553. ElMessage.warning('没有可导出的PPT内容,请先生成PPT')
  7554. return
  7555. }
  7556. try {
  7557. // 动态导入PptxGenJS
  7558. const PptxGenJS = (await import('pptxgenjs')).default
  7559. // 创建新的PPT实例
  7560. const pptx = new PptxGenJS()
  7561. // 设置PPT页面尺寸(16:9比例,无黑边)
  7562. pptx.defineLayout({
  7563. name: 'CUSTOM_16_9',
  7564. width: 10,
  7565. height: 5.625
  7566. })
  7567. pptx.layout = 'CUSTOM_16_9'
  7568. // 设置PPT属性
  7569. pptx.author = '安全培训系统'
  7570. pptx.company = '蜀道科技'
  7571. pptx.subject = '安全培训演示文稿'
  7572. pptx.title = outlineTitle.value || '安全培训演示文稿'
  7573. // 遍历生成的所有幻灯片(使用修改后的数据)
  7574. for (let i = 0; i < generatedPPT.value.length; i++) {
  7575. const slideData = generatedPPT.value[i]
  7576. console.log(`正在转换第 ${i + 1} 页:`, slideData.type)
  7577. await convertSlideToPptx(pptx, slideData)
  7578. }
  7579. // 生成并下载PPTX文件
  7580. const fileName = `安全培训-${outlineTitle.value || '演示文稿'}-${new Date().toISOString().split('T')[0]}.pptx`
  7581. await pptx.writeFile({ fileName })
  7582. console.log('PPTX文件已生成并下载')
  7583. ElMessage.success(`成功导出PPTX文件!\n文件名: ${fileName}`)
  7584. } catch (error) {
  7585. console.error('导出PPTX失败:', error)
  7586. ElMessage.error('导出PPTX失败: ' + error.message)
  7587. }
  7588. }
  7589. // 将单个幻灯片转换为PPTX格式
  7590. const convertSlideToPptx = async (pptx, slideData) => {
  7591. // 添加新幻灯片
  7592. const slide = pptx.addSlide()
  7593. // 设置背景
  7594. if (slideData.background) {
  7595. if (slideData.background.type === 'solid') {
  7596. const bgColor = convertColorForPptx(slideData.background.color || '#FFFFFF')
  7597. slide.background = { color: bgColor }
  7598. } else if (slideData.background.type === 'gradient' && slideData.background.gradient) {
  7599. // PptxGenJS对渐变支持有限,使用第一个颜色作为背景
  7600. const firstColor = slideData.background.gradient.colors[0]?.color || '#FFFFFF'
  7601. const bgColor = convertColorForPptx(firstColor)
  7602. slide.background = { color: bgColor }
  7603. }
  7604. }
  7605. // 处理每个元素
  7606. for (const element of slideData.elements) {
  7607. await addElementToPptxSlide(slide, element)
  7608. }
  7609. }
  7610. // 将元素添加到PPTX幻灯片
  7611. const addElementToPptxSlide = async (slide, element) => {
  7612. try {
  7613. // 转换坐标和尺寸 (从960x540到标准PPT尺寸)
  7614. const scaleX = 10 // PPTX使用英寸,960px ≈ 10英寸
  7615. const scaleY = 5.625 // 540px ≈ 5.625英寸
  7616. const x = (element.left / 960) * scaleX
  7617. const y = (element.top / 540) * scaleY
  7618. const w = (element.width / 960) * scaleX
  7619. const h = (element.height / 540) * scaleY
  7620. switch (element.type) {
  7621. case 'text':
  7622. addTextToPptx(slide, element, x, y, w, h)
  7623. break
  7624. case 'image':
  7625. await addImageToPptx(slide, element, x, y, w, h)
  7626. break
  7627. case 'shape':
  7628. addShapeToPptx(slide, element, x, y, w, h)
  7629. break
  7630. }
  7631. } catch (error) {
  7632. console.warn(`添加元素失败 ${element.type}:`, error)
  7633. }
  7634. }
  7635. // 添加文本到PPTX
  7636. const addTextToPptx = (slide, element, x, y, w, h) => {
  7637. // 提取纯文本内容
  7638. const textContent = extractTextFromHtml(element.content)
  7639. // 提取样式信息
  7640. const style = extractStyleFromHtml(element.content)
  7641. // 优先使用HTML中的颜色,其次使用元素默认颜色
  7642. const textColor = style.color || element.defaultColor || '#000000'
  7643. // 计算透明度(PPTX使用0-100的百分比)
  7644. const opacity = element.opacity !== undefined ? Math.round(element.opacity * 100) : 100
  7645. // 调试文本对齐
  7646. if (element.content && element.content.includes('text-align: center')) {
  7647. console.log(`文本对齐调试 ${element.id || 'unknown'}:`, {
  7648. content: element.content,
  7649. extractedAlign: style.align,
  7650. finalAlign: style.align || 'left'
  7651. })
  7652. }
  7653. slide.addText(textContent, {
  7654. x: x,
  7655. y: y,
  7656. w: w,
  7657. h: h,
  7658. fontSize: style.fontSize || 16,
  7659. color: convertColorForPptx(textColor),
  7660. fontFace: element.defaultFontName || '微软雅黑',
  7661. align: style.align || 'left',
  7662. valign: 'middle',
  7663. bold: style.bold || false,
  7664. wrap: true,
  7665. transparency: 100 - opacity, // PPTX使用transparency(0=不透明,100=完全透明)
  7666. line: null // 禁用文本边框
  7667. })
  7668. }
  7669. // 添加图片到PPTX
  7670. const addImageToPptx = async (slide, element, x, y, w, h) => {
  7671. if (!element.src) {
  7672. console.warn('图片元素没有src属性:', element)
  7673. return
  7674. }
  7675. try {
  7676. // 计算透明度(PPTX使用0-100的百分比)
  7677. const opacity = element.opacity !== undefined ? Math.round(element.opacity * 100) : 100
  7678. const transparency = 100 - opacity
  7679. console.log('处理图片元素:', {
  7680. id: element.id,
  7681. srcType: element.src.startsWith('data:image') ? 'base64' : 'url',
  7682. srcLength: element.src.length,
  7683. opacity: opacity,
  7684. transparency: transparency
  7685. })
  7686. // 如果是base64图片,需要检查是否为SVG格式
  7687. if (element.src.startsWith('data:image')) {
  7688. console.log('使用base64图片数据')
  7689. // 验证base64数据格式
  7690. if (!element.src.includes(',')) {
  7691. throw new Error('Base64数据格式错误:缺少逗号分隔符')
  7692. }
  7693. const [header, data] = element.src.split(',')
  7694. if (!header || !data) {
  7695. throw new Error('Base64数据格式错误:header或data为空')
  7696. }
  7697. // 检查数据长度(base64数据不能为空)
  7698. if (data.length < 100) {
  7699. throw new Error('Base64数据过短,可能已损坏')
  7700. }
  7701. let imageData = element.src
  7702. // 检查是否为SVG格式,如果是则转换为PNG
  7703. if (element.src.includes('data:image/svg+xml')) {
  7704. console.log('检测到SVG图像,开始转换为PNG格式')
  7705. try {
  7706. imageData = await convertSvgToPng(element.src)
  7707. console.log('SVG转PNG成功,使用转换后的PNG数据')
  7708. } catch (svgError) {
  7709. console.error('SVG转PNG失败:', svgError)
  7710. throw new Error('SVG图像转换失败: ' + svgError.message)
  7711. }
  7712. }
  7713. try {
  7714. slide.addImage({
  7715. data: imageData,
  7716. x: x,
  7717. y: y,
  7718. w: w,
  7719. h: h,
  7720. transparency: transparency,
  7721. sizing: {
  7722. type: 'cover', // 填满目标区域,超出部分裁剪
  7723. w: w,
  7724. h: h
  7725. },
  7726. line: null // 禁用图片边框
  7727. })
  7728. console.log('Base64图片添加成功')
  7729. } catch (addImageError) {
  7730. console.error('PptxGenJS添加图片失败:', addImageError)
  7731. // 尝试使用不同的方式添加图片
  7732. try {
  7733. // 移除data:image/xxx;base64,前缀,只保留纯base64数据
  7734. const pureBase64 = imageData.split(',')[1]
  7735. slide.addImage({
  7736. data: pureBase64,
  7737. x: x,
  7738. y: y,
  7739. w: w,
  7740. h: h,
  7741. transparency: transparency,
  7742. sizing: {
  7743. type: 'cover', // 填满目标区域,超出部分裁剪
  7744. w: w,
  7745. h: h
  7746. },
  7747. line: null // 禁用图片边框
  7748. })
  7749. console.log('使用纯base64数据添加成功')
  7750. } catch (retryError) {
  7751. console.error('重试添加图片也失败:', retryError)
  7752. throw addImageError // 抛出原始错误
  7753. }
  7754. }
  7755. } else {
  7756. // 如果是URL,需要转换为base64
  7757. console.log('转换URL图片为base64:', element.src)
  7758. const base64Data = await convertImageToBase64(element.src)
  7759. slide.addImage({
  7760. data: base64Data,
  7761. x: x,
  7762. y: y,
  7763. w: w,
  7764. h: h,
  7765. transparency: transparency,
  7766. sizing: {
  7767. type: 'cover', // 填满目标区域,超出部分裁剪
  7768. w: w,
  7769. h: h
  7770. },
  7771. line: null // 禁用图片边框
  7772. })
  7773. console.log('URL图片转换并添加成功')
  7774. }
  7775. } catch (error) {
  7776. console.error('添加图片失败:', error)
  7777. console.error('图片元素详情:', element)
  7778. // 图片加载失败时,添加一个占位文本
  7779. slide.addText(`图片加载失败: ${error.message}`, {
  7780. x: x,
  7781. y: y,
  7782. w: w,
  7783. h: h,
  7784. fontSize: 10,
  7785. color: 'FF0000',
  7786. align: 'center',
  7787. valign: 'middle',
  7788. bold: true
  7789. })
  7790. }
  7791. }
  7792. // 添加形状到PPTX
  7793. const addShapeToPptx = (slide, element, x, y, w, h) => {
  7794. // 获取颜色值,优先使用fill属性
  7795. const fillColor = element.fill || element.color || '#007bff'
  7796. // 计算透明度
  7797. let transparency = 0
  7798. if (fillColor.startsWith('rgba(')) {
  7799. // 如果颜色是rgba格式,优先使用其alpha通道
  7800. const rgbaMatch = fillColor.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/)
  7801. if (rgbaMatch) {
  7802. const alpha = parseFloat(rgbaMatch[4])
  7803. transparency = Math.round((1 - alpha) * 100)
  7804. }
  7805. } else if (element.opacity !== undefined) {
  7806. // 如果颜色不是rgba但元素有opacity属性,使用它
  7807. transparency = 100 - Math.round(element.opacity * 100)
  7808. }
  7809. const convertedColor = convertColorForPptx(fillColor)
  7810. // 调试overlay元素
  7811. if (element.id && element.id.includes('overlay')) {
  7812. console.log(`Overlay元素调试 ${element.id}:`, {
  7813. originalFill: element.fill,
  7814. fillColor: fillColor,
  7815. convertedColor: convertedColor,
  7816. opacity: element.opacity,
  7817. transparency: transparency,
  7818. alphaFromRgba: fillColor.startsWith('rgba(') ? parseFloat(fillColor.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/)?.[4] || '1') : null
  7819. })
  7820. }
  7821. slide.addShape('rect', {
  7822. x: x,
  7823. y: y,
  7824. w: w,
  7825. h: h,
  7826. fill: {
  7827. type: 'solid',
  7828. color: convertedColor,
  7829. transparency: transparency
  7830. },
  7831. line: null // 完全禁用边框
  7832. })
  7833. }
  7834. // 从HTML提取纯文本
  7835. const extractTextFromHtml = (html) => {
  7836. if (!html) return ''
  7837. // 创建临时DOM元素来解析HTML
  7838. const tempDiv = document.createElement('div')
  7839. tempDiv.innerHTML = html
  7840. return tempDiv.textContent || tempDiv.innerText || ''
  7841. }
  7842. // 从HTML提取样式
  7843. const extractStyleFromHtml = (html) => {
  7844. if (!html) return {}
  7845. const style = {}
  7846. // 检查字体大小 - 让导出字号和预览保持一致
  7847. const fontSizeMatch = html.match(/font-size:\s*(\d+)px/)
  7848. if (fontSizeMatch) {
  7849. const previewFontSize = parseInt(fontSizeMatch[1])
  7850. // 为了让导出字号和预览视觉效果一致,我们进一步缩小导出字号
  7851. // 预览字体大小 = 原始字体大小 * (1105/960)
  7852. // 导出时再缩小一点,让效果和预览一致
  7853. const scaleFactor = 1105 / 960
  7854. const additionalScale = 0.7 // 额外缩小30%,让导出和预览视觉效果一致
  7855. style.fontSize = Math.round(previewFontSize / scaleFactor * additionalScale)
  7856. console.log(`字体大小转换: 预览${previewFontSize}px -> 导出${style.fontSize}px (缩放因子: ${scaleFactor.toFixed(3)}, 额外缩放: ${additionalScale})`)
  7857. }
  7858. // 检查颜色
  7859. const colorMatch = html.match(/color:\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|rgb\([^)]+\)|rgba\([^)]+\))/)
  7860. if (colorMatch) {
  7861. style.color = colorMatch[1]
  7862. }
  7863. // 检查对齐方式
  7864. if (html.includes('text-align: center')) {
  7865. style.align = 'center'
  7866. } else if (html.includes('text-align: right')) {
  7867. style.align = 'right'
  7868. } else {
  7869. style.align = 'left' // 默认左对齐
  7870. }
  7871. // 检查粗体
  7872. if (html.includes('<strong>') || html.includes('<b>')) {
  7873. style.bold = true
  7874. }
  7875. return style
  7876. }
  7877. // 将SVG转换为PNG格式的base64
  7878. const convertSvgToPng = (svgDataUrl) => {
  7879. return new Promise((resolve, reject) => {
  7880. try {
  7881. // 创建Image对象来加载SVG
  7882. const img = new Image()
  7883. img.onload = () => {
  7884. try {
  7885. // 创建Canvas
  7886. const canvas = document.createElement('canvas')
  7887. const ctx = canvas.getContext('2d')
  7888. // 设置Canvas尺寸
  7889. canvas.width = img.width || 960
  7890. canvas.height = img.height || 540
  7891. // 设置高质量渲染
  7892. ctx.imageSmoothingEnabled = true
  7893. ctx.imageSmoothingQuality = 'high'
  7894. // 绘制SVG到Canvas
  7895. ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
  7896. // 转换为PNG格式的base64
  7897. const pngDataUrl = canvas.toDataURL('image/png', 1.0)
  7898. console.log('SVG转PNG成功:', {
  7899. originalSvgLength: svgDataUrl.length,
  7900. pngDataUrlLength: pngDataUrl.length,
  7901. canvasSize: `${canvas.width}x${canvas.height}`
  7902. })
  7903. resolve(pngDataUrl)
  7904. } catch (canvasError) {
  7905. console.error('Canvas转换失败:', canvasError)
  7906. reject(new Error('SVG转PNG失败: ' + canvasError.message))
  7907. }
  7908. }
  7909. img.onerror = (error) => {
  7910. console.error('SVG图像加载失败:', error)
  7911. reject(new Error('SVG图像加载失败'))
  7912. }
  7913. // 加载SVG图像
  7914. img.src = svgDataUrl
  7915. } catch (error) {
  7916. console.error('SVG转换初始化失败:', error)
  7917. reject(new Error('SVG转换初始化失败: ' + error.message))
  7918. }
  7919. })
  7920. }
  7921. // 将图片URL转换为base64
  7922. const convertImageToBase64 = (url) => {
  7923. return new Promise((resolve, reject) => {
  7924. const img = new Image()
  7925. img.crossOrigin = 'anonymous'
  7926. img.onload = () => {
  7927. const canvas = document.createElement('canvas')
  7928. const ctx = canvas.getContext('2d')
  7929. canvas.width = img.width
  7930. canvas.height = img.height
  7931. ctx.drawImage(img, 0, 0)
  7932. try {
  7933. const dataURL = canvas.toDataURL('image/png')
  7934. resolve(dataURL)
  7935. } catch (error) {
  7936. reject(error)
  7937. }
  7938. }
  7939. img.onerror = () => {
  7940. reject(new Error('图片加载失败'))
  7941. }
  7942. img.src = url
  7943. })
  7944. }
  7945. // 颜色转换函数 - 将各种颜色格式转换为PPTX需要的格式
  7946. const convertColorForPptx = (color) => {
  7947. if (!color) return 'FFFFFF'
  7948. // 如果已经是6位十六进制格式(无#),直接返回
  7949. if (/^[0-9A-Fa-f]{6}$/.test(color)) {
  7950. return color.toUpperCase()
  7951. }
  7952. // 如果是#开头的十六进制格式
  7953. if (color.startsWith('#')) {
  7954. const hex = color.substring(1)
  7955. // 处理3位十六进制颜色(如#f0f)
  7956. if (hex.length === 3) {
  7957. return (hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]).toUpperCase()
  7958. }
  7959. // 处理6位十六进制颜色(如#ff00ff)
  7960. if (hex.length === 6) {
  7961. return hex.toUpperCase()
  7962. }
  7963. }
  7964. // 处理rgb()格式
  7965. const rgbMatch = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/)
  7966. if (rgbMatch) {
  7967. const r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0')
  7968. const g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0')
  7969. const b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0')
  7970. return (r + g + b).toUpperCase()
  7971. }
  7972. // 处理rgba()格式(忽略透明度)
  7973. const rgbaMatch = color.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*[\d.]+\s*\)/)
  7974. if (rgbaMatch) {
  7975. const r = parseInt(rgbaMatch[1]).toString(16).padStart(2, '0')
  7976. const g = parseInt(rgbaMatch[2]).toString(16).padStart(2, '0')
  7977. const b = parseInt(rgbaMatch[3]).toString(16).padStart(2, '0')
  7978. return (r + g + b).toUpperCase()
  7979. }
  7980. // 处理命名颜色
  7981. const namedColors = {
  7982. 'white': 'FFFFFF',
  7983. 'black': '000000',
  7984. 'red': 'FF0000',
  7985. 'green': '008000',
  7986. 'blue': '0000FF',
  7987. 'yellow': 'FFFF00',
  7988. 'orange': 'FFA500',
  7989. 'purple': '800080',
  7990. 'pink': 'FFC0CB',
  7991. 'gray': '808080',
  7992. 'grey': '808080'
  7993. }
  7994. const normalizedColor = color.toLowerCase()
  7995. if (namedColors[normalizedColor]) {
  7996. return namedColors[normalizedColor]
  7997. }
  7998. // 如果无法识别,默认返回黑色
  7999. console.warn(`无法识别的颜色格式: ${color},使用默认黑色`)
  8000. return '000000'
  8001. }
  8002. const updateSlideThumbnails = () => {
  8003. // 优先使用generatedPPT(红色主题模板),如果没有则使用pptSlides(通用类PPT)
  8004. const pptData = generatedPPT.value && generatedPPT.value.length > 0 ? generatedPPT.value : pptSlides.value
  8005. // 如果都没有数据,直接使用template5的5张图片
  8006. if (!pptData || pptData.length === 0) {
  8007. slideImages.value = [
  8008. template5Slide1,
  8009. template5Slide2,
  8010. template5Slide3,
  8011. template5Slide4,
  8012. template5Slide5
  8013. ]
  8014. console.log('PPT内容为空,使用默认template5图片,共', slideImages.value.length, '张')
  8015. return
  8016. }
  8017. // 根据PPT内容生成缩略图
  8018. slideImages.value = pptData.map((slide, index) => {
  8019. // 如果是红色主题模板,使用模板7的图片
  8020. if (selectedTemplateStyle.value === 'red') {
  8021. const template7Images = [
  8022. template7Slide1, // 第1页 - 封面
  8023. template7Slide2, // 第2页 - 目录
  8024. template7Slide3, // 第3页 - 过渡
  8025. template7Slide4, // 第4页 - 内容
  8026. template7Slide5 // 第5页 - 结束
  8027. ]
  8028. // 直接根据索引选择模板7图片,确保每张都不同
  8029. console.log(`幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
  8030. // 直接使用索引来选择图片,确保每张都不同
  8031. return template7Images[index % 5]
  8032. }
  8033. // 如果是蓝色科技主题模板,使用模板8的图片
  8034. if (selectedTemplateStyle.value === 'blueTech') {
  8035. const template8Images = [
  8036. template8Slide1, // 第1页 - 封面
  8037. template8Slide2, // 第2页 - 目录
  8038. template8Slide3, // 第3页 - 过渡
  8039. template8Slide4, // 第4页 - 内容
  8040. template8Slide5 // 第5页 - 结束
  8041. ]
  8042. // 直接根据索引选择模板8图片,确保每张都不同
  8043. console.log(`幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
  8044. // 直接使用索引来选择图片,确保每张都不同
  8045. return template8Images[index % 5]
  8046. }
  8047. // 否则使用template5文件夹中的图片,循环使用5张图片
  8048. const template5Images = [
  8049. template5Slide1,
  8050. template5Slide2,
  8051. template5Slide3,
  8052. template5Slide4,
  8053. template5Slide5
  8054. ]
  8055. return template5Images[index % 5] // 循环使用5张图片
  8056. })
  8057. console.log('缩略图更新完成,共', slideImages.value.length, '页')
  8058. }
  8059. // 根据模板风格和幻灯片类型生成缩略图
  8060. const generateThumbnailForStyle = (slideType, style) => {
  8061. // 根据风格选择背景图片
  8062. let backgroundImage = ''
  8063. if (style === 'redElegant' || style === 'red') {
  8064. backgroundImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0911_1757633045.png'
  8065. } else if (style === 'blueTech') {
  8066. backgroundImage = 'http://172.16.17.52:8060/gdsc-ai-aqzs/images/2025/0911_1757633045.png'
  8067. } else {
  8068. // 默认风格使用template5图片
  8069. const template5Images = [
  8070. template5Slide1, // cover
  8071. template5Slide2, // contents
  8072. template5Slide3, // transition
  8073. template5Slide4, // content
  8074. template5Slide5 // end
  8075. ]
  8076. switch (slideType) {
  8077. case 'cover':
  8078. return template5Slide1
  8079. case 'contents':
  8080. return template5Slide2
  8081. case 'transition':
  8082. return template5Slide3
  8083. case 'content':
  8084. return template5Slide4
  8085. case 'end':
  8086. return template5Slide5
  8087. default:
  8088. return template5Images[0]
  8089. }
  8090. }
  8091. // 对于红色和蓝色风格,所有页面都使用对应的背景图片
  8092. return backgroundImage
  8093. }
  8094. // 更新动态模板缩略图
  8095. const updateDynamicTemplateThumbnails = () => {
  8096. if (!generatedPPT.value || generatedPPT.value.length === 0) {
  8097. // 即使没有PPT数据,也要根据风格更新缩略图
  8098. if (selectedTemplateStyle.value === 'redElegant' || selectedTemplateStyle.value === 'red') {
  8099. // 使用模板7的图片
  8100. slideImages.value = [
  8101. template7Slide1,
  8102. template7Slide2,
  8103. template7Slide3,
  8104. template7Slide4,
  8105. template7Slide5
  8106. ]
  8107. } else if (selectedTemplateStyle.value === 'blueTech') {
  8108. // 使用模板8的图片
  8109. slideImages.value = [
  8110. template8Slide1,
  8111. template8Slide2,
  8112. template8Slide3,
  8113. template8Slide4,
  8114. template8Slide5
  8115. ]
  8116. } else {
  8117. slideImages.value = [
  8118. template5Slide1,
  8119. template5Slide2,
  8120. template5Slide3,
  8121. template5Slide4,
  8122. template5Slide5
  8123. ]
  8124. }
  8125. // 强制Vue更新 - 使用数组重新赋值触发响应式
  8126. slideImages.value = [...slideImages.value]
  8127. console.log('动态模板内容为空,使用风格化图片,共', slideImages.value.length, '张')
  8128. console.log('当前风格:', selectedTemplateStyle.value)
  8129. console.log('更新后的缩略图数组:', slideImages.value)
  8130. return
  8131. }
  8132. // 根据动态模板内容生成缩略图
  8133. slideImages.value = generatedPPT.value.map((slide, index) => {
  8134. // 如果是红色主题,使用模板7图片
  8135. if (selectedTemplateStyle.value === 'redElegant' || selectedTemplateStyle.value === 'red') {
  8136. const template7Images = [
  8137. template7Slide1, // 第1页 - 封面
  8138. template7Slide2, // 第2页 - 目录
  8139. template7Slide3, // 第3页 - 过渡
  8140. template7Slide4, // 第4页 - 内容
  8141. template7Slide5 // 第5页 - 结束
  8142. ]
  8143. // 直接根据索引选择模板7图片,确保每张都不同
  8144. console.log(`动态模板幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
  8145. // 直接使用索引来选择图片,确保每张都不同
  8146. return template7Images[index % 5]
  8147. }
  8148. // 如果是蓝色科技主题,使用模板8图片
  8149. if (selectedTemplateStyle.value === 'blueTech') {
  8150. const template8Images = [
  8151. template8Slide1, // 第1页 - 封面
  8152. template8Slide2, // 第2页 - 目录
  8153. template8Slide3, // 第3页 - 过渡
  8154. template8Slide4, // 第4页 - 内容
  8155. template8Slide5 // 第5页 - 结束
  8156. ]
  8157. // 直接根据索引选择模板8图片,确保每张都不同
  8158. console.log(`动态模板幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
  8159. // 直接使用索引来选择图片,确保每张都不同
  8160. return template8Images[index % 5]
  8161. }
  8162. return generateThumbnailForStyle(slide.type, selectedTemplateStyle.value)
  8163. })
  8164. // 强制Vue更新 - 使用数组重新赋值触发响应式
  8165. slideImages.value = [...slideImages.value]
  8166. console.log('动态模板缩略图更新完成,共', slideImages.value.length, '页')
  8167. }
  8168. const selectTemplate = async (index) => {
  8169. selectedTemplate.value = index
  8170. const selectedTemplateData = templateStyles.value[index]
  8171. console.log('选择模板:', selectedTemplateData.title)
  8172. // 设置模板风格
  8173. if (selectedTemplateData.style) {
  8174. selectedTemplateStyle.value = selectedTemplateData.style
  8175. console.log('设置模板风格:', selectedTemplateData.style)
  8176. }
  8177. // 检查是否为动态模板
  8178. if (selectedTemplateData.type === 'dynamic') {
  8179. isDynamicTemplate.value = true
  8180. // 立即更新缩略图以显示风格变化
  8181. if (generatedPPT.value && generatedPPT.value.length > 0) {
  8182. updateDynamicTemplateThumbnails()
  8183. console.log('缩略图已更新为', selectedTemplateData.style, '风格')
  8184. } else {
  8185. // 即使没有生成的PPT,也要更新缩略图显示风格
  8186. console.log('准备更新缩略图,当前风格:', selectedTemplateStyle.value)
  8187. updateDynamicTemplateThumbnails()
  8188. console.log('缩略图已更新为', selectedTemplateData.style, '风格(无PPT数据)')
  8189. console.log('最终缩略图数组:', slideImages.value)
  8190. }
  8191. // 强制触发Vue响应式更新
  8192. nextTick(() => {
  8193. slideImages.value = [...slideImages.value]
  8194. console.log('强制更新缩略图完成')
  8195. })
  8196. // 如果有大纲数据,生成预览
  8197. if (outlineData.value && outlineData.value.length > 0) {
  8198. try {
  8199. const previewResult = previewOutlineMatch(outlineData.value, outlineTitle.value)
  8200. if (previewResult.success) {
  8201. templatePreview.value = previewResult.preview
  8202. templateAnalysis.value = previewResult.preview
  8203. console.log('动态模板预览生成成功:', templatePreview.value)
  8204. } else {
  8205. console.error('动态模板预览生成失败:', previewResult.error)
  8206. ElMessage.error('动态模板预览生成失败: ' + previewResult.error)
  8207. }
  8208. } catch (error) {
  8209. console.error('动态模板预览生成异常:', error)
  8210. ElMessage.error('动态模板预览生成异常: ' + error.message)
  8211. }
  8212. }
  8213. } else {
  8214. isDynamicTemplate.value = false
  8215. templatePreview.value = null
  8216. templateAnalysis.value = null
  8217. // 处理静态模板(如Template7)
  8218. if (selectedTemplateData.type === 'static' && selectedTemplateData.templateData) {
  8219. console.log('选择静态模板:', selectedTemplateData.title, '等待用户点击应用模板')
  8220. // 立即更新缩略图以显示风格变化(与动态模板保持一致)
  8221. console.log('准备更新静态模板缩略图,当前风格:', selectedTemplateStyle.value)
  8222. updateSlideThumbnails()
  8223. console.log('静态模板缩略图已更新为', selectedTemplateData.style, '风格')
  8224. // 为Template7生成预览信息,就像动态模板一样
  8225. if (outlineData.value && outlineData.value.length > 0) {
  8226. try {
  8227. const previewResult = previewOutlineMatch(outlineData.value, outlineTitle.value)
  8228. if (previewResult.success) {
  8229. templatePreview.value = previewResult.preview
  8230. templateAnalysis.value = previewResult.preview
  8231. console.log('Template7预览生成成功:', templatePreview.value)
  8232. } else {
  8233. console.error('Template7预览生成失败:', previewResult.error)
  8234. }
  8235. } catch (error) {
  8236. console.error('Template7预览生成异常:', error)
  8237. }
  8238. }
  8239. }
  8240. }
  8241. }
  8242. const applyTemplate = async () => {
  8243. console.log('应用模板:', templateStyles.value[selectedTemplate.value].title)
  8244. // 设置应用模板状态
  8245. isApplyingTemplate.value = true
  8246. isProcessing.value = true
  8247. try {
  8248. // 检查是否为动态模板
  8249. if (isDynamicTemplate.value) {
  8250. console.log('使用动态模板生成PPT...')
  8251. let testOutline, testTitle, testDescription
  8252. // 根据用户选择决定使用哪个数据源
  8253. if (outlineData.value && outlineData.value.length > 0) {
  8254. // 使用用户大纲数据,并转换为兼容格式
  8255. testOutline = await convertUserOutlineToCompatibleFormat(outlineData.value)
  8256. testTitle = outlineTitle.value || '用户生成的大纲' // 使用大纲标题作为PPT标题
  8257. testDescription = `${testOutline.length}章节结构演示`
  8258. console.log('使用用户大纲数据:', testTitle, `共${testOutline.length}个章节`)
  8259. console.log('转换后的用户大纲数据结构:', JSON.stringify(testOutline, null, 2))
  8260. } else if (selectedMockData.value !== null && selectedMockData.value !== undefined) {
  8261. // 使用选中的mock数据
  8262. const selectedOption = mockDataOptions.value[selectedMockData.value]
  8263. testOutline = selectedOption.data
  8264. testTitle = selectedOption.title
  8265. testDescription = selectedOption.description
  8266. console.log('使用mock数据:', selectedOption.title, selectedOption.description)
  8267. } else {
  8268. // 使用默认测试数据
  8269. testOutline = mockDataOptions.value[0].data
  8270. testTitle = mockDataOptions.value[0].title
  8271. testDescription = mockDataOptions.value[0].description
  8272. console.log('使用默认测试数据:', mockDataOptions.value[0].title)
  8273. }
  8274. // 生成动态模板
  8275. const result = await matchOutlineAndGeneratePPT(testOutline, testTitle, selectedTemplateStyle.value)
  8276. console.log('生成的动态模板结果:', result)
  8277. if (!result.success) {
  8278. throw new Error(result.error || '动态模板生成失败')
  8279. }
  8280. // 获取模板数据
  8281. const dynamicTemplate = result.data.template
  8282. console.log('提取的模板数据:', dynamicTemplate)
  8283. // 验证模板数据格式
  8284. if (!Array.isArray(dynamicTemplate)) {
  8285. console.error('模板数据不是数组格式:', typeof dynamicTemplate, dynamicTemplate)
  8286. throw new Error('模板数据格式不正确,期望数组但得到: ' + typeof dynamicTemplate)
  8287. }
  8288. // 启用预览模式
  8289. showDownloadOptions.value = true
  8290. currentPPTSlideIndex.value = 0
  8291. isPPTPreviewMode.value = true
  8292. console.log('已启用PPT预览模式,开始逐页生成效果...')
  8293. // 实现逐页生成效果
  8294. await generatePPTWithAnimation(dynamicTemplate, testOutline, testTitle)
  8295. console.log('动态模板应用完成,共', generatedPPT.value.length, '张幻灯片')
  8296. // 显示警告和建议
  8297. if (result.warnings && result.warnings.length > 0) {
  8298. ElMessage.warning('模板应用成功,但有以下建议: ' + result.warnings.join(', '))
  8299. }
  8300. if (result.recommendations && result.recommendations.length > 0) {
  8301. console.log('优化建议:', result.recommendations)
  8302. }
  8303. // 更新缩略图
  8304. updateDynamicTemplateThumbnails()
  8305. // 显示成功消息
  8306. const successMessage = outlineData.value && outlineData.value.length > 0 ?
  8307. `动态模板应用完成!使用用户大纲数据 (${testOutline.length}个章节)` :
  8308. `动态模板应用完成!使用数据: ${testTitle} (${testDescription})`
  8309. ElMessage.success(successMessage)
  8310. // 保存PPT数据到本地存储
  8311. if (generatedPPT.value && generatedPPT.value.length > 0) {
  8312. localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
  8313. console.log('动态模板PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
  8314. }
  8315. // 显示下载选项
  8316. showDownloadOptions.value = true
  8317. // 保存步骤信息到后端
  8318. await saveStepToBackend(true, true) // 强制保存新生成的PPT
  8319. // 更新历史记录状态
  8320. await getHistoryRecordList()
  8321. // 重新设置当前历史记录的选中状态
  8322. historyData.value.forEach((item) => {
  8323. item.isActive = item.id === ai_conversation_id.value
  8324. })
  8325. // 确保缩略图条滚动到第一页
  8326. nextTick(() => {
  8327. const thumbnailStripEl = thumbnailStrip.value
  8328. if (thumbnailStripEl) {
  8329. thumbnailStripEl.scrollLeft = 0
  8330. }
  8331. })
  8332. } else {
  8333. console.log('使用静态模板生成PPT...')
  8334. // 检查是否为Template7静态模板
  8335. const selectedTemplateData = templateStyles.value[selectedTemplate.value]
  8336. console.log('检查模板数据:', {
  8337. title: selectedTemplateData.title,
  8338. type: selectedTemplateData.type,
  8339. hasTemplateData: !!selectedTemplateData.templateData,
  8340. templateDataLength: selectedTemplateData.templateData?.length
  8341. })
  8342. if (selectedTemplateData.type === 'static' && selectedTemplateData.templateData) {
  8343. // 检查是否为红色主题模板
  8344. if (selectedTemplateData.title === '红色主题PPT') {
  8345. console.log('应用Template7红色主题模板:', selectedTemplateData.title)
  8346. try {
  8347. // 使用我们精心设计的template_7.json结构,但支持动态多页生成和动画效果
  8348. if (outlineData.value && outlineData.value.length > 0) {
  8349. console.log('开始基于template_7.json生成动态多页结构...')
  8350. // 显示生成进度
  8351. isGeneratingTrainingMaterial.value = true
  8352. // 生成红色主题模板数据
  8353. const redThemeTemplate = generateDynamicTemplate7FromStatic(outlineData.value, outlineTitle.value)
  8354. // 启用PPT预览模式 - 与通用类PPT保持一致
  8355. showDownloadOptions.value = true
  8356. currentPPTSlideIndex.value = 0
  8357. isPPTPreviewMode.value = true
  8358. console.log('已启用PPT预览模式,开始逐页生成效果...')
  8359. // 实现逐页生成效果(与通用类PPT相同的动画逻辑)
  8360. await generatePPTWithAnimation(redThemeTemplate, outlineData.value, outlineTitle.value)
  8361. isGeneratingTrainingMaterial.value = false
  8362. console.log('Template7动态多页结构生成完成,共', generatedPPT.value.length, '页')
  8363. } else {
  8364. // 如果没有大纲数据,使用默认的5页结构
  8365. const defaultTemplate = JSON.parse(JSON.stringify(selectedTemplateData.templateData))
  8366. // 启用PPT预览模式 - 与通用类PPT保持一致
  8367. showDownloadOptions.value = true
  8368. currentPPTSlideIndex.value = 0
  8369. isPPTPreviewMode.value = true
  8370. console.log('已启用PPT预览模式,开始逐页生成效果...')
  8371. // 实现逐页生成效果
  8372. await generatePPTWithAnimation(defaultTemplate, [], outlineTitle.value || '默认标题')
  8373. console.log('Template7默认模板加载成功,共', generatedPPT.value.length, '页')
  8374. }
  8375. // 更新缩略图 - 与通用类PPT保持一致的处理流程
  8376. updateDynamicTemplateThumbnails()
  8377. // 显示成功消息
  8378. const successMessage = outlineData.value && outlineData.value.length > 0 ?
  8379. `红色主题模板应用完成!使用用户大纲数据 (${outlineData.value.length}个章节)` :
  8380. `红色主题模板应用完成!使用默认内容`
  8381. ElMessage.success(successMessage)
  8382. // 保存PPT数据到本地存储
  8383. if (generatedPPT.value && generatedPPT.value.length > 0) {
  8384. localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
  8385. console.log('Template7静态模板PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
  8386. }
  8387. // 显示下载选项
  8388. showDownloadOptions.value = true
  8389. currentPPTSlideIndex.value = 0
  8390. // 保存步骤信息到后端
  8391. await saveStepToBackend(true, true) // 强制保存新生成的PPT
  8392. // 更新历史记录状态
  8393. await getHistoryRecordList()
  8394. // 重新设置当前历史记录的选中状态
  8395. historyData.value.forEach((item) => {
  8396. item.isActive = item.id === ai_conversation_id.value
  8397. })
  8398. // 确保缩略图条滚动到第一页
  8399. nextTick(() => {
  8400. const thumbnailStripEl = thumbnailStrip.value
  8401. if (thumbnailStripEl) {
  8402. thumbnailStripEl.scrollLeft = 0
  8403. }
  8404. })
  8405. } catch (error) {
  8406. console.error('应用Template7静态模板失败:', error)
  8407. ElMessage.error('应用Template7静态模板失败: ' + error.message)
  8408. }
  8409. }
  8410. // 检查是否为蓝色科技主题模板
  8411. if (selectedTemplateData.title === '蓝色科技主题PPT') {
  8412. console.log('应用Template8蓝色科技主题模板:', selectedTemplateData.title)
  8413. try {
  8414. // 使用我们精心设计的template_8.json结构,但支持动态多页生成和动画效果
  8415. if (outlineData.value && outlineData.value.length > 0) {
  8416. console.log('开始基于template_8.json生成动态多页结构...')
  8417. // 显示生成进度
  8418. isGeneratingTrainingMaterial.value = true
  8419. // 生成蓝色科技主题模板数据
  8420. const blueTechThemeTemplate = generateDynamicTemplate8FromStatic(outlineData.value, outlineTitle.value)
  8421. // 启用PPT预览模式 - 与通用类PPT保持一致
  8422. showDownloadOptions.value = true
  8423. currentPPTSlideIndex.value = 0
  8424. isPPTPreviewMode.value = true
  8425. console.log('已启用PPT预览模式,开始逐页生成效果...')
  8426. // 实现逐页生成效果(与通用类PPT相同的动画逻辑)
  8427. await generatePPTWithAnimation(blueTechThemeTemplate, outlineData.value, outlineTitle.value)
  8428. isGeneratingTrainingMaterial.value = false
  8429. console.log('Template8动态多页结构生成完成,共', generatedPPT.value.length, '页')
  8430. } else {
  8431. // 如果没有大纲数据,使用默认的5页结构
  8432. const defaultTemplate = JSON.parse(JSON.stringify(selectedTemplateData.templateData))
  8433. // 启用PPT预览模式 - 与通用类PPT保持一致
  8434. showDownloadOptions.value = true
  8435. currentPPTSlideIndex.value = 0
  8436. isPPTPreviewMode.value = true
  8437. console.log('已启用PPT预览模式,开始逐页生成效果...')
  8438. // 实现逐页生成效果
  8439. await generatePPTWithAnimation(defaultTemplate, [], outlineTitle.value || '默认标题')
  8440. console.log('Template8默认模板加载成功,共', generatedPPT.value.length, '页')
  8441. }
  8442. // 更新缩略图 - 与通用类PPT保持一致的处理流程
  8443. updateDynamicTemplateThumbnails()
  8444. // 显示成功消息
  8445. const successMessage = outlineData.value && outlineData.value.length > 0 ?
  8446. `蓝色科技主题模板应用完成!使用用户大纲数据 (${outlineData.value.length}个章节)` :
  8447. `蓝色科技主题模板应用完成!使用默认内容`
  8448. ElMessage.success(successMessage)
  8449. // 保存PPT数据到本地存储
  8450. if (generatedPPT.value && generatedPPT.value.length > 0) {
  8451. localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
  8452. console.log('Template8静态模板PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
  8453. }
  8454. } catch (error) {
  8455. isGeneratingTrainingMaterial.value = false
  8456. console.error('应用Template8静态模板失败:', error)
  8457. ElMessage.error('应用Template8静态模板失败: ' + error.message)
  8458. }
  8459. }
  8460. } else if (outlineData.value && outlineData.value.length > 0) {
  8461. // 原有的静态模板逻辑
  8462. console.log('开始生成PPT数据...')
  8463. // 第一步:转换大纲为AIPPT格式(包含空内容)
  8464. const aipptData = convertOutlineToAIPPT(outlineData.value, outlineTitle.value)
  8465. console.log('生成的AIPPT数据:', aipptData)
  8466. // 第二步:直接让AI填充AIPPT格式的数据
  8467. const filledAIPPTData = await fillAIPPTContent(aipptData, outlineTitle.value)
  8468. console.log('AI填充后的AIPPT数据:', filledAIPPTData)
  8469. // 第三步:直接使用填充后的数据(需要保存步骤)
  8470. await loadAIPPTData(filledAIPPTData, true)
  8471. console.log('模板应用完成,PPT数据已加载')
  8472. // 等待数据完全处理完成
  8473. await nextTick()
  8474. // 确保数据被正确保存到本地存储
  8475. if (generatedPPT.value && generatedPPT.value.length > 0) {
  8476. localStorage.setItem('generatedPPT', JSON.stringify(generatedPPT.value))
  8477. console.log('PPT数据已保存到本地存储:', generatedPPT.value.length, '张幻灯片')
  8478. }
  8479. // 显示成功消息
  8480. ElMessage.success('模板应用完成,内容已填充!')
  8481. // 更新历史记录状态
  8482. await getHistoryRecordList()
  8483. // 重新设置当前历史记录的选中状态
  8484. historyData.value.forEach((item) => {
  8485. item.isActive = item.id === ai_conversation_id.value
  8486. })
  8487. // 显示下载选项(模板和大纲结合页面)
  8488. showDownloadOptions.value = true
  8489. // 确保缩略图条滚动到第一页
  8490. nextTick(() => {
  8491. const thumbnailStripEl = thumbnailStrip.value
  8492. if (thumbnailStripEl) {
  8493. thumbnailStripEl.scrollLeft = 0
  8494. console.log('缩略图条已滚动到第一页')
  8495. }
  8496. })
  8497. } else {
  8498. // 如果没有大纲数据,直接加载默认数据
  8499. await loadAIPPTData()
  8500. ElMessage.success('模板应用完成!')
  8501. showDownloadOptions.value = true
  8502. }
  8503. }
  8504. } catch (error) {
  8505. console.error('应用模板失败:', error)
  8506. ElMessage.error('应用模板失败: ' + error.message)
  8507. } finally {
  8508. // 重置应用模板状态
  8509. isApplyingTemplate.value = false
  8510. isProcessing.value = false
  8511. }
  8512. }
  8513. // 填充静态模板内容函数
  8514. const fillStaticTemplateContent = async (pptData, outlineData, title) => {
  8515. console.log('开始填充静态模板内容:', { title, chapters: outlineData.length })
  8516. try {
  8517. // 处理封面页
  8518. const coverSlide = pptData.find(slide => slide.type === 'cover')
  8519. if (coverSlide) {
  8520. // 填充标题
  8521. const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
  8522. if (titleElement) {
  8523. titleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: #ffffff; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  8524. console.log('封面标题已填充:', title)
  8525. }
  8526. // 填充副标题
  8527. const subtitleElement = coverSlide.elements.find(el => el.id === 'subtitle-text')
  8528. if (subtitleElement) {
  8529. const subtitle = outlineData.length > 0 ? `${outlineData.length}个章节的详细内容` : '详细内容介绍'
  8530. subtitleElement.content = `<p style="text-align: center;"><span style="font-size: 18px; color: rgba(255,255,255,0.9);">${subtitle}</span></p>`
  8531. console.log('封面副标题已填充:', subtitle)
  8532. }
  8533. }
  8534. // 处理目录页
  8535. const contentsSlide = pptData.find(slide => slide.type === 'contents')
  8536. if (contentsSlide && outlineData.length > 0) {
  8537. // 填充目录项
  8538. for (let i = 0; i < Math.min(outlineData.length, 6); i++) {
  8539. const chapter = outlineData[i]
  8540. const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
  8541. if (itemElement) {
  8542. itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
  8543. console.log(`目录项${i + 1}已填充:`, chapter.title)
  8544. }
  8545. }
  8546. // 隐藏多余的目录项
  8547. for (let i = outlineData.length; i < 6; i++) {
  8548. const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
  8549. if (itemElement) {
  8550. itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
  8551. }
  8552. }
  8553. }
  8554. // 处理过渡页和内容页
  8555. let slideIndex = 0
  8556. for (let chapterIndex = 0; chapterIndex < outlineData.length; chapterIndex++) {
  8557. const chapter = outlineData[chapterIndex]
  8558. // 查找过渡页
  8559. const transitionSlide = pptData.find(slide => slide.type === 'transition' && slideIndex === 0)
  8560. if (transitionSlide) {
  8561. // 填充过渡页标题
  8562. const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
  8563. if (transitionTitleElement) {
  8564. transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  8565. console.log('过渡页标题已填充:', chapter.title)
  8566. }
  8567. // 填充过渡页内容(使用AI生成)
  8568. const transitionContentElement = transitionSlide.elements.find(el => el.id === 'transition-content')
  8569. if (transitionContentElement) {
  8570. try {
  8571. console.log(`🤖 正在为红色PPT过渡页生成章节介绍内容: ${chapter.title}`)
  8572. // 调用AI API生成章节介绍内容
  8573. const aiResponse = await apis.reProduceSingleQuestion({
  8574. message: `请为PPT章节"${chapter.title}"生成一个简洁的章节介绍内容。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.控制在20-30字以内 5.不要包含任何编号`
  8575. })
  8576. if (aiResponse && aiResponse.data) {
  8577. const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  8578. console.log(`✅ 红色PPT过渡页章节介绍生成完成: ${aiContent}`)
  8579. transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${aiContent}</span></p>`
  8580. } else {
  8581. // AI调用失败,使用备用内容
  8582. const fallbackContent = chapter.sections && chapter.sections.length > 0
  8583. ? `本章将介绍${chapter.title}的相关内容`
  8584. : `${chapter.title}的详细说明`
  8585. transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${fallbackContent}</span></p>`
  8586. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  8587. }
  8588. } catch (aiError) {
  8589. console.error(`❌ AI生成红色PPT过渡页内容失败:`, aiError)
  8590. // AI生成失败,使用备用内容
  8591. const fallbackContent = chapter.sections && chapter.sections.length > 0
  8592. ? `本章将介绍${chapter.title}的相关内容`
  8593. : `${chapter.title}的详细说明`
  8594. transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${fallbackContent}</span></p>`
  8595. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  8596. }
  8597. }
  8598. slideIndex++
  8599. }
  8600. // 查找内容页
  8601. const contentSlide = pptData.find(slide => slide.type === 'content' && slideIndex === 0)
  8602. if (contentSlide && chapter.sections && chapter.sections.length > 0) {
  8603. // 填充内容页标题
  8604. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  8605. if (contentTitleElement) {
  8606. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  8607. console.log('内容页标题已填充:', chapter.title)
  8608. }
  8609. // 填充内容项
  8610. const sections = chapter.sections.slice(0, 3) // 最多显示3个要点
  8611. for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
  8612. const section = sections[sectionIndex]
  8613. // 填充要点标题(使用AI生成)
  8614. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${sectionIndex + 1}`)
  8615. if (itemTitleElement) {
  8616. try {
  8617. console.log(`🤖 正在为红色PPT生成要点标题: ${section.title}`)
  8618. // 使用getTitleForElement函数获取正确的标题,与通用类PPT保持一致
  8619. const titleForElement = getTitleForElement(outlineData, slideIndex, sectionIndex)
  8620. // 检查是否需要AI生成标题
  8621. if (titleForElement.includes('待AI生成子小节')) {
  8622. // 需要AI生成子小节标题
  8623. console.log(`🤖 正在为子小节生成标题: ${titleForElement}`)
  8624. // 获取小节标题用于AI生成
  8625. const sectionTitle = getSectionTitleForAI(outlineData, slideIndex, sectionIndex)
  8626. // 根据不同的标记生成不同的提示词
  8627. let prompt = ''
  8628. if (titleForElement.includes('待AI生成子小节1')) {
  8629. prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第一个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
  8630. } else if (titleForElement.includes('待AI生成子小节2')) {
  8631. prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第二个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
  8632. } else if (titleForElement.includes('待AI生成子小节3')) {
  8633. prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第三个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
  8634. } else if (titleForElement.includes('待AI生成子小节4')) {
  8635. prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成第四个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
  8636. } else {
  8637. prompt = `请为PPT幻灯片的小节"${sectionTitle}"生成一个专业的子小节标题。要求:1.标题简洁明了 2.专业准确 3.适合PPT展示 4.控制在8-15字以内 5.不要包含任何编号 6.直接返回标题`
  8638. }
  8639. // 调用AI生成单个子小节标题(为当前元素生成)
  8640. const aiResponse = await apis.reProduceSingleQuestion({
  8641. message: prompt
  8642. })
  8643. if (aiResponse && aiResponse.data) {
  8644. const aiTitle = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  8645. console.log(`✅ 红色PPT子小节标题生成完成: ${aiTitle}`)
  8646. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${aiTitle}</span></strong></p>`
  8647. } else {
  8648. // AI调用失败,使用备用标题
  8649. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${section.title}</span></strong></p>`
  8650. console.log(`🔄 使用备用标题: ${section.title}`)
  8651. }
  8652. } else {
  8653. // 直接使用获取到的标题
  8654. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${titleForElement}</span></strong></p>`
  8655. console.log(`✅ 使用现有标题: ${titleForElement}`)
  8656. }
  8657. } catch (aiError) {
  8658. console.error(`❌ AI生成红色PPT要点标题失败:`, aiError)
  8659. // AI生成失败,使用备用标题
  8660. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${section.title}</span></strong></p>`
  8661. console.log(`🔄 使用备用标题: ${section.title}`)
  8662. }
  8663. }
  8664. // 填充要点内容(使用AI生成)
  8665. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${sectionIndex + 1}`)
  8666. if (itemContentElement) {
  8667. try {
  8668. console.log(`🤖 正在为红色PPT生成要点内容: ${section.title}`)
  8669. // 使用getTitleForAIContent函数获取正确的内容标题,与通用类PPT保持一致
  8670. const titleForAI = getTitleForAIContent(outlineData, slideIndex, sectionIndex)
  8671. // 调用AI API生成要点内容
  8672. const aiResponse = await apis.reProduceSingleQuestion({
  8673. message: `请为PPT幻灯片生成专业的内容,主题是:${titleForAI}。要求:1.内容专业准确 2.语言简洁明了 3.适合PPT展示 4.严格控制字数在30-45字以内 5.不要包含任何编号(如"子小节3:"、"要点1:"等)6.直接返回内容,不要添加前缀 7.要有独特性和创新性,避免与其他内容重复 8.从不同角度阐述主题。这是关于"${outlineTitle.value}"的PPT演示文稿,当前章节是"${titleForAI}"`
  8674. })
  8675. if (aiResponse && aiResponse.data) {
  8676. const aiContent = aiResponse.data.reply || aiResponse.data.content || aiResponse.data.message || aiResponse.data
  8677. console.log(`✅ 红色PPT要点内容生成完成: ${aiContent}`)
  8678. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${aiContent}</span></p>`
  8679. } else {
  8680. // AI调用失败,使用备用内容
  8681. const fallbackContent = section.subsections && section.subsections.length > 0
  8682. ? section.subsections.map(sub => sub.title).join('、')
  8683. : section.content || '详细说明内容'
  8684. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${fallbackContent}</span></p>`
  8685. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  8686. }
  8687. } catch (aiError) {
  8688. console.error(`❌ AI生成红色PPT要点内容失败:`, aiError)
  8689. // AI生成失败,使用备用内容
  8690. const fallbackContent = section.subsections && section.subsections.length > 0
  8691. ? section.subsections.map(sub => sub.title).join('、')
  8692. : section.content || '详细说明内容'
  8693. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${fallbackContent}</span></p>`
  8694. console.log(`🔄 使用备用内容: ${fallbackContent}`)
  8695. }
  8696. }
  8697. }
  8698. // 隐藏多余的要点
  8699. for (let sectionIndex = sections.length; sectionIndex < 3; sectionIndex++) {
  8700. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${sectionIndex + 1}`)
  8701. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${sectionIndex + 1}`)
  8702. if (itemTitleElement) {
  8703. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;"></span></strong></p>`
  8704. }
  8705. if (itemContentElement) {
  8706. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;"></span></p>`
  8707. }
  8708. }
  8709. slideIndex++
  8710. }
  8711. }
  8712. // 处理结束页
  8713. const endSlide = pptData.find(slide => slide.type === 'end')
  8714. if (endSlide) {
  8715. const endTitleElement = endSlide.elements.find(el => el.id === 'end-title')
  8716. if (endTitleElement) {
  8717. endTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: #ffffff; text-shadow: 2px 2px 6px rgba(0,0,0,0.3);">谢谢聆听</span></strong></p>`
  8718. console.log('结束页标题已填充')
  8719. }
  8720. const endSubtitleElement = endSlide.elements.find(el => el.id === 'end-subtitle')
  8721. if (endSubtitleElement) {
  8722. endSubtitleElement.content = `<p style="text-align: center;"><span style="font-size: 18px; color: rgba(255,255,255,0.9);">感谢您的时间与关注</span></p>`
  8723. console.log('结束页副标题已填充')
  8724. }
  8725. }
  8726. console.log('静态模板内容填充完成')
  8727. } catch (error) {
  8728. console.error('填充静态模板内容失败:', error)
  8729. throw error
  8730. }
  8731. }
  8732. // 切换预览模式
  8733. const togglePreviewMode = () => {
  8734. previewMode.value = previewMode.value === 'edit' ? 'preview' : 'edit'
  8735. console.log('切换预览模式:', previewMode.value)
  8736. if (previewMode.value === 'preview') {
  8737. ElMessage.success('已切换到预览模式')
  8738. } else {
  8739. ElMessage.success('已切换到编辑模式')
  8740. }
  8741. }
  8742. // PPT预览相关方法
  8743. const getCurrentPPTSlide = () => {
  8744. const slide = generatedPPT.value[currentPPTSlideIndex.value] || null
  8745. console.log('获取当前PPT幻灯片:', slide)
  8746. console.log('当前索引:', currentPPTSlideIndex.value)
  8747. console.log('总幻灯片数:', generatedPPT.value.length)
  8748. return slide
  8749. }
  8750. // 获取大图显示的内容(在动画过程中显示"正在生成...")
  8751. const getCurrentPPTSlideForMainView = () => {
  8752. const slide = generatedPPT.value[currentPPTSlideIndex.value] || null
  8753. if (!slide) return null
  8754. // 如果正在生成中,显示"正在生成..."的内容
  8755. if (isGeneratingTrainingMaterial.value && slide.elements) {
  8756. const generatingSlide = JSON.parse(JSON.stringify(slide))
  8757. generatingSlide.elements = generatingSlide.elements.map(el => ({
  8758. ...el,
  8759. content: (el.textType === 'itemContent' && el.content && el.content.includes('待AI填充')) ?
  8760. `<p style="text-align: center;"><span style="font-size: 16px; color: ${el.defaultColor};">正在生成...</span></p>` :
  8761. el.content
  8762. }))
  8763. return generatingSlide
  8764. }
  8765. return slide
  8766. }
  8767. const getCurrentPPTSlideBackground = () => {
  8768. const slide = getCurrentPPTSlideForMainView()
  8769. if (!slide) return '#ffffff'
  8770. if (slide.background?.type === 'gradient') {
  8771. const gradient = slide.background.gradient
  8772. if (gradient.type === 'linear') {
  8773. const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
  8774. return `linear-gradient(135deg, ${colors})`
  8775. } else if (gradient.type === 'radial') {
  8776. const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
  8777. return `radial-gradient(circle, ${colors})`
  8778. }
  8779. }
  8780. return slide.background?.color || '#667eea'
  8781. }
  8782. // 获取幻灯片背景(用于缩略图)
  8783. const getSlideBackground = (slide) => {
  8784. if (!slide) return '#667eea'
  8785. // 检查是否有背景设置
  8786. if (slide.background) {
  8787. if (slide.background.type === 'gradient') {
  8788. const gradient = slide.background.gradient
  8789. if (gradient.type === 'linear') {
  8790. const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
  8791. return `linear-gradient(135deg, ${colors})`
  8792. } else if (gradient.type === 'radial') {
  8793. const colors = gradient.colors.map(c => `${c.color} ${c.pos}%`).join(', ')
  8794. return `radial-gradient(circle, ${colors})`
  8795. }
  8796. } else if (slide.background.color) {
  8797. // 确保白色背景能正确显示
  8798. return slide.background.color
  8799. }
  8800. }
  8801. // 如果没有背景设置,检查是否有白色背景的元素
  8802. if (slide.elements && slide.elements.length > 0) {
  8803. const whiteElement = slide.elements.find(el =>
  8804. el.type === 'shape' && el.fill === '#ffffff' ||
  8805. el.type === 'shape' && el.fill === 'white'
  8806. )
  8807. if (whiteElement) {
  8808. return '#ffffff'
  8809. }
  8810. }
  8811. return '#667eea'
  8812. }
  8813. // 获取缩略图元素样式
  8814. const getThumbnailElementStyle = (element) => {
  8815. // 获取当前根字体大小,用于响应amfe-flexible缩放
  8816. const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
  8817. const baseFontSize = 192 // amfe-flexible的基础字体大小 (1920px设计稿 / 10)
  8818. const scaleFactor = rootFontSize / baseFontSize
  8819. // 使用响应式的缩放比例计算,与主图保持一致
  8820. const scaleX = (1105 * scaleFactor) / 960
  8821. const scaleY = (603 * scaleFactor) / 540
  8822. const style = {
  8823. position: 'absolute',
  8824. left: ((element.left || element.x || 0) * scaleX) + 'px',
  8825. top: ((element.top || element.y || 0) * scaleY) + 'px',
  8826. width: ((element.width || 100) * scaleX) + 'px',
  8827. height: ((element.height || 50) * scaleY) + 'px',
  8828. zIndex: element.zIndex || 1
  8829. }
  8830. // 添加与主图相同的样式属性
  8831. if (element.defaultColor) {
  8832. style.color = element.defaultColor
  8833. }
  8834. if (element.defaultFontName) {
  8835. style.fontFamily = element.defaultFontName
  8836. }
  8837. // 添加透明度支持
  8838. if (element.opacity !== undefined) {
  8839. style.opacity = element.opacity
  8840. }
  8841. // 处理形状元素的背景色
  8842. if (element.type === 'shape' && element.fill) {
  8843. style.backgroundColor = element.fill
  8844. style.borderRadius = element.viewBox ? '8px' : '0'
  8845. }
  8846. // 处理文本元素的对齐方式
  8847. if (element.type === 'text' && element.content) {
  8848. // 从HTML内容中提取text-align样式
  8849. const textAlignMatch = element.content.match(/text-align:\s*([^;]+)/)
  8850. if (textAlignMatch) {
  8851. style.textAlign = textAlignMatch[1].trim()
  8852. }
  8853. // 特殊处理目录项:确保居中显示
  8854. if (element.textType === 'item' && element.content.includes('目录项')) {
  8855. style.textAlign = 'center'
  8856. }
  8857. }
  8858. return style
  8859. }
  8860. // 获取响应式字体大小
  8861. const getResponsiveFontSize = (baseFontSize) => {
  8862. const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
  8863. const baseRootFontSize = 192 // amfe-flexible的基础字体大小 (1920px设计稿 / 10)
  8864. const scaleFactor = rootFontSize / baseRootFontSize
  8865. return Math.round(baseFontSize * scaleFactor)
  8866. }
  8867. // 监听窗口大小变化,重新计算PPT元素样式
  8868. const handleWindowResize = () => {
  8869. // 强制重新渲染PPT预览
  8870. if (generatedPPT.value.length > 0) {
  8871. // 触发响应式更新
  8872. generatedPPT.value = [...generatedPPT.value]
  8873. }
  8874. }
  8875. // 添加窗口大小变化监听器
  8876. onMounted(() => {
  8877. window.addEventListener('resize', handleWindowResize)
  8878. // 自动启动下载监听(因为默认选中) - 注释掉,因为版本没有步骤四
  8879. // if (isDownloadListenerActive.value) {
  8880. // startDownloadListener()
  8881. // }
  8882. })
  8883. onUnmounted(() => {
  8884. window.removeEventListener('resize', handleWindowResize)
  8885. // 清理下载监听插件 - 注释掉,因为版本没有步骤四
  8886. // if (isDownloadListenerActive.value) {
  8887. // stopDownloadListener()
  8888. // }
  8889. })
  8890. const getElementStyle = (element) => {
  8891. // 获取当前根字体大小,用于响应amfe-flexible缩放
  8892. const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
  8893. const baseFontSize = 192 // amfe-flexible的基础字体大小 (1920px设计稿 / 10)
  8894. const scaleFactor = rootFontSize / baseFontSize
  8895. // 使用响应式的目标尺寸,考虑amfe-flexible缩放
  8896. const targetWidth = 1105 * scaleFactor // slide-preview 的 max-width
  8897. const targetHeight = 603 * scaleFactor // slide-preview 的 height
  8898. // 原始PPT尺寸 (960x540)
  8899. const originalWidth = 960
  8900. const originalHeight = 540
  8901. // 计算响应式的缩放比例
  8902. const scaleX = targetWidth / originalWidth
  8903. const scaleY = targetHeight / originalHeight
  8904. return {
  8905. position: 'absolute',
  8906. left: ((element.left || element.x) * scaleX) + 'px',
  8907. top: ((element.top || element.y) * scaleY) + 'px',
  8908. width: (element.width * scaleX) + 'px',
  8909. height: (element.height * scaleY) + 'px',
  8910. zIndex: element.zIndex || 1
  8911. }
  8912. }
  8913. const getElementTypeName = (type) => {
  8914. const typeNames = {
  8915. 'text': '文本',
  8916. 'image': '图片',
  8917. 'shape': '形状'
  8918. }
  8919. return typeNames[type] || type
  8920. }
  8921. const getShapeStyle = (element) => {
  8922. return {
  8923. width: '100%',
  8924. height: '100%',
  8925. backgroundColor: element.fill,
  8926. borderRadius: element.viewBox ? '8px' : '0',
  8927. opacity: element.opacity || 1
  8928. }
  8929. }
  8930. const selectPPTElement = (index) => {
  8931. selectedPPTElementIndex.value = index
  8932. console.log('选中PPT元素:', index)
  8933. }
  8934. const handleDoubleClick = (index) => {
  8935. editingPPTElementIndex.value = index
  8936. const element = getCurrentPPTSlide().elements[index]
  8937. editingPPTHtml.value = element.content
  8938. console.log('开始编辑PPT元素:', index)
  8939. }
  8940. // 禁用的双击处理函数(PPT预览模式下不允许文字编辑)
  8941. const handleDoubleClickDisabled = (index) => {
  8942. console.log('PPT预览模式下不允许文字编辑')
  8943. ElMessage.info('PPT预览模式下不允许编辑文字,只能更换图片')
  8944. }
  8945. const onPPTInlineInput = (event) => {
  8946. editingPPTHtml.value = event.target.innerHTML
  8947. }
  8948. const savePPTInlineEdit = (index) => {
  8949. const slide = getCurrentPPTSlide()
  8950. if (slide.elements[index]) {
  8951. slide.elements[index].content = editingPPTHtml.value
  8952. console.log('文本编辑保存成功')
  8953. ElMessage.success('文本编辑已保存')
  8954. // 自动保存修改后的PPT数据
  8955. saveModifiedPPTData()
  8956. }
  8957. editingPPTElementIndex.value = -1
  8958. editingPPTHtml.value = ''
  8959. console.log('保存PPT元素编辑:', index)
  8960. }
  8961. const startPPTDrag = (event, index) => {
  8962. // 如果是双击事件,不启动拖拽
  8963. if (event.detail >= 2) {
  8964. return
  8965. }
  8966. // 获取当前元素
  8967. const slide = getCurrentPPTSlide()
  8968. const element = slide.elements[index]
  8969. // PPT预览模式下,文字元素不允许拖拽
  8970. if (element.type === 'text') {
  8971. console.log('PPT预览模式下,文字元素不允许拖拽')
  8972. return
  8973. }
  8974. // 如果当前正在编辑文字元素,不启动拖拽
  8975. if (editingPPTElementIndex.value === index && element.type === 'text' && editingPPTHtml.value !== '') {
  8976. return
  8977. }
  8978. if (selectedPPTElementIndex.value !== index) {
  8979. selectPPTElement(index)
  8980. }
  8981. // 开始拖拽
  8982. const startX = event.clientX
  8983. const startY = event.clientY
  8984. const startLeft = element.left
  8985. const startTop = element.top
  8986. const onMouseMove = (moveEvent) => {
  8987. const deltaX = moveEvent.clientX - startX
  8988. const deltaY = moveEvent.clientY - startY
  8989. element.left = startLeft + deltaX
  8990. element.top = startTop + deltaY
  8991. }
  8992. const onMouseUp = () => {
  8993. document.removeEventListener('mousemove', onMouseMove)
  8994. document.removeEventListener('mouseup', onMouseUp)
  8995. // 自动保存修改
  8996. saveModifiedPPTData()
  8997. console.log('拖拽完成,已自动保存')
  8998. }
  8999. document.addEventListener('mousemove', onMouseMove)
  9000. document.addEventListener('mouseup', onMouseUp)
  9001. event.preventDefault()
  9002. }
  9003. const startPPTResize = (event, index, direction) => {
  9004. if (selectedPPTElementIndex.value !== index) {
  9005. selectPPTElement(index)
  9006. }
  9007. const slide = getCurrentPPTSlide()
  9008. const element = slide.elements[index]
  9009. // PPT预览模式下,文字元素不允许调整大小
  9010. if (element.type === 'text') {
  9011. console.log('PPT预览模式下,文字元素不允许调整大小')
  9012. return
  9013. }
  9014. const startX = event.clientX
  9015. const startY = event.clientY
  9016. const startWidth = element.width
  9017. const startHeight = element.height
  9018. const startLeft = element.left
  9019. const startTop = element.top
  9020. const onMouseMove = (moveEvent) => {
  9021. const deltaX = moveEvent.clientX - startX
  9022. const deltaY = moveEvent.clientY - startY
  9023. switch (direction) {
  9024. case 'se': // 右下角
  9025. element.width = Math.max(20, startWidth + deltaX)
  9026. element.height = Math.max(20, startHeight + deltaY)
  9027. break
  9028. case 'sw': // 左下角
  9029. element.width = Math.max(20, startWidth - deltaX)
  9030. element.height = Math.max(20, startHeight + deltaY)
  9031. element.left = startLeft + (startWidth - element.width)
  9032. break
  9033. case 'ne': // 右上角
  9034. element.width = Math.max(20, startWidth + deltaX)
  9035. element.height = Math.max(20, startHeight - deltaY)
  9036. element.top = startTop + (startHeight - element.height)
  9037. break
  9038. case 'nw': // 左上角
  9039. element.width = Math.max(20, startWidth - deltaX)
  9040. element.height = Math.max(20, startHeight - deltaY)
  9041. element.left = startLeft + (startWidth - element.width)
  9042. element.top = startTop + (startHeight - element.height)
  9043. break
  9044. }
  9045. }
  9046. const onMouseUp = () => {
  9047. document.removeEventListener('mousemove', onMouseMove)
  9048. document.removeEventListener('mouseup', onMouseUp)
  9049. // 自动保存修改
  9050. saveModifiedPPTData()
  9051. console.log('缩放完成,已自动保存')
  9052. }
  9053. document.addEventListener('mousemove', onMouseMove)
  9054. document.addEventListener('mouseup', onMouseUp)
  9055. event.preventDefault()
  9056. }
  9057. // 将图片文件转换为base64
  9058. const convertFileToBase64 = (file) => {
  9059. return new Promise((resolve, reject) => {
  9060. const reader = new FileReader()
  9061. reader.onload = () => {
  9062. const result = reader.result
  9063. // 验证base64数据
  9064. if (!result || typeof result !== 'string') {
  9065. reject(new Error('图片读取结果无效'))
  9066. return
  9067. }
  9068. if (!result.startsWith('data:image/')) {
  9069. reject(new Error('不是有效的图片数据'))
  9070. return
  9071. }
  9072. if (!result.includes(',')) {
  9073. reject(new Error('Base64数据格式错误'))
  9074. return
  9075. }
  9076. const [, data] = result.split(',')
  9077. if (!data || data.length < 100) {
  9078. reject(new Error('Base64数据过短'))
  9079. return
  9080. }
  9081. console.log('图片转换成功:', {
  9082. fileName: file.name,
  9083. fileSize: file.size,
  9084. base64Length: result.length,
  9085. dataLength: data.length
  9086. })
  9087. resolve(result)
  9088. }
  9089. reader.onerror = () => {
  9090. reject(new Error('图片读取失败: ' + reader.error?.message))
  9091. }
  9092. reader.readAsDataURL(file)
  9093. })
  9094. }
  9095. // 高质量图片转换函数(使用Canvas保持原始质量)
  9096. const convertFileToHighQualityBase64 = (file, targetWidth = null, targetHeight = null) => {
  9097. return new Promise((resolve, reject) => {
  9098. const reader = new FileReader()
  9099. reader.onload = () => {
  9100. const img = new Image()
  9101. img.onload = () => {
  9102. try {
  9103. // 创建Canvas
  9104. const canvas = document.createElement('canvas')
  9105. const ctx = canvas.getContext('2d')
  9106. let canvasWidth, canvasHeight
  9107. if (targetWidth && targetHeight) {
  9108. // 如果有目标尺寸,使用cover模式(填满目标区域,超出部分裁剪)
  9109. canvasWidth = targetWidth
  9110. canvasHeight = targetHeight
  9111. } else {
  9112. // 没有目标尺寸,保持原始尺寸
  9113. canvasWidth = img.width
  9114. canvasHeight = img.height
  9115. }
  9116. // 设置Canvas尺寸
  9117. canvas.width = canvasWidth
  9118. canvas.height = canvasHeight
  9119. // 设置高质量渲染
  9120. ctx.imageSmoothingEnabled = true
  9121. ctx.imageSmoothingQuality = 'high'
  9122. // 绘制图片到Canvas,使用cover模式(填满目标区域,超出部分裁剪)
  9123. if (targetWidth && targetHeight) {
  9124. // 计算cover模式的缩放比例和偏移
  9125. const aspectRatio = img.width / img.height
  9126. const targetAspectRatio = targetWidth / targetHeight
  9127. let sourceWidth, sourceHeight, sourceX, sourceY
  9128. if (aspectRatio > targetAspectRatio) {
  9129. // 图片更宽,以高度为准,裁剪左右
  9130. sourceHeight = img.height
  9131. sourceWidth = img.height * targetAspectRatio
  9132. sourceX = (img.width - sourceWidth) / 2
  9133. sourceY = 0
  9134. } else {
  9135. // 图片更高,以宽度为准,裁剪上下
  9136. sourceWidth = img.width
  9137. sourceHeight = img.width / targetAspectRatio
  9138. sourceX = 0
  9139. sourceY = (img.height - sourceHeight) / 2
  9140. }
  9141. // 绘制裁剪后的图片到目标尺寸
  9142. ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, canvasWidth, canvasHeight)
  9143. } else {
  9144. // 没有目标尺寸,直接绘制原始图片
  9145. ctx.drawImage(img, 0, 0)
  9146. }
  9147. // 转换为base64,使用最高质量
  9148. const base64 = canvas.toDataURL('image/png', 1.0) // 使用PNG格式,质量1.0
  9149. console.log('高质量图片转换成功:', {
  9150. fileName: file.name,
  9151. fileSize: file.size,
  9152. originalSize: `${img.width}x${img.height}`,
  9153. canvasSize: `${canvasWidth}x${canvasHeight}`,
  9154. targetSize: targetWidth && targetHeight ? `${targetWidth}x${targetHeight}` : 'none',
  9155. base64Length: base64.length,
  9156. format: 'PNG'
  9157. })
  9158. resolve(base64)
  9159. } catch (error) {
  9160. console.error('Canvas转换失败,使用原始方法:', error)
  9161. // 如果Canvas转换失败,回退到原始方法
  9162. resolve(reader.result)
  9163. }
  9164. }
  9165. img.onerror = () => {
  9166. console.error('图片加载失败,使用原始方法')
  9167. // 如果图片加载失败,回退到原始方法
  9168. resolve(reader.result)
  9169. }
  9170. img.src = reader.result
  9171. }
  9172. reader.onerror = () => {
  9173. reject(new Error('图片读取失败: ' + reader.error?.message))
  9174. }
  9175. reader.readAsDataURL(file)
  9176. })
  9177. }
  9178. const changePPTImage = async (index) => {
  9179. console.log('更换PPT图片被触发,索引:', index)
  9180. const input = document.createElement('input')
  9181. input.type = 'file'
  9182. input.accept = 'image/*'
  9183. input.onchange = async (event) => {
  9184. const file = event.target.files[0]
  9185. if (file) {
  9186. try {
  9187. // 检查文件大小(限制为5MB)
  9188. const maxSize = 5 * 1024 * 1024 // 5MB
  9189. if (file.size > maxSize) {
  9190. ElMessage.error('图片大小不能超过5MB')
  9191. return
  9192. }
  9193. // 显示处理进度
  9194. ElMessage.info('正在处理图片...')
  9195. // 获取当前图片元素的目标尺寸
  9196. const slide = getCurrentPPTSlide()
  9197. const element = slide.elements[index]
  9198. const targetWidth = element.width
  9199. const targetHeight = element.height
  9200. console.log('图片目标尺寸:', { targetWidth, targetHeight })
  9201. // 将图片转换为高质量base64,保持宽高比
  9202. const base64Data = await convertFileToHighQualityBase64(file, targetWidth, targetHeight)
  9203. console.log('图片转换为高质量base64成功,长度:', base64Data.length)
  9204. // 调试信息:显示base64数据的前100个字符
  9205. console.log('Base64数据预览:', base64Data.substring(0, 100) + '...')
  9206. // 更新PPT元素
  9207. if (element && element.type === 'image') {
  9208. element.src = base64Data
  9209. console.log('图片更换成功,使用base64数据')
  9210. ElMessage.success('图片更换成功')
  9211. // 自动保存修改后的PPT数据
  9212. saveModifiedPPTData()
  9213. // 立即保存到后端(不更新封面图)
  9214. try {
  9215. console.log('开始保存图片更换后的PPT数据到后端...')
  9216. await saveStepToBackend(false) // 传入false,不更新封面图
  9217. console.log('图片更换后的PPT数据已保存到后端')
  9218. // 立即更新历史记录列表中的数据
  9219. const currentHistoryItem = historyData.value.find(item => item.id === ai_conversation_id.value)
  9220. if (currentHistoryItem) {
  9221. currentHistoryItem.ppt_json_content = JSON.stringify(generatedPPT.value)
  9222. console.log('已更新历史记录列表中的PPT数据')
  9223. }
  9224. } catch (error) {
  9225. console.error('保存图片更换后的PPT数据失败:', error)
  9226. ElMessage.warning('图片更换成功,但保存到后端失败')
  9227. }
  9228. }
  9229. } catch (error) {
  9230. console.error('图片处理过程中发生错误:', error)
  9231. ElMessage.error('图片处理失败: ' + error.message)
  9232. }
  9233. }
  9234. }
  9235. input.click()
  9236. }
  9237. // 上传图片到服务器
  9238. const uploadImageToServer = async (file) => {
  9239. try {
  9240. console.log('开始上传图片到服务器...')
  9241. // 创建FormData,后端需要的是 'image' 字段
  9242. const formData = new FormData()
  9243. formData.append('image', file)
  9244. // 使用正确的上传接口
  9245. const response = await apis.uploadImage(formData)
  9246. console.log('图片上传响应:', response)
  9247. if (response.statusCode === 200) {
  9248. console.log('图片上传成功:', response.fileUrl)
  9249. return {
  9250. statusCode: 200,
  9251. fileUrl: response.fileUrl,
  9252. message: '上传成功'
  9253. }
  9254. } else {
  9255. console.error('图片上传失败:', response)
  9256. return {
  9257. statusCode: response.statusCode || 500,
  9258. message: response.message || '上传失败'
  9259. }
  9260. }
  9261. } catch (error) {
  9262. console.error('图片上传过程中发生错误:', error)
  9263. return {
  9264. statusCode: 500,
  9265. message: error.message
  9266. }
  9267. }
  9268. }
  9269. // 处理图片选择
  9270. const handleImageSelect = (event) => {
  9271. const file = event.target.files[0]
  9272. if (!file) return
  9273. // 检查文件类型
  9274. if (!file.type.startsWith('image/')) {
  9275. ElMessage.warning('请选择图片文件')
  9276. event.target.value = ''
  9277. return
  9278. }
  9279. // 检查文件大小(5MB限制)
  9280. const maxSize = 5 * 1024 * 1024
  9281. if (file.size > maxSize) {
  9282. ElMessage.warning('图片大小不能超过5MB')
  9283. event.target.value = ''
  9284. return
  9285. }
  9286. try {
  9287. // 创建本地URL
  9288. const imageUrl = URL.createObjectURL(file)
  9289. // 更新选中的图片元素
  9290. if (selectedImageIndex.value !== null && generatedPPT.value.length > 0) {
  9291. const currentSlide = getCurrentPPTSlide()
  9292. if (currentSlide && currentSlide.elements[selectedImageIndex.value]) {
  9293. currentSlide.elements[selectedImageIndex.value].src = imageUrl
  9294. console.log('图片已更换:', imageUrl)
  9295. ElMessage.success('图片更换成功')
  9296. }
  9297. }
  9298. // 清理选中的图片索引
  9299. selectedImageIndex.value = null
  9300. } catch (error) {
  9301. console.error('图片更换失败:', error)
  9302. ElMessage.error('图片更换失败,请重试')
  9303. } finally {
  9304. event.target.value = ''
  9305. }
  9306. }
  9307. // 将大纲数据转换为AIPPT.json格式
  9308. const convertOutlineToAIPPT = (outlineData, outlineTitle) => {
  9309. try {
  9310. console.log('开始转换大纲为AIPPT.json格式')
  9311. console.log('输入大纲数据:', outlineData)
  9312. console.log('标题:', outlineTitle)
  9313. const aipptSlides = []
  9314. // 1. 添加封面页
  9315. aipptSlides.push({
  9316. type: 'cover',
  9317. data: {
  9318. title: outlineTitle || '安全培训大纲',
  9319. text: '基于AI生成的培训大纲,包含相关内容'
  9320. }
  9321. })
  9322. console.log('已添加封面页')
  9323. // 2. 生成目录项
  9324. const tocItems = outlineData.map((chapter, index) => chapter.title)
  9325. console.log('目录项:', tocItems)
  9326. // 添加目录页
  9327. aipptSlides.push({
  9328. type: 'contents',
  9329. data: {
  9330. items: tocItems
  9331. }
  9332. })
  9333. console.log('已添加目录页')
  9334. // 3. 遍历章节生成幻灯片
  9335. outlineData.forEach((chapter, chapterIndex) => {
  9336. console.log(`处理章节 ${chapterIndex + 1}:`, chapter.title)
  9337. console.log(`章节内容:`, chapter)
  9338. // 添加章节过渡页
  9339. aipptSlides.push({
  9340. type: 'transition',
  9341. data: {
  9342. title: chapter.title,
  9343. text: chapter.content || `本章将介绍${chapter.title}的相关内容`
  9344. }
  9345. })
  9346. console.log(`已添加章节 ${chapterIndex + 1} 的过渡页`)
  9347. // 遍历小节,每个小节生成一个内容页(恢复昨天的逻辑)
  9348. if (chapter.sections && chapter.sections.length > 0) {
  9349. chapter.sections.forEach((section, sectionIndex) => {
  9350. console.log(`处理小节 ${sectionIndex + 1}:`, section.title)
  9351. console.log(`小节内容:`, section)
  9352. // 添加内容页
  9353. let contentItems = []
  9354. // 添加小节内容(保持昨天的逻辑,让AI填充)
  9355. if (section.subsections && section.subsections.length > 0) {
  9356. section.subsections.forEach((subsection, subsectionIndex) => {
  9357. contentItems.push({
  9358. title: '',
  9359. text: ''
  9360. })
  9361. })
  9362. } else {
  9363. // 如果没有子小节,使用小节的内容
  9364. contentItems.push({
  9365. title: '',
  9366. text: ''
  9367. })
  9368. }
  9369. // 确保每个content页面至少有4个items(参考AIPPT.json格式)
  9370. while (contentItems.length < 4) {
  9371. // 添加空的items,让AI填充
  9372. contentItems.push({
  9373. title: '',
  9374. text: ''
  9375. })
  9376. }
  9377. // 限制最多4个items(与AIPPT.json保持一致)
  9378. contentItems = contentItems.slice(0, 4)
  9379. aipptSlides.push({
  9380. type: 'content',
  9381. data: {
  9382. title: section.title,
  9383. items: contentItems
  9384. }
  9385. })
  9386. console.log(`已添加小节 ${sectionIndex + 1} 的内容页`)
  9387. })
  9388. } else {
  9389. // 如果章节没有小节,添加空的内容页
  9390. const emptyContentItems = [
  9391. {
  9392. title: '',
  9393. text: ''
  9394. },
  9395. {
  9396. title: '',
  9397. text: ''
  9398. },
  9399. {
  9400. title: '',
  9401. text: ''
  9402. },
  9403. {
  9404. title: '',
  9405. text: ''
  9406. }
  9407. ]
  9408. aipptSlides.push({
  9409. type: 'content',
  9410. data: {
  9411. title: chapter.title,
  9412. items: emptyContentItems
  9413. }
  9414. })
  9415. console.log(`已添加章节 ${chapterIndex + 1} 的空内容页`)
  9416. }
  9417. })
  9418. // 添加结束页
  9419. aipptSlides.push({
  9420. type: 'end'
  9421. })
  9422. console.log('转换完成,生成的AIPPT.json格式数据:')
  9423. console.log(JSON.stringify(aipptSlides, null, 2))
  9424. // 统计各类型幻灯片数量
  9425. const slideTypes = {}
  9426. aipptSlides.forEach(slide => {
  9427. slideTypes[slide.type] = (slideTypes[slide.type] || 0) + 1
  9428. })
  9429. console.log('幻灯片类型统计:', slideTypes)
  9430. console.log('总幻灯片数量:', aipptSlides.length)
  9431. return aipptSlides
  9432. } catch (error) {
  9433. console.error('转换大纲为AIPPT.json格式失败:', error)
  9434. return []
  9435. }
  9436. }
  9437. // 加载AIPPT.json数据
  9438. const loadAIPPTData = async (convertedAIPPTData = null, shouldSaveStep = false) => {
  9439. try {
  9440. console.log('开始加载template5数据...')
  9441. // 使用导入的template5.json数据
  9442. console.log('template5.json数据加载成功:', template5JsonData)
  9443. // 根据当前大纲数据生成AIPPT格式的数据
  9444. let aipptData
  9445. if (convertedAIPPTData) {
  9446. // 使用传入的转换后的AIPPT数据
  9447. aipptData = convertedAIPPTData
  9448. console.log('使用传入的AIPPT数据:', aipptData)
  9449. } else if (outlineData.value && outlineData.value.length > 0) {
  9450. // 使用当前大纲数据生成AIPPT格式
  9451. aipptData = convertOutlineToAIPPT(outlineData.value, outlineTitle.value)
  9452. console.log('根据大纲生成的AIPPT数据:', aipptData)
  9453. } else {
  9454. // 如果没有大纲数据,使用导入的默认AIPPT.json数据
  9455. aipptData = aipptJsonData
  9456. console.log('加载默认AIPPT.json数据:', aipptData)
  9457. }
  9458. // 将AIPPT.json数据与template5模板结合
  9459. const convertedSlides = aipptData.map((slide, slideIndex) => {
  9460. // 根据slide.type找到对应的template5模板
  9461. const templateSlide = template5JsonData.find(t => t.type === slide.type)
  9462. if (templateSlide) {
  9463. // 复制模板结构,包括背景、图片、形状等
  9464. const newSlide = JSON.parse(JSON.stringify(templateSlide))
  9465. // 更新幻灯片ID
  9466. newSlide.id = `generated-slide-${slideIndex}`
  9467. // 根据不同类型填充内容
  9468. switch (slide.type) {
  9469. case 'cover':
  9470. newSlide.elements.forEach(element => {
  9471. if (element.textType === 'title') {
  9472. element.content = `<p style="text-align: center;"><strong><span style="font-size: 48px; color: ${element.defaultColor}; text-shadow: 2px 2px 8px rgba(0,0,0,0.5);">${slide.data.title}</span></strong></p>`
  9473. } else if (element.textType === 'content') {
  9474. element.content = `<p style="text-align: center;"><span style="font-size: 24px; color: ${element.defaultColor};">${slide.data.text}</span></p>`
  9475. }
  9476. })
  9477. break
  9478. case 'contents':
  9479. if (slide.data.items) {
  9480. let itemIndex = 0
  9481. newSlide.elements.forEach(element => {
  9482. if (element.textType === 'item' && itemIndex < slide.data.items.length) {
  9483. const itemText = slide.data.items[itemIndex]
  9484. element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">${itemIndex + 1}. ${itemText}</span></p>`
  9485. itemIndex++
  9486. }
  9487. })
  9488. }
  9489. break
  9490. case 'content':
  9491. newSlide.elements.forEach(element => {
  9492. if (element.textType === 'title') {
  9493. element.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: ${element.defaultColor};">${slide.data.title}</span></strong></p>`
  9494. }
  9495. })
  9496. if (slide.data.items && Array.isArray(slide.data.items)) {
  9497. let itemIndex = 0
  9498. newSlide.elements.forEach(element => {
  9499. if (element.textType === 'itemTitle' && itemIndex < slide.data.items.length) {
  9500. const item = slide.data.items[itemIndex]
  9501. element.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: ${element.defaultColor};">${item.title}</span></strong></p>`
  9502. } else if (element.textType === 'itemContent' && itemIndex < slide.data.items.length) {
  9503. const item = slide.data.items[itemIndex]
  9504. element.content = `<p style="text-align: center;"><span style="font-size: 14px; color: ${element.defaultColor};">${item.text}</span></p>`
  9505. itemIndex++
  9506. }
  9507. })
  9508. }
  9509. break
  9510. case 'transition':
  9511. newSlide.elements.forEach(element => {
  9512. if (element.textType === 'title') {
  9513. element.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: ${element.defaultColor};">${slide.data.title}</span></strong></p>`
  9514. } else if (element.textType === 'content') {
  9515. element.content = `<p style="text-align: center;"><span style="font-size: 16px; color: ${element.defaultColor};">${slide.data.text}</span></p>`
  9516. }
  9517. })
  9518. break
  9519. case 'end':
  9520. newSlide.elements.forEach(element => {
  9521. if (element.textType === 'title') {
  9522. element.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: ${element.defaultColor};">谢谢聆听</span></strong></p>`
  9523. } else if (element.textType === 'content') {
  9524. element.content = `<p style="text-align: center;"><span style="font-size: 18px; color: ${element.defaultColor};">感谢您的时间与关注</span></p>`
  9525. }
  9526. })
  9527. break
  9528. }
  9529. return newSlide
  9530. } else {
  9531. // 如果没有找到对应的模板,使用默认模板
  9532. console.warn(`未找到类型为 ${slide.type} 的模板,使用默认模板`)
  9533. return {
  9534. id: `generated-slide-${slideIndex}`,
  9535. type: slide.type,
  9536. elements: [{
  9537. id: `title-${slideIndex}`,
  9538. type: 'text',
  9539. content: `<h1 style="text-align: center; font-size: 48px; color: #1F2937;">${slide.data.title || '标题'}</h1>`,
  9540. left: 100,
  9541. top: 150,
  9542. width: 760,
  9543. height: 100,
  9544. defaultColor: '#1F2937',
  9545. defaultFontName: 'Arial',
  9546. zIndex: 1
  9547. }],
  9548. background: { color: '#ffffff' }
  9549. }
  9550. }
  9551. })
  9552. // 直接设置生成的PPT数据
  9553. generatedPPT.value = convertedSlides
  9554. console.log('已设置生成的PPT数据:', generatedPPT.value.length, '张幻灯片')
  9555. isPPTPreviewMode.value = true
  9556. currentPPTSlideIndex.value = 0
  9557. console.log('AIPPT数据与template5模板结合完成:', generatedPPT.value)
  9558. console.log('PPT预览模式已启用:', isPPTPreviewMode.value)
  9559. console.log('当前PPT幻灯片索引:', currentPPTSlideIndex.value)
  9560. console.log('生成的PPT数据长度:', generatedPPT.value.length)
  9561. // 强制触发Vue响应式更新
  9562. await nextTick()
  9563. console.log('Vue响应式更新完成')
  9564. // 只有在需要保存步骤时才调用保存API
  9565. if (shouldSaveStep) {
  9566. console.log('需要保存步骤信息到后端')
  9567. await saveStepToBackend(true, true) // 强制保存新生成的PPT
  9568. } else {
  9569. console.log('跳过保存步骤信息,仅加载PPT数据')
  9570. }
  9571. // 确保所有数据处理完成
  9572. return Promise.resolve()
  9573. } catch (error) {
  9574. console.error('加载AIPPT.json或template5.json失败:', error)
  9575. // 如果加载失败,使用默认数据
  9576. generateDefaultPPTData()
  9577. return Promise.reject(error)
  9578. }
  9579. }
  9580. // 生成默认PPT数据(备用方案)
  9581. const generateDefaultPPTData = () => {
  9582. generatedPPT.value = [
  9583. {
  9584. id: 'slide-1',
  9585. background: '#ffffff',
  9586. elements: [
  9587. {
  9588. id: 'title-1',
  9589. type: 'text',
  9590. textType: 'title',
  9591. content: '<p style="text-align: center;"><strong><span style="font-size: 48px; color: #1F2937;">犯罪心理学研究</span></strong></p>',
  9592. x: 100,
  9593. y: 100,
  9594. width: 600,
  9595. height: 100,
  9596. defaultColor: '#1F2937',
  9597. defaultFontName: 'Arial',
  9598. zIndex: 1
  9599. },
  9600. {
  9601. id: 'content-1',
  9602. type: 'text',
  9603. textType: 'content',
  9604. content: '<p style="text-align: center;"><span style="font-size: 20px; color: #6B7280;">探索犯罪心理的成因、特征及干预策略</span></p>',
  9605. x: 100,
  9606. y: 250,
  9607. width: 600,
  9608. height: 50,
  9609. defaultColor: '#6B7280',
  9610. defaultFontName: 'Arial',
  9611. zIndex: 1
  9612. }
  9613. ]
  9614. }
  9615. ]
  9616. isPPTPreviewMode.value = true
  9617. currentPPTSlideIndex.value = 0
  9618. console.log('默认PPT数据生成完成:', generatedPPT.value)
  9619. }
  9620. // 步骤四相关方法
  9621. const selectDownloadOption = (index) => {
  9622. selectedDownloadOption.value = index
  9623. console.log('选择下载选项:', downloadOptions.value[index].title)
  9624. }
  9625. const downloadNow = () => {
  9626. console.log('立即下载:', downloadOptions.value[selectedDownloadOption.value].title)
  9627. // 这里可以添加下载逻辑
  9628. }
  9629. const goBackToTemplate = () => {
  9630. showDownloadOptions.value = false
  9631. selectedDownloadOption.value = 0
  9632. // 清理PPT预览相关状态
  9633. generatedPPT.value = []
  9634. currentPPTSlideIndex.value = 0
  9635. selectedPPTElementIndex.value = -1
  9636. editingPPTElementIndex.value = -1
  9637. editingPPTHtml.value = ''
  9638. zoom.value = 1
  9639. selectedImageIndex.value = null
  9640. // 重置为模板预览状态
  9641. currentSlideIndex.value = 0
  9642. // 重新加载模板预览数据
  9643. loadLocalPPT()
  9644. updateSlideThumbnails()
  9645. console.log('已回到模板选择页面,PPT预览状态已清理,模板预览已重新加载')
  9646. }
  9647. // 处理AI回复中的特殊字符和表情符号
  9648. const processAIResponse = (text) => {
  9649. if (!text) return text
  9650. console.log('原始AI回复:', text)
  9651. console.log('原始文本长度:', text.length)
  9652. console.log('原始文本字符码:', Array.from(text).map(char => char.charCodeAt(0)))
  9653. try {
  9654. // 方法1:尝试直接解码
  9655. if (text.includes('%')) {
  9656. const decoded = decodeURIComponent(text)
  9657. console.log('URL解码后:', decoded)
  9658. return decoded
  9659. }
  9660. // 方法2:处理可能的编码问题
  9661. if (text.includes('??')) {
  9662. // 如果包含问号,尝试修复编码
  9663. const cleaned = text.replace(/\?\?/g, '')
  9664. console.log('清理问号后:', cleaned)
  9665. return cleaned
  9666. }
  9667. // 方法3:处理Unicode转义序列
  9668. if (text.includes('\\u')) {
  9669. const unicodeDecoded = text.replace(/\\u[\dA-F]{4}/gi, (match) => {
  9670. return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16))
  9671. })
  9672. console.log('Unicode解码后:', unicodeDecoded)
  9673. return unicodeDecoded
  9674. }
  9675. // 方法4:处理HTML实体
  9676. if (text.includes('&')) {
  9677. const textarea = document.createElement('textarea')
  9678. textarea.innerHTML = text
  9679. const htmlDecoded = textarea.value
  9680. console.log('HTML解码后:', htmlDecoded)
  9681. return htmlDecoded
  9682. }
  9683. console.log('无需特殊处理,直接返回')
  9684. return text
  9685. } catch (error) {
  9686. console.warn('字符编码处理失败:', error)
  9687. return text
  9688. }
  9689. }
  9690. // 安全的HTML清理函数
  9691. const sanitizeHtml = (html) => {
  9692. if (!html) return html
  9693. // 只允许安全的HTML标签
  9694. const allowedTags = {
  9695. 'br': {},
  9696. 'strong': {},
  9697. 'em': {},
  9698. 'h1': {},
  9699. 'h2': {},
  9700. 'h3': {},
  9701. 'h4': {},
  9702. 'h5': {},
  9703. 'h6': {},
  9704. 'ul': {},
  9705. 'li': {},
  9706. 'code': {}
  9707. }
  9708. // 创建临时DOM元素来清理HTML
  9709. const tempDiv = document.createElement('div')
  9710. tempDiv.innerHTML = html
  9711. // 递归清理节点
  9712. const cleanNode = (node) => {
  9713. if (node.nodeType === Node.TEXT_NODE) {
  9714. return node.textContent
  9715. }
  9716. if (node.nodeType === Node.ELEMENT_NODE) {
  9717. const tagName = node.tagName.toLowerCase()
  9718. // 只保留允许的标签
  9719. if (allowedTags[tagName]) {
  9720. const cleanElement = document.createElement(tagName)
  9721. // 递归处理子节点
  9722. for (let child of node.childNodes) {
  9723. const cleanChild = cleanNode(child)
  9724. if (cleanChild) {
  9725. if (typeof cleanChild === 'string') {
  9726. cleanElement.appendChild(document.createTextNode(cleanChild))
  9727. } else {
  9728. cleanElement.appendChild(cleanChild)
  9729. }
  9730. }
  9731. }
  9732. return cleanElement.outerHTML
  9733. } else {
  9734. // 不允许的标签,只保留文本内容
  9735. let textContent = ''
  9736. for (let child of node.childNodes) {
  9737. textContent += cleanNode(child) || ''
  9738. }
  9739. return textContent
  9740. }
  9741. }
  9742. return ''
  9743. }
  9744. // 清理整个HTML
  9745. let cleanHtml = ''
  9746. for (let child of tempDiv.childNodes) {
  9747. cleanHtml += cleanNode(child) || ''
  9748. }
  9749. return cleanHtml
  9750. }
  9751. // 将Markdown格式转换为HTML格式
  9752. const markdownToHtml = (text) => {
  9753. if (!text) return text
  9754. console.log('开始转换Markdown:', text)
  9755. let html = text
  9756. // 清理可能的HTML标签残留
  9757. html = html.replace(/<\/?[^>]*>/g, '')
  9758. console.log('清理HTML标签后:', html)
  9759. // 处理加粗文本 **text** -> <strong>text</strong>
  9760. html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  9761. // 处理斜体文本 *text* -> <em>text</em>
  9762. html = html.replace(/\*(.*?)\*/g, '<em>$1</em>')
  9763. // 处理标题 - 支持行首的标题(井号后可以有空格也可以没有空格)
  9764. html = html.replace(/^#{1,6}\s*(.+?)$/gm, (match, content) => {
  9765. console.log('标题匹配:', match, '内容:', content)
  9766. // 检查井号的数量来确定标题级别
  9767. const hashCount = (match.match(/#/g) || []).length
  9768. const level = Math.min(hashCount, 6)
  9769. const result = `<h${level}>${content.trim()}</h${level}>`
  9770. console.log('标题转换结果:', result)
  9771. return result
  9772. })
  9773. // 处理列表项 - 主列表项用2空格缩进,嵌套列表项用8空格缩进
  9774. html = html.replace(/^- (.*?)$/gm, (match, content) => {
  9775. // 检查是否已经有缩进(嵌套列表)
  9776. if (match.startsWith(' - ') || match.startsWith(' - ')) {
  9777. return ' ' + content // 8空格缩进
  9778. }
  9779. return ' ' + content // 2空格缩进
  9780. })
  9781. // 处理数字列表 1. text -> 主列表项用2空格缩进,嵌套列表项用8空格缩进
  9782. html = html.replace(/^\d+\. (.*?)$/gm, (match, content) => {
  9783. // 检查是否已经有缩进(嵌套列表)
  9784. if (match.startsWith(' ') || match.startsWith(' ')) {
  9785. return ' ' + content // 8空格缩进
  9786. }
  9787. return ' ' + content // 2空格缩进
  9788. })
  9789. // 处理换行符 - 在所有Markdown转换完成后进行
  9790. html = html.replace(/\n/g, '<br>')
  9791. // 处理<br>标签后的标题(换行符转换后)
  9792. html = html.replace(/<br>#{1,6}\s*(.+?)(?=<br>|$)/g, (match, content) => {
  9793. console.log('<br>标签后标题匹配:', match, '内容:', content)
  9794. // 检查井号的数量来确定标题级别
  9795. const hashCount = (match.match(/#/g) || []).length
  9796. const level = Math.min(hashCount, 6)
  9797. const result = `<br><h${level}>${content.trim()}</h${level}>`
  9798. console.log('<br>标签后标题转换结果:', result)
  9799. return result
  9800. })
  9801. // 处理代码块 ```code``` -> <code>code</code>
  9802. html = html.replace(/```(.*?)```/gs, '<code>$1</code>')
  9803. // 处理行内代码 `code` -> <code>code</code>
  9804. html = html.replace(/`(.*?)`/g, '<code>$1</code>')
  9805. // 最终清理:确保没有不完整的HTML标签
  9806. html = html.replace(/<\/?[^>]*$/g, '')
  9807. console.log('Markdown转换后:', html)
  9808. // 使用安全的HTML清理函数
  9809. const finalHtml = sanitizeHtml(html)
  9810. console.log('最终清理后:', finalHtml)
  9811. return finalHtml
  9812. }
  9813. // 复制功能
  9814. const copyToClipboard = async (text) => {
  9815. try {
  9816. // 检查 Clipboard API 是否可用
  9817. if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
  9818. try {
  9819. await navigator.clipboard.writeText(text)
  9820. ElMessage.success('复制成功!')
  9821. return
  9822. } catch (clipboardError) {
  9823. console.warn('Clipboard API 失败,使用降级方案:', clipboardError)
  9824. }
  9825. }
  9826. // 降级方案:使用传统复制方法
  9827. const textArea = document.createElement('textarea')
  9828. textArea.value = text
  9829. textArea.style.position = 'fixed'
  9830. textArea.style.left = '-999999px'
  9831. textArea.style.top = '-999999px'
  9832. document.body.appendChild(textArea)
  9833. textArea.focus()
  9834. textArea.select()
  9835. try {
  9836. const successful = document.execCommand('copy')
  9837. if (successful) {
  9838. ElMessage.success('复制成功!')
  9839. } else {
  9840. throw new Error('execCommand 复制失败')
  9841. }
  9842. } catch (execError) {
  9843. console.error('传统复制方法也失败:', execError)
  9844. ElMessage.error('复制失败,请手动选择文本复制')
  9845. } finally {
  9846. document.body.removeChild(textArea)
  9847. }
  9848. } catch (err) {
  9849. console.error('复制失败:', err)
  9850. ElMessage.error('复制失败,请手动选择文本复制')
  9851. }
  9852. }
  9853. // 复制用户消息
  9854. const copyUserMessage = (message) => {
  9855. copyToClipboard(message.content)
  9856. }
  9857. // 复制AI消息
  9858. const copyAIMessage = (message) => {
  9859. // 优先使用displayContent,如果没有则使用content
  9860. const textToCopy = message.displayContent || message.content
  9861. copyToClipboard(textToCopy)
  9862. }
  9863. // 点赞和点踩功能
  9864. const handleThumbsUp = (message) => {
  9865. console.log('点赞消息:', message.id)
  9866. // 如果已经点赞,则取消点赞
  9867. if (message.userFeedback === 'like') {
  9868. message.userFeedback = null
  9869. } else {
  9870. // 设置点赞,取消点踩
  9871. message.userFeedback = 'like'
  9872. }
  9873. // 可以在这里发送反馈给后端
  9874. // sendFeedbackToBackend(message.id, message.userFeedback)
  9875. }
  9876. const handleThumbsDown = (message) => {
  9877. console.log('点踩消息:', message.id)
  9878. // 如果已经点踩,则取消点踩
  9879. if (message.userFeedback === 'dislike') {
  9880. message.userFeedback = null
  9881. } else {
  9882. // 设置点踩,取消点赞
  9883. message.userFeedback = 'dislike'
  9884. }
  9885. // 可以在这里发送反馈给后端
  9886. // sendFeedbackToBackend(message.id, message.userFeedback)
  9887. }
  9888. // 删除弹窗相关方法
  9889. const handleDeleteClick = async (messageIndex) => {
  9890. console.log('点击删除按钮,消息索引:', messageIndex)
  9891. // 这里可以添加删除确认弹窗逻辑
  9892. try {
  9893. await ElMessageBox.confirm('确定要删除这条消息吗?', '确认删除', {
  9894. confirmButtonText: '确定',
  9895. cancelButtonText: '取消',
  9896. type: 'warning'
  9897. })
  9898. chatMessages.value.splice(messageIndex, 1)
  9899. } catch {
  9900. // 用户取消删除
  9901. }
  9902. }
  9903. // 文件输入框引用
  9904. const fileInput = ref(null)
  9905. // 图片输入框引用
  9906. const imageInput = ref(null)
  9907. // 缩略图区域引用
  9908. const thumbnailStrip = ref(null)
  9909. // 语音朗读相关方法
  9910. const handleVoiceRead = (message) => {
  9911. if (speakingMessageId.value === message.id) {
  9912. // 如果正在朗读这条消息,则停止
  9913. stopSpeaking()
  9914. speakingMessageId.value = null
  9915. } else {
  9916. // 如果朗读其他消息,先停止当前朗读
  9917. if (speakingMessageId.value) {
  9918. stopSpeaking()
  9919. }
  9920. // 开始朗读新消息
  9921. const textToRead = message.displayContent || message.content
  9922. if (textToRead && textToRead.trim()) {
  9923. // 清理HTML标签,只朗读纯文本
  9924. const cleanText = textToRead.replace(/<[^>]*>/g, '')
  9925. speakText(cleanText, {
  9926. lang: 'zh-CN',
  9927. rate: 0.9,
  9928. pitch: 1,
  9929. volume: 1
  9930. })
  9931. speakingMessageId.value = message.id
  9932. }
  9933. }
  9934. }
  9935. // Template8蓝色科技主题缩略图生成函数
  9936. const generateTemplate8Thumbnails = () => {
  9937. console.log('开始生成Template8蓝色科技主题缩略图')
  9938. if (!generatedPPT.value || generatedPPT.value.length === 0) {
  9939. console.error('无有效的PPT数据用于生成缩略图')
  9940. return
  9941. }
  9942. // 为Template8使用实际的图片文件作为缩略图
  9943. const template8Images = [
  9944. template8Slide1, // 第1页 - 封面
  9945. template8Slide2, // 第2页 - 目录
  9946. template8Slide3, // 第3页 - 过渡
  9947. template8Slide4, // 第4页 - 内容
  9948. template8Slide5 // 第5页 - 结束
  9949. ]
  9950. const newThumbnails = []
  9951. generatedPPT.value.forEach((slide, index) => {
  9952. // 直接根据索引选择模板8图片,确保每张都不同
  9953. console.log(`Template8缩略图幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
  9954. // 直接使用索引来选择图片,确保每张都不同
  9955. const thumbnailImage = template8Images[index % 5]
  9956. newThumbnails.push(thumbnailImage)
  9957. })
  9958. // 更新slideImages
  9959. slideImages.value = newThumbnails
  9960. console.log('Template8蓝色科技主题缩略图生成完成,共', newThumbnails.length, '张')
  9961. }
  9962. // Template7红色主题缩略图生成函数
  9963. const generateTemplate7Thumbnails = () => {
  9964. console.log('开始生成Template7红色主题缩略图')
  9965. if (!generatedPPT.value || generatedPPT.value.length === 0) {
  9966. console.error('无有效的PPT数据用于生成缩略图')
  9967. return
  9968. }
  9969. // 为Template7使用实际的图片文件作为缩略图
  9970. const template7Images = [
  9971. template7Slide1, // 第1页 - 封面
  9972. template7Slide2, // 第2页 - 目录
  9973. template7Slide3, // 第3页 - 过渡
  9974. template7Slide4, // 第4页 - 内容
  9975. template7Slide5 // 第5页 - 结束
  9976. ]
  9977. const newThumbnails = []
  9978. generatedPPT.value.forEach((slide, index) => {
  9979. // 直接根据索引选择模板7图片,确保每张都不同
  9980. console.log(`Template7缩略图幻灯片 ${index + 1} 类型: ${slide.type || 'content'}, ID: ${slide.id}`)
  9981. // 直接使用索引来选择图片,确保每张都不同
  9982. const thumbnailImage = template7Images[index % 5]
  9983. newThumbnails.push(thumbnailImage)
  9984. })
  9985. // 更新slideImages
  9986. slideImages.value = newThumbnails
  9987. console.log('Template7红色主题缩略图生成完成,共', newThumbnails.length, '张')
  9988. }
  9989. // 生成红色主题缩略图的具体实现
  9990. const generateRedThemeThumbnail = (slide, index) => {
  9991. // 根据幻灯片类型和索引生成红色主题的缩略图
  9992. const slideType = slide.type || 'content'
  9993. // 使用Canvas生成红色主题缩略图
  9994. const canvas = document.createElement('canvas')
  9995. const ctx = canvas.getContext('2d')
  9996. canvas.width = 160
  9997. canvas.height = 90
  9998. // 设置红色主题背景 - 使用更丰富的红色渐变
  9999. const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
  10000. gradient.addColorStop(0, '#E74C3C') // 红色主题的主色调
  10001. gradient.addColorStop(0.5, '#C0392B') // 中等红色
  10002. gradient.addColorStop(1, '#A93226') // 深红色
  10003. ctx.fillStyle = gradient
  10004. ctx.fillRect(0, 0, canvas.width, canvas.height)
  10005. // 添加红色主题装饰元素
  10006. ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
  10007. ctx.beginPath()
  10008. ctx.arc(canvas.width - 20, 20, 15, 0, Math.PI * 2)
  10009. ctx.fill()
  10010. // 添加文本标识
  10011. ctx.fillStyle = '#ffffff'
  10012. ctx.font = 'bold 14px Arial'
  10013. ctx.textAlign = 'center'
  10014. ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
  10015. ctx.shadowBlur = 2
  10016. ctx.shadowOffsetX = 1
  10017. ctx.shadowOffsetY = 1
  10018. switch (slideType) {
  10019. case 'cover':
  10020. ctx.fillText('封面', canvas.width / 2, canvas.height / 2 - 5)
  10021. ctx.font = '10px Arial'
  10022. ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
  10023. break
  10024. case 'contents':
  10025. ctx.fillText('目录', canvas.width / 2, canvas.height / 2 - 5)
  10026. ctx.font = '10px Arial'
  10027. ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
  10028. break
  10029. case 'transition':
  10030. ctx.fillText('过渡', canvas.width / 2, canvas.height / 2 - 5)
  10031. ctx.font = '10px Arial'
  10032. ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
  10033. break
  10034. case 'content':
  10035. ctx.fillText(`内容${index}`, canvas.width / 2, canvas.height / 2 - 5)
  10036. ctx.font = '10px Arial'
  10037. ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
  10038. break
  10039. case 'end':
  10040. ctx.fillText('结束', canvas.width / 2, canvas.height / 2 - 5)
  10041. ctx.font = '10px Arial'
  10042. ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
  10043. break
  10044. default:
  10045. ctx.fillText(`页面${index + 1}`, canvas.width / 2, canvas.height / 2 - 5)
  10046. ctx.font = '10px Arial'
  10047. ctx.fillText('红色主题', canvas.width / 2, canvas.height / 2 + 10)
  10048. }
  10049. // 添加装饰元素
  10050. ctx.strokeStyle = 'rgba(255,255,255,0.3)'
  10051. ctx.lineWidth = 1
  10052. ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20)
  10053. return canvas.toDataURL()
  10054. }
  10055. // 重置幻灯片位置到首页
  10056. const resetSlidePosition = () => {
  10057. console.log('重置幻灯片位置到首页')
  10058. // 重置所有相关的页面索引
  10059. currentSlideIndex.value = 0
  10060. currentPPTSlideIndex.value = 0
  10061. selectedImageIndex.value = null
  10062. selectedPPTElementIndex.value = -1
  10063. editingPPTElementIndex.value = -1
  10064. // 重置缩略图滚动位置
  10065. nextTick(() => {
  10066. const thumbnailStripEl = document.querySelector('.thumbnail-strip')
  10067. if (thumbnailStripEl) {
  10068. thumbnailStripEl.scrollLeft = 0
  10069. }
  10070. })
  10071. console.log('页面位置已重置到首页')
  10072. }
  10073. // 带进度的Template7生成函数
  10074. const generateTemplate7WithProgress = async (outlineData, title) => {
  10075. console.log('开始带进度的Template7生成:', outlineData.length, '个章节')
  10076. const slides = []
  10077. // 1. 封面页
  10078. await new Promise(resolve => setTimeout(resolve, 300))
  10079. const coverSlide = JSON.parse(JSON.stringify(template7JsonData[0]))
  10080. coverSlide.id = 'template7-dynamic-cover'
  10081. const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
  10082. if (titleElement) {
  10083. titleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: #ffffff; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  10084. }
  10085. slides.push(coverSlide)
  10086. generatedPPT.value = [...slides]
  10087. // 2. 目录页
  10088. await new Promise(resolve => setTimeout(resolve, 300))
  10089. const contentsSlide = JSON.parse(JSON.stringify(template7JsonData[1]))
  10090. contentsSlide.id = 'template7-dynamic-contents'
  10091. // 填充目录项
  10092. outlineData.forEach((chapter, index) => {
  10093. const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
  10094. if (itemElement) {
  10095. itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
  10096. }
  10097. })
  10098. // 隐藏多余的目录项
  10099. for (let i = outlineData.length; i < 6; i++) {
  10100. const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
  10101. if (itemElement) {
  10102. itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
  10103. }
  10104. }
  10105. slides.push(contentsSlide)
  10106. generatedPPT.value = [...slides]
  10107. // 3. 为每个章节生成过渡页和内容页
  10108. for (let chapterIndex = 0; chapterIndex < outlineData.length; chapterIndex++) {
  10109. const chapter = outlineData[chapterIndex]
  10110. // 章节过渡页
  10111. await new Promise(resolve => setTimeout(resolve, 300))
  10112. const transitionSlide = JSON.parse(JSON.stringify(template7JsonData[2]))
  10113. transitionSlide.id = `template7-dynamic-transition-${chapterIndex}`
  10114. const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
  10115. if (transitionTitleElement) {
  10116. transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  10117. }
  10118. slides.push(transitionSlide)
  10119. generatedPPT.value = [...slides]
  10120. // 为每个小节生成内容页
  10121. if (chapter.sections && chapter.sections.length > 0) {
  10122. for (let sectionIndex = 0; sectionIndex < chapter.sections.length; sectionIndex++) {
  10123. const section = chapter.sections[sectionIndex]
  10124. await new Promise(resolve => setTimeout(resolve, 300))
  10125. const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
  10126. contentSlide.id = `template7-dynamic-content-${chapterIndex}-${sectionIndex}`
  10127. // 填充内容页标题
  10128. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10129. if (contentTitleElement) {
  10130. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${section.title}</span></strong></p>`
  10131. }
  10132. // 动态生成子小节内容
  10133. const subsections = section.subsections || []
  10134. const actualSubsectionCount = Math.min(subsections.length, 4) // 最多4个子小节
  10135. // 填充实际存在的子小节
  10136. for (let subIndex = 0; subIndex < actualSubsectionCount; subIndex++) {
  10137. const subsection = subsections[subIndex]
  10138. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
  10139. if (itemTitleElement) {
  10140. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
  10141. }
  10142. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
  10143. if (itemContentElement) {
  10144. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content || '待AI填充'}</span></p>`
  10145. }
  10146. }
  10147. // 隐藏多余的子小节元素
  10148. for (let subIndex = actualSubsectionCount; subIndex < 4; subIndex++) {
  10149. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
  10150. if (itemTitleElement) {
  10151. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;"></span></strong></p>`
  10152. }
  10153. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
  10154. if (itemContentElement) {
  10155. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;"></span></p>`
  10156. }
  10157. }
  10158. slides.push(contentSlide)
  10159. generatedPPT.value = [...slides]
  10160. }
  10161. } else {
  10162. // 如果章节没有小节,生成一个默认内容页
  10163. await new Promise(resolve => setTimeout(resolve, 300))
  10164. const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
  10165. contentSlide.id = `template7-dynamic-content-${chapterIndex}-default`
  10166. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10167. if (contentTitleElement) {
  10168. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  10169. }
  10170. slides.push(contentSlide)
  10171. generatedPPT.value = [...slides]
  10172. }
  10173. }
  10174. // 4. 结束页
  10175. await new Promise(resolve => setTimeout(resolve, 300))
  10176. const endSlide = JSON.parse(JSON.stringify(template7JsonData[4]))
  10177. endSlide.id = 'template7-dynamic-end'
  10178. slides.push(endSlide)
  10179. generatedPPT.value = [...slides]
  10180. console.log('带进度的Template7生成完成,共', slides.length, '页')
  10181. }
  10182. // 基于template_8.json生成动态多页结构(保持我们精心设计的蓝色科技主题)
  10183. const generateDynamicTemplate8FromStatic = (outlineData, title) => {
  10184. console.log('开始基于template_8.json生成动态多页结构:', outlineData.length, '个章节')
  10185. const slides = []
  10186. // 1. 封面页 - 使用template_8.json的封面设计
  10187. const coverSlide = JSON.parse(JSON.stringify(template8JsonData[0]))
  10188. coverSlide.id = 'template8-dynamic-cover'
  10189. // 调试信息:检查封面页的背景
  10190. console.log('封面页背景信息:', coverSlide.background)
  10191. console.log('封面页元素数量:', coverSlide.elements?.length)
  10192. // 填充封面标题
  10193. const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
  10194. if (titleElement) {
  10195. titleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: #ffffff; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  10196. }
  10197. slides.push(coverSlide)
  10198. // 2. 目录页 - 使用template_8.json的目录设计
  10199. const contentsSlide = JSON.parse(JSON.stringify(template8JsonData[1]))
  10200. contentsSlide.id = 'template8-dynamic-contents'
  10201. // 填充目录项
  10202. outlineData.forEach((chapter, index) => {
  10203. const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
  10204. if (itemElement) {
  10205. itemElement.content = `<p style="text-align: center;"><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
  10206. }
  10207. })
  10208. slides.push(contentsSlide)
  10209. // 3. 生成章节内容
  10210. outlineData.forEach((chapter, chapterIndex) => {
  10211. // 章节过渡页 - 使用template_8.json的过渡页设计
  10212. const transitionSlide = JSON.parse(JSON.stringify(template8JsonData[2]))
  10213. transitionSlide.id = `template8-dynamic-transition-${chapterIndex}`
  10214. // 填充过渡页标题
  10215. const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
  10216. if (transitionTitleElement) {
  10217. transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #47acc5;">${chapter.title}</span></strong></p>`
  10218. }
  10219. // 填充过渡页内容
  10220. const transitionContentElement = transitionSlide.elements.find(el => el.id === 'transition-content')
  10221. if (transitionContentElement) {
  10222. transitionContentElement.content = `<p style="text-align: center;"><span style="font-size: 16px; color: #666666;">${chapter.content || `本章将介绍${chapter.title}的相关内容`}</span></p>`
  10223. }
  10224. slides.push(transitionSlide)
  10225. // 生成小节内容页
  10226. if (chapter.sections && chapter.sections.length > 0) {
  10227. chapter.sections.forEach((section, sectionIndex) => {
  10228. // 内容页 - 使用template_8.json的内容页设计
  10229. const contentSlide = JSON.parse(JSON.stringify(template8JsonData[3]))
  10230. contentSlide.id = `template8-dynamic-content-${chapterIndex}-${sectionIndex}`
  10231. // 填充内容页标题
  10232. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10233. if (contentTitleElement) {
  10234. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #47acc5;">${section.title}</span></strong></p>`
  10235. }
  10236. // 填充内容项
  10237. if (section.content && Array.isArray(section.content)) {
  10238. section.content.forEach((item, itemIndex) => {
  10239. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${itemIndex + 1}`)
  10240. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${itemIndex + 1}`)
  10241. if (itemTitleElement && item.title) {
  10242. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${item.title}</span></strong></p>`
  10243. }
  10244. if (itemContentElement && item.content) {
  10245. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${item.content}</span></p>`
  10246. }
  10247. })
  10248. }
  10249. slides.push(contentSlide)
  10250. })
  10251. } else {
  10252. // 如果章节没有小节,创建默认内容页
  10253. const contentSlide = JSON.parse(JSON.stringify(template8JsonData[3]))
  10254. contentSlide.id = `template8-dynamic-content-${chapterIndex}-default`
  10255. // 填充内容页标题
  10256. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10257. if (contentTitleElement) {
  10258. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #47acc5;">${chapter.title}</span></strong></p>`
  10259. }
  10260. slides.push(contentSlide)
  10261. }
  10262. })
  10263. // 4. 结束页 - 使用template_8.json的结束页设计
  10264. const endSlide = JSON.parse(JSON.stringify(template8JsonData[4]))
  10265. endSlide.id = 'template8-dynamic-end'
  10266. slides.push(endSlide)
  10267. console.log('基于template_8.json的动态多页结构生成完成,共', slides.length, '页')
  10268. return slides
  10269. }
  10270. // 基于template_7.json生成动态多页结构(保持我们精心设计的红色主题)
  10271. const generateDynamicTemplate7FromStatic = (outlineData, title) => {
  10272. console.log('开始基于template_7.json生成动态多页结构:', outlineData.length, '个章节')
  10273. const slides = []
  10274. // 1. 封面页 - 使用template_7.json的封面设计
  10275. const coverSlide = JSON.parse(JSON.stringify(template7JsonData[0]))
  10276. coverSlide.id = 'template7-dynamic-cover'
  10277. // 调试信息:检查封面页的背景
  10278. console.log('封面页背景信息:', coverSlide.background)
  10279. console.log('封面页元素数量:', coverSlide.elements?.length)
  10280. // 填充封面标题
  10281. const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
  10282. if (titleElement) {
  10283. titleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: #ffffff; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  10284. }
  10285. slides.push(coverSlide)
  10286. // 2. 目录页 - 使用template_7.json的目录设计
  10287. const contentsSlide = JSON.parse(JSON.stringify(template7JsonData[1]))
  10288. contentsSlide.id = 'template7-dynamic-contents'
  10289. // 填充目录项 - 只填充实际存在的章节
  10290. outlineData.forEach((chapter, index) => {
  10291. const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
  10292. if (itemElement) {
  10293. itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
  10294. }
  10295. })
  10296. // 隐藏多余的目录项
  10297. for (let i = outlineData.length; i < 6; i++) {
  10298. const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
  10299. if (itemElement) {
  10300. itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
  10301. itemElement.opacity = 0 // 设置为不可见
  10302. }
  10303. }
  10304. // 根据实际目录项数量调整布局
  10305. const actualItemCount = Math.min(outlineData.length, 6)
  10306. console.log(`目录页实际显示${actualItemCount}个目录项`)
  10307. if (actualItemCount === 2) {
  10308. // 2个目录项:调整位置让它们居中显示
  10309. const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
  10310. const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
  10311. if (item1) item1.top = 250
  10312. if (item2) item2.top = 300
  10313. } else if (actualItemCount === 3) {
  10314. // 3个目录项:调整位置让它们均匀分布
  10315. const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
  10316. const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
  10317. const item3 = contentsSlide.elements.find(el => el.id === 'item-3')
  10318. if (item1) item1.top = 220
  10319. if (item2) item2.top = 270
  10320. if (item3) item3.top = 320
  10321. } else if (actualItemCount === 4) {
  10322. // 4个目录项:保持默认位置
  10323. // 默认位置:item-1(200), item-2(250), item-3(300), item-4(350)
  10324. } else if (actualItemCount === 5) {
  10325. // 5个目录项:调整位置让它们均匀分布
  10326. const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
  10327. const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
  10328. const item3 = contentsSlide.elements.find(el => el.id === 'item-3')
  10329. const item4 = contentsSlide.elements.find(el => el.id === 'item-4')
  10330. const item5 = contentsSlide.elements.find(el => el.id === 'item-5')
  10331. if (item1) item1.top = 180
  10332. if (item2) item2.top = 230
  10333. if (item3) item3.top = 280
  10334. if (item4) item4.top = 330
  10335. if (item5) item5.top = 380
  10336. } else if (actualItemCount === 6) {
  10337. // 6个目录项:调整位置让它们均匀分布
  10338. const item1 = contentsSlide.elements.find(el => el.id === 'item-1')
  10339. const item2 = contentsSlide.elements.find(el => el.id === 'item-2')
  10340. const item3 = contentsSlide.elements.find(el => el.id === 'item-3')
  10341. const item4 = contentsSlide.elements.find(el => el.id === 'item-4')
  10342. const item5 = contentsSlide.elements.find(el => el.id === 'item-5')
  10343. const item6 = contentsSlide.elements.find(el => el.id === 'item-6')
  10344. if (item1) item1.top = 160
  10345. if (item2) item2.top = 210
  10346. if (item3) item3.top = 260
  10347. if (item4) item4.top = 310
  10348. if (item5) item5.top = 360
  10349. if (item6) item6.top = 410
  10350. }
  10351. slides.push(contentsSlide)
  10352. // 3. 为每个章节生成过渡页和内容页
  10353. outlineData.forEach((chapter, chapterIndex) => {
  10354. // 章节过渡页 - 使用template_7.json的过渡页设计
  10355. const transitionSlide = JSON.parse(JSON.stringify(template7JsonData[2]))
  10356. transitionSlide.id = `template7-dynamic-transition-${chapterIndex}`
  10357. // 填充过渡页标题
  10358. const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
  10359. if (transitionTitleElement) {
  10360. transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  10361. }
  10362. slides.push(transitionSlide)
  10363. // 为每个小节生成内容页
  10364. if (chapter.sections && chapter.sections.length > 0) {
  10365. chapter.sections.forEach((section, sectionIndex) => {
  10366. // 内容页 - 使用template_7.json的内容页设计
  10367. const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
  10368. contentSlide.id = `template7-dynamic-content-${chapterIndex}-${sectionIndex}`
  10369. // 填充内容页标题
  10370. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10371. if (contentTitleElement) {
  10372. contentTitleElement.textType = 'title'
  10373. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${section.title}</span></strong></p>`
  10374. }
  10375. // 确保子小节数据同步到大纲数据中,与通用PPT保持一致
  10376. let actualSubsections = section.subsections || []
  10377. // 如果没有子小节,生成默认子小节并同步到大纲数据
  10378. if (actualSubsections.length === 0) {
  10379. const randomCount = Math.floor(Math.random() * 3) + 2 // 2-4个
  10380. const defaultTitles = ['待AI生成子小节1', '待AI生成子小节2', '待AI生成子小节3', '待AI生成子小节4']
  10381. actualSubsections = []
  10382. for (let i = 0; i < randomCount; i++) {
  10383. actualSubsections.push({
  10384. title: defaultTitles[i] || `要点${i + 1}`,
  10385. content: '待AI填充'
  10386. })
  10387. }
  10388. // 重要:同步到大纲数据中,确保getTitleForElement能正确访问
  10389. section.subsections = actualSubsections
  10390. }
  10391. const actualSubsectionCount = Math.min(actualSubsections.length, 4) // 最多4个子小节
  10392. console.log(`章节${chapterIndex + 1}小节${sectionIndex + 1}有${actualSubsections.length}个子小节,实际显示${actualSubsectionCount}个`)
  10393. // 填充实际存在的子小节
  10394. for (let subIndex = 0; subIndex < actualSubsectionCount; subIndex++) {
  10395. const subsection = actualSubsections[subIndex]
  10396. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
  10397. if (itemTitleElement) {
  10398. itemTitleElement.textType = 'itemTitle'
  10399. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
  10400. console.log(`填充子小节标题${subIndex + 1}: ${subsection.title}`)
  10401. }
  10402. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
  10403. if (itemContentElement) {
  10404. // 确保元素有正确的textType属性,以便动画函数能识别
  10405. itemContentElement.textType = 'itemContent'
  10406. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content || '待AI填充'}</span></p>`
  10407. console.log(`填充子小节内容${subIndex + 1}: ${subsection.content || '待AI填充'}`)
  10408. }
  10409. }
  10410. // 隐藏多余的子小节元素(完全移除)
  10411. for (let subIndex = actualSubsectionCount; subIndex < 4; subIndex++) {
  10412. // 移除多余的元素
  10413. contentSlide.elements = contentSlide.elements.filter(el =>
  10414. el.id !== `itemTitle-${subIndex + 1}` &&
  10415. el.id !== `itemContent-${subIndex + 1}` &&
  10416. el.id !== `item-bg-${subIndex + 1}` &&
  10417. el.id !== `item-icon-${subIndex + 1}`
  10418. )
  10419. }
  10420. // 根据实际子小节数量调整布局
  10421. if (actualSubsectionCount === 2) {
  10422. // 2个子小节:调整位置,让它们居中显示
  10423. // 默认位置:item-bg-1(140), item-bg-2(240) -> 调整为:item-bg-1(190), item-bg-2(290)
  10424. const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
  10425. const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
  10426. if (item1) item1.top = 190 // 向下移动50px
  10427. if (item2) item2.top = 290 // 向下移动50px
  10428. // 调整对应的文字和图标位置
  10429. const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
  10430. const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
  10431. const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
  10432. if (title1) title1.top = 200
  10433. if (content1) content1.top = 230
  10434. if (icon1) icon1.top = 205
  10435. const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
  10436. const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
  10437. const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
  10438. if (title2) title2.top = 300
  10439. if (content2) content2.top = 330
  10440. if (icon2) icon2.top = 305
  10441. // 调整三角形装饰图像位置,让它居中
  10442. const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
  10443. if (decorationElement) {
  10444. // 计算2个子小节的总高度:从190到290,总高度100px
  10445. // 装饰图像应该居中,所以top = 190 + (100/2) - (400/2) = 190 + 50 - 200 = 40
  10446. decorationElement.top = 40
  10447. }
  10448. } else if (actualSubsectionCount === 3) {
  10449. // 3个子小节:明确设置位置,确保间距一致
  10450. // 默认位置:item-bg-1(140), item-bg-2(240), item-bg-3(340) - 间距100px
  10451. const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
  10452. const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
  10453. const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
  10454. if (item1) item1.top = 140
  10455. if (item2) item2.top = 240
  10456. if (item3) item3.top = 340
  10457. // 调整对应的文字和图标位置
  10458. const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
  10459. const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
  10460. const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
  10461. if (title1) title1.top = 150
  10462. if (content1) content1.top = 180
  10463. if (icon1) icon1.top = 155
  10464. const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
  10465. const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
  10466. const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
  10467. if (title2) title2.top = 250
  10468. if (content2) content2.top = 280
  10469. if (icon2) icon2.top = 255
  10470. const title3 = contentSlide.elements.find(el => el.id === 'itemTitle-3')
  10471. const content3 = contentSlide.elements.find(el => el.id === 'itemContent-3')
  10472. const icon3 = contentSlide.elements.find(el => el.id === 'item-icon-3')
  10473. if (title3) title3.top = 350
  10474. if (content3) content3.top = 380
  10475. if (icon3) icon3.top = 355
  10476. // 调整三角形装饰图像位置,让它居中
  10477. const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
  10478. if (decorationElement) {
  10479. // 计算3个子小节的总高度:从140到340,总高度200px
  10480. // 装饰图像应该居中,所以top = 140 + (200/2) - (400/2) = 140 + 100 - 200 = 40
  10481. decorationElement.top = 40
  10482. }
  10483. } else if (actualSubsectionCount === 4) {
  10484. // 4个子小节:调整位置,让它们均匀分布,增加间距
  10485. // 默认位置:item-bg-1(140), item-bg-2(240), item-bg-3(340), item-bg-4(440) -> 调整为:item-bg-1(120), item-bg-2(220), item-bg-3(320), item-bg-4(420)
  10486. const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
  10487. const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
  10488. const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
  10489. const item4 = contentSlide.elements.find(el => el.id === 'item-bg-4')
  10490. if (item1) item1.top = 120
  10491. if (item2) item2.top = 220
  10492. if (item3) item3.top = 320
  10493. if (item4) item4.top = 420
  10494. // 调整对应的文字和图标位置
  10495. const elements = [
  10496. { id: 'itemTitle-1', top: 130 }, { id: 'itemContent-1', top: 160 }, { id: 'item-icon-1', top: 135 },
  10497. { id: 'itemTitle-2', top: 230 }, { id: 'itemContent-2', top: 260 }, { id: 'item-icon-2', top: 235 },
  10498. { id: 'itemTitle-3', top: 330 }, { id: 'itemContent-3', top: 360 }, { id: 'item-icon-3', top: 335 },
  10499. { id: 'itemTitle-4', top: 430 }, { id: 'itemContent-4', top: 460 }, { id: 'item-icon-4', top: 435 }
  10500. ]
  10501. elements.forEach(({ id, top }) => {
  10502. const element = contentSlide.elements.find(el => el.id === id)
  10503. if (element) element.top = top
  10504. })
  10505. // 调整三角形装饰图像位置,让它再下来一点
  10506. const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
  10507. if (decorationElement) {
  10508. // 计算4个子小节的总高度:从120到420,总高度300px
  10509. // 装饰图像应该再下来一点,所以top = 120 + (300/2) - (400/2) + 30 = 120 + 150 - 200 + 30 = 100
  10510. decorationElement.top = 100
  10511. }
  10512. }
  10513. slides.push(contentSlide)
  10514. })
  10515. } else {
  10516. // 如果章节没有小节,生成一个默认内容页
  10517. const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
  10518. contentSlide.id = `template7-dynamic-content-${chapterIndex}-default`
  10519. // 填充内容页标题
  10520. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10521. if (contentTitleElement) {
  10522. contentTitleElement.textType = 'title'
  10523. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  10524. }
  10525. // 为默认内容页随机生成2-4个子小节(模板7现在支持4个子小节)
  10526. const randomCount = Math.floor(Math.random() * 3) + 2 // 2-4个
  10527. const defaultTitles = ['主要内容', '详细说明', '补充内容', '总结要点']
  10528. const defaultContents = ['待AI填充', '待AI填充', '待AI填充', '待AI填充']
  10529. const defaultSubsections = []
  10530. for (let i = 0; i < randomCount; i++) {
  10531. defaultSubsections.push({
  10532. title: defaultTitles[i] || `要点${i + 1}`,
  10533. content: defaultContents[i] || '待AI填充'
  10534. })
  10535. }
  10536. // 填充子小节
  10537. for (let subIndex = 0; subIndex < randomCount; subIndex++) {
  10538. const subsection = defaultSubsections[subIndex]
  10539. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
  10540. if (itemTitleElement) {
  10541. itemTitleElement.textType = 'itemTitle'
  10542. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
  10543. }
  10544. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
  10545. if (itemContentElement) {
  10546. itemContentElement.textType = 'itemContent'
  10547. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content}</span></p>`
  10548. }
  10549. }
  10550. // 移除多余的子小节
  10551. for (let subIndex = randomCount; subIndex < 4; subIndex++) {
  10552. contentSlide.elements = contentSlide.elements.filter(el =>
  10553. el.id !== `itemTitle-${subIndex + 1}` &&
  10554. el.id !== `itemContent-${subIndex + 1}` &&
  10555. el.id !== `item-bg-${subIndex + 1}` &&
  10556. el.id !== `item-icon-${subIndex + 1}`
  10557. )
  10558. }
  10559. // 根据实际子小节数量调整布局
  10560. if (randomCount === 2) {
  10561. // 2个子小节:调整位置,让它们居中显示
  10562. // 默认位置:item-bg-1(140), item-bg-2(240) -> 调整为:item-bg-1(190), item-bg-2(290)
  10563. const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
  10564. const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
  10565. if (item1) item1.top = 190 // 向下移动50px
  10566. if (item2) item2.top = 290 // 向下移动50px
  10567. // 调整对应的文字和图标位置
  10568. const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
  10569. const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
  10570. const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
  10571. if (title1) title1.top = 200
  10572. if (content1) content1.top = 230
  10573. if (icon1) icon1.top = 205
  10574. const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
  10575. const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
  10576. const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
  10577. if (title2) title2.top = 300
  10578. if (content2) content2.top = 330
  10579. if (icon2) icon2.top = 305
  10580. // 调整三角形装饰图像位置,让它居中
  10581. const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
  10582. if (decorationElement) {
  10583. // 计算2个子小节的总高度:从190到290,总高度100px
  10584. // 装饰图像应该居中,所以top = 190 + (100/2) - (400/2) = 190 + 50 - 200 = 40
  10585. decorationElement.top = 40
  10586. }
  10587. } else if (randomCount === 3) {
  10588. // 3个子小节:明确设置位置,确保间距一致
  10589. // 默认位置:item-bg-1(140), item-bg-2(240), item-bg-3(340) - 间距100px
  10590. const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
  10591. const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
  10592. const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
  10593. if (item1) item1.top = 140
  10594. if (item2) item2.top = 240
  10595. if (item3) item3.top = 340
  10596. // 调整对应的文字和图标位置
  10597. const title1 = contentSlide.elements.find(el => el.id === 'itemTitle-1')
  10598. const content1 = contentSlide.elements.find(el => el.id === 'itemContent-1')
  10599. const icon1 = contentSlide.elements.find(el => el.id === 'item-icon-1')
  10600. if (title1) title1.top = 150
  10601. if (content1) content1.top = 180
  10602. if (icon1) icon1.top = 155
  10603. const title2 = contentSlide.elements.find(el => el.id === 'itemTitle-2')
  10604. const content2 = contentSlide.elements.find(el => el.id === 'itemContent-2')
  10605. const icon2 = contentSlide.elements.find(el => el.id === 'item-icon-2')
  10606. if (title2) title2.top = 250
  10607. if (content2) content2.top = 280
  10608. if (icon2) icon2.top = 255
  10609. const title3 = contentSlide.elements.find(el => el.id === 'itemTitle-3')
  10610. const content3 = contentSlide.elements.find(el => el.id === 'itemContent-3')
  10611. const icon3 = contentSlide.elements.find(el => el.id === 'item-icon-3')
  10612. if (title3) title3.top = 350
  10613. if (content3) content3.top = 380
  10614. if (icon3) icon3.top = 355
  10615. // 调整三角形装饰图像位置,让它居中
  10616. const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
  10617. if (decorationElement) {
  10618. // 计算3个子小节的总高度:从140到340,总高度200px
  10619. // 装饰图像应该居中,所以top = 140 + (200/2) - (400/2) = 140 + 100 - 200 = 40
  10620. decorationElement.top = 40
  10621. }
  10622. } else if (randomCount === 4) {
  10623. // 4个子小节:调整位置,让它们均匀分布,增加间距
  10624. // 默认位置:item-bg-1(140), item-bg-2(240), item-bg-3(340), item-bg-4(440) -> 调整为:item-bg-1(120), item-bg-2(220), item-bg-3(320), item-bg-4(420)
  10625. const item1 = contentSlide.elements.find(el => el.id === 'item-bg-1')
  10626. const item2 = contentSlide.elements.find(el => el.id === 'item-bg-2')
  10627. const item3 = contentSlide.elements.find(el => el.id === 'item-bg-3')
  10628. const item4 = contentSlide.elements.find(el => el.id === 'item-bg-4')
  10629. if (item1) item1.top = 120
  10630. if (item2) item2.top = 220
  10631. if (item3) item3.top = 320
  10632. if (item4) item4.top = 420
  10633. // 调整对应的文字和图标位置
  10634. const elements = [
  10635. { id: 'itemTitle-1', top: 130 }, { id: 'itemContent-1', top: 160 }, { id: 'item-icon-1', top: 135 },
  10636. { id: 'itemTitle-2', top: 230 }, { id: 'itemContent-2', top: 260 }, { id: 'item-icon-2', top: 235 },
  10637. { id: 'itemTitle-3', top: 330 }, { id: 'itemContent-3', top: 360 }, { id: 'item-icon-3', top: 335 },
  10638. { id: 'itemTitle-4', top: 430 }, { id: 'itemContent-4', top: 460 }, { id: 'item-icon-4', top: 435 }
  10639. ]
  10640. elements.forEach(({ id, top }) => {
  10641. const element = contentSlide.elements.find(el => el.id === id)
  10642. if (element) element.top = top
  10643. })
  10644. // 调整三角形装饰图像位置,让它再下来一点
  10645. const decorationElement = contentSlide.elements.find(el => el.id === 'content-side-decoration')
  10646. if (decorationElement) {
  10647. // 计算4个子小节的总高度:从120到420,总高度300px
  10648. // 装饰图像应该再下来一点,所以top = 120 + (300/2) - (400/2) + 30 = 120 + 150 - 200 + 30 = 100
  10649. decorationElement.top = 100
  10650. }
  10651. }
  10652. slides.push(contentSlide)
  10653. }
  10654. })
  10655. // 4. 结束页 - 使用template_7.json的结束页设计
  10656. const endSlide = JSON.parse(JSON.stringify(template7JsonData[4]))
  10657. endSlide.id = 'template7-dynamic-end'
  10658. slides.push(endSlide)
  10659. console.log('基于template_7.json的动态多页结构生成完成,共', slides.length, '页')
  10660. return slides
  10661. }
  10662. // 动态生成Template7多页结构(基于红色主题)
  10663. const generateDynamicTemplate7 = (outlineData, title) => {
  10664. console.log('开始动态生成Template7多页结构:', outlineData.length, '个章节')
  10665. const slides = []
  10666. // 1. 封面页 - 使用template_7.json的封面设计
  10667. const coverSlide = JSON.parse(JSON.stringify(template7JsonData[0]))
  10668. coverSlide.id = 'template7-dynamic-cover'
  10669. // 填充封面标题
  10670. const titleElement = coverSlide.elements.find(el => el.id === 'title-text')
  10671. if (titleElement) {
  10672. titleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 40px; color: #ffffff; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">${title}</span></strong></p>`
  10673. }
  10674. slides.push(coverSlide)
  10675. // 2. 目录页 - 使用template_7.json的目录设计
  10676. const contentsSlide = JSON.parse(JSON.stringify(template7JsonData[1]))
  10677. contentsSlide.id = 'template7-dynamic-contents'
  10678. // 填充目录项
  10679. outlineData.forEach((chapter, index) => {
  10680. const itemElement = contentsSlide.elements.find(el => el.id === `item-${index + 1}`)
  10681. if (itemElement) {
  10682. itemElement.content = `<p><span style="font-size: 18px; color: #333333;">${chapter.title}</span></p>`
  10683. }
  10684. })
  10685. // 隐藏多余的目录项
  10686. for (let i = outlineData.length; i < 6; i++) {
  10687. const itemElement = contentsSlide.elements.find(el => el.id === `item-${i + 1}`)
  10688. if (itemElement) {
  10689. itemElement.content = `<p><span style="font-size: 18px; color: #333333;"></span></p>`
  10690. }
  10691. }
  10692. slides.push(contentsSlide)
  10693. // 3. 为每个章节生成过渡页和内容页
  10694. outlineData.forEach((chapter, chapterIndex) => {
  10695. // 章节过渡页 - 使用template_7.json的过渡页设计
  10696. const transitionSlide = JSON.parse(JSON.stringify(template7JsonData[2]))
  10697. transitionSlide.id = `template7-dynamic-transition-${chapterIndex}`
  10698. // 填充过渡页标题
  10699. const transitionTitleElement = transitionSlide.elements.find(el => el.id === 'transition-title')
  10700. if (transitionTitleElement) {
  10701. transitionTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 32px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  10702. }
  10703. slides.push(transitionSlide)
  10704. // 为每个小节生成内容页
  10705. if (chapter.sections && chapter.sections.length > 0) {
  10706. chapter.sections.forEach((section, sectionIndex) => {
  10707. // 内容页 - 使用template_7.json的内容页设计
  10708. const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
  10709. contentSlide.id = `template7-dynamic-content-${chapterIndex}-${sectionIndex}`
  10710. // 填充内容页标题
  10711. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10712. if (contentTitleElement) {
  10713. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${section.title}</span></strong></p>`
  10714. }
  10715. // 填充小节内容
  10716. const subsections = section.subsections || []
  10717. subsections.forEach((subsection, subIndex) => {
  10718. const itemTitleElement = contentSlide.elements.find(el => el.id === `itemTitle-${subIndex + 1}`)
  10719. if (itemTitleElement) {
  10720. itemTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 18px; color: #333333;">${subsection.title}</span></strong></p>`
  10721. }
  10722. const itemContentElement = contentSlide.elements.find(el => el.id === `itemContent-${subIndex + 1}`)
  10723. if (itemContentElement) {
  10724. itemContentElement.content = `<p style="text-align: center;"><span style="font-size: 14px; color: #666666;">${subsection.content || '待AI填充'}</span></p>`
  10725. }
  10726. })
  10727. slides.push(contentSlide)
  10728. })
  10729. } else {
  10730. // 如果章节没有小节,生成一个默认内容页
  10731. const contentSlide = JSON.parse(JSON.stringify(template7JsonData[3]))
  10732. contentSlide.id = `template7-dynamic-content-${chapterIndex}-default`
  10733. // 填充内容页标题
  10734. const contentTitleElement = contentSlide.elements.find(el => el.id === 'content-title')
  10735. if (contentTitleElement) {
  10736. contentTitleElement.content = `<p style="text-align: center;"><strong><span style="font-size: 28px; color: #e74c3c;">${chapter.title}</span></strong></p>`
  10737. }
  10738. slides.push(contentSlide)
  10739. }
  10740. })
  10741. // 4. 结束页 - 使用template_7.json的结束页设计
  10742. const endSlide = JSON.parse(JSON.stringify(template7JsonData[4]))
  10743. endSlide.id = 'template7-dynamic-end'
  10744. slides.push(endSlide)
  10745. console.log('Template7动态多页结构生成完成,共', slides.length, '页')
  10746. return slides
  10747. }
  10748. // 检查消息是否正在朗读
  10749. const isSpeaking = (messageId) => {
  10750. return speakingMessageId.value === messageId
  10751. }
  10752. // 处理输入框输入,限制2000字
  10753. const handleInput = () => {
  10754. if (messageText.value.length > 2000) {
  10755. messageText.value = messageText.value.substring(0, 2000)
  10756. ElMessage.warning('消息长度不能超过2000字')
  10757. }
  10758. }
  10759. // 获取功能卡片数据
  10760. const getFunctionCards = async () => {
  10761. try {
  10762. console.log('开始获取安全培训功能卡片...')
  10763. const response = await apis.getFunctionCard({ function_type: 1 }) // 1为安全培训类型
  10764. console.log('功能卡片响应:', response)
  10765. if (response.statusCode === 200) {
  10766. functionCards.value = response.data
  10767. console.log('功能卡片数据已设置:', functionCards.value)
  10768. } else {
  10769. console.error('获取功能卡片失败:', response.statusCode)
  10770. }
  10771. } catch (error) {
  10772. console.error('获取功能卡片失败:', error)
  10773. }
  10774. }
  10775. // 获取热点问题数据
  10776. const getHotQuestions = async () => {
  10777. try {
  10778. console.log('开始获取安全培训热点问题...')
  10779. const response = await apis.getHotQuestion({ question_type: 1 }) // 1为安全培训类型
  10780. console.log('热点问题响应:', response)
  10781. if (response.statusCode === 200) {
  10782. hotQuestions.value = response.data
  10783. console.log('热点问题数据已设置:', hotQuestions.value)
  10784. } else {
  10785. console.error('获取热点问题失败:', response.statusCode)
  10786. }
  10787. } catch (error) {
  10788. console.error('获取热点问题失败:', error)
  10789. }
  10790. }
  10791. // 获取OSS上传配置
  10792. const getOSSUploadConfig = async () => {
  10793. try {
  10794. console.log('开始获取OSS配置...')
  10795. const response = await apis.uploadOss({})
  10796. console.log('OSS配置响应:', response)
  10797. if (response.statusCode === 200) {
  10798. const res = response
  10799. console.log('OSS配置数据:', res)
  10800. uploadData.OssAccessKeyId = res.accessid
  10801. uploadData.policy = res.policy
  10802. uploadData.Signature = res.signature
  10803. uploadData.host = res.host
  10804. uploadData.dir = res.dir || 'uploads/safety/'
  10805. uploadData.key = uploadData.dir + '${filename}'
  10806. console.log('OSS上传配置已设置:', uploadData)
  10807. return true
  10808. } else {
  10809. console.error('接口返回状态码不是200:', response.statusCode)
  10810. throw new Error('获取OSS配置失败')
  10811. }
  10812. } catch (error) {
  10813. console.error('获取OSS配置失败:', error)
  10814. ElMessage.error('获取上传配置失败,请重试')
  10815. return false
  10816. }
  10817. }
  10818. // 上传文件到阿里云OSS(web直传)
  10819. const uploadFileToOSS = async (file) => {
  10820. try {
  10821. console.log('开始上传文件:', file.name)
  10822. console.log('当前OSS配置状态:', uploadData)
  10823. // 如果还没有OSS配置,先获取
  10824. if (!uploadData.host) {
  10825. console.log('OSS配置为空,开始获取配置...')
  10826. const configResult = await getOSSUploadConfig()
  10827. if (!configResult) {
  10828. throw new Error('获取OSS配置失败')
  10829. }
  10830. }
  10831. // 验证OSS配置是否完整
  10832. if (!uploadData.host || !uploadData.policy || !uploadData.OssAccessKeyId || !uploadData.Signature) {
  10833. console.error('OSS配置不完整:', uploadData)
  10834. throw new Error('OSS配置不完整,请重试')
  10835. }
  10836. // 构建OSS上传参数
  10837. const formData = new FormData()
  10838. formData.append('key', uploadData.dir + file.name)
  10839. formData.append('policy', uploadData.policy)
  10840. formData.append('OSSAccessKeyId', uploadData.OssAccessKeyId)
  10841. formData.append('Signature', uploadData.Signature)
  10842. formData.append('success_action_status', '200')
  10843. formData.append('file', file)
  10844. console.log('开始上传到OSS:', uploadData.host)
  10845. // 直接上传到OSS
  10846. const uploadResponse = await fetch(uploadData.host, {
  10847. method: 'POST',
  10848. body: formData
  10849. })
  10850. console.log('OSS上传响应:', uploadResponse)
  10851. if (uploadResponse.ok) {
  10852. // 上传成功,创建文件信息对象
  10853. const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
  10854. const ossUrl = uploadData.host + '/' + uploadData.dir + file.name
  10855. selectedFile.value = {
  10856. file,
  10857. name: file.name,
  10858. size: file.size,
  10859. type: fileExtension,
  10860. icon: getFileIcon(fileExtension),
  10861. ossUrl: ossUrl, // OSS访问链接
  10862. ossKey: uploadData.dir + file.name
  10863. }
  10864. ElMessage.success('文件上传成功')
  10865. } else {
  10866. const errorText = await uploadResponse.text()
  10867. console.error('OSS上传失败响应:', errorText)
  10868. throw new Error(`上传失败: ${uploadResponse.status} - ${errorText}`)
  10869. }
  10870. } catch (error) {
  10871. console.error('文件上传失败:', error)
  10872. throw error
  10873. }
  10874. }
  10875. // 获取文件图标
  10876. const getFileIcon = (fileType) => {
  10877. switch (fileType) {
  10878. case '.doc':
  10879. case '.docx':
  10880. return wordDocIcon
  10881. default:
  10882. return '📎'
  10883. }
  10884. }
  10885. // 格式化文件大小
  10886. const formatFileSize = (bytes) => {
  10887. if (bytes === 0) return '0 B'
  10888. const k = 1024
  10889. const sizes = ['B', 'KB', 'MB', 'GB']
  10890. const i = Math.floor(Math.log(bytes) / Math.log(k))
  10891. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  10892. }
  10893. // 删除选中的文件
  10894. const removeSelectedFile = () => {
  10895. if (selectedFile.value) {
  10896. selectedFile.value = null
  10897. }
  10898. }
  10899. // 触发文件上传
  10900. const triggerFileUpload = () => {
  10901. if (selectedFile.value) {
  10902. ElMessage.warning('只能上传一个文件,请先删除当前文件')
  10903. return
  10904. }
  10905. fileInput.value?.click()
  10906. }
  10907. // 文件处理工具函数
  10908. const validateFile = (file) => {
  10909. // 检查文件大小
  10910. if (file.size > fileConfig.maxSize) {
  10911. throw new Error('文件大小不能超过20MB')
  10912. }
  10913. // 检查文件类型
  10914. const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
  10915. if (!fileConfig.allowedTypes.includes(fileExtension)) {
  10916. throw new Error('只支持.docx格式的Word文档。如果是.doc格式,请先另存为.docx格式。')
  10917. }
  10918. return fileExtension
  10919. }
  10920. // 读取Word文件内容
  10921. const readWordFile = async (file) => {
  10922. try {
  10923. console.log('开始读取Word文件:', file.name, '文件大小:', file.size)
  10924. // 检查文件是否为空
  10925. if (file.size === 0) {
  10926. throw new Error('Word文件为空')
  10927. }
  10928. // 使用mammoth.js库读取Word文档
  10929. console.log('正在导入mammoth库...')
  10930. const mammoth = await import('mammoth')
  10931. console.log('mammoth库导入成功')
  10932. // 将文件转换为ArrayBuffer
  10933. const arrayBuffer = await file.arrayBuffer()
  10934. console.log('文件转换为ArrayBuffer成功,大小:', arrayBuffer.byteLength)
  10935. // 提取文本内容
  10936. console.log('开始提取文本内容...')
  10937. const result = await mammoth.extractRawText({ arrayBuffer })
  10938. console.log('Word文件读取完成,内容长度:', result.value.length)
  10939. return result.value
  10940. } catch (error) {
  10941. console.error('Word文件读取失败,详细错误:', error)
  10942. console.error('错误堆栈:', error.stack)
  10943. // 提供更具体的错误信息
  10944. if (error.message.includes('Invalid file format')) {
  10945. throw new Error('Word文件格式无效或已损坏')
  10946. } else if (error.message.includes('File is empty')) {
  10947. throw new Error('Word文件为空')
  10948. } else {
  10949. throw new Error(`Word文件读取失败: ${error.message}`)
  10950. }
  10951. }
  10952. }
  10953. // 处理文件标签格式的回显
  10954. const processFileDisplay = (text, file) => {
  10955. if (!file) {
  10956. // 如果没有文件对象,尝试从文本中提取文件信息
  10957. return processFileDisplayFromText(text)
  10958. }
  10959. // 查找文件标签并替换为显示格式
  10960. const fileDisplay = `
  10961. 📄 文件信息:
  10962. 文件名:${file.name}
  10963. 文件大小:${formatFileSize(file.size)}
  10964. 文件类型:${file.type}
  10965. 📝 文件内容:
  10966. ${file.content}
  10967. ---
  10968. `
  10969. // 替换文件标签为显示格式
  10970. let processedText = text
  10971. .replace(/<word>.*?<\/word>/gs, fileDisplay)
  10972. .replace(/<filename>.*?<\/filename>/g, '')
  10973. .replace(/<filesize>.*?<\/filesize>/g, '')
  10974. return processedText
  10975. }
  10976. // 从文本中提取文件信息并转换为显示格式
  10977. const processFileDisplayFromText = (text) => {
  10978. // 提取文件名
  10979. const filenameMatch = text.match(/<filename>(.*?)<\/filename>/)
  10980. const filename = filenameMatch ? filenameMatch[1] : '未知文件'
  10981. // 提取文件大小
  10982. const filesizeMatch = text.match(/<filesize>(.*?)<\/filesize>/)
  10983. const filesize = filesizeMatch ? parseInt(filesizeMatch[1]) : 0
  10984. // 提取文件内容
  10985. const wordMatch = text.match(/<word>(.*?)<\/word>/s)
  10986. const fileContent = wordMatch ? wordMatch[1].trim() : '无内容'
  10987. // 创建文件显示格式
  10988. const fileDisplay = `
  10989. 📄 文件信息:
  10990. 文件名:${filename}
  10991. 文件大小:${formatFileSize(filesize)}
  10992. 文件类型:${filename.endsWith('.docx') ? '.docx' : filename.endsWith('.doc') ? '.doc' : '未知'}
  10993. 📝 文件内容:
  10994. ${fileContent}
  10995. ---
  10996. `
  10997. // 替换文件标签为显示格式
  10998. let processedText = text
  10999. .replace(/<word>.*?<\/word>/gs, fileDisplay)
  11000. .replace(/<filename>.*?<\/filename>/g, '')
  11001. .replace(/<filesize>.*?<\/filesize>/g, '')
  11002. return processedText
  11003. }
  11004. // 处理文件选择
  11005. const handleFileSelect = async (event) => {
  11006. const file = event.target.files[0]
  11007. if (!file) return
  11008. try {
  11009. // 验证文件
  11010. const fileExtension = validateFile(file)
  11011. isUploadingFile.value = true
  11012. console.log('开始读取文件内容:', file.name)
  11013. // 只处理Word文档
  11014. const extractedContent = await readWordFile(file)
  11015. // 创建文件信息对象
  11016. selectedFile.value = {
  11017. file,
  11018. name: file.name,
  11019. size: file.size,
  11020. type: fileExtension,
  11021. icon: getFileIcon(fileExtension),
  11022. content: extractedContent // 存储提取的内容
  11023. }
  11024. // 显示提取的内容长度
  11025. const contentLength = extractedContent.length
  11026. console.log('文件内容提取完成,字符数:', contentLength)
  11027. ElMessage.success(`文件读取成功,提取了${contentLength}个字符的内容`)
  11028. } catch (error) {
  11029. console.error('文件读取失败:', error)
  11030. ElMessage.error(error.message || '文件读取失败,请重试')
  11031. } finally {
  11032. isUploadingFile.value = false
  11033. event.target.value = ''
  11034. }
  11035. }
  11036. // 语音输入相关方法
  11037. const handleVoiceClick = () => {
  11038. console.log('点击语音按钮')
  11039. if (isListening.value) {
  11040. // 如果正在录音,则停止
  11041. stopVoiceInput()
  11042. } else {
  11043. // 开始语音输入
  11044. startVoiceInput()
  11045. }
  11046. }
  11047. const startVoiceInput = () => {
  11048. console.log('开始语音输入')
  11049. // 开始语音识别
  11050. const success = startListening()
  11051. if (!success) {
  11052. ElMessage.error('语音识别启动失败,请检查麦克风权限')
  11053. }
  11054. }
  11055. const stopVoiceInput = () => {
  11056. console.log('停止语音输入')
  11057. stopListening()
  11058. // 语音识别完成后,将结果填入输入框
  11059. if (transcript.value.trim()) {
  11060. messageText.value = transcript.value
  11061. }
  11062. }
  11063. // 复制整个大纲
  11064. const copyEntireOutline = async () => {
  11065. try {
  11066. if (!outlineData.value || outlineData.value.length === 0) {
  11067. ElMessage.warning('暂无大纲内容可复制')
  11068. return
  11069. }
  11070. // 构建大纲文本(纯文本格式,无井号)
  11071. let outlineText = `${outlineTitle.value || '安全培训大纲'}\n\n`
  11072. outlineData.value.forEach((chapter, chapterIndex) => {
  11073. outlineText += `${chapter.title}\n\n`
  11074. if (chapter.sections && chapter.sections.length > 0) {
  11075. chapter.sections.forEach((section, sectionIndex) => {
  11076. outlineText += ` ${section.title}\n\n`
  11077. if (section.subsections && section.subsections.length > 0) {
  11078. section.subsections.forEach((subsection, subsectionIndex) => {
  11079. outlineText += ` ${subsection.title}\n\n`
  11080. if (subsection.subsubsections && subsection.subsubsections.length > 0) {
  11081. subsection.subsubsections.forEach((subsubsection, subsubsectionIndex) => {
  11082. outlineText += ` - ${subsubsection.title}\n`
  11083. if (subsubsection.content) {
  11084. outlineText += ` ${subsubsection.content}\n`
  11085. }
  11086. })
  11087. outlineText += '\n'
  11088. }
  11089. })
  11090. }
  11091. })
  11092. }
  11093. })
  11094. // 不添加统计信息,只复制纯大纲内容
  11095. // 检查 Clipboard API 是否可用
  11096. if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
  11097. try {
  11098. await navigator.clipboard.writeText(outlineText)
  11099. ElMessage.success('复制成功')
  11100. return
  11101. } catch (clipboardError) {
  11102. console.warn('Clipboard API 失败,使用降级方案:', clipboardError)
  11103. }
  11104. }
  11105. // 降级方案:使用传统复制方法
  11106. const textArea = document.createElement('textarea')
  11107. textArea.value = outlineText
  11108. textArea.style.position = 'fixed'
  11109. textArea.style.left = '-999999px'
  11110. textArea.style.top = '-999999px'
  11111. document.body.appendChild(textArea)
  11112. textArea.focus()
  11113. textArea.select()
  11114. try {
  11115. const successful = document.execCommand('copy')
  11116. if (successful) {
  11117. ElMessage.success('大纲已复制到剪贴板')
  11118. } else {
  11119. throw new Error('execCommand 复制失败')
  11120. }
  11121. } catch (execError) {
  11122. console.error('传统复制方法也失败:', execError)
  11123. ElMessage.error('复制失败,请手动选择文本复制')
  11124. } finally {
  11125. document.body.removeChild(textArea)
  11126. }
  11127. } catch (error) {
  11128. console.error('复制大纲失败:', error)
  11129. ElMessage.error('复制失败,请手动选择文本复制')
  11130. }
  11131. }
  11132. // 下载大纲为Word文档
  11133. const downloadOutlineAsWord = async () => {
  11134. try {
  11135. if (!outlineData.value || outlineData.value.length === 0) {
  11136. ElMessage.warning('暂无大纲内容可下载')
  11137. return
  11138. }
  11139. // 构建Word文档内容(HTML格式,兼容Microsoft Office Word)
  11140. let htmlContent = `<!DOCTYPE html>
  11141. <html xmlns:o="urn:schemas-microsoft-com:office:office"
  11142. xmlns:w="urn:schemas-microsoft-com:office:word"
  11143. xmlns="http://www.w3.org/TR/REC-html40">
  11144. <head>
  11145. <meta charset="utf-8">
  11146. <meta name="ProgId" content="Word.Document">
  11147. <meta name="Generator" content="Microsoft Word 15">
  11148. <meta name="Originator" content="Microsoft Word 15">
  11149. <title>${outlineTitle.value || '安全培训大纲'}</title>
  11150. <!--[if gte mso 9]>
  11151. <xml>
  11152. <w:WordDocument>
  11153. <w:View>Print</w:View>
  11154. <w:Zoom>100</w:Zoom>
  11155. <w:DoNotPromptForConvert/>
  11156. <w:DoNotShowRevisions/>
  11157. <w:DoNotPrintRevisions/>
  11158. <w:DoNotShowComments/>
  11159. <w:DoNotShowInsertionsAndDeletions/>
  11160. <w:DoNotShowPropertyChanges/>
  11161. <w:Compatibility>
  11162. <w:BreakWrappedTables/>
  11163. <w:SnapToGridInCell/>
  11164. <w:WrapTextWithPunct/>
  11165. <w:UseAsianBreakRules/>
  11166. <w:DontGrowAutofit/>
  11167. </w:Compatibility>
  11168. </w:WordDocument>
  11169. </xml>
  11170. <![endif]-->
  11171. <style>
  11172. body {
  11173. font-family: "Microsoft YaHei", Arial, sans-serif;
  11174. font-size: 14px;
  11175. line-height: 1.6;
  11176. margin: 24px;
  11177. color: #000;
  11178. }
  11179. .header {
  11180. text-align: center;
  11181. margin-bottom: 14px;
  11182. }
  11183. .outline-title {
  11184. font-size: 24px;
  11185. font-weight: bold;
  11186. margin-bottom: 14px;
  11187. color: #000;
  11188. }
  11189. h1, h2, h3, h4, h5, h6 {
  11190. color: #000;
  11191. font-weight: bold;
  11192. font-family: "Microsoft YaHei", Arial, sans-serif;
  11193. margin-top: 20px;
  11194. margin-bottom: 15px;
  11195. }
  11196. h1 {
  11197. font-size: 20px;
  11198. border-bottom: 2px solid #000;
  11199. padding-bottom: 10px;
  11200. }
  11201. h2 {
  11202. font-size: 18px;
  11203. margin-top: 20px;
  11204. margin-bottom: 12px;
  11205. }
  11206. h3 {
  11207. font-size: 16px;
  11208. margin-top: 15px;
  11209. margin-bottom: 8px;
  11210. }
  11211. h4 {
  11212. font-size: 14px;
  11213. margin-top: 12px;
  11214. margin-bottom: 6px;
  11215. }
  11216. ul, li {
  11217. color: #000;
  11218. font-family: "Microsoft YaHei", Arial, sans-serif;
  11219. }
  11220. li {
  11221. margin-bottom: 4px;
  11222. }
  11223. .stats {
  11224. background: #f8f9fa;
  11225. padding: 20px;
  11226. border-radius: 8px;
  11227. margin-top: 30px;
  11228. border: 1px solid #ddd;
  11229. }
  11230. .stats h3 {
  11231. color: #2c3e50;
  11232. margin-top: 0;
  11233. font-size: 16px;
  11234. }
  11235. .stats p {
  11236. margin: 8px 0;
  11237. color: #555;
  11238. font-size: 14px;
  11239. }
  11240. </style>
  11241. </head>
  11242. <body>
  11243. <div class="header">
  11244. <div class="outline-title">${outlineTitle.value || '安全培训大纲'}</div>
  11245. </div>
  11246. `
  11247. // 添加章节内容(使用新的结构格式)
  11248. outlineData.value.forEach((chapter, chapterIndex) => {
  11249. htmlContent += `<h2>${chapter.title}</h2>`
  11250. if (chapter.sections && chapter.sections.length > 0) {
  11251. chapter.sections.forEach((section, sectionIndex) => {
  11252. htmlContent += `<h3>${section.title}</h3>`
  11253. if (section.subsections && section.subsections.length > 0) {
  11254. section.subsections.forEach((subsection, subsectionIndex) => {
  11255. htmlContent += `<h4>${subsection.title}</h4>`
  11256. if (subsection.subsubsections && subsection.subsubsections.length > 0) {
  11257. htmlContent += `<ul>`
  11258. subsection.subsubsections.forEach((subsubsection, subsubsectionIndex) => {
  11259. htmlContent += `<li><strong>${subsubsection.title}</strong>`
  11260. if (subsubsection.content) {
  11261. htmlContent += `<br>${subsubsection.content}`
  11262. }
  11263. htmlContent += `</li>`
  11264. })
  11265. htmlContent += `</ul>`
  11266. }
  11267. })
  11268. }
  11269. })
  11270. }
  11271. })
  11272. // 添加统计信息
  11273. if (outlineStats.value) {
  11274. htmlContent += `
  11275. <div class="stats">
  11276. <h3>大纲统计信息</h3>
  11277. <p><strong>总章节数:</strong>${outlineStats.value.totalChapters || '未知'}章</p>
  11278. <p><strong>总小节数:</strong>${outlineStats.value.totalSections || '未知'}小节</p>
  11279. <p><strong>预计PPT页数:</strong>${outlineStats.value.estimatedPages || '未知'}</p>
  11280. <p><strong>预计讲解时长:</strong>${outlineStats.value.estimatedTime || '未知'}</p>
  11281. </div>
  11282. `
  11283. }
  11284. htmlContent += `
  11285. </body>
  11286. </html>
  11287. `
  11288. // 创建Blob对象 - 使用Word兼容的MIME类型
  11289. const blob = new Blob([htmlContent], { type: 'application/msword' })
  11290. // 创建下载链接
  11291. const url = URL.createObjectURL(blob)
  11292. const link = document.createElement('a')
  11293. link.href = url
  11294. link.download = `${outlineTitle.value || '安全培训大纲'}.doc`
  11295. document.body.appendChild(link)
  11296. link.click()
  11297. document.body.removeChild(link)
  11298. // 清理URL对象
  11299. URL.revokeObjectURL(url)
  11300. ElMessage.success('下载成功')
  11301. } catch (error) {
  11302. console.error('下载大纲失败:', error)
  11303. ElMessage.error('下载失败,请重试')
  11304. }
  11305. }
  11306. </script>
  11307. <style lang="less" scoped>
  11308. // 全局图片无边框样式
  11309. img {
  11310. border: none !important;
  11311. outline: none !important;
  11312. }
  11313. // 删除图标样式
  11314. .delete-icon {
  11315. width: 16px;
  11316. height: 16px;
  11317. }
  11318. body,
  11319. html {
  11320. background-color: #E8F4FD;
  11321. margin: 0;
  11322. padding: 0;
  11323. }
  11324. .chat-container {
  11325. display: flex;
  11326. height: 100vh;
  11327. font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
  11328. }
  11329. /* 中间历史记录栏 */
  11330. .history-sidebar {
  11331. width: 280px;
  11332. min-width: 280px;
  11333. flex-shrink: 0;
  11334. background: #E8F0FF;
  11335. display: flex;
  11336. flex-direction: column;
  11337. }
  11338. /* 中间历史记录栏样式 */
  11339. .history-sidebar {
  11340. padding: 24px 16px 0 16px;
  11341. .history-header {
  11342. background: transparent;
  11343. .section-title {
  11344. font-size: 16px;
  11345. font-weight: 600;
  11346. color: #2C3E50;
  11347. }
  11348. .new-chat-btn {
  11349. width: 248px;
  11350. height: 40px;
  11351. cursor: pointer;
  11352. transition: opacity 0.3s ease;
  11353. object-fit: contain;
  11354. display: block;
  11355. margin-top: 16px;
  11356. margin-bottom: 16px;
  11357. &:hover {
  11358. opacity: 0.8;
  11359. }
  11360. }
  11361. }
  11362. .history-list {
  11363. flex: 1;
  11364. overflow-y: auto;
  11365. width: 248px;
  11366. height: 84px;
  11367. /* 隐藏滚动条 */
  11368. &::-webkit-scrollbar {
  11369. display: none;
  11370. }
  11371. -ms-overflow-style: none;
  11372. /* IE and Edge */
  11373. scrollbar-width: none;
  11374. /* Firefox */
  11375. .history-item {
  11376. background: white;
  11377. border-radius: 8px;
  11378. padding: 12px 15px 12px 15px;
  11379. margin-bottom: 8px;
  11380. cursor: pointer;
  11381. transition: all 0.3s ease;
  11382. border-left: 3px solid transparent;
  11383. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  11384. display: flex;
  11385. align-items: flex-start;
  11386. gap: 12px;
  11387. position: relative;
  11388. &:hover {
  11389. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  11390. .delete-btn {
  11391. opacity: 1;
  11392. visibility: visible;
  11393. }
  11394. }
  11395. &.active {
  11396. border-left-color: #3E7BFA;
  11397. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
  11398. cursor: default;
  11399. opacity: 0.8;
  11400. &:hover {
  11401. transform: none;
  11402. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.2);
  11403. }
  11404. }
  11405. .history-icon {
  11406. width: 80px;
  11407. height: 60px;
  11408. flex-shrink: 0;
  11409. .history-icon-img {
  11410. width: 100%;
  11411. height: 100%;
  11412. object-fit: contain;
  11413. border-radius: 4px;
  11414. }
  11415. }
  11416. .history-content {
  11417. flex: 1;
  11418. min-width: 0;
  11419. display: flex;
  11420. flex-direction: column;
  11421. justify-content: space-between;
  11422. height: 60px;
  11423. // margin-right: 12px;
  11424. .history-title {
  11425. font-size: 14px;
  11426. line-height: 1.4;
  11427. color: #1F2937;
  11428. overflow: hidden;
  11429. display: -webkit-box;
  11430. -webkit-line-clamp: 2;
  11431. -webkit-box-orient: vertical;
  11432. text-overflow: ellipsis;
  11433. flex: 1;
  11434. max-height: calc(14px * 1.4 * 2);
  11435. /* 确保最大高度正好是两行 */
  11436. word-break: break-word;
  11437. }
  11438. .history-meta {
  11439. display: flex;
  11440. justify-content: space-between;
  11441. align-items: center;
  11442. margin-top: auto;
  11443. .history-time {
  11444. font-size: 12px;
  11445. color: #7C8DB5;
  11446. }
  11447. .history-pages {
  11448. font-size: 12px;
  11449. color: #6B7280;
  11450. }
  11451. }
  11452. }
  11453. .delete-btn {
  11454. opacity: 0;
  11455. visibility: hidden;
  11456. transition: all 0.2s ease;
  11457. cursor: pointer;
  11458. padding: 2px;
  11459. border-radius: 3px;
  11460. color: #7c8db5;
  11461. display: flex;
  11462. align-items: center;
  11463. justify-content: center;
  11464. margin-left: 8px;
  11465. flex-shrink: 0;
  11466. position: absolute;
  11467. top: 50%;
  11468. right: 2px;
  11469. transform: translateY(-50%);
  11470. &:hover {
  11471. color: #ff4757;
  11472. background-color: rgba(255, 71, 87, 0.05);
  11473. }
  11474. &.always-visible {
  11475. opacity: 1;
  11476. visibility: visible;
  11477. }
  11478. }
  11479. }
  11480. .empty-history {
  11481. display: flex;
  11482. flex-direction: column;
  11483. align-items: center;
  11484. justify-content: center;
  11485. margin-top: 236px;
  11486. .empty-icon {
  11487. width: 147px;
  11488. height: 148px;
  11489. object-fit: contain;
  11490. margin-bottom: 16px;
  11491. }
  11492. .empty-text {
  11493. font-size: 16px;
  11494. color: #9C9FA7;
  11495. text-align: center;
  11496. }
  11497. }
  11498. }
  11499. }
  11500. /* 主工作区域 */
  11501. .main-work {
  11502. flex: 1;
  11503. background: #EBF3FF;
  11504. display: flex;
  11505. flex-direction: column;
  11506. }
  11507. /* 工作头部 */
  11508. .work-header {
  11509. background: transparent;
  11510. padding: 30px 0px 0px 18px;
  11511. h2 {
  11512. margin: 0;
  11513. font-size: 20px;
  11514. font-weight: 600;
  11515. color: #2C3E50;
  11516. }
  11517. }
  11518. /* 工作内容区域 */
  11519. .work-content {
  11520. flex: 1;
  11521. overflow-y: auto;
  11522. display: flex;
  11523. flex-direction: column;
  11524. align-items: center;
  11525. position: relative;
  11526. margin-top: 24px;
  11527. padding: 0px 30px;
  11528. /* 隐藏滚动条样式 */
  11529. &::-webkit-scrollbar {
  11530. width: 0;
  11531. background: transparent;
  11532. }
  11533. &::-webkit-scrollbar-track {
  11534. background: transparent;
  11535. }
  11536. &::-webkit-scrollbar-thumb {
  11537. background: transparent;
  11538. }
  11539. }
  11540. /* 聊天内容区域 */
  11541. .chat-content {
  11542. flex: 1;
  11543. overflow-y: auto;
  11544. display: flex;
  11545. flex-direction: column;
  11546. align-items: center;
  11547. margin-top: 24px;
  11548. width: 1060px;
  11549. }
  11550. /* 旋转动画 */
  11551. .rotating {
  11552. animation: rotate 1s linear infinite;
  11553. }
  11554. @keyframes rotate {
  11555. from {
  11556. transform: rotate(0deg);
  11557. }
  11558. to {
  11559. transform: rotate(360deg);
  11560. }
  11561. }
  11562. /* 大纲内容禁用状态 */
  11563. .outline-content.disabled {
  11564. position: relative;
  11565. pointer-events: none;
  11566. opacity: 0.8;
  11567. }
  11568. /* 生成中遮罩层 */
  11569. .generating-overlay {
  11570. position: absolute;
  11571. top: 0;
  11572. left: 0;
  11573. right: 0;
  11574. bottom: 0;
  11575. background: rgba(255, 255, 255, 0.9);
  11576. display: flex;
  11577. align-items: center;
  11578. justify-content: center;
  11579. z-index: 100;
  11580. border-radius: 12px;
  11581. }
  11582. .generating-content {
  11583. text-align: center;
  11584. padding: 40px;
  11585. }
  11586. .generating-content p {
  11587. font-size: 18px;
  11588. color: #6B7280;
  11589. margin: 0;
  11590. font-weight: 500;
  11591. }
  11592. /* 应用模板中遮罩层 */
  11593. .applying-overlay {
  11594. position: absolute;
  11595. top: 0;
  11596. left: 0;
  11597. right: 0;
  11598. bottom: 0;
  11599. background: rgba(255, 255, 255, 0.7);
  11600. display: flex;
  11601. align-items: center;
  11602. justify-content: center;
  11603. z-index: 1000;
  11604. border-radius: 12px;
  11605. overflow: hidden;
  11606. }
  11607. /* 禁用状态样式 */
  11608. .history-sidebar.disabled {
  11609. pointer-events: none;
  11610. opacity: 0.8;
  11611. }
  11612. .history-sidebar.disabled .new-chat-btn {
  11613. cursor: not-allowed;
  11614. opacity: 0.8;
  11615. }
  11616. .history-item.disabled {
  11617. cursor: not-allowed !important;
  11618. opacity: 0.8;
  11619. }
  11620. .applying-content {
  11621. text-align: center;
  11622. padding: 40px;
  11623. }
  11624. .applying-content p {
  11625. font-size: 18px;
  11626. color: #6B7280;
  11627. margin: 0;
  11628. font-weight: 500;
  11629. }
  11630. /* 模板内容禁用状态 */
  11631. .template-content.disabled {
  11632. position: relative;
  11633. pointer-events: none;
  11634. opacity: 0.8;
  11635. }
  11636. /* step3内容禁用状态 */
  11637. .step3-content.disabled {
  11638. position: relative;
  11639. pointer-events: none;
  11640. opacity: 0.8;
  11641. }
  11642. /* 下载内容禁用状态 */
  11643. .download-content.disabled {
  11644. position: relative;
  11645. pointer-events: none;
  11646. opacity: 0.8;
  11647. }
  11648. /* 加载状态样式 */
  11649. .loading-overlay {
  11650. position: absolute;
  11651. top: 0;
  11652. left: 0;
  11653. right: 0;
  11654. bottom: 0;
  11655. background: rgba(255, 255, 255, 0.95);
  11656. display: flex;
  11657. flex-direction: column;
  11658. justify-content: center;
  11659. align-items: center;
  11660. z-index: 1000;
  11661. border-radius: 12px;
  11662. backdrop-filter: blur(2px);
  11663. transition: all 0.3s ease;
  11664. }
  11665. .loading-content {
  11666. text-align: center;
  11667. padding: 40px;
  11668. background: rgba(255, 255, 255, 0.9);
  11669. border-radius: 16px;
  11670. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  11671. border: 1px solid rgba(255, 255, 255, 0.2);
  11672. }
  11673. .loading-spinner {
  11674. width: 48px;
  11675. height: 48px;
  11676. border: 4px solid #f3f3f3;
  11677. border-top: 4px solid #409eff;
  11678. border-radius: 50%;
  11679. animation: spin 1s linear infinite;
  11680. margin: 0 auto 24px auto;
  11681. }
  11682. .loading-text {
  11683. color: #6B7280;
  11684. font-size: 18px;
  11685. margin: 0;
  11686. font-weight: 500;
  11687. }
  11688. /* 历史记录加载状态样式 */
  11689. .history-loading {
  11690. display: flex;
  11691. flex-direction: column;
  11692. align-items: center;
  11693. justify-content: center;
  11694. padding: 40px 20px;
  11695. min-height: 200px;
  11696. }
  11697. .history-loading .loading-spinner {
  11698. width: 32px;
  11699. height: 32px;
  11700. border: 3px solid #f3f3f3;
  11701. border-top: 3px solid #409eff;
  11702. border-radius: 50%;
  11703. animation: spin 1s linear infinite;
  11704. margin: 0 auto 16px auto;
  11705. }
  11706. .history-loading .loading-text {
  11707. color: #6B7280;
  11708. font-size: 14px;
  11709. margin: 0;
  11710. font-weight: 400;
  11711. }
  11712. .loading-subtitle {
  11713. color: #9CA3AF;
  11714. font-size: 14px;
  11715. margin-top: 8px;
  11716. font-weight: 400;
  11717. }
  11718. @keyframes spin {
  11719. 0% {
  11720. transform: rotate(0deg);
  11721. }
  11722. 100% {
  11723. transform: rotate(360deg);
  11724. }
  11725. }
  11726. @keyframes pulse {
  11727. 0%,
  11728. 100% {
  11729. transform: scale(1);
  11730. opacity: 1;
  11731. }
  11732. 50% {
  11733. transform: scale(1.05);
  11734. opacity: 0.8;
  11735. }
  11736. }
  11737. @keyframes blink {
  11738. 0%,
  11739. 50% {
  11740. opacity: 1;
  11741. }
  11742. 51%,
  11743. 100% {
  11744. opacity: 0.3;
  11745. }
  11746. }
  11747. /* AI生成的幻灯片样式 */
  11748. .ai-generated-slide {
  11749. width: 100%;
  11750. height: 100%;
  11751. display: flex;
  11752. align-items: center;
  11753. justify-content: center;
  11754. }
  11755. .slide-content {
  11756. width: 960px;
  11757. height: 540px;
  11758. position: relative;
  11759. border-radius: 8px;
  11760. overflow: hidden;
  11761. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  11762. }
  11763. .slide-element {
  11764. position: absolute;
  11765. }
  11766. /* 按钮禁用状态样式 */
  11767. .action-btn:disabled,
  11768. .eval-btn:disabled {
  11769. cursor: not-allowed;
  11770. }
  11771. .action-btn:disabled:hover,
  11772. .eval-btn:disabled:hover {
  11773. background: inherit;
  11774. border-color: inherit;
  11775. transform: none;
  11776. }
  11777. /* AI助手介绍 */
  11778. .ai-intro {
  11779. display: flex;
  11780. flex-direction: column;
  11781. align-items: center;
  11782. margin-bottom: 96px;
  11783. .ai-avatar {
  11784. width: 80px;
  11785. height: 80px;
  11786. border-radius: 20px;
  11787. display: flex;
  11788. align-items: center;
  11789. justify-content: center;
  11790. margin-bottom: 20px;
  11791. .ai-avatar-img {
  11792. width: 100%;
  11793. height: 100%;
  11794. object-fit: contain;
  11795. }
  11796. }
  11797. .ai-greeting {
  11798. text-align: center;
  11799. h3 {
  11800. font-size: 24px;
  11801. font-weight: 600;
  11802. color: #2C3E50;
  11803. margin: 0 0 8px 0;
  11804. }
  11805. p {
  11806. font-size: 16px;
  11807. color: #7C8DB5;
  11808. margin: 0;
  11809. }
  11810. }
  11811. }
  11812. /* 聊天消息区域 */
  11813. .chat-messages {
  11814. width: 100%;
  11815. max-width: 1060px;
  11816. padding: 0 75px 0 35px;
  11817. display: flex;
  11818. flex-direction: column;
  11819. gap: 24px;
  11820. padding-bottom: 40px;
  11821. /* 添加底部间距 */
  11822. }
  11823. /* 消息项样式 */
  11824. .message-item {
  11825. display: flex;
  11826. flex-direction: column;
  11827. gap: 12px;
  11828. }
  11829. /* 用户消息样式 */
  11830. .user-message {
  11831. display: flex;
  11832. flex-direction: column;
  11833. align-items: flex-end;
  11834. gap: 8px;
  11835. .message-content {
  11836. background: #004BFF20;
  11837. color: #000000;
  11838. padding: 16px 20px;
  11839. border-radius: 18px 0px 18px 18px;
  11840. max-width: 900px;
  11841. word-wrap: break-word;
  11842. line-height: 1.5;
  11843. font-size: 16px;
  11844. .message-file {
  11845. margin-bottom: 12px;
  11846. .file-display {
  11847. display: flex;
  11848. align-items: center;
  11849. background: rgba(255, 255, 255, 0.8);
  11850. border: 1px solid #E5E7EB;
  11851. border-radius: 12px;
  11852. padding: 16px;
  11853. max-width: 400px;
  11854. .file-icon {
  11855. font-size: 32px;
  11856. margin-right: 16px;
  11857. width: 48px;
  11858. text-align: center;
  11859. .file-icon-img {
  11860. width: 32px;
  11861. height: 32px;
  11862. object-fit: contain;
  11863. }
  11864. }
  11865. .file-details {
  11866. flex: 1;
  11867. .file-name {
  11868. font-size: 14px;
  11869. font-weight: 500;
  11870. color: #374151;
  11871. margin-bottom: 4px;
  11872. overflow: hidden;
  11873. text-overflow: ellipsis;
  11874. white-space: nowrap;
  11875. max-width: 200px;
  11876. }
  11877. .file-size {
  11878. font-size: 12px;
  11879. color: #6B7280;
  11880. }
  11881. }
  11882. }
  11883. }
  11884. .message-text {
  11885. // margin-top: 8px;
  11886. }
  11887. }
  11888. .message-actions {
  11889. display: flex;
  11890. // gap: 8px;
  11891. .action-btn {
  11892. background: none;
  11893. border: none;
  11894. padding: 6px 4px;
  11895. border-radius: 4px;
  11896. font-size: 12px;
  11897. color: #6b7280;
  11898. cursor: pointer;
  11899. transition: all 0.2s ease;
  11900. display: flex;
  11901. align-items: center;
  11902. gap: 4px;
  11903. &:hover {
  11904. color: #374151;
  11905. }
  11906. .action-icon {
  11907. width: 16px;
  11908. height: 16px;
  11909. object-fit: contain;
  11910. }
  11911. }
  11912. }
  11913. }
  11914. /* AI消息样式 */
  11915. .ai-message {
  11916. display: flex;
  11917. gap: 12px;
  11918. .ai-avatar-small {
  11919. width: 40px;
  11920. height: 40px;
  11921. flex-shrink: 0;
  11922. .ai-icon {
  11923. width: 100%;
  11924. height: 100%;
  11925. object-fit: contain;
  11926. }
  11927. }
  11928. .message-content {
  11929. background: white;
  11930. color: #374151;
  11931. padding: 16px 20px;
  11932. border-radius: 0px 18px 18px 18px;
  11933. max-width: 900px;
  11934. width: fit-content;
  11935. min-width: 200px;
  11936. word-wrap: break-word;
  11937. line-height: 1.5;
  11938. font-size: 16px;
  11939. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  11940. white-space: pre-line;
  11941. transition: width 0.3s ease;
  11942. .ai-text {
  11943. min-height: 20px;
  11944. line-height: 1.5;
  11945. margin-bottom: 16px;
  11946. .typing-indicator {
  11947. color: #9CA3AF;
  11948. font-style: italic;
  11949. font-size: 14px;
  11950. display: flex;
  11951. align-items: center;
  11952. gap: 8px;
  11953. white-space: nowrap;
  11954. .thinking-animation {
  11955. display: flex;
  11956. gap: 4px;
  11957. .dot {
  11958. width: 6px;
  11959. height: 6px;
  11960. background: #9CA3AF;
  11961. border-radius: 50%;
  11962. animation: thinking 1.4s infinite ease-in-out;
  11963. &:nth-child(1) {
  11964. animation-delay: -0.32s;
  11965. }
  11966. &:nth-child(2) {
  11967. animation-delay: -0.16s;
  11968. }
  11969. &:nth-child(3) {
  11970. animation-delay: 0s;
  11971. }
  11972. }
  11973. }
  11974. }
  11975. .ai-content {
  11976. line-height: 1.6;
  11977. // 加粗文本样式
  11978. strong {
  11979. font-weight: 600;
  11980. color: #1f2937;
  11981. }
  11982. // 斜体文本样式
  11983. em {
  11984. font-style: italic;
  11985. color: #4b5563;
  11986. }
  11987. // 标题样式
  11988. h3 {
  11989. font-size: 18px;
  11990. font-weight: 600;
  11991. color: #1f2937;
  11992. margin: 16px 0 8px 0;
  11993. border-bottom: 2px solid #e5e7eb;
  11994. padding-bottom: 4px;
  11995. }
  11996. h4 {
  11997. font-size: 16px;
  11998. font-weight: 600;
  11999. color: #1f2937;
  12000. margin: 12px 0 6px 0;
  12001. }
  12002. // 列表样式
  12003. ul {
  12004. margin: 8px 0;
  12005. padding-left: 20px;
  12006. li {
  12007. margin: 4px 0;
  12008. line-height: 1.5;
  12009. }
  12010. }
  12011. // 代码样式
  12012. code {
  12013. background: #f3f4f6;
  12014. padding: 2px 6px;
  12015. border-radius: 4px;
  12016. font-family: 'Courier New', monospace;
  12017. font-size: 14px;
  12018. color: #dc2626;
  12019. }
  12020. // 换行样式
  12021. br {
  12022. margin: 4px 0;
  12023. }
  12024. }
  12025. }
  12026. .divider {
  12027. height: 1px;
  12028. background: #e5e7eb;
  12029. margin: 16px 0 12px 0;
  12030. }
  12031. .message-actions {
  12032. display: flex;
  12033. justify-content: space-between;
  12034. align-items: center;
  12035. .left-actions {
  12036. display: flex;
  12037. gap: 8px;
  12038. flex-wrap: wrap;
  12039. }
  12040. .right-actions {
  12041. display: flex;
  12042. }
  12043. .action-btn {
  12044. background: none;
  12045. border: none;
  12046. padding: 6px 5px;
  12047. border-radius: 4px;
  12048. cursor: pointer;
  12049. transition: all 0.2s ease;
  12050. display: flex;
  12051. align-items: center;
  12052. gap: 4px;
  12053. font-size: 12px;
  12054. color: #6b7280;
  12055. &:hover {
  12056. color: #374151;
  12057. background: rgba(59, 130, 246, 0.1);
  12058. }
  12059. .action-icon {
  12060. width: 16px;
  12061. height: 16px;
  12062. object-fit: contain;
  12063. }
  12064. // 点赞和点踩按钮的特殊样式
  12065. &.thumbs-up-btn {
  12066. transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  12067. &:hover {
  12068. color: #059669;
  12069. transform: scale(1.1);
  12070. }
  12071. &.active {
  12072. color: #059669;
  12073. transform: scale(1.2);
  12074. .action-icon {
  12075. filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(142deg) brightness(104%) contrast(97%);
  12076. }
  12077. }
  12078. &:active {
  12079. transform: scale(0.9);
  12080. transition: all 0.1s ease;
  12081. }
  12082. }
  12083. &.thumbs-down-btn {
  12084. transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  12085. &:hover {
  12086. color: #dc2626;
  12087. transform: scale(1.1);
  12088. }
  12089. &.active {
  12090. color: #dc2626;
  12091. transform: scale(1.2);
  12092. .action-icon {
  12093. filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(0deg) brightness(104%) contrast(97%) !important;
  12094. }
  12095. }
  12096. &:active {
  12097. transform: scale(0.9);
  12098. transition: all 0.1s ease;
  12099. }
  12100. }
  12101. }
  12102. }
  12103. }
  12104. }
  12105. /* 打字动画 */
  12106. @keyframes thinking {
  12107. 0%,
  12108. 80%,
  12109. 100% {
  12110. transform: scale(0.8);
  12111. opacity: 0.5;
  12112. }
  12113. 40% {
  12114. transform: scale(1);
  12115. opacity: 1;
  12116. }
  12117. }
  12118. /* 功能卡片 */
  12119. .function-cards {
  12120. display: grid;
  12121. grid-template-columns: repeat(2, 1fr);
  12122. gap: 16px;
  12123. margin-top: 56px;
  12124. max-width: 592px;
  12125. .function-card {
  12126. background: white;
  12127. width: 288px;
  12128. height: 112px;
  12129. padding: 20px 0px;
  12130. padding-left: 20px;
  12131. border-radius: 12px;
  12132. border: 1px solid #E5E8EB;
  12133. cursor: pointer;
  12134. transition: all 0.3s ease;
  12135. display: flex;
  12136. flex-direction: column;
  12137. box-sizing: border-box;
  12138. &:hover {
  12139. border-color: #667eea;
  12140. transform: translateY(-2px);
  12141. box-shadow: 0 4px 20px rgba(102, 126, 234, 0.1);
  12142. }
  12143. .card-header {
  12144. display: flex;
  12145. align-items: center;
  12146. gap: 16px;
  12147. margin-bottom: 8px;
  12148. .card-icon {
  12149. width: 40px;
  12150. height: 40px;
  12151. display: flex;
  12152. align-items: center;
  12153. justify-content: center;
  12154. background: #F3F5FF;
  12155. border-radius: 8px;
  12156. flex-shrink: 0;
  12157. .card-icon-img {
  12158. width: 40px;
  12159. height: 40px;
  12160. object-fit: contain;
  12161. }
  12162. }
  12163. h4 {
  12164. font-size: 16px;
  12165. font-weight: 600;
  12166. color: #2C3E50;
  12167. margin: 0;
  12168. line-height: 1.5;
  12169. flex: 1;
  12170. }
  12171. }
  12172. .card-description {
  12173. margin-left: 55px;
  12174. p {
  12175. font-size: 14px;
  12176. color: #7C8DB5;
  12177. margin: 0;
  12178. line-height: 1.4;
  12179. }
  12180. }
  12181. }
  12182. }
  12183. /* 步骤二:培训大纲界面 */
  12184. .step2-content {
  12185. width: 100%;
  12186. // max-width: 1200px;
  12187. margin: 0 auto;
  12188. .outline-container {
  12189. display: flex;
  12190. gap: 32px;
  12191. height: 100%;
  12192. }
  12193. .outline-main {
  12194. flex: 1;
  12195. background: white;
  12196. border-radius: 12px;
  12197. width: 100px;
  12198. min-height: 943px;
  12199. height: calc(100vh - 80px);
  12200. /* 自适应浏览器高度,减去头部和边距 */
  12201. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  12202. overflow: hidden;
  12203. margin-bottom: 20px;
  12204. .outline-header {
  12205. display: flex;
  12206. align-items: center;
  12207. justify-content: space-between;
  12208. padding: 24px;
  12209. border-bottom: 1px solid #E5E7EB;
  12210. .outline-title-container {
  12211. flex: 1;
  12212. min-width: 0;
  12213. margin-right: 16px;
  12214. }
  12215. .outline-title {
  12216. font-size: 22px;
  12217. font-weight: 600;
  12218. color: #1F2937;
  12219. margin: 0;
  12220. cursor: pointer;
  12221. transition: all 0.2s ease;
  12222. word-break: break-word;
  12223. &:hover:not(.disabled) {
  12224. color: #3E7BFA;
  12225. background: rgba(62, 123, 250, 0.05);
  12226. border-radius: 4px;
  12227. padding: 4px 8px;
  12228. margin: -4px -8px;
  12229. }
  12230. &.disabled {
  12231. cursor: not-allowed;
  12232. }
  12233. }
  12234. .outline-actions {
  12235. display: flex;
  12236. gap: 6px;
  12237. // margin-right: 24px;
  12238. .action-btn {
  12239. display: flex;
  12240. align-items: center;
  12241. gap: 6px;
  12242. color: #374151;
  12243. font-size: 14px;
  12244. cursor: pointer;
  12245. transition: all 0.3s ease;
  12246. background: none;
  12247. border: none;
  12248. &:hover {
  12249. background: none;
  12250. border-color: transparent;
  12251. }
  12252. .action-icon {
  12253. width: 16px;
  12254. height: 16px;
  12255. }
  12256. }
  12257. .exam-btn {
  12258. color: #22B850;
  12259. font-weight: 500;
  12260. &:hover {
  12261. background: none;
  12262. color: #22B850;
  12263. }
  12264. .action-icon {
  12265. width: 16px;
  12266. height: 16px;
  12267. }
  12268. }
  12269. }
  12270. .edit-input-container {
  12271. flex: 1;
  12272. min-width: 0;
  12273. margin-right: 16px;
  12274. }
  12275. .edit-textarea {
  12276. width: 100%;
  12277. border: 2px solid #3E7BFA;
  12278. border-radius: 6px;
  12279. padding: 8px 12px;
  12280. font-size: 12px;
  12281. color: #6B7280;
  12282. background: white;
  12283. outline: none;
  12284. transition: all 0.2s ease;
  12285. resize: vertical;
  12286. min-height: 60px;
  12287. font-family: inherit;
  12288. line-height: 1.6;
  12289. &:focus {
  12290. border-color: #3E7BFA;
  12291. box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
  12292. }
  12293. &.title-edit-textarea {
  12294. font-size: 22px;
  12295. font-weight: 600;
  12296. color: #1F2937;
  12297. min-height: 60px;
  12298. }
  12299. }
  12300. .edit-input {
  12301. width: 100%;
  12302. border: 2px solid #3E7BFA;
  12303. border-radius: 6px;
  12304. padding: 8px 12px;
  12305. font-size: inherit;
  12306. font-weight: inherit;
  12307. color: inherit;
  12308. background: white;
  12309. outline: none;
  12310. transition: all 0.2s ease;
  12311. &:focus {
  12312. border-color: #2563EB;
  12313. box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
  12314. }
  12315. &.title-edit-input {
  12316. font-size: 22px;
  12317. font-weight: 600;
  12318. color: #1F2937;
  12319. }
  12320. }
  12321. }
  12322. .outline-content {
  12323. padding: 32px;
  12324. height: calc(100% - 80px);
  12325. /* 自适应高度,减去头部高度 */
  12326. overflow-y: auto;
  12327. position: relative;
  12328. .outline-content-scrollable {
  12329. height: 100%;
  12330. /* 占满父容器高度 */
  12331. overflow-y: auto;
  12332. padding-right: 16px;
  12333. /* 自定义滚动条样式 */
  12334. &::-webkit-scrollbar {
  12335. width: 8px;
  12336. }
  12337. &::-webkit-scrollbar-track {
  12338. background: #f1f1f1;
  12339. border-radius: 4px;
  12340. }
  12341. &::-webkit-scrollbar-thumb {
  12342. background: #c1c1c1;
  12343. border-radius: 4px;
  12344. &:hover {
  12345. background: #a8a8a8;
  12346. }
  12347. }
  12348. }
  12349. .zoom-controls {
  12350. position: absolute;
  12351. top: 16px;
  12352. right: 16px;
  12353. display: flex;
  12354. gap: 4px;
  12355. z-index: 10;
  12356. margin-right: 12px;
  12357. .zoom-btn {
  12358. border: none;
  12359. background: none;
  12360. cursor: pointer;
  12361. display: flex;
  12362. align-items: center;
  12363. justify-content: center;
  12364. .zoom-icon {
  12365. width: 16px;
  12366. height: 16px;
  12367. }
  12368. }
  12369. }
  12370. .edit-input-container {
  12371. flex: 1;
  12372. margin-right: 8px;
  12373. }
  12374. .edit-input-wrapper {
  12375. width: 100%;
  12376. position: relative;
  12377. }
  12378. .edit-options-inline {
  12379. position: absolute;
  12380. right: 8px;
  12381. top: 50%;
  12382. transform: translateY(-50%);
  12383. display: flex;
  12384. gap: 4px;
  12385. z-index: 10;
  12386. .edit-option-btn {
  12387. background: none;
  12388. border: none;
  12389. padding: 4px;
  12390. border-radius: 3px;
  12391. cursor: pointer;
  12392. transition: all 0.2s ease;
  12393. display: flex;
  12394. align-items: center;
  12395. justify-content: center;
  12396. width: 28px;
  12397. height: 28px;
  12398. &:hover {
  12399. background: rgba(62, 123, 250, 0.1);
  12400. transform: scale(1.1);
  12401. }
  12402. &.delete-btn:hover {
  12403. background: rgba(239, 68, 68, 0.05);
  12404. }
  12405. .edit-icon {
  12406. width: 16px;
  12407. height: 16px;
  12408. object-fit: contain;
  12409. }
  12410. }
  12411. }
  12412. .add-chapter-container {
  12413. margin-top: 16px;
  12414. padding: 16px;
  12415. text-align: center;
  12416. .add-chapter-btn {
  12417. display: inline-flex;
  12418. align-items: center;
  12419. gap: 8px;
  12420. padding: 12px 24px;
  12421. background: linear-gradient(135deg, #3E7BFA 0%, #5A9BFF 100%);
  12422. color: white;
  12423. border: none;
  12424. border-radius: 8px;
  12425. font-size: 14px;
  12426. font-weight: 500;
  12427. cursor: pointer;
  12428. transition: all 0.3s ease;
  12429. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.3);
  12430. &:hover {
  12431. transform: translateY(-2px);
  12432. box-shadow: 0 4px 12px rgba(62, 123, 250, 0.4);
  12433. background: linear-gradient(135deg, #2D6BFA 0%, #4A8BFF 100%);
  12434. }
  12435. &:active {
  12436. transform: translateY(0);
  12437. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.3);
  12438. }
  12439. .add-icon {
  12440. width: 16px;
  12441. height: 16px;
  12442. object-fit: contain;
  12443. filter: brightness(0) invert(1);
  12444. }
  12445. }
  12446. }
  12447. .edit-input {
  12448. width: 100%;
  12449. border: 2px solid #3E7BFA;
  12450. border-radius: 6px;
  12451. padding: 8px 12px;
  12452. font-size: inherit;
  12453. font-weight: inherit;
  12454. color: inherit;
  12455. background: white;
  12456. outline: none;
  12457. transition: all 0.2s ease;
  12458. &:focus {
  12459. border-color: #2563EB;
  12460. box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
  12461. }
  12462. &.title-edit-input {
  12463. font-size: 22px;
  12464. font-weight: 600;
  12465. color: #1F2937;
  12466. }
  12467. &.chapter-edit-input {
  12468. font-size: 20px;
  12469. font-weight: 700;
  12470. color: #111827;
  12471. padding-right: 80px;
  12472. /* 为图标留出空间 */
  12473. }
  12474. &.section-edit-input {
  12475. font-size: 16px;
  12476. font-weight: 600;
  12477. color: #374151;
  12478. padding-left: 24px;
  12479. padding-right: 80px;
  12480. /* 为图标留出空间 */
  12481. }
  12482. &.subsection-edit-input {
  12483. font-size: 14px;
  12484. font-weight: 400;
  12485. color: #6B7280;
  12486. padding-left: 48px;
  12487. padding-right: 50px;
  12488. /* 为图标留出空间,子小节只有删除按钮 */
  12489. }
  12490. &.subsubsection-edit-input {
  12491. font-size: 13px;
  12492. font-weight: 500;
  12493. color: #9CA3AF;
  12494. padding-right: 50px;
  12495. }
  12496. }
  12497. .edit-textarea {
  12498. width: 100%;
  12499. border: 2px solid #3E7BFA;
  12500. border-radius: 6px;
  12501. padding: 8px 12px;
  12502. font-size: 12px;
  12503. color: #6B7280;
  12504. background: white;
  12505. outline: none;
  12506. transition: all 0.2s ease;
  12507. resize: vertical;
  12508. min-height: 60px;
  12509. font-family: inherit;
  12510. line-height: 1.6;
  12511. &:focus {
  12512. border-color: #3E7BFA;
  12513. box-shadow: 0 0 0 3px rgba(62, 123, 250, 0.1);
  12514. }
  12515. &.title-edit-textarea {
  12516. font-size: 22px;
  12517. font-weight: 600;
  12518. color: #1F2937;
  12519. min-height: 50px;
  12520. }
  12521. &.chapter-edit-textarea {
  12522. font-size: 18px;
  12523. font-weight: 500;
  12524. color: #374151;
  12525. min-height: 60px;
  12526. }
  12527. &.section-edit-textarea {
  12528. font-size: 16px;
  12529. font-weight: 500;
  12530. color: #4B5563;
  12531. min-height: 50px;
  12532. }
  12533. &.subsection-edit-textarea {
  12534. font-size: 14px;
  12535. font-weight: 400;
  12536. color: #6B7280;
  12537. min-height: 45px;
  12538. }
  12539. &.subsubsection-edit-textarea {
  12540. font-size: 12px;
  12541. font-weight: 400;
  12542. color: #6B7280;
  12543. min-height: 40px;
  12544. }
  12545. }
  12546. .outline-chapter {
  12547. // margin-bottom: 32px;
  12548. position: relative;
  12549. cursor: move;
  12550. transition: all 0.3s ease;
  12551. border: 2px solid transparent;
  12552. border-radius: 8px;
  12553. background: white;
  12554. padding: 8px 0px 0px 8px;
  12555. &:hover {
  12556. border-color: #3E7BFA;
  12557. box-shadow: 0 4px 12px rgba(62, 123, 250, 0.15);
  12558. }
  12559. &.dragging {
  12560. opacity: 0.5;
  12561. transform: rotate(2deg);
  12562. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  12563. z-index: 1000;
  12564. }
  12565. &.drag-over {
  12566. border-color: #10B981;
  12567. background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(16, 185, 129, 0.1) 100%);
  12568. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
  12569. transform: scale(1.02);
  12570. }
  12571. // 拖拽提示点
  12572. &::before {
  12573. content: '';
  12574. position: absolute;
  12575. top: 12px;
  12576. left: 12px;
  12577. width: 4px;
  12578. height: 4px;
  12579. background: #9CA3AF;
  12580. border-radius: 50%;
  12581. opacity: 0;
  12582. transition: opacity 0.3s ease;
  12583. }
  12584. &:hover::before {
  12585. opacity: 1;
  12586. }
  12587. &.dragging::before {
  12588. opacity: 0;
  12589. }
  12590. .chapter-header {
  12591. display: flex;
  12592. align-items: center;
  12593. justify-content: space-between;
  12594. margin-bottom: 20px;
  12595. position: relative;
  12596. }
  12597. .chapter-title {
  12598. font-size: 20px;
  12599. font-weight: 700;
  12600. color: #111827;
  12601. margin: 0;
  12602. cursor: pointer;
  12603. transition: all 0.2s ease;
  12604. flex: 1;
  12605. &:hover {
  12606. color: #3E7BFA;
  12607. background: rgba(62, 123, 250, 0.05);
  12608. border-radius: 4px;
  12609. }
  12610. }
  12611. .outline-section {
  12612. .section-container {
  12613. margin-bottom: 16px;
  12614. position: relative;
  12615. }
  12616. .section-header {
  12617. display: flex;
  12618. align-items: center;
  12619. justify-content: space-between;
  12620. margin-bottom: 12px;
  12621. position: relative;
  12622. }
  12623. .section-title {
  12624. font-size: 16px;
  12625. color: #374151;
  12626. margin: 0;
  12627. line-height: 1.5;
  12628. font-weight: 600;
  12629. padding-left: 24px;
  12630. position: relative;
  12631. cursor: pointer;
  12632. transition: all 0.2s ease;
  12633. flex: 1;
  12634. &:hover {
  12635. color: #3E7BFA;
  12636. background: rgba(62, 123, 250, 0.05);
  12637. border-radius: 4px;
  12638. }
  12639. }
  12640. .section-subsection {
  12641. .subsection-container {
  12642. margin-bottom: 8px;
  12643. position: relative;
  12644. }
  12645. .subsection-header {
  12646. display: flex;
  12647. align-items: center;
  12648. justify-content: space-between;
  12649. position: relative;
  12650. }
  12651. .subsection-title {
  12652. font-size: 14px;
  12653. color: #6B7280;
  12654. margin: 0;
  12655. line-height: 1.5;
  12656. font-weight: 700;
  12657. padding-left: 48px;
  12658. position: relative;
  12659. cursor: pointer;
  12660. transition: all 0.2s ease;
  12661. flex: 1;
  12662. &:hover {
  12663. color: #3E7BFA;
  12664. background: rgba(62, 123, 250, 0.05);
  12665. border-radius: 4px;
  12666. }
  12667. }
  12668. // 具体内容要点样式(-开头)
  12669. .subsubsection-container {
  12670. margin-left: 20px;
  12671. margin-top: 8px;
  12672. .subsubsection-item {
  12673. margin-bottom: 12px;
  12674. border-left: 2px solid #E5E7EB;
  12675. padding-left: 16px;
  12676. }
  12677. .subsubsection-header {
  12678. display: flex;
  12679. align-items: center;
  12680. justify-content: space-between;
  12681. position: relative;
  12682. margin-bottom: 8px;
  12683. }
  12684. .subsubsection-title {
  12685. font-size: 13px;
  12686. color: #6B7280;
  12687. margin: 0;
  12688. line-height: 1.4;
  12689. font-weight: 400;
  12690. cursor: pointer;
  12691. transition: all 0.2s ease;
  12692. flex: 1;
  12693. &:hover {
  12694. color: #3E7BFA;
  12695. background: rgba(62, 123, 250, 0.05);
  12696. border-radius: 4px;
  12697. }
  12698. }
  12699. .subsubsection-content {
  12700. margin-top: 0;
  12701. .subsubsection-text {
  12702. font-size: 12px;
  12703. color: #6B7280;
  12704. line-height: 1.6;
  12705. cursor: pointer;
  12706. padding: 0;
  12707. background: transparent;
  12708. border-radius: 0;
  12709. border: none;
  12710. transition: all 0.2s ease;
  12711. min-height: 0;
  12712. &:hover {
  12713. background: transparent;
  12714. border-color: transparent;
  12715. }
  12716. &:empty::before {
  12717. content: "点击添加正文内容...";
  12718. color: #9CA3AF;
  12719. font-style: italic;
  12720. }
  12721. }
  12722. }
  12723. }
  12724. }
  12725. }
  12726. // 编辑相关样式
  12727. .edit-options {
  12728. display: flex;
  12729. gap: 4px;
  12730. opacity: 0;
  12731. transition: opacity 0.2s ease;
  12732. position: absolute;
  12733. right: 0;
  12734. top: 50%;
  12735. transform: translateY(-50%);
  12736. background: white;
  12737. border-radius: 6px;
  12738. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  12739. padding: 4px;
  12740. z-index: 10;
  12741. }
  12742. .outline-chapter:hover .edit-options,
  12743. .section-container:hover .edit-options,
  12744. .subsection-container:hover .edit-options {
  12745. opacity: 1;
  12746. }
  12747. .edit-input-container {
  12748. flex: 1;
  12749. margin-right: 8px;
  12750. }
  12751. }
  12752. }
  12753. }
  12754. }
  12755. .outline-sidebar {
  12756. width: 400px;
  12757. display: flex;
  12758. flex-direction: column;
  12759. gap: 24px;
  12760. .sidebar-section {
  12761. background: white;
  12762. border-radius: 12px;
  12763. overflow: hidden;
  12764. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  12765. .section-header {
  12766. display: flex;
  12767. align-items: center;
  12768. gap: 8px;
  12769. padding: 20px 20px 0px 20px;
  12770. // background: #E8F0FF;
  12771. .section-icon {
  12772. width: 20px;
  12773. height: 20px;
  12774. }
  12775. h5 {
  12776. font-size: 16px;
  12777. font-weight: 600;
  12778. color: #1F2937;
  12779. margin: 0;
  12780. }
  12781. }
  12782. .section-content {
  12783. padding: 20px 20px 12px 20px;
  12784. .stat-item {
  12785. display: flex;
  12786. justify-content: space-between;
  12787. align-items: center;
  12788. margin-bottom: 12px;
  12789. .stat-label {
  12790. font-size: 14px;
  12791. color: #6B7280;
  12792. }
  12793. .stat-value {
  12794. font-size: 14px;
  12795. font-weight: 600;
  12796. color: #1F2937;
  12797. }
  12798. }
  12799. .tip-list {
  12800. list-style: none;
  12801. padding: 0;
  12802. margin: 0;
  12803. li {
  12804. font-size: 14px;
  12805. color: #6B7280;
  12806. line-height: 1.6;
  12807. margin-bottom: 8px;
  12808. padding-left: 16px;
  12809. position: relative;
  12810. &:before {
  12811. content: "•";
  12812. position: absolute;
  12813. left: 0;
  12814. color: #3E7BFA;
  12815. }
  12816. }
  12817. }
  12818. .evaluation-question {
  12819. font-size: 14px;
  12820. color: #374151;
  12821. margin: 0 0 16px 0;
  12822. line-height: 1.5;
  12823. }
  12824. .evaluation-buttons {
  12825. display: flex;
  12826. gap: 12px;
  12827. .eval-btn {
  12828. flex: 1;
  12829. display: flex;
  12830. align-items: center;
  12831. justify-content: center;
  12832. gap: 6px;
  12833. padding: 10px 16px;
  12834. border: 1px solid #D1D5DB;
  12835. border-radius: 8px;
  12836. background: #F3F4F6;
  12837. color: #4B5563;
  12838. font-size: 14px;
  12839. cursor: pointer;
  12840. transition: all 0.3s ease;
  12841. &.satisfied {
  12842. &.active {
  12843. background: rgba(34, 184, 80, 0.1);
  12844. color: #22B850;
  12845. border-color: #22B850;
  12846. }
  12847. &:hover {
  12848. background: rgba(34, 184, 80, 0.15);
  12849. border-color: #22B850;
  12850. }
  12851. }
  12852. &.unsatisfied {
  12853. &.active {
  12854. background: rgba(255, 77, 79, 0.1);
  12855. color: #FF4D4F;
  12856. border-color: #FF4D4F;
  12857. }
  12858. &:hover {
  12859. background: rgba(255, 77, 79, 0.15);
  12860. border-color: #FF4D4F;
  12861. }
  12862. }
  12863. .eval-icon {
  12864. width: 16px;
  12865. height: 16px;
  12866. filter: brightness(0) saturate(100%) invert(45%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(0%) contrast(0%);
  12867. transition: filter 0.3s ease;
  12868. }
  12869. &.satisfied.active .eval-icon {
  12870. filter: brightness(0) saturate(100%) invert(67%) sepia(61%) saturate(1237%) hue-rotate(86deg) brightness(95%) contrast(89%);
  12871. }
  12872. &.unsatisfied.active .eval-icon {
  12873. filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2875%) hue-rotate(340deg) brightness(104%) contrast(107%);
  12874. }
  12875. }
  12876. }
  12877. }
  12878. }
  12879. .sidebar-actions {
  12880. display: flex;
  12881. gap: 12px;
  12882. justify-content: center;
  12883. margin-top: 12px;
  12884. .action-btn {
  12885. display: flex;
  12886. align-items: center;
  12887. justify-content: center;
  12888. gap: 8px;
  12889. padding: 16px 24px;
  12890. border: none;
  12891. border-radius: 8px;
  12892. font-size: 15px;
  12893. font-weight: 500;
  12894. cursor: pointer;
  12895. transition: all 0.3s ease;
  12896. &.secondary {
  12897. background: white;
  12898. color: #3E7BFA;
  12899. border: 1px solid #3E7BFA;
  12900. &:hover {
  12901. background: none;
  12902. border-color: transparent;
  12903. }
  12904. .action-icon {
  12905. width: 16px;
  12906. height: 16px;
  12907. }
  12908. }
  12909. &.primary {
  12910. background: #3E7BFA;
  12911. color: white;
  12912. &:hover {
  12913. background: #2563EB;
  12914. }
  12915. .action-icon {
  12916. width: 16px;
  12917. height: 16px;
  12918. }
  12919. }
  12920. &.exam {
  12921. background: #10B981;
  12922. color: white;
  12923. &:hover {
  12924. background: #059669;
  12925. }
  12926. .action-icon {
  12927. width: 16px;
  12928. height: 16px;
  12929. }
  12930. }
  12931. &.wps {
  12932. background: #FF6B35;
  12933. color: white;
  12934. &:hover {
  12935. background: #E55A2B;
  12936. }
  12937. .action-icon {
  12938. width: 16px;
  12939. height: 16px;
  12940. }
  12941. }
  12942. }
  12943. }
  12944. }
  12945. /* 推荐问题 */
  12946. .recommended-questions {
  12947. display: flex;
  12948. flex-wrap: wrap;
  12949. gap: 5px;
  12950. justify-content: center;
  12951. max-width: 1000px;
  12952. margin: 0 auto 0 auto;
  12953. padding: 0 30px;
  12954. .question-tag {
  12955. background: white;
  12956. padding: 9px 16px;
  12957. border-radius: 20px;
  12958. border: 1px solid #E5E8EB;
  12959. font-size: 14px;
  12960. color: #5A6C7D;
  12961. cursor: pointer;
  12962. transition: all 0.3s ease;
  12963. display: flex;
  12964. align-items: center;
  12965. gap: 8px;
  12966. white-space: nowrap;
  12967. &:hover {
  12968. border-color: #667eea;
  12969. color: #667eea;
  12970. transform: translateY(-1px);
  12971. }
  12972. .question-icon {
  12973. width: 16px;
  12974. height: 16px;
  12975. object-fit: contain;
  12976. flex-shrink: 0;
  12977. }
  12978. }
  12979. }
  12980. /* 文件预览区域 */
  12981. .file-preview-section {
  12982. margin-bottom: 12px;
  12983. .file-preview {
  12984. position: relative;
  12985. display: flex;
  12986. align-items: center;
  12987. background: rgba(255, 255, 255, 0.9);
  12988. border: 1px solid #E5E7EB;
  12989. border-radius: 12px;
  12990. padding: 12px;
  12991. max-width: 400px;
  12992. .file-icon {
  12993. font-size: 32px;
  12994. margin-right: 12px;
  12995. width: 48px;
  12996. text-align: center;
  12997. .file-icon-img {
  12998. width: 32px;
  12999. height: 32px;
  13000. object-fit: contain;
  13001. }
  13002. }
  13003. .file-info {
  13004. flex: 1;
  13005. .file-name {
  13006. font-size: 14px;
  13007. font-weight: 500;
  13008. color: #374151;
  13009. margin-bottom: 4px;
  13010. overflow: hidden;
  13011. text-overflow: ellipsis;
  13012. white-space: nowrap;
  13013. max-width: 200px;
  13014. }
  13015. .file-size {
  13016. font-size: 12px;
  13017. color: #6B7280;
  13018. }
  13019. }
  13020. .remove-file-btn {
  13021. width: 24px;
  13022. height: 24px;
  13023. border: none;
  13024. background: rgba(239, 68, 68, 0.1);
  13025. color: #DC2626;
  13026. border-radius: 50%;
  13027. cursor: pointer;
  13028. display: flex;
  13029. align-items: center;
  13030. justify-content: center;
  13031. transition: all 0.2s ease;
  13032. &:hover {
  13033. background: rgba(239, 68, 68, 0.2);
  13034. transform: scale(1.1);
  13035. }
  13036. .remove-icon {
  13037. font-size: 16px;
  13038. font-weight: bold;
  13039. }
  13040. }
  13041. }
  13042. }
  13043. /* 底部输入区域 */
  13044. .chat-input-section {
  13045. background: transparent;
  13046. padding: 9px 30px;
  13047. .input-container {
  13048. max-width: 900px;
  13049. margin: 0 auto;
  13050. .input-box {
  13051. display: flex;
  13052. align-items: center;
  13053. // gap: 12px;
  13054. background: white;
  13055. border-radius: 16px;
  13056. padding: 8px 20px;
  13057. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  13058. transition: box-shadow 0.3s ease;
  13059. border: 1px solid #3E7BFA;
  13060. height: 57px;
  13061. &:focus-within {
  13062. box-shadow: 0 2px 12px rgba(62, 123, 250, 0.2);
  13063. }
  13064. .attach-btn,
  13065. .voice-btn {
  13066. background: none;
  13067. border: none;
  13068. cursor: pointer;
  13069. padding: 8px;
  13070. border-radius: 6px;
  13071. transition: all 0.3s ease;
  13072. display: flex;
  13073. align-items: center;
  13074. justify-content: center;
  13075. position: relative;
  13076. &:hover:not(:disabled) {
  13077. background: rgba(102, 126, 234, 0.1);
  13078. }
  13079. &:disabled {
  13080. cursor: not-allowed;
  13081. }
  13082. &.recording {
  13083. background: rgba(239, 68, 68, 0.1);
  13084. animation: pulse 1.5s ease-in-out infinite;
  13085. }
  13086. .icon-container {
  13087. width: 20px;
  13088. height: 20px;
  13089. display: flex;
  13090. align-items: center;
  13091. justify-content: center;
  13092. flex-shrink: 0;
  13093. position: relative;
  13094. }
  13095. .recording-indicator {
  13096. position: absolute;
  13097. top: -2px;
  13098. right: -2px;
  13099. width: 8px;
  13100. height: 8px;
  13101. background: #ef4444;
  13102. border-radius: 50%;
  13103. animation: blink 1s ease-in-out infinite;
  13104. }
  13105. .action-icon {
  13106. width: 20px;
  13107. height: 20px;
  13108. height: 20px !important;
  13109. max-width: 20px !important;
  13110. max-height: 20px !important;
  13111. object-fit: contain;
  13112. flex-shrink: 0;
  13113. }
  13114. }
  13115. .message-input {
  13116. flex: 1;
  13117. border: none;
  13118. background: transparent;
  13119. font-size: 16px;
  13120. color: #2C3E50;
  13121. outline: none;
  13122. transition: opacity 0.3s ease;
  13123. &::placeholder {
  13124. color: #A0A6B8;
  13125. }
  13126. &:disabled {
  13127. cursor: not-allowed;
  13128. }
  13129. }
  13130. .divider {
  13131. width: 1px;
  13132. height: 31px;
  13133. background-color: #D6D5DE;
  13134. margin: 0 4px;
  13135. }
  13136. .send-btn {
  13137. background: none;
  13138. border: none;
  13139. cursor: pointer;
  13140. border-radius: 6px;
  13141. margin-left: 12px;
  13142. transition: background 0.3s ease;
  13143. display: flex;
  13144. align-items: center;
  13145. justify-content: center;
  13146. &:hover:not(:disabled) {
  13147. background: rgba(102, 126, 234, 0.1);
  13148. }
  13149. &:disabled {
  13150. cursor: not-allowed;
  13151. }
  13152. .send-icon {
  13153. width: 90px;
  13154. height: 40px;
  13155. object-fit: contain;
  13156. }
  13157. }
  13158. }
  13159. }
  13160. }
  13161. /* 步骤三:PPT模板选择界面 */
  13162. .step3-content {
  13163. width: 100%;
  13164. min-height: 888px;
  13165. height: calc(100vh - 160px);
  13166. /* 自适应浏览器高度,减去头部和边距 */
  13167. margin: 0 auto;
  13168. .preview-actions {
  13169. display: flex;
  13170. justify-content: flex-end;
  13171. gap: 12px;
  13172. margin-top: 24px;
  13173. padding-right: 347px;
  13174. .action-btn {
  13175. display: flex;
  13176. align-items: center;
  13177. justify-content: center;
  13178. gap: 8px;
  13179. padding: 12px 20px;
  13180. border: none;
  13181. border-radius: 8px;
  13182. font-size: 14px;
  13183. font-weight: 500;
  13184. cursor: pointer;
  13185. transition: all 0.3s ease;
  13186. &.secondary {
  13187. background: white;
  13188. color: #3E7BFA;
  13189. border: 1px solid #3E7BFA;
  13190. &:hover {
  13191. background: rgba(62, 123, 250, 0.05);
  13192. }
  13193. }
  13194. &.primary {
  13195. background: #3E7BFA;
  13196. color: white;
  13197. &:hover {
  13198. background: #2563EB;
  13199. }
  13200. &:disabled {
  13201. background: #9CA3AF;
  13202. cursor: not-allowed;
  13203. opacity: 0.6;
  13204. }
  13205. }
  13206. }
  13207. }
  13208. .template-container {
  13209. display: flex;
  13210. gap: 18px;
  13211. height: 100%;
  13212. }
  13213. .template-preview {
  13214. flex: 1;
  13215. background: white;
  13216. border-radius: 12px;
  13217. padding: 21px 41px 0px 25px;
  13218. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  13219. .preview-header {
  13220. display: flex;
  13221. justify-content: space-between;
  13222. align-items: center;
  13223. margin-bottom: 24px;
  13224. .preview-title {
  13225. font-size: 16px;
  13226. font-weight: 600;
  13227. color: #1F2937;
  13228. margin: 0;
  13229. }
  13230. .save-status {
  13231. font-size: 14px;
  13232. color: #6B7280;
  13233. }
  13234. }
  13235. .main-carousel {
  13236. position: relative;
  13237. display: flex;
  13238. align-items: center;
  13239. justify-content: center;
  13240. margin-bottom: 27px;
  13241. }
  13242. // PPT编辑工作台样式
  13243. .ppt-editor-workspace {
  13244. width: 100%;
  13245. .editor-canvas {
  13246. // background: white;
  13247. // border: 2px solid #E5E7EB;
  13248. // border-radius: 12px;
  13249. // padding: 40px;
  13250. // min-height: 603px;
  13251. box-shadow: none;
  13252. display: flex;
  13253. justify-content: center;
  13254. align-items: center;
  13255. min-height: 603px;
  13256. // PPT预览模式样式
  13257. .slide-preview {
  13258. width: 100%;
  13259. max-width: 1105px;
  13260. height: 603px;
  13261. position: relative;
  13262. border-radius: 12px;
  13263. overflow: hidden;
  13264. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  13265. background: transparent;
  13266. .ppt-preview-tip {
  13267. position: absolute;
  13268. top: 10px;
  13269. left: 10px;
  13270. background: rgba(0, 0, 0, 0.7);
  13271. color: white;
  13272. padding: 8px 12px;
  13273. border-radius: 6px;
  13274. font-size: 12px;
  13275. z-index: 1000;
  13276. p {
  13277. margin: 0;
  13278. }
  13279. }
  13280. .preview-element {
  13281. position: absolute;
  13282. cursor: pointer;
  13283. // border: 2px solid transparent;
  13284. transition: border-color 0.2s ease;
  13285. user-select: none;
  13286. &.selected {
  13287. border: 2px solid transparent;
  13288. box-shadow: none;
  13289. }
  13290. &:hover {
  13291. border: 2px solid transparent;
  13292. }
  13293. .resize-handle {
  13294. display: none;
  13295. }
  13296. .drag-handle {
  13297. display: none;
  13298. }
  13299. img {
  13300. width: 100%;
  13301. height: 100%;
  13302. object-fit: cover;
  13303. }
  13304. div {
  13305. width: 100%;
  13306. height: 100%;
  13307. display: flex;
  13308. align-items: center;
  13309. justify-content: center;
  13310. text-align: center;
  13311. padding: 10px;
  13312. box-sizing: border-box;
  13313. }
  13314. .inline-editor {
  13315. outline: 2px dashed #4c9ffe;
  13316. background: rgba(255, 255, 255, 0.2);
  13317. direction: ltr !important;
  13318. text-align: center !important;
  13319. unicode-bidi: normal !important;
  13320. }
  13321. .shape {
  13322. width: 100%;
  13323. height: 100%;
  13324. }
  13325. }
  13326. .preview-element {
  13327. position: absolute;
  13328. cursor: pointer;
  13329. transition: all 0.2s ease;
  13330. &.selected {
  13331. outline: none;
  13332. outline-offset: 0;
  13333. }
  13334. .resize-handle {
  13335. display: none;
  13336. }
  13337. .drag-handle {
  13338. display: none;
  13339. }
  13340. .inline-editor {
  13341. width: 100%;
  13342. height: 100%;
  13343. outline: none;
  13344. border: 1px solid #3E7BFA;
  13345. background: rgba(255, 255, 255, 0.9);
  13346. padding: 4px;
  13347. border-radius: 4px;
  13348. }
  13349. .shape {
  13350. width: 100%;
  13351. height: 100%;
  13352. }
  13353. img {
  13354. width: 100%;
  13355. height: 100%;
  13356. object-fit: cover;
  13357. border-radius: 0;
  13358. }
  13359. }
  13360. }
  13361. // 编辑模式样式
  13362. .edit-mode {
  13363. width: 100%;
  13364. height: 100%;
  13365. .slide-editor {
  13366. width: 100%;
  13367. height: 100%;
  13368. .slide-content {
  13369. outline: none;
  13370. .slide-title {
  13371. font-size: 32px;
  13372. font-weight: 700;
  13373. color: #1F2937;
  13374. margin: 0 0 24px 0;
  13375. text-align: center;
  13376. border-bottom: 3px solid #3E7BFA;
  13377. padding-bottom: 16px;
  13378. &:focus {
  13379. background: rgba(62, 123, 250, 0.05);
  13380. border-radius: 8px;
  13381. padding: 8px;
  13382. margin: -8px -8px 16px -8px;
  13383. }
  13384. }
  13385. .slide-body {
  13386. font-size: 18px;
  13387. color: #374151;
  13388. line-height: 1.6;
  13389. p {
  13390. margin: 16px 0;
  13391. &:focus {
  13392. background: rgba(62, 123, 250, 0.05);
  13393. border-radius: 6px;
  13394. padding: 8px;
  13395. margin: 8px -8px;
  13396. }
  13397. }
  13398. }
  13399. }
  13400. }
  13401. }
  13402. }
  13403. }
  13404. .carousel-btn {
  13405. position: absolute;
  13406. top: 50%;
  13407. transform: translateY(-50%);
  13408. background: #FFFFFF80;
  13409. border: 1px solid #D1D5DB;
  13410. border-radius: 50%;
  13411. width: 40px;
  13412. height: 40px;
  13413. display: flex;
  13414. align-items: center;
  13415. justify-content: center;
  13416. cursor: pointer;
  13417. transition: all 0.3s ease;
  13418. z-index: 10;
  13419. &:hover {
  13420. background: none;
  13421. border-color: transparent;
  13422. }
  13423. &.prev {
  13424. left: 18px;
  13425. }
  13426. &.next {
  13427. right: 18px;
  13428. }
  13429. .carousel-icon {
  13430. width: 6px;
  13431. height: 10px;
  13432. }
  13433. }
  13434. .main-slide {
  13435. width: 100%;
  13436. max-width: 1105px;
  13437. height: 603px;
  13438. border-radius: 8px;
  13439. overflow: hidden;
  13440. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  13441. .slide-image {
  13442. width: 100%;
  13443. height: 100%;
  13444. object-fit: cover;
  13445. }
  13446. }
  13447. }
  13448. .thumbnail-nav {
  13449. display: flex;
  13450. flex-direction: column;
  13451. align-items: center;
  13452. .slide-counter {
  13453. font-size: 14px;
  13454. color: #6B7280;
  13455. text-align: center;
  13456. margin-bottom: 36px;
  13457. display: flex;
  13458. align-items: center;
  13459. justify-content: center;
  13460. gap: 16px;
  13461. .progress-dots {
  13462. display: flex;
  13463. gap: 4px;
  13464. align-items: center;
  13465. .progress-dot {
  13466. width: 8px;
  13467. height: 8px;
  13468. border-radius: 50%;
  13469. background: #D1D5DB;
  13470. cursor: pointer;
  13471. transition: all 0.3s ease;
  13472. &:hover {
  13473. background: #D1D5DB;
  13474. transform: scale(1.2);
  13475. }
  13476. &.active {
  13477. background: #3E7BFA;
  13478. width: 16px;
  13479. border-radius: 8px;
  13480. transform: scale(1);
  13481. box-shadow: 0 0 6px rgba(62, 123, 250, 0.4);
  13482. }
  13483. }
  13484. }
  13485. }
  13486. .thumbnail-strip {
  13487. display: flex;
  13488. gap: 11px;
  13489. max-width: 1050px;
  13490. /* 限制最大宽度 */
  13491. margin: 0 auto;
  13492. /* 居中显示 */
  13493. overflow-x: auto;
  13494. /* 横向滚动 */
  13495. justify-content: flex-start;
  13496. /* 从左侧开始排列 */
  13497. // padding: 0 10px;
  13498. /* 自定义滚动条样式 */
  13499. &::-webkit-scrollbar {
  13500. height: 6px;
  13501. }
  13502. &::-webkit-scrollbar-track {
  13503. background: #f1f1f1;
  13504. border-radius: 3px;
  13505. }
  13506. &::-webkit-scrollbar-thumb {
  13507. background: #c1c1c1;
  13508. border-radius: 3px;
  13509. &:hover {
  13510. background: #a8a8a8;
  13511. }
  13512. }
  13513. .thumbnail-item {
  13514. width: 135px;
  13515. height: 79px;
  13516. border-radius: 6px;
  13517. overflow: hidden;
  13518. cursor: pointer;
  13519. border: 2px solid transparent;
  13520. transition: all 0.3s ease;
  13521. position: relative;
  13522. flex-shrink: 0;
  13523. /* 防止缩略图被压缩 */
  13524. margin: 0 auto;
  13525. /* 确保单个缩略图也能居中 */
  13526. &:hover {
  13527. border-color: #3E7BFA;
  13528. transform: translateY(-2px);
  13529. }
  13530. &.active {
  13531. border-color: #3E7BFA;
  13532. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.3);
  13533. }
  13534. .thumbnail-preview {
  13535. width: 100%;
  13536. height: 100%;
  13537. position: relative;
  13538. overflow: hidden;
  13539. border-radius: 4px;
  13540. }
  13541. .thumbnail-content {
  13542. width: 100%;
  13543. height: 100%;
  13544. position: relative;
  13545. transform: scale(0.22);
  13546. transform-origin: -70px -20px;
  13547. }
  13548. .thumbnail-element {
  13549. position: absolute;
  13550. transform: scale(1);
  13551. }
  13552. .thumbnail-element img {
  13553. max-width: 100%;
  13554. max-height: 100%;
  13555. object-fit: contain;
  13556. }
  13557. .thumbnail-element div {
  13558. font-size: 6px;
  13559. line-height: 1.1;
  13560. overflow: hidden;
  13561. text-overflow: ellipsis;
  13562. white-space: nowrap;
  13563. word-break: break-word;
  13564. /* 确保文本样式与主图一致 */
  13565. font-weight: inherit;
  13566. text-align: inherit;
  13567. text-shadow: inherit;
  13568. }
  13569. .thumbnail-element div p {
  13570. margin: 0;
  13571. padding: 0;
  13572. font-size: inherit;
  13573. line-height: inherit;
  13574. color: inherit;
  13575. font-weight: inherit;
  13576. text-align: inherit;
  13577. text-shadow: inherit;
  13578. }
  13579. .thumbnail-element div strong {
  13580. font-weight: bold;
  13581. }
  13582. .thumbnail-element div span {
  13583. font-size: inherit;
  13584. color: inherit;
  13585. font-weight: inherit;
  13586. }
  13587. .thumbnail-image {
  13588. width: 100%;
  13589. height: 100%;
  13590. object-fit: cover;
  13591. }
  13592. .thumbnail-number {
  13593. position: absolute;
  13594. bottom: 4px;
  13595. right: 4px;
  13596. background: rgba(0, 0, 0, 0.5);
  13597. color: #FFFFFF;
  13598. font-size: 12px;
  13599. font-weight: 500;
  13600. padding: 2px 6px;
  13601. border-radius: 3.81px;
  13602. line-height: 1;
  13603. min-width: 16px;
  13604. text-align: center;
  13605. }
  13606. }
  13607. }
  13608. }
  13609. }
  13610. .template-sidebar {
  13611. background: #FFFFFF;
  13612. border-radius: 8px;
  13613. // width: 320px;
  13614. display: flex;
  13615. flex-direction: column;
  13616. gap: 16px;
  13617. padding: 20px 19px 16px 19px;
  13618. .sidebar-title {
  13619. font-size: 15px;
  13620. font-weight: 600;
  13621. color: #1F2937;
  13622. margin: 0 0 16px 0;
  13623. }
  13624. .template-list {
  13625. display: flex;
  13626. flex-direction: column;
  13627. gap: 20px;
  13628. .template-item {
  13629. background: white;
  13630. border-radius: 12px;
  13631. padding: 16px;
  13632. cursor: pointer;
  13633. transition: all 0.3s ease;
  13634. border: 2px solid #E4E4E4;
  13635. &:hover {
  13636. border-color: #E5E7EB;
  13637. transform: translateY(-2px);
  13638. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  13639. }
  13640. &.active {
  13641. border-color: #3E7BFA;
  13642. background: rgba(62, 123, 250, 0.05);
  13643. }
  13644. .template-thumbnail {
  13645. width: 100%;
  13646. height: 144px;
  13647. width: 256px;
  13648. border-radius: 8px;
  13649. overflow: hidden;
  13650. margin-bottom: 12px;
  13651. border: none;
  13652. position: relative;
  13653. .template-img {
  13654. width: 100%;
  13655. height: 100%;
  13656. object-fit: cover;
  13657. border: none !important;
  13658. outline: none !important;
  13659. }
  13660. .dynamic-badge {
  13661. position: absolute;
  13662. top: 8px;
  13663. right: 8px;
  13664. background: #10b981;
  13665. color: white;
  13666. font-size: 12px;
  13667. padding: 4px 8px;
  13668. border-radius: 4px;
  13669. font-weight: 600;
  13670. z-index: 10;
  13671. }
  13672. }
  13673. .template-info {
  13674. .template-title {
  13675. font-size: 15px;
  13676. font-weight: 600;
  13677. color: #1F2937;
  13678. margin: 0 0 8px 0;
  13679. line-height: 1.4;
  13680. }
  13681. .template-meta {
  13682. display: flex;
  13683. justify-content: space-between;
  13684. align-items: center;
  13685. .update-time {
  13686. font-size: 13px;
  13687. color: #6B7280;
  13688. }
  13689. .page-count {
  13690. font-size: 13px;
  13691. color: #6B7280;
  13692. font-weight: 500;
  13693. }
  13694. }
  13695. .template-description {
  13696. font-size: 13px;
  13697. color: #4a5568;
  13698. margin-top: 8px;
  13699. line-height: 1.4;
  13700. }
  13701. }
  13702. }
  13703. }
  13704. .dynamic-preview {
  13705. margin-top: 16px;
  13706. padding: 16px;
  13707. background: #f7fafc;
  13708. border-radius: 8px;
  13709. border: 1px solid #e2e8f0;
  13710. .preview-title {
  13711. font-size: 14px;
  13712. font-weight: 600;
  13713. color: #2d3748;
  13714. margin: 0 0 12px 0;
  13715. }
  13716. .preview-stats {
  13717. display: grid;
  13718. grid-template-columns: 1fr 1fr;
  13719. gap: 8px;
  13720. margin-bottom: 12px;
  13721. .stat-item {
  13722. display: flex;
  13723. justify-content: space-between;
  13724. align-items: center;
  13725. padding: 6px 8px;
  13726. background: white;
  13727. border-radius: 4px;
  13728. border: 1px solid #e2e8f0;
  13729. .stat-label {
  13730. font-size: 12px;
  13731. color: #718096;
  13732. }
  13733. .stat-value {
  13734. font-size: 12px;
  13735. font-weight: 600;
  13736. color: #2d3748;
  13737. &.complexity-simple {
  13738. color: #10b981;
  13739. }
  13740. &.complexity-medium {
  13741. color: #f59e0b;
  13742. }
  13743. &.complexity-complex {
  13744. color: #ef4444;
  13745. }
  13746. }
  13747. }
  13748. }
  13749. .recommendations,
  13750. .warnings {
  13751. margin-top: 12px;
  13752. .recommendations-title,
  13753. .warnings-title {
  13754. font-size: 12px;
  13755. font-weight: 600;
  13756. color: #2d3748;
  13757. margin: 0 0 8px 0;
  13758. }
  13759. .recommendations-list,
  13760. .warnings-list {
  13761. margin: 0;
  13762. padding-left: 16px;
  13763. li {
  13764. font-size: 12px;
  13765. color: #4a5568;
  13766. margin-bottom: 4px;
  13767. line-height: 1.3;
  13768. }
  13769. }
  13770. &.warnings {
  13771. .warnings-title {
  13772. color: #ef4444;
  13773. }
  13774. .warnings-list li {
  13775. color: #dc2626;
  13776. }
  13777. }
  13778. }
  13779. }
  13780. }
  13781. .template-sidebar {
  13782. background: #FFFFFF;
  13783. border-radius: 8px;
  13784. // width: 320px;
  13785. display: flex;
  13786. flex-direction: column;
  13787. gap: 16px;
  13788. padding: 20px 19px 16px 19px;
  13789. .sidebar-title {
  13790. font-size: 15px;
  13791. font-weight: 600;
  13792. color: #1F2937;
  13793. // margin: 0 0 16px 0;
  13794. }
  13795. .template-list {
  13796. display: flex;
  13797. flex-direction: column;
  13798. gap: 20px;
  13799. .template-item {
  13800. background: white;
  13801. border-radius: 12px;
  13802. padding: 16px;
  13803. cursor: pointer;
  13804. transition: all 0.3s ease;
  13805. border: 2px solid #E4E4E4;
  13806. &:hover {
  13807. border-color: #E5E7EB;
  13808. transform: translateY(-2px);
  13809. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  13810. }
  13811. &.active {
  13812. border-color: #3E7BFA;
  13813. background: rgba(62, 123, 250, 0.05);
  13814. }
  13815. .template-thumbnail {
  13816. width: 100%;
  13817. height: 144px;
  13818. width: 256px;
  13819. border-radius: 8px;
  13820. overflow: hidden;
  13821. margin-bottom: 12px;
  13822. .template-img {
  13823. width: 100%;
  13824. height: 100%;
  13825. object-fit: cover;
  13826. }
  13827. }
  13828. .template-info {
  13829. .template-title {
  13830. font-size: 15px;
  13831. font-weight: 600;
  13832. color: #1F2937;
  13833. margin: 0 0 8px 0;
  13834. line-height: 1.4;
  13835. }
  13836. .template-meta {
  13837. display: flex;
  13838. justify-content: space-between;
  13839. align-items: center;
  13840. .update-time {
  13841. font-size: 13px;
  13842. color: #6B7280;
  13843. }
  13844. .page-count {
  13845. font-size: 13px;
  13846. color: #6B7280;
  13847. font-weight: 500;
  13848. }
  13849. }
  13850. }
  13851. }
  13852. }
  13853. // 下载选项样式
  13854. .download-content {
  13855. .download-options {
  13856. display: flex;
  13857. flex-direction: column;
  13858. gap: 16px;
  13859. .download-option {
  13860. background: #F9FAFB;
  13861. border-radius: 8px;
  13862. padding: 13px;
  13863. width: 292px;
  13864. height: 66px;
  13865. cursor: pointer;
  13866. transition: all 0.3s ease;
  13867. border: 2px solid transparent;
  13868. display: flex;
  13869. align-items: center;
  13870. gap: 12px;
  13871. position: relative;
  13872. &:hover {
  13873. background: none;
  13874. border-color: transparent;
  13875. }
  13876. &.active {
  13877. background: white;
  13878. border-color: #3E7BFA;
  13879. box-shadow: 0 2px 8px rgba(62, 123, 250, 0.1);
  13880. }
  13881. .option-icon {
  13882. width: 40px;
  13883. height: 40px;
  13884. flex-shrink: 0;
  13885. .option-img {
  13886. width: 100%;
  13887. height: 100%;
  13888. object-fit: contain;
  13889. }
  13890. }
  13891. .option-info {
  13892. flex: 1;
  13893. .option-title {
  13894. font-size: 16px;
  13895. font-weight: 600;
  13896. color: #111827;
  13897. margin: 0 0 4px 0;
  13898. }
  13899. .option-description {
  13900. font-size: 12px;
  13901. color: #6B7280;
  13902. margin: 0;
  13903. line-height: 1.4;
  13904. }
  13905. }
  13906. .option-check {
  13907. width: 24px;
  13908. height: 24px;
  13909. flex-shrink: 0;
  13910. .check-icon {
  13911. width: 100%;
  13912. height: 100%;
  13913. object-fit: contain;
  13914. }
  13915. }
  13916. }
  13917. }
  13918. .download-actions {
  13919. display: flex;
  13920. flex-direction: column;
  13921. gap: 12px;
  13922. margin-top: 485px;
  13923. .action-btn {
  13924. display: flex;
  13925. align-items: center;
  13926. justify-content: center;
  13927. gap: 8px;
  13928. padding: 12px 20px;
  13929. border: none;
  13930. border-radius: 8px;
  13931. font-size: 14px;
  13932. font-weight: 500;
  13933. cursor: pointer;
  13934. transition: all 0.3s ease;
  13935. &.secondary {
  13936. background: white;
  13937. color: #3E7BFA;
  13938. border: 1px solid #3E7BFA;
  13939. &:hover {
  13940. background: rgba(62, 123, 250, 0.05);
  13941. }
  13942. }
  13943. &.primary {
  13944. background: #3E7BFA;
  13945. color: white;
  13946. &:hover {
  13947. background: #2563EB;
  13948. }
  13949. .download-icon {
  13950. width: 16px;
  13951. height: 16px;
  13952. }
  13953. }
  13954. }
  13955. }
  13956. }
  13957. }
  13958. </style>